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:
parent
abe78b1212
commit
a8f1dda004
4 changed files with 54 additions and 1 deletions
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue