Use ciso8601 for parsing MySQLdb datetimes (#71818)

* Use ciso8601 for parsing MySQLDB datetimes

The default parser is this:

5340191feb/MySQLdb/times.py (L66)

* tweak

* tweak

* add coverage for building the MySQLdb connect conv param
This commit is contained in:
J. Nick Koston 2022-05-13 20:26:09 -04:00 committed by GitHub
parent abe78b1212
commit a8f1dda004
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 54 additions and 1 deletions

View file

@ -10,6 +10,7 @@ from homeassistant.helpers.json import JSONEncoder
DATA_INSTANCE = "recorder_instance"
SQLITE_URL_PREFIX = "sqlite://"
MYSQLDB_URL_PREFIX = "mysql://"
DOMAIN = "recorder"
CONF_DB_INTEGRITY_CHECK = "db_integrity_check"

View file

@ -41,6 +41,7 @@ from .const import (
DB_WORKER_PREFIX,
KEEPALIVE_TIME,
MAX_QUEUE_BACKLOG,
MYSQLDB_URL_PREFIX,
SQLITE_URL_PREFIX,
SupportedDialect,
)
@ -77,6 +78,7 @@ from .tasks import (
WaitTask,
)
from .util import (
build_mysqldb_conv,
dburl_to_path,
end_incomplete_runs,
is_second_sunday,
@ -1014,6 +1016,14 @@ class Recorder(threading.Thread):
kwargs["pool_reset_on_return"] = None
elif self.db_url.startswith(SQLITE_URL_PREFIX):
kwargs["poolclass"] = RecorderPool
elif self.db_url.startswith(MYSQLDB_URL_PREFIX):
# If they have configured MySQLDB but don't have
# the MySQLDB module installed this will throw
# an ImportError which we suppress here since
# sqlalchemy will give them a better error when
# it tried to import it below.
with contextlib.suppress(ImportError):
kwargs["connect_args"] = {"conv": build_mysqldb_conv()}
else:
kwargs["echo"] = False

View file

@ -15,6 +15,7 @@ from awesomeversion import (
AwesomeVersionException,
AwesomeVersionStrategy,
)
import ciso8601
from sqlalchemy import text
from sqlalchemy.engine.cursor import CursorFetchStrategy
from sqlalchemy.exc import OperationalError, SQLAlchemyError
@ -331,6 +332,30 @@ def _extract_version_from_server_response(
return None
def _datetime_or_none(value: str) -> datetime | None:
"""Fast version of mysqldb DateTime_or_None.
https://github.com/PyMySQL/mysqlclient/blob/v2.1.0/MySQLdb/times.py#L66
"""
try:
return ciso8601.parse_datetime(value)
except ValueError:
return None
def build_mysqldb_conv() -> dict:
"""Build a MySQLDB conv dict that uses cisco8601 to parse datetimes."""
# Late imports since we only call this if they are using mysqldb
from MySQLdb.constants import ( # pylint: disable=import-outside-toplevel,import-error
FIELD_TYPE,
)
from MySQLdb.converters import ( # pylint: disable=import-outside-toplevel,import-error
conversions,
)
return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none}
def setup_connection_for_dialect(
instance: Recorder,
dialect_name: str,

View file

@ -2,7 +2,7 @@
from datetime import datetime, timedelta
import os
import sqlite3
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, Mock, patch
import pytest
from sqlalchemy import text
@ -641,3 +641,20 @@ def test_is_second_sunday():
assert is_second_sunday(datetime(2022, 5, 8, 0, 0, 0, tzinfo=dt_util.UTC)) is True
assert is_second_sunday(datetime(2022, 1, 10, 0, 0, 0, tzinfo=dt_util.UTC)) is False
def test_build_mysqldb_conv():
"""Test building the MySQLdb connect conv param."""
mock_converters = Mock(conversions={"original": "preserved"})
mock_constants = Mock(FIELD_TYPE=Mock(DATETIME="DATETIME"))
with patch.dict(
"sys.modules",
**{"MySQLdb.constants": mock_constants, "MySQLdb.converters": mock_converters},
):
conv = util.build_mysqldb_conv()
assert conv["original"] == "preserved"
assert conv["DATETIME"]("INVALID") is None
assert conv["DATETIME"]("2022-05-13T22:33:12.741") == datetime(
2022, 5, 13, 22, 33, 12, 741000, tzinfo=None
)