diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f017b502..a50d0591e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,8 @@ on: - 'gh-readonly-queue/**' workflow_dispatch: +env: + DEFAULT_PYTHON_VERSION: '3.11' jobs: test: @@ -20,9 +22,6 @@ jobs: os: - ubuntu-20.04 python: - - "3.8" - - "3.9" - - "3.10" - "3.11" runs-on: ${{ matrix.os }} @@ -34,6 +33,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + cache: 'pip' - uses: actions/cache@v3 with: @@ -60,7 +60,8 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: '3.11' + cache: 'pip' - name: Install build dependencies run: | @@ -91,6 +92,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + cache: 'pip' - name: Install build dependencies run: | @@ -168,6 +170,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + cache: 'pip' - name: Install build dependencies if: steps.mike-metadata.outputs.version diff --git a/README.md b/README.md index d55dd8cd0..ff016e1c7 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ A development kit to interact with the Frequenz development platform. ## Supported Python versions -* For x86_64 Python 3.8 - 3.11 are supported (tested). -* For arm64 only Python 3.8 is supported (due to some dependencies that only support 3.8). +* Only Python 3.11 is fully supported (tested). ## Contributing diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d07cf4ec2..4fee8cb94 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,16 +2,20 @@ ## Summary - +This release drops support for Python versions older than 3.11. ## Upgrading +* Now Python 3.11 is the minimum supported version. All users must upgrade to Python 3.11 (including virtual environments used for development). + * Now `float` is used everywhere for representing power (before power metrics were `float` but setting power was done using `int`). * `frequenz.sdk.actor.power_distributing`: the `power` attribute of the `Request` class has been updated from `int` to a `float`. * `frequenz.sdk.microgrid`: the `set_power()` method of both the `MicrogridApiClient` and `MicrogridGrpcClient` classes now expect a `float` value for the `power_w` parameter instead of `int`. * The `LogicalMeter` no longer takes a `component_graph` parameter. +* Now `frequenz.sdk.timeseries.Sample` uses a more sensible comparison. Before this release `Sample`s were compared only based on the `timestamp`. This was due to a limitation in Python versions earlier than 3.10. Now that the minimum supported version is 3.11 this hack is not needed anymore and `Sample`s are compared using both `timestamp` and `value` as most people probably expects. + ## New Features diff --git a/pyproject.toml b/pyproject.toml index f001a4c32..7d34521e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [build-system] requires = [ - "setuptools >= 65.3.0, < 66", - "setuptools_scm[toml] >= 7.0.5, < 8", - "wheel" + "setuptools >= 65.3.0, < 66", + "setuptools_scm[toml] >= 7.0.5, < 8", + "wheel", ] build-backend = "setuptools.build_meta" @@ -11,87 +11,76 @@ name = "frequenz-sdk" description = "Frequenz Python SDK" readme = "README.md" license = { text = "MIT" } -keywords = [ "frequenz", "sdk", "microgrid", "actor" ] +keywords = ["frequenz", "sdk", "microgrid", "actor"] classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Topic :: Software Development :: Libraries", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries", ] -requires-python = ">= 3.8, < 4" +requires-python = ">= 3.11, < 4" dependencies = [ - "frequenz-api-microgrid >= 0.11.0, < 0.12.0", - "frequenz-channels >= 0.14.0, < 0.15.0", - "google-api-python-client >= 2.71, < 3", - "grpcio >= 1.51.1, < 2", - "grpcio-tools >= 1.51.1, < 2", - "networkx >= 2.8, < 4", - "numpy >= 1.24.2, < 2", - "protobuf >= 4.21.6, < 5", - "pydantic >= 1.9", - "toml >= 0.10", - "tqdm >= 4.38.0, < 5", - "typing_extensions >= 4.4.0, < 5", - "watchfiles >= 0.15.0", + "frequenz-api-microgrid >= 0.11.0, < 0.12.0", + "frequenz-channels >= 0.14.0, < 0.15.0", + "google-api-python-client >= 2.71, < 3", + "grpcio >= 1.51.1, < 2", + "grpcio-tools >= 1.51.1, < 2", + "networkx >= 2.8, < 4", + "numpy >= 1.24.2, < 2", + "protobuf >= 4.21.6, < 5", + "pydantic >= 1.9", + "toml >= 0.10", + "tqdm >= 4.38.0, < 5", + "typing_extensions >= 4.4.0, < 5", + "watchfiles >= 0.15.0", ] -dynamic = [ "version" ] +dynamic = ["version"] [[project.authors]] -name ="Frequenz Energy-as-a-Service GmbH" +name = "Frequenz Energy-as-a-Service GmbH" email = "floss@frequenz.com" [project.optional-dependencies] docs-gen = [ - "mike == 1.1.2", - "mkdocs-gen-files == 0.5.0", - "mkdocs-literate-nav == 0.6.0", - "mkdocs-material == 9.1.14", - "mkdocs-section-index == 0.3.5", - "mkdocstrings[python] == 0.21.2", + "mike == 1.1.2", + "mkdocs-gen-files == 0.5.0", + "mkdocs-literate-nav == 0.6.0", + "mkdocs-material == 9.1.14", + "mkdocs-section-index == 0.3.5", + "mkdocstrings[python] == 0.21.2", ] docs-lint = [ - "pydocstyle == 6.3.0", - "darglint == 1.8.1", - "tomli == 2.0.1", # Needed by pydocstyle to read pyproject.toml -] -format = [ - "black == 23.3.0", - "isort == 5.12.0", -] -nox = [ - "nox == 2023.4.22", - "toml == 0.10.2", + "pydocstyle == 6.3.0", + "darglint == 1.8.1", + "tomli == 2.0.1", # Needed by pydocstyle to read pyproject.toml ] +format = ["black == 23.3.0", "isort == 5.12.0"] +nox = ["nox == 2023.4.22", "toml == 0.10.2"] pytest = [ - "pytest == 7.3.1", - "pytest-cov == 4.0.0", - "pytest-mock == 3.10.0", - "pytest-asyncio == 0.21.0", - "time-machine == 2.9.0", - "async-solipsism == 0.5", - # For checking docstring code examples - "sybil == 5.0.2", - "pylint == 2.17.4", + "pytest == 7.3.1", + "pytest-cov == 4.0.0", + "pytest-mock == 3.10.0", + "pytest-asyncio == 0.21.0", + "time-machine == 2.9.0", + "async-solipsism == 0.5", + # For checking docstring code examples + "sybil == 5.0.2", + "pylint == 2.17.4", ] mypy = [ - "mypy == 1.3.0", - "grpc-stubs == 1.24.12", # This dependency introduces breaking changes in patch releases - # For checking the noxfile, docs/ script, and tests - "frequenz-sdk[docs-gen,nox,pytest]", + "mypy == 1.3.0", + "grpc-stubs == 1.24.12", # This dependency introduces breaking changes in patch releases + # For checking the noxfile, docs/ script, and tests + "frequenz-sdk[docs-gen,nox,pytest]", ] pylint = [ - "pylint == 2.17.4", - # For checking the noxfile, docs/ script, and tests - "frequenz-sdk[docs-gen,nox,pytest]", -] -dev = [ - "frequenz-sdk[docs-gen,docs-lint,format,nox,pytest,mypy,pylint]", + "pylint == 2.17.4", + # For checking the noxfile, docs/ script, and tests + "frequenz-sdk[docs-gen,nox,pytest]", ] +dev = ["frequenz-sdk[docs-gen,docs-lint,format,nox,pytest,mypy,pylint]"] [project.urls] Changelog = "https://github.com/frequenz-floss/frequenz-sdk-python/releases" @@ -111,21 +100,21 @@ target-version = ['py38'] include = '\.pyi?$' [tool.pylint.similarities] -ignore-comments=['yes'] -ignore-docstrings=['yes'] -ignore-imports=['no'] -min-similarity-lines=40 +ignore-comments = ['yes'] +ignore-docstrings = ['yes'] +ignore-imports = ['no'] +min-similarity-lines = 40 [tool.pylint.messages_control] disable = [ - "too-few-public-methods", - # disabled because it conflicts with isort - "wrong-import-order", - "ungrouped-imports" + "too-few-public-methods", + # disabled because it conflicts with isort + "wrong-import-order", + "ungrouped-imports", ] [tool.pylint.design] -max-attributes=12 +max-attributes = 12 [tool.isort] profile = "black" @@ -134,21 +123,18 @@ src_paths = ["src", "examples", "tests"] [tool.pytest.ini_options] asyncio_mode = "auto" -required_plugins = [ "pytest-asyncio", "pytest-mock" ] +required_plugins = ["pytest-asyncio", "pytest-mock"] [[tool.mypy.overrides]] module = [ - "grpc.aio", - "grpc.aio.*", - # There is a stubs package available, but it's not working: - # https://github.com/eggplants/networkx-stubs/issues/1 - "networkx", + "grpc.aio", + "grpc.aio.*", + # There is a stubs package available, but it's not working: + # https://github.com/eggplants/networkx-stubs/issues/1 + "networkx", ] ignore_missing_imports = true [[tool.mypy.overrides]] -module = [ - "async_solipsism", - "async_solipsism.*", -] +module = ["async_solipsism", "async_solipsism.*"] ignore_missing_imports = true diff --git a/src/frequenz/sdk/timeseries/_base_types.py b/src/frequenz/sdk/timeseries/_base_types.py index c5cc8d2a6..46f0e7c4e 100644 --- a/src/frequenz/sdk/timeseries/_base_types.py +++ b/src/frequenz/sdk/timeseries/_base_types.py @@ -3,21 +3,15 @@ """Timeseries basic types.""" -from __future__ import annotations - import functools -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime, timezone -from typing import Callable, Iterator, Optional, overload +from typing import Callable, Iterator, Self, overload UNIX_EPOCH = datetime.fromtimestamp(0.0, tz=timezone.utc) """The UNIX epoch (in UTC).""" -# Ordering by timestamp is a bit arbitrary, and it is not always what might be -# wanted. We are using this order now because usually we need to do binary -# searches on sequences of samples, and the Python `bisect` module doesn't -# support providing a key until Python 3.10. @dataclass(frozen=True, order=True) class Sample: """A measurement taken at a particular point in time. @@ -27,10 +21,10 @@ class Sample: coherent view on a group of component metrics for a particular timestamp. """ - timestamp: datetime = field(compare=True) + timestamp: datetime """The time when this sample was generated.""" - value: Optional[float] = field(compare=False, default=None) + value: float | None = None """The value of this sample.""" @@ -46,13 +40,13 @@ class Sample3Phase: timestamp: datetime """The time when this sample was generated.""" - value_p1: Optional[float] + value_p1: float | None """The value of the 1st phase in this sample.""" - value_p2: Optional[float] + value_p2: float | None """The value of the 2nd phase in this sample.""" - value_p3: Optional[float] + value_p3: float | None """The value of the 3rd phase in this sample.""" def __iter__(self) -> Iterator[float | None]: @@ -117,7 +111,7 @@ def min(self, default: float | None = None) -> float | None: def map( self, function: Callable[[float], float], default: float | None = None - ) -> Sample3Phase: + ) -> Self: """Apply the given function on each of the phase values and return the result. If a phase value is `None`, replace it with `default` instead. @@ -127,10 +121,10 @@ def map( default: The value to apply if a phase value is `None`. Returns: - A new `Sample3Phase` instance, with the given function applied on values - for each of the phases. + A new instance, with the given function applied on values for each of the + phases. """ - return Sample3Phase( + return self.__class__( timestamp=self.timestamp, value_p1=default if self.value_p1 is None else function(self.value_p1), value_p2=default if self.value_p2 is None else function(self.value_p2), diff --git a/src/frequenz/sdk/timeseries/_resampling.py b/src/frequenz/sdk/timeseries/_resampling.py index 24646395c..a12e9e658 100644 --- a/src/frequenz/sdk/timeseries/_resampling.py +++ b/src/frequenz/sdk/timeseries/_resampling.py @@ -705,11 +705,12 @@ def resample(self, timestamp: datetime) -> Sample: ) minimum_relevant_timestamp = timestamp - period * conf.max_data_age_in_periods - # We need to pass a dummy Sample to bisect because it only support - # specifying a key extraction function in Python 3.10, so we need to - # compare samples at the moment. - min_index = bisect(self._buffer, Sample(minimum_relevant_timestamp, None)) - max_index = bisect(self._buffer, Sample(timestamp, None)) + min_index = bisect( + self._buffer, + minimum_relevant_timestamp, + key=lambda s: s.timestamp, + ) + max_index = bisect(self._buffer, timestamp, key=lambda s: s.timestamp) # Using itertools for slicing doesn't look very efficient, but # experiments with a custom (ring) buffer that can slice showed that # it is not that bad. See: