Bump SQLAlchemy to 2.0.36 (#126683)

* Bump SQLAlchemy to 2.0.35

changelog: https://docs.sqlalchemy.org/en/20/changelog/changelog_20.html#change-2.0.35

* fix mocking

* adjust to .36

* remove ignored as these are now typed

* fix SQLAlchemy
This commit is contained in:
J. Nick Koston 2024-11-30 21:07:51 -06:00 committed by GitHub
parent 44ed83a829
commit ffeefd4856
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 62 additions and 59 deletions

View File

@ -143,7 +143,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev" apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
skip-binary: aiohttp;multidict;yarl skip-binary: aiohttp;multidict;yarl;SQLAlchemy
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements.txt" requirements: "requirements.txt"

View File

@ -162,14 +162,14 @@ class Unused(CHAR):
"""An unused column type that behaves like a string.""" """An unused column type that behaves like a string."""
@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] @compiles(UnusedDateTime, "mysql", "mariadb", "sqlite")
@compiles(Unused, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] @compiles(Unused, "mysql", "mariadb", "sqlite")
def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str:
"""Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite.""" """Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite."""
return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite) return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite)
@compiles(Unused, "postgresql") # type: ignore[misc,no-untyped-call] @compiles(Unused, "postgresql")
def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str:
"""Compile Unused as CHAR(1) on postgresql.""" """Compile Unused as CHAR(1) on postgresql."""
return "CHAR(1)" # Uses 1 byte return "CHAR(1)" # Uses 1 byte

View File

@ -7,7 +7,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": [ "requirements": [
"SQLAlchemy==2.0.31", "SQLAlchemy==2.0.36",
"fnv-hash-fast==1.0.2", "fnv-hash-fast==1.0.2",
"psutil-home-assistant==0.0.1" "psutil-home-assistant==0.0.1"
] ]

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sql", "documentation": "https://www.home-assistant.io/integrations/sql",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["SQLAlchemy==2.0.31", "sqlparse==0.5.0"] "requirements": ["SQLAlchemy==2.0.36", "sqlparse==0.5.0"]
} }

View File

@ -59,7 +59,7 @@ pyudev==0.24.1
PyYAML==6.0.2 PyYAML==6.0.2
requests==2.32.3 requests==2.32.3
securetar==2024.11.0 securetar==2024.11.0
SQLAlchemy==2.0.31 SQLAlchemy==2.0.36
standard-aifc==3.13.0;python_version>='3.13' standard-aifc==3.13.0;python_version>='3.13'
standard-telnetlib==3.13.0;python_version>='3.13' standard-telnetlib==3.13.0;python_version>='3.13'
typing-extensions>=4.12.2,<5.0 typing-extensions>=4.12.2,<5.0

View File

@ -66,7 +66,7 @@ dependencies = [
"PyYAML==6.0.2", "PyYAML==6.0.2",
"requests==2.32.3", "requests==2.32.3",
"securetar==2024.11.0", "securetar==2024.11.0",
"SQLAlchemy==2.0.31", "SQLAlchemy==2.0.36",
"standard-aifc==3.13.0;python_version>='3.13'", "standard-aifc==3.13.0;python_version>='3.13'",
"standard-telnetlib==3.13.0;python_version>='3.13'", "standard-telnetlib==3.13.0;python_version>='3.13'",
"typing-extensions>=4.12.2,<5.0", "typing-extensions>=4.12.2,<5.0",

View File

@ -37,7 +37,7 @@ python-slugify==8.0.4
PyYAML==6.0.2 PyYAML==6.0.2
requests==2.32.3 requests==2.32.3
securetar==2024.11.0 securetar==2024.11.0
SQLAlchemy==2.0.31 SQLAlchemy==2.0.36
standard-aifc==3.13.0;python_version>='3.13' standard-aifc==3.13.0;python_version>='3.13'
standard-telnetlib==3.13.0;python_version>='3.13' standard-telnetlib==3.13.0;python_version>='3.13'
typing-extensions>=4.12.2,<5.0 typing-extensions>=4.12.2,<5.0

View File

@ -116,7 +116,7 @@ RtmAPI==0.7.2
# homeassistant.components.recorder # homeassistant.components.recorder
# homeassistant.components.sql # homeassistant.components.sql
SQLAlchemy==2.0.31 SQLAlchemy==2.0.36
# homeassistant.components.tami4 # homeassistant.components.tami4
Tami4EdgeAPI==3.0 Tami4EdgeAPI==3.0

View File

@ -110,7 +110,7 @@ RtmAPI==0.7.2
# homeassistant.components.recorder # homeassistant.components.recorder
# homeassistant.components.sql # homeassistant.components.sql
SQLAlchemy==2.0.31 SQLAlchemy==2.0.36
# homeassistant.components.tami4 # homeassistant.components.tami4
Tami4EdgeAPI==3.0 Tami4EdgeAPI==3.0

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@ -597,9 +598,6 @@ async def test_options_flow_db_url_empty(
"homeassistant.components.sql.async_setup_entry", "homeassistant.components.sql.async_setup_entry",
return_value=True, return_value=True,
), ),
patch(
"homeassistant.components.sql.config_flow.sqlalchemy.create_engine",
),
): ):
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
@ -621,7 +619,9 @@ async def test_options_flow_db_url_empty(
async def test_full_flow_not_recorder_db( async def test_full_flow_not_recorder_db(
recorder_mock: Recorder, hass: HomeAssistant recorder_mock: Recorder,
hass: HomeAssistant,
tmp_path: Path,
) -> None: ) -> None:
"""Test full config flow with not using recorder db.""" """Test full config flow with not using recorder db."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -629,20 +629,19 @@ async def test_full_flow_not_recorder_db(
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
db_path = tmp_path / "db.db"
db_path_str = f"sqlite:///{db_path}"
with ( with (
patch( patch(
"homeassistant.components.sql.async_setup_entry", "homeassistant.components.sql.async_setup_entry",
return_value=True, return_value=True,
), ),
patch(
"homeassistant.components.sql.config_flow.sqlalchemy.create_engine",
),
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
"db_url": "sqlite://path/to/db.db", "db_url": db_path_str,
"name": "Get Value", "name": "Get Value",
"query": "SELECT 5 as value", "query": "SELECT 5 as value",
"column": "value", "column": "value",
@ -654,7 +653,7 @@ async def test_full_flow_not_recorder_db(
assert result2["title"] == "Get Value" assert result2["title"] == "Get Value"
assert result2["options"] == { assert result2["options"] == {
"name": "Get Value", "name": "Get Value",
"db_url": "sqlite://path/to/db.db", "db_url": db_path_str,
"query": "SELECT 5 as value", "query": "SELECT 5 as value",
"column": "value", "column": "value",
} }
@ -671,15 +670,12 @@ async def test_full_flow_not_recorder_db(
"homeassistant.components.sql.async_setup_entry", "homeassistant.components.sql.async_setup_entry",
return_value=True, return_value=True,
), ),
patch(
"homeassistant.components.sql.config_flow.sqlalchemy.create_engine",
),
): ):
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
"query": "SELECT 5 as value", "query": "SELECT 5 as value",
"db_url": "sqlite://path/to/db.db", "db_url": db_path_str,
"column": "value", "column": "value",
"unit_of_measurement": "MiB", "unit_of_measurement": "MiB",
}, },
@ -689,7 +685,7 @@ async def test_full_flow_not_recorder_db(
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == { assert result["data"] == {
"name": "Get Value", "name": "Get Value",
"db_url": "sqlite://path/to/db.db", "db_url": db_path_str,
"query": "SELECT 5 as value", "query": "SELECT 5 as value",
"column": "value", "column": "value",
"unit_of_measurement": "MiB", "unit_of_measurement": "MiB",
@ -697,14 +693,12 @@ async def test_full_flow_not_recorder_db(
# Need to test same again to mitigate issue with db_url removal # Need to test same again to mitigate issue with db_url removal
result = await hass.config_entries.options.async_init(entry.entry_id) result = await hass.config_entries.options.async_init(entry.entry_id)
with patch(
"homeassistant.components.sql.config_flow.sqlalchemy.create_engine",
):
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
"query": "SELECT 5 as value", "query": "SELECT 5 as value",
"db_url": "sqlite://path/to/db.db", "db_url": db_path_str,
"column": "value", "column": "value",
"unit_of_measurement": "MB", "unit_of_measurement": "MB",
}, },
@ -714,7 +708,7 @@ async def test_full_flow_not_recorder_db(
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == { assert result["data"] == {
"name": "Get Value", "name": "Get Value",
"db_url": "sqlite://path/to/db.db", "db_url": db_path_str,
"query": "SELECT 5 as value", "query": "SELECT 5 as value",
"column": "value", "column": "value",
"unit_of_measurement": "MB", "unit_of_measurement": "MB",
@ -722,7 +716,7 @@ async def test_full_flow_not_recorder_db(
assert entry.options == { assert entry.options == {
"name": "Get Value", "name": "Get Value",
"db_url": "sqlite://path/to/db.db", "db_url": db_path_str,
"query": "SELECT 5 as value", "query": "SELECT 5 as value",
"column": "value", "column": "value",
"unit_of_measurement": "MB", "unit_of_measurement": "MB",

View File

@ -3,12 +3,13 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from pathlib import Path
import sqlite3
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from sqlalchemy import text as sql_text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from homeassistant.components.recorder import Recorder from homeassistant.components.recorder import Recorder
@ -143,29 +144,37 @@ async def test_query_no_value(
assert text in caplog.text assert text in caplog.text
async def test_query_mssql_no_result( async def test_query_on_disk_sqlite_no_result(
recorder_mock: Recorder, hass: HomeAssistant, caplog: pytest.LogCaptureFixture recorder_mock: Recorder,
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
tmp_path: Path,
) -> None: ) -> None:
"""Test the SQL sensor with a query that returns no value.""" """Test the SQL sensor with a query that returns no value."""
db_path = tmp_path / "test.db"
db_path_str = f"sqlite:///{db_path}"
def make_test_db():
"""Create a test database."""
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE users (value INTEGER)")
conn.commit()
conn.close()
await hass.async_add_executor_job(make_test_db)
config = { config = {
"db_url": "mssql://", "db_url": db_path_str,
"query": "SELECT 5 as value where 1=2", "query": "SELECT value from users",
"column": "value", "column": "value",
"name": "count_tables", "name": "count_users",
} }
with (
patch("homeassistant.components.sql.sensor.sqlalchemy"),
patch(
"homeassistant.components.sql.sensor.sqlalchemy.text",
return_value=sql_text("SELECT TOP 1 5 as value where 1=2"),
),
):
await init_integration(hass, config) await init_integration(hass, config)
state = hass.states.get("sensor.count_tables") state = hass.states.get("sensor.count_users")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
text = "SELECT TOP 1 5 AS VALUE WHERE 1=2 returned no results" text = "SELECT value from users LIMIT 1; returned no results"
assert text in caplog.text assert text in caplog.text