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" DATA_INSTANCE = "recorder_instance"
SQLITE_URL_PREFIX = "sqlite://" SQLITE_URL_PREFIX = "sqlite://"
MYSQLDB_URL_PREFIX = "mysql://"
DOMAIN = "recorder" DOMAIN = "recorder"
CONF_DB_INTEGRITY_CHECK = "db_integrity_check" CONF_DB_INTEGRITY_CHECK = "db_integrity_check"

View file

@ -41,6 +41,7 @@ from .const import (
DB_WORKER_PREFIX, DB_WORKER_PREFIX,
KEEPALIVE_TIME, KEEPALIVE_TIME,
MAX_QUEUE_BACKLOG, MAX_QUEUE_BACKLOG,
MYSQLDB_URL_PREFIX,
SQLITE_URL_PREFIX, SQLITE_URL_PREFIX,
SupportedDialect, SupportedDialect,
) )
@ -77,6 +78,7 @@ from .tasks import (
WaitTask, WaitTask,
) )
from .util import ( from .util import (
build_mysqldb_conv,
dburl_to_path, dburl_to_path,
end_incomplete_runs, end_incomplete_runs,
is_second_sunday, is_second_sunday,
@ -1014,6 +1016,14 @@ class Recorder(threading.Thread):
kwargs["pool_reset_on_return"] = None kwargs["pool_reset_on_return"] = None
elif self.db_url.startswith(SQLITE_URL_PREFIX): elif self.db_url.startswith(SQLITE_URL_PREFIX):
kwargs["poolclass"] = RecorderPool 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: else:
kwargs["echo"] = False kwargs["echo"] = False

View file

@ -15,6 +15,7 @@ from awesomeversion import (
AwesomeVersionException, AwesomeVersionException,
AwesomeVersionStrategy, AwesomeVersionStrategy,
) )
import ciso8601
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.engine.cursor import CursorFetchStrategy from sqlalchemy.engine.cursor import CursorFetchStrategy
from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.exc import OperationalError, SQLAlchemyError
@ -331,6 +332,30 @@ def _extract_version_from_server_response(
return None 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( def setup_connection_for_dialect(
instance: Recorder, instance: Recorder,
dialect_name: str, dialect_name: str,

View file

@ -2,7 +2,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import os import os
import sqlite3 import sqlite3
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, Mock, patch
import pytest import pytest
from sqlalchemy import text 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, 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 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
)