Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
- 'gh-readonly-queue/**'
workflow_dispatch:

env:
DEFAULT_PYTHON_VERSION: '3.11'

jobs:
test:
Expand All @@ -20,9 +22,6 @@ jobs:
os:
- ubuntu-20.04
python:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
runs-on: ${{ matrix.os }}

Expand All @@ -34,6 +33,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
cache: 'pip'

- uses: actions/cache@v3
with:
Expand All @@ -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: |
Expand Down Expand Up @@ -91,6 +92,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
cache: 'pip'

- name: Install build dependencies
run: |
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

## Summary

<!-- Here goes a general summary of what this release is about -->
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

<!-- Here goes the main new features and examples or instructions on how to use them -->
Expand Down
154 changes: 70 additions & 84 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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
28 changes: 11 additions & 17 deletions src/frequenz/sdk/timeseries/_base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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."""


Expand All @@ -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]:
Expand Down Expand Up @@ -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.
Expand All @@ -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),
Expand Down
11 changes: 6 additions & 5 deletions src/frequenz/sdk/timeseries/_resampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down