From fe216cff17da108cdeaf25e3c3c224651e825f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Thu, 22 Oct 2026 19:26:08 +0300 Subject: [PATCH 1/2] Drop Python 3.10 support --- .github/workflows/linting.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/tests.yaml | 3 +- .github/workflows/typing.yaml | 2 +- README.md | 2 +- poetry.lock | 82 ++++------------------------------ pyproject.toml | 5 +-- 7 files changed, 15 insertions(+), 83 deletions(-) diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml index d014fde..c731435 100644 --- a/.github/workflows/linting.yaml +++ b/.github/workflows/linting.yaml @@ -9,7 +9,7 @@ on: # yamllint disable-line rule:truthy workflow_dispatch: env: - DEFAULT_PYTHON: "3.10" + DEFAULT_PYTHON: "3.11" jobs: codespell: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8cd9e92..d15769b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,7 +7,7 @@ on: # yamllint disable-line rule:truthy - published env: - DEFAULT_PYTHON: "3.10" + DEFAULT_PYTHON: "3.11" jobs: release: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 127bedf..b29d614 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,7 +9,7 @@ on: # yamllint disable-line rule:truthy workflow_dispatch: env: - DEFAULT_PYTHON: "3.10" + DEFAULT_PYTHON: "3.11" jobs: pytest: @@ -18,7 +18,6 @@ jobs: strategy: matrix: python: - - "3.10" - "3.11" - "3.12" - "3.13" diff --git a/.github/workflows/typing.yaml b/.github/workflows/typing.yaml index e345c27..e43c37f 100644 --- a/.github/workflows/typing.yaml +++ b/.github/workflows/typing.yaml @@ -9,7 +9,7 @@ on: # yamllint disable-line rule:truthy workflow_dispatch: env: - DEFAULT_PYTHON: "3.10" + DEFAULT_PYTHON: "3.11" jobs: mypy: diff --git a/README.md b/README.md index ac083d5..5966d8b 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ manager. You need at least: -- Python 3.10+ +- Python 3.11+ - [Poetry][poetry-install] Install all packages, including all development requirements: diff --git a/poetry.lock b/poetry.lock index 32e09d4..25bafbb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -186,7 +186,6 @@ files = [ [package.dependencies] aiohappyeyeballs = ">=2.5.0" aiosignal = ">=1.4.0" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<8.0" @@ -253,7 +252,6 @@ files = [ ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} @@ -278,7 +276,7 @@ files = [ [package.dependencies] aiohttp = [ {version = ">=3.7.0", markers = "python_version >= \"3.10\" and python_version < \"3.12\""}, - {version = ">=3.7.0,<3.8 || >=3.9.dev0", markers = "python_version >= \"3.12\""}, + {version = ">=3.7.0,<3.8.dev0 || >=3.9.dev0", markers = "python_version >= \"3.12\""}, ] pytest-asyncio = {version = ">=0.17.0", markers = "python_version >= \"3.7\""} @@ -361,21 +359,17 @@ files = [ {file = "astroid-4.3.4.tar.gz", hash = "sha256:d515a105722b72098bbe82d430d65e635f742b6cbac3bdfaf8b7c188b87c5e39"}, ] -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - [[package]] name = "async-timeout" version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] -markers = {dev = "python_version == \"3.10\""} [[package]] name = "attrs" @@ -412,19 +406,6 @@ files = [ [package.dependencies] cryptography = "*" -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - [[package]] name = "black" version = "26.10.1" @@ -469,8 +450,6 @@ packaging = ">=22.0" pathspec = ">=1.0.0" platformdirs = ">=2" pytokens = ">=0.4.0,<0.5.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -788,9 +767,6 @@ files = [ {file = "coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] @@ -886,7 +862,6 @@ files = [ [package.dependencies] packaging = "*" -tomli = {version = "*", markers = "python_version < \"3.11\""} [package.extras] all = ["pipenv", "poetry", "pyyaml"] @@ -894,22 +869,6 @@ conda = ["pyyaml"] pipenv = ["pipenv"] poetry = ["poetry"] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "filelock" version = "3.16.1" @@ -1580,9 +1539,6 @@ files = [ {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - [[package]] name = "mypy" version = "2.4.0" @@ -1650,7 +1606,6 @@ ast-serialize = ">=0.11.2,<1.0.0" librt = {version = ">=0.16.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = [ {version = ">=4.6.0", markers = "python_version < \"3.15\""}, {version = ">=4.14.0", markers = "python_version >= \"3.15\""}, @@ -1809,7 +1764,6 @@ files = [ [package.dependencies] "ruamel.yaml" = ">=0.15" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "propcache" @@ -2119,14 +2073,12 @@ files = [ astroid = ">=4.3.4,<=4.4" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, ] -isort = ">=5,!=5.13,<10" +isort = ">=5,<5.13 || >5.13,<10" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2" -tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" [package.extras] @@ -2147,12 +2099,10 @@ files = [ [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} iniconfig = ">=1.0.1" packaging = ">=22" pluggy = ">=1.5,<2" pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] @@ -2170,7 +2120,6 @@ files = [ ] [package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} pytest = ">=8.4,<10" typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} @@ -2478,7 +2427,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -2509,7 +2457,7 @@ description = "C version of reader, parser and emitter for ruamel.yaml derived f optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.12\" and platform_python_implementation == \"CPython\"" +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, @@ -2614,7 +2562,6 @@ pydantic = ">=2.6.0" ruamel-yaml = ">=0.17.21" safety-schemas = "0.0.16" tenacity = ">=8.1.0" -tomli = {version = "*", markers = "python_version < \"3.11\""} tomlkit = "*" truststore = {version = ">=0.10.4", markers = "python_version >= \"3.10\""} typer = ">=0.16.0,<0.26.0" @@ -2696,19 +2643,6 @@ files = [ doc = ["reno", "sphinx"] test = ["pytest", "tornado (>=4.5)", "typeguard"] -[[package]] -name = "tomli" -version = "2.1.0" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, - {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, -] - [[package]] name = "tomlkit" version = "0.13.2" @@ -2796,7 +2730,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {main = "python_version <= \"3.12\""} +markers = {main = "python_version < \"3.13\""} [[package]] name = "typing-inspection" @@ -3020,5 +2954,5 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" -python-versions = "^3.10" -content-hash = "5692d55a8f675d4fa0842af6b47fd87a0032adc4568e95f191d166f87539377b" +python-versions = "^3.11" +content-hash = "2b81c6fe0a6ab26f210ebd9c7bf6fe7fa53dd73a171aacf0f3f27e9bad6972ac" diff --git a/pyproject.toml b/pyproject.toml index 1869cc4..625ce92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -31,7 +30,7 @@ example = "examples.energy:start" aiohttp = ">=3.0.0" aiozoneinfo = "0.*" async-timeout = ">=4.0.3,<6.0.0" -python = "^3.10" +python = "^3.11" yarl = ">=1.6.0" [tool.poetry.group.dev.dependencies] @@ -139,7 +138,7 @@ addopts = "--cov" asyncio_mode = "auto" [tool.ruff] -target-version = "py310" +target-version = "py311" [tool.ruff.lint] select = ["ALL"] From 8cabcc39cdd8c87fec63bcf6cf3763bbd82ba66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Thu, 22 Oct 2026 19:30:09 +0300 Subject: [PATCH 2/2] Fix ruff --- spothinta_api/models.py | 6 +++--- spothinta_api/spothinta.py | 5 ++--- tests/test_models.py | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/spothinta_api/models.py b/spothinta_api/models.py index 6bdc952..df680fa 100644 --- a/spothinta_api/models.py +++ b/spothinta_api/models.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING, Any if TYPE_CHECKING: @@ -274,7 +274,7 @@ def prices_today(self) -> dict[datetime, float]: # not 24, due to the shifted/repeated hour. We count UTC hours that # correspond to the local date to handle DST correctly. day_start = datetime.combine(today, datetime.min.time(), tzinfo=self.time_zone) - day_start_utc = day_start.astimezone(timezone.utc) + day_start_utc = day_start.astimezone(UTC) # Count UTC hours that fall within this local date hour_count = 0 @@ -317,7 +317,7 @@ def prices_tomorrow(self) -> dict[datetime, float]: datetime.min.time(), tzinfo=self.time_zone, ) - day_start_utc = day_start.astimezone(timezone.utc) + day_start_utc = day_start.astimezone(UTC) # Count UTC hours that fall within this local date hour_count = 0 diff --git a/spothinta_api/spothinta.py b/spothinta_api/spothinta.py index dd6f35d..90ae6e0 100644 --- a/spothinta_api/spothinta.py +++ b/spothinta_api/spothinta.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import socket from dataclasses import dataclass from datetime import timedelta @@ -27,7 +26,7 @@ from .models import Electricity if TYPE_CHECKING: - from typing_extensions import Self + from typing import Self VERSION = metadata.version(__package__) @@ -94,7 +93,7 @@ async def _request( ssl=True, ) response.raise_for_status() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: msg = "Timeout occurred while connecting to the API." raise SpotHintaConnectionError( msg, diff --git a/tests/test_models.py b/tests/test_models.py index 133cc88..533dec8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,6 @@ """Test the models.""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from zoneinfo import ZoneInfo import pytest @@ -45,7 +45,7 @@ async def test_model(aresponses: ResponsesMockServer) -> None: assert energy.current_price == 0.062 assert energy.intervals_priced_equal_or_lower == 1 # The price for another interval - another_interval = datetime(2023, 5, 6, 17, 0, tzinfo=timezone.utc) + another_interval = datetime(2023, 5, 6, 17, 0, tzinfo=UTC) assert energy.price_at_time(another_interval) == 0.1055 assert energy.lowest_price_time_today == datetime.strptime( "2023-05-06 15:00:00+03:00", @@ -95,7 +95,7 @@ async def test_model_15_minute_resolution(aresponses: ResponsesMockServer) -> No assert energy.current_price == 0.00157 assert energy.intervals_priced_equal_or_lower == 63 # The price for another interval - another_interval = datetime(2025, 10, 4, 17, 0, tzinfo=timezone.utc) + another_interval = datetime(2025, 10, 4, 17, 0, tzinfo=UTC) assert energy.price_at_time(another_interval) == 0.0033 assert energy.lowest_price_time_today == datetime.strptime( "2025-10-04 5:00:00+03:00", @@ -146,7 +146,7 @@ async def test_model_se1_15_minute_resolution(aresponses: ResponsesMockServer) - assert energy.current_price == 0.00128 assert energy.intervals_priced_equal_or_lower == 51 # The price for another interval - another_interval = datetime(2025, 10, 4, 18, 0, tzinfo=timezone.utc) + another_interval = datetime(2025, 10, 4, 18, 0, tzinfo=UTC) assert energy.price_at_time(another_interval) == 0.00242 assert energy.lowest_price_time_today == datetime.strptime( "2025-10-04 03:00:00+00:00", @@ -170,7 +170,7 @@ async def test_model_se1_15_minute_resolution(aresponses: ResponsesMockServer) - @pytest.mark.freeze_time("2025-10-04 16:00:00+02:00") async def test_prices_today_uses_region_local_date_for_utc_timestamps() -> None: """Use local date boundaries for SE1 even when source timestamps are UTC.""" - start_utc = datetime(2025, 10, 3, 22, 0, tzinfo=timezone.utc) + start_utc = datetime(2025, 10, 3, 22, 0, tzinfo=UTC) prices = {start_utc + i * timedelta(minutes=15): i / 100000 for i in range(96)} energy = Electricity( prices=prices, @@ -214,7 +214,7 @@ async def test_model_no_prices_for_tomorrow(aresponses: ResponsesMockServer) -> assert energy.current_price == 0.062 assert energy.intervals_priced_equal_or_lower == 1 # The price for another interval - another_interval = datetime(2023, 5, 6, 17, 0, tzinfo=timezone.utc) + another_interval = datetime(2023, 5, 6, 17, 0, tzinfo=UTC) assert energy.price_at_time(another_interval) == 0.1055 assert energy.lowest_price_time_today == datetime.strptime( "2023-05-06 15:00:00+03:00", @@ -296,7 +296,7 @@ async def test_only_data_for_tomorrow(aresponses: ResponsesMockServer) -> None: assert energy.lowest_price_today is None assert energy.intervals_priced_equal_or_lower == 0 # The price for another interval - another_interval = datetime(2023, 5, 6, 17, 0, tzinfo=timezone.utc) + another_interval = datetime(2023, 5, 6, 17, 0, tzinfo=UTC) assert energy.price_at_time(another_interval) is None assert energy.lowest_price_time_today is None assert energy.highest_price_time_today is None @@ -337,7 +337,7 @@ async def test_model_15_minute_resolution_partial_data_tomorrow( assert energy.current_price == 0.00626 assert energy.intervals_priced_equal_or_lower == 11 # The price for another interval - another_interval = datetime(2026, 4, 1, 20, 16, tzinfo=timezone.utc) + another_interval = datetime(2026, 4, 1, 20, 16, tzinfo=UTC) assert energy.price_at_time(another_interval) == 0.01832 assert energy.lowest_price_time_today == datetime.strptime( "2026-04-01 15:00:00+03:00", @@ -383,7 +383,7 @@ async def test_model_15_minute_resolution_partial_data_today( assert energy.current_price is None assert energy.intervals_priced_equal_or_lower == 0 # The price for another interval - another_interval = datetime(2026, 4, 2, 20, 16, tzinfo=timezone.utc) + another_interval = datetime(2026, 4, 2, 20, 16, tzinfo=UTC) assert energy.price_at_time(another_interval) is None assert energy.lowest_price_time_today is None assert energy.lowest_price_time_tomorrow is None @@ -517,7 +517,7 @@ async def test_model_60_minute_resolution_non_interval_start( # Should return the price for 15:00 (0.062) assert energy.current_price == 0.062 # Also test price_at_time at a non-interval-start time - moment_15_30 = datetime(2023, 5, 6, 12, 30, tzinfo=timezone.utc) + moment_15_30 = datetime(2023, 5, 6, 12, 30, tzinfo=UTC) assert energy.price_at_time(moment_15_30) == 0.062