frequenz-core 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2024 Frequenz Energy-as-a-Service GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ exclude .cookiecutter-replay.json
2
+ exclude .editorconfig
3
+ exclude .gitignore
4
+ exclude CODEOWNERS
5
+ exclude CONTRIBUTING.md
6
+ exclude mkdocs.yml
7
+ exclude noxfile.py
8
+ exclude src/conftest.py
9
+ recursive-exclude .github *
10
+ recursive-exclude benchmarks *
11
+ recursive-exclude docs *
12
+ recursive-exclude tests *
13
+ recursive-include py *.pyi
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: frequenz-core
3
+ Version: 1.0.0
4
+ Summary: Core utilities to complement Python's standard library
5
+ Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
6
+ License: MIT
7
+ Project-URL: Documentation, https://frequenz-floss.github.io/frequenz-core-python/
8
+ Project-URL: Changelog, https://github.com/frequenz-floss/frequenz-core-python/releases
9
+ Project-URL: Issues, https://github.com/frequenz-floss/frequenz-core-python/issues
10
+ Project-URL: Repository, https://github.com/frequenz-floss/frequenz-core-python
11
+ Project-URL: Support, https://github.com/frequenz-floss/frequenz-core-python/discussions/categories/support
12
+ Keywords: asyncio,collections,core,datetime,frequenz,lib,library,math,python,stdlib,typing
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: <4,>=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: typing-extensions<5,>=4.13.0
24
+ Provides-Extra: dev-flake8
25
+ Requires-Dist: flake8==7.2.0; extra == "dev-flake8"
26
+ Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
27
+ Requires-Dist: flake8-pyproject==1.2.3; extra == "dev-flake8"
28
+ Requires-Dist: pydoclint==0.6.6; extra == "dev-flake8"
29
+ Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
30
+ Provides-Extra: dev-formatting
31
+ Requires-Dist: black==25.1.0; extra == "dev-formatting"
32
+ Requires-Dist: isort==6.0.1; extra == "dev-formatting"
33
+ Provides-Extra: dev-mkdocs
34
+ Requires-Dist: Markdown==3.8; extra == "dev-mkdocs"
35
+ Requires-Dist: black==25.1.0; extra == "dev-mkdocs"
36
+ Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
37
+ Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
38
+ Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
39
+ Requires-Dist: mkdocs-macros-plugin==1.3.7; extra == "dev-mkdocs"
40
+ Requires-Dist: mkdocs-material==9.6.14; extra == "dev-mkdocs"
41
+ Requires-Dist: mkdocstrings[python]==0.29.1; extra == "dev-mkdocs"
42
+ Requires-Dist: mkdocstrings-python==1.16.11; extra == "dev-mkdocs"
43
+ Requires-Dist: frequenz-repo-config[lib]==0.13.4; extra == "dev-mkdocs"
44
+ Provides-Extra: dev-mypy
45
+ Requires-Dist: mypy==1.16.0; extra == "dev-mypy"
46
+ Requires-Dist: types-Markdown==3.8.0.20250415; extra == "dev-mypy"
47
+ Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
48
+ Provides-Extra: dev-noxfile
49
+ Requires-Dist: nox==2025.5.1; extra == "dev-noxfile"
50
+ Requires-Dist: frequenz-repo-config[lib]==0.13.4; extra == "dev-noxfile"
51
+ Provides-Extra: dev-pylint
52
+ Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
53
+ Provides-Extra: dev-pytest
54
+ Requires-Dist: pytest==8.3.5; extra == "dev-pytest"
55
+ Requires-Dist: pylint==3.3.7; extra == "dev-pytest"
56
+ Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.13.4; extra == "dev-pytest"
57
+ Requires-Dist: pytest-mock==3.14.1; extra == "dev-pytest"
58
+ Requires-Dist: pytest-asyncio==0.26.0; extra == "dev-pytest"
59
+ Requires-Dist: async-solipsism==0.7; extra == "dev-pytest"
60
+ Requires-Dist: hypothesis==6.132.0; extra == "dev-pytest"
61
+ Provides-Extra: dev
62
+ Requires-Dist: frequenz-core[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
63
+ Dynamic: license-file
64
+
65
+ # Frequenz Core Library
66
+
67
+ [![Build Status](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml/badge.svg)](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml)
68
+ [![PyPI Package](https://img.shields.io/pypi/v/frequenz-core)](https://pypi.org/project/frequenz-core/)
69
+ [![Docs](https://img.shields.io/badge/docs-latest-informational)](https://frequenz-floss.github.io/frequenz-core-python/)
70
+
71
+ ## Introduction
72
+
73
+ Core utilities to complement Python's standard library.
74
+
75
+ ## Documentation
76
+
77
+ For information on how to use this library, please refer to the
78
+ [documentation](https://frequenz-floss.github.io/frequenz-core-python/).
79
+
80
+ ## Supported Platforms
81
+
82
+ The following platforms are officially supported (tested):
83
+
84
+ - **Python:** 3.11
85
+ - **Operating System:** Ubuntu Linux 20.04
86
+ - **Architectures:** amd64, arm64
87
+
88
+ ## Contributing
89
+
90
+ If you want to know how to build this project and contribute to it, please
91
+ check out the [Contributing Guide](CONTRIBUTING.md).
@@ -0,0 +1,27 @@
1
+ # Frequenz Core Library
2
+
3
+ [![Build Status](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml/badge.svg)](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml)
4
+ [![PyPI Package](https://img.shields.io/pypi/v/frequenz-core)](https://pypi.org/project/frequenz-core/)
5
+ [![Docs](https://img.shields.io/badge/docs-latest-informational)](https://frequenz-floss.github.io/frequenz-core-python/)
6
+
7
+ ## Introduction
8
+
9
+ Core utilities to complement Python's standard library.
10
+
11
+ ## Documentation
12
+
13
+ For information on how to use this library, please refer to the
14
+ [documentation](https://frequenz-floss.github.io/frequenz-core-python/).
15
+
16
+ ## Supported Platforms
17
+
18
+ The following platforms are officially supported (tested):
19
+
20
+ - **Python:** 3.11
21
+ - **Operating System:** Ubuntu Linux 20.04
22
+ - **Architectures:** amd64, arm64
23
+
24
+ ## Contributing
25
+
26
+ If you want to know how to build this project and contribute to it, please
27
+ check out the [Contributing Guide](CONTRIBUTING.md).
@@ -0,0 +1,14 @@
1
+ # Frequenz Core Library Release Notes
2
+
3
+ ## Summary
4
+
5
+ This is the initial release of the Frequenz Core Library, which provides a set of fundamental tools and utilities for Python.
6
+
7
+ The library currently includes:
8
+
9
+ - `datetime`: For utilities related to dates and times.
10
+ * `id`: For creating unique system-wide ID types.
11
+ * `math`: For utilities related to math.
12
+ * `typing`: For type annotations and type-checking utilities.
13
+
14
+ But more tools will be added in the future.
@@ -0,0 +1,181 @@
1
+ # License: MIT
2
+ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
+
4
+ [build-system]
5
+ requires = [
6
+ "setuptools == 80.9.0",
7
+ "setuptools_scm[toml] == 8.3.1",
8
+ "frequenz-repo-config[lib] == 0.13.4",
9
+ ]
10
+ build-backend = "setuptools.build_meta"
11
+
12
+ [project]
13
+ name = "frequenz-core"
14
+ description = "Core utilities to complement Python's standard library"
15
+ readme = "README.md"
16
+ license = { text = "MIT" }
17
+ keywords = [
18
+ "asyncio",
19
+ "collections",
20
+ "core",
21
+ "datetime",
22
+ "frequenz",
23
+ "lib",
24
+ "library",
25
+ "math",
26
+ "python",
27
+ "stdlib",
28
+ "typing",
29
+ ]
30
+ classifiers = [
31
+ "Development Status :: 5 - Production/Stable",
32
+ "Intended Audience :: Developers",
33
+ "License :: OSI Approved :: MIT License",
34
+ "Programming Language :: Python :: 3",
35
+ "Programming Language :: Python :: 3 :: Only",
36
+ "Topic :: Software Development :: Libraries",
37
+ "Typing :: Typed",
38
+ ]
39
+ requires-python = ">= 3.11, < 4"
40
+ dependencies = ["typing-extensions >= 4.13.0, < 5"]
41
+ dynamic = ["version"]
42
+
43
+ [[project.authors]]
44
+ name = "Frequenz Energy-as-a-Service GmbH"
45
+ email = "floss@frequenz.com"
46
+
47
+ [project.optional-dependencies]
48
+ dev-flake8 = [
49
+ "flake8 == 7.2.0",
50
+ "flake8-docstrings == 1.7.0",
51
+ "flake8-pyproject == 1.2.3", # For reading the flake8 config from pyproject.toml
52
+ "pydoclint == 0.6.6",
53
+ "pydocstyle == 6.3.0",
54
+ ]
55
+ dev-formatting = ["black == 25.1.0", "isort == 6.0.1"]
56
+ dev-mkdocs = [
57
+ "Markdown == 3.8",
58
+ "black == 25.1.0",
59
+ "mike == 2.1.3",
60
+ "mkdocs-gen-files == 0.5.0",
61
+ "mkdocs-literate-nav == 0.6.2",
62
+ "mkdocs-macros-plugin == 1.3.7",
63
+ "mkdocs-material == 9.6.14",
64
+ "mkdocstrings[python] == 0.29.1",
65
+ "mkdocstrings-python == 1.16.11",
66
+ "frequenz-repo-config[lib] == 0.13.4",
67
+ ]
68
+ dev-mypy = [
69
+ "mypy == 1.16.0",
70
+ "types-Markdown == 3.8.0.20250415",
71
+ # For checking the noxfile, docs/ script, and tests
72
+ "frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]",
73
+ ]
74
+ dev-noxfile = ["nox == 2025.5.1", "frequenz-repo-config[lib] == 0.13.4"]
75
+ dev-pylint = [
76
+ # dev-pytest already defines a dependency to pylint because of the examples
77
+ # For checking the noxfile, docs/ script, and tests
78
+ "frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]",
79
+ ]
80
+ dev-pytest = [
81
+ "pytest == 8.3.5",
82
+ "pylint == 3.3.7", # We need this to check for the examples
83
+ "frequenz-repo-config[extra-lint-examples] == 0.13.4",
84
+ "pytest-mock == 3.14.1",
85
+ "pytest-asyncio == 0.26.0",
86
+ "async-solipsism == 0.7",
87
+ "hypothesis == 6.132.0",
88
+ ]
89
+ dev = [
90
+ "frequenz-core[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
91
+ ]
92
+
93
+ [project.urls]
94
+ Documentation = "https://frequenz-floss.github.io/frequenz-core-python/"
95
+ Changelog = "https://github.com/frequenz-floss/frequenz-core-python/releases"
96
+ Issues = "https://github.com/frequenz-floss/frequenz-core-python/issues"
97
+ Repository = "https://github.com/frequenz-floss/frequenz-core-python"
98
+ Support = "https://github.com/frequenz-floss/frequenz-core-python/discussions/categories/support"
99
+
100
+ [tool.black]
101
+ line-length = 88
102
+ target-version = ['py311']
103
+ include = '\.pyi?$'
104
+
105
+ [tool.isort]
106
+ profile = "black"
107
+ line_length = 88
108
+ src_paths = ["benchmarks", "examples", "src", "tests"]
109
+
110
+ [tool.flake8]
111
+ # We give some flexibility to go over 88, there are cases like long URLs or
112
+ # code in documenation that have extra indentation. Black will still take care
113
+ # of making everything that can be 88 wide, 88 wide.
114
+ max-line-length = 100
115
+ extend-ignore = [
116
+ "E203", # Whitespace before ':' (conflicts with black)
117
+ "W503", # Line break before binary operator (conflicts with black)
118
+ ]
119
+ # pydoclint options
120
+ style = "google"
121
+ check-return-types = false
122
+ check-yield-types = false
123
+ arg-type-hints-in-docstring = false
124
+ arg-type-hints-in-signature = true
125
+ allow-init-docstring = true
126
+
127
+ [tool.pylint.similarities]
128
+ ignore-comments = ['yes']
129
+ ignore-docstrings = ['yes']
130
+ ignore-imports = ['no']
131
+ min-similarity-lines = 40
132
+
133
+ [tool.pylint.messages_control]
134
+ disable = [
135
+ "too-few-public-methods",
136
+ "too-many-return-statements",
137
+ # disabled because it conflicts with isort
138
+ "wrong-import-order",
139
+ "ungrouped-imports",
140
+ # pylint's unsubscriptable check is buggy and is not needed because
141
+ # it is a type-check, for which we already have mypy.
142
+ "unsubscriptable-object",
143
+ # Checked by flake8
144
+ "f-string-without-interpolation",
145
+ "line-too-long",
146
+ "missing-function-docstring",
147
+ "redefined-outer-name",
148
+ "unnecessary-lambda-assignment",
149
+ "unused-import",
150
+ "unused-variable",
151
+ # Checked by mypy
152
+ "no-member",
153
+ "possibly-used-before-assignment",
154
+ "no-name-in-module",
155
+ ]
156
+
157
+ [tool.pytest.ini_options]
158
+ addopts = "-W=all -Werror -Wdefault::DeprecationWarning -Wdefault::PendingDeprecationWarning -vv"
159
+ testpaths = ["tests", "src"]
160
+ asyncio_mode = "auto"
161
+ asyncio_default_fixture_loop_scope = "function"
162
+ required_plugins = ["pytest-asyncio", "pytest-mock"]
163
+
164
+ [tool.mypy]
165
+ explicit_package_bases = true
166
+ namespace_packages = true
167
+ # This option disables mypy cache, and it is sometimes useful to enable it if
168
+ # you are getting weird intermittent error, or error in the CI but not locally
169
+ # (or vice versa). In particular errors saying that type: ignore is not
170
+ # used but getting the original ignored error when removing the type: ignore.
171
+ # See for example: https://github.com/python/mypy/issues/2960
172
+ #no_incremental = true
173
+ packages = ["frequenz.core"]
174
+ strict = true
175
+
176
+ [[tool.mypy.overrides]]
177
+ module = ["mkdocs_macros.*", "sybil", "sybil.*", "async_solipsism"]
178
+ ignore_missing_imports = true
179
+
180
+ [tool.setuptools_scm]
181
+ version_scheme = "post-release"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ # License: MIT
2
+ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Core utilities to complement Python's standard library."""
@@ -0,0 +1,13 @@
1
+ # License: MIT
2
+ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Validate docstring code examples.
5
+
6
+ Code examples are often wrapped in triple backticks (```) within docstrings.
7
+ This plugin extracts these code examples and validates them using pylint.
8
+ """
9
+
10
+ from frequenz.repo.config.pytest import examples
11
+ from sybil import Sybil
12
+
13
+ pytest_collect_file = Sybil(**examples.get_sybil_arguments()).pytest()
@@ -0,0 +1,10 @@
1
+ # License: MIT
2
+ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Timeseries basic types."""
5
+
6
+ from datetime import datetime, timezone
7
+ from typing import Final
8
+
9
+ UNIX_EPOCH: Final[datetime] = datetime.fromtimestamp(0.0, tz=timezone.utc)
10
+ """The UNIX epoch (in UTC)."""
@@ -0,0 +1,194 @@
1
+ # License: MIT
2
+ # Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3
+
4
+ r'''Provides strongly-typed unique identifiers for entities.
5
+
6
+ This module offers a base class, [`BaseId`][frequenz.core.id.BaseId], which can be
7
+ subclassed to create distinct ID types for different components or concepts within
8
+ a system.
9
+
10
+ These IDs ensure type safety, meaning that an ID for one type of entity (e.g., a
11
+ sensor) cannot be mistakenly used where an ID for another type (e.g., a
12
+ microgrid) is expected.
13
+
14
+ # Creating Custom ID Types
15
+
16
+ To define a new ID type, create a class that inherits from
17
+ [`BaseId`][frequenz.core.id.BaseId] and provide a unique `str_prefix` as a keyword
18
+ argument in the class definition. This prefix is used in the string representation of
19
+ the ID and must be unique across all ID types.
20
+
21
+ Note:
22
+ The `str_prefix` must be unique across all ID types. If you try to use a
23
+ prefix that is already registered, a `ValueError` will be raised when defining
24
+ the class.
25
+
26
+ To encourage consistency, the class name must end with the suffix "Id" (e.g.,
27
+ `MyNewId`). This check can be bypassed by passing `allow_custom_name=True` when
28
+ defining the class (e.g., `class MyCustomName(BaseId, str_prefix="MCN",
29
+ allow_custom_name=True):`).
30
+
31
+ Tip:
32
+ Use the [`@typing.final`][typing.final] decorator to prevent subclassing of
33
+ ID classes.
34
+
35
+ Example: Creating a standard ID type
36
+ ```python
37
+ from typing import final
38
+ from frequenz.core.id import BaseId
39
+
40
+ @final
41
+ class InverterId(BaseId, str_prefix="INV"):
42
+ """A unique identifier for an inverter."""
43
+
44
+ inv_id = InverterId(123)
45
+ print(inv_id) # Output: INV123
46
+ print(int(inv_id)) # Output: 123
47
+ ```
48
+
49
+ Example: Creating an ID type with a non-standard name
50
+ ```python
51
+ from typing import final
52
+ from frequenz.core.id import BaseId
53
+
54
+ @final
55
+ class CustomNameForId(BaseId, str_prefix="CST", allow_custom_name=True):
56
+ """An ID with a custom name, not ending in 'Id'."""
57
+
58
+ custom_id = CustomNameForId(456)
59
+ print(custom_id) # Output: CST456
60
+ print(int(custom_id)) # Output: 456
61
+ ```
62
+ '''
63
+
64
+
65
+ from typing import Any, ClassVar, Self, cast
66
+
67
+
68
+ class BaseId:
69
+ """A base class for unique identifiers.
70
+
71
+ Subclasses must provide a unique `str_prefix` keyword argument during
72
+ definition, which is used in the string representation of the ID.
73
+
74
+ By default, subclass names must end with "Id". This can be overridden by
75
+ passing `allow_custom_name=True` during class definition.
76
+
77
+ For more information and examples, see the [module's
78
+ documentation][frequenz.core.id].
79
+ """
80
+
81
+ _id: int
82
+ _str_prefix: ClassVar[str]
83
+ _registered_prefixes: ClassVar[set[str]] = set()
84
+
85
+ def __new__(cls, *_: Any, **__: Any) -> Self:
86
+ """Create a new instance of the ID class, only if it is a subclass of BaseId."""
87
+ if cls is BaseId:
88
+ raise TypeError("BaseId cannot be instantiated directly. Use a subclass.")
89
+ return super().__new__(cls)
90
+
91
+ def __init_subclass__(
92
+ cls,
93
+ *,
94
+ str_prefix: str,
95
+ allow_custom_name: bool = False,
96
+ **kwargs: Any,
97
+ ) -> None:
98
+ """Initialize a subclass, set its string prefix, and perform checks.
99
+
100
+ Args:
101
+ str_prefix: The string prefix for the ID type (e.g., "MID").
102
+ Must be unique across all ID types.
103
+ allow_custom_name: If True, bypasses the check that the class name
104
+ must end with "Id". Defaults to False.
105
+ **kwargs: Forwarded to the parent's __init_subclass__.
106
+
107
+ Raises:
108
+ ValueError: If the `str_prefix` is already registered by another
109
+ ID type.
110
+ TypeError: If `allow_custom_name` is False and the class name
111
+ does not end with "Id".
112
+ """
113
+ super().__init_subclass__(**kwargs)
114
+
115
+ if str_prefix in BaseId._registered_prefixes:
116
+ raise ValueError(
117
+ f"Prefix '{str_prefix}' is already registered. "
118
+ "ID prefixes must be unique."
119
+ )
120
+ BaseId._registered_prefixes.add(str_prefix)
121
+
122
+ if not allow_custom_name and not cls.__name__.endswith("Id"):
123
+ raise TypeError(
124
+ f"Class name '{cls.__name__}' for an ID class must end with 'Id' "
125
+ "(e.g., 'SomeId'), or use `allow_custom_name=True`."
126
+ )
127
+
128
+ cls._str_prefix = str_prefix
129
+
130
+ def __init__(self, id_: int, /) -> None:
131
+ """Initialize this instance.
132
+
133
+ Args:
134
+ id_: The numeric unique identifier.
135
+
136
+ Raises:
137
+ ValueError: If the ID is negative.
138
+ """
139
+ if id_ < 0:
140
+ raise ValueError(f"{type(self).__name__} can't be negative.")
141
+ self._id = id_
142
+
143
+ @property
144
+ def str_prefix(self) -> str:
145
+ """The prefix used for the string representation of this ID."""
146
+ return self._str_prefix
147
+
148
+ def __int__(self) -> int:
149
+ """Return the numeric ID of this instance."""
150
+ return self._id
151
+
152
+ def __eq__(self, other: object) -> bool:
153
+ """Check if this instance is equal to another object.
154
+
155
+ Equality is defined as being of the exact same type and having the same
156
+ underlying ID.
157
+ """
158
+ # pylint thinks this is not an unidiomatic typecheck, but in this case
159
+ # it is not. isinstance() returns True for subclasses, which is not
160
+ # what we want here, as different ID types should never be equal.
161
+ # pylint: disable-next=unidiomatic-typecheck
162
+ if type(other) is not type(self):
163
+ return NotImplemented
164
+ # We already checked type(other) is type(self), but mypy doesn't
165
+ # understand that, so we need to cast it to Self.
166
+ other_id = cast(Self, other)
167
+ return self._id == other_id._id
168
+
169
+ def __lt__(self, other: object) -> bool:
170
+ """Check if this instance is less than another object.
171
+
172
+ Comparison is only defined between instances of the exact same type.
173
+ """
174
+ # pylint: disable-next=unidiomatic-typecheck
175
+ if type(other) is not type(self):
176
+ return NotImplemented
177
+ other_id = cast(Self, other)
178
+ return self._id < other_id._id
179
+
180
+ def __hash__(self) -> int:
181
+ """Return the hash of this instance.
182
+
183
+ The hash is based on the exact type and the underlying ID to ensure
184
+ that IDs of different types but with the same numeric value have different hashes.
185
+ """
186
+ return hash((type(self), self._id))
187
+
188
+ def __repr__(self) -> str:
189
+ """Return the string representation of this instance."""
190
+ return f"{type(self).__name__}({self._id!r})"
191
+
192
+ def __str__(self) -> str:
193
+ """Return the short string representation of this instance."""
194
+ return f"{self._str_prefix}{self._id}"
@@ -0,0 +1,116 @@
1
+ # License: MIT
2
+ # Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Math tools."""
5
+
6
+ import math
7
+ from dataclasses import dataclass
8
+ from typing import Generic, Protocol, Self, TypeVar, cast
9
+
10
+
11
+ def is_close_to_zero(value: float, abs_tol: float = 1e-9) -> bool:
12
+ """Check if a floating point value is close to zero.
13
+
14
+ A value of 1e-9 is a commonly used absolute tolerance to balance precision
15
+ and robustness for floating-point numbers comparisons close to zero. Note
16
+ that this is also the default value for the relative tolerance.
17
+ For more technical details, see https://peps.python.org/pep-0485/#behavior-near-zero
18
+
19
+ Args:
20
+ value: The floating point value to compare to.
21
+ abs_tol: The minimum absolute tolerance. Defaults to 1e-9.
22
+
23
+ Returns:
24
+ Whether the floating point value is close to zero.
25
+ """
26
+ zero: float = 0.0
27
+ return math.isclose(a=value, b=zero, abs_tol=abs_tol)
28
+
29
+
30
+ class LessThanComparable(Protocol):
31
+ """A protocol that requires the `__lt__` method to compare values."""
32
+
33
+ def __lt__(self, other: Self, /) -> bool:
34
+ """Return whether self is less than other."""
35
+
36
+
37
+ LessThanComparableOrNoneT = TypeVar(
38
+ "LessThanComparableOrNoneT", bound=LessThanComparable | None
39
+ )
40
+ """Type variable for a value that a `LessThanComparable` or `None`."""
41
+
42
+
43
+ @dataclass(frozen=True, repr=False)
44
+ class Interval(Generic[LessThanComparableOrNoneT]):
45
+ """An interval to test if a value is within its limits.
46
+
47
+ The `start` and `end` are inclusive, meaning that the `start` and `end` limites are
48
+ included in the range when checking if a value is contained by the interval.
49
+
50
+ If the `start` or `end` is `None`, it means that the interval is unbounded in that
51
+ direction.
52
+
53
+ If `start` is bigger than `end`, a `ValueError` is raised.
54
+
55
+ The type stored in the interval must be comparable, meaning that it must implement
56
+ the `__lt__` method to be able to compare values.
57
+ """
58
+
59
+ start: LessThanComparableOrNoneT
60
+ """The start of the interval."""
61
+
62
+ end: LessThanComparableOrNoneT
63
+ """The end of the interval."""
64
+
65
+ def __post_init__(self) -> None:
66
+ """Check if the start is less than or equal to the end."""
67
+ if self.start is None or self.end is None:
68
+ return
69
+ start = cast(LessThanComparable, self.start)
70
+ end = cast(LessThanComparable, self.end)
71
+ if start > end:
72
+ raise ValueError(
73
+ f"The start ({self.start}) can't be bigger than end ({self.end})"
74
+ )
75
+
76
+ def __contains__(self, item: LessThanComparableOrNoneT) -> bool:
77
+ """
78
+ Check if the value is within the range of the container.
79
+
80
+ Args:
81
+ item: The value to check.
82
+
83
+ Returns:
84
+ bool: True if value is within the range, otherwise False.
85
+ """
86
+ if item is None:
87
+ return False
88
+ casted_item = cast(LessThanComparable, item)
89
+
90
+ if self.start is None and self.end is None:
91
+ return True
92
+ if self.start is None:
93
+ start = cast(LessThanComparable, self.end)
94
+ return not casted_item > start
95
+ if self.end is None:
96
+ return not self.start > item
97
+ # mypy seems to get confused here, not being able to narrow start and end to
98
+ # just LessThanComparable, complaining with:
99
+ # error: Unsupported left operand type for <= (some union)
100
+ # But we know if they are not None, they should be LessThanComparable, and
101
+ # actually mypy is being able to figure it out in the lines above, just not in
102
+ # this one, so it should be safe to cast.
103
+ return not (
104
+ casted_item < cast(LessThanComparable, self.start)
105
+ or casted_item > cast(LessThanComparable, self.end)
106
+ )
107
+
108
+ def __repr__(self) -> str:
109
+ """Return a string representation of this instance."""
110
+ return f"Interval({self.start!r}, {self.end!r})"
111
+
112
+ def __str__(self) -> str:
113
+ """Return a string representation of this instance."""
114
+ start = "∞" if self.start is None else str(self.start)
115
+ end = "∞" if self.end is None else str(self.end)
116
+ return f"[{start}, {end}]"
@@ -0,0 +1,36 @@
1
+ # License: MIT
2
+ # Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Tools for dealing with Python modules."""
5
+
6
+
7
+ def get_public_module_name(module_name: str) -> str | None:
8
+ """Get the name of the public module containing the given module name.
9
+
10
+ * Modules are considered private if they start with `_`.
11
+ * All modules inside a private module are also considered private, even if they
12
+ don't start with `_`.
13
+ * If there is no leading public part, `None` is returned.
14
+
15
+ Example:
16
+ Here are a few examples of how this function will resolve module names:
17
+
18
+ * `some.pub` -> `some.pub`
19
+ * `some.pub._some._priv` -> `some.pub`
20
+ * `some.pub._some._priv.public` -> `some.pub`
21
+ * `some.pub._some._priv.public._private` -> `some.pub`
22
+ * `_priv` -> `None`
23
+
24
+ Args:
25
+ module_name: The fully qualified name of the module to get the public name for
26
+ (normally the `__name__` built-in variable).
27
+
28
+ Returns:
29
+ The name of the public module containing the given module name.
30
+ """
31
+ public_parts: list[str] = []
32
+ for part in module_name.split("."):
33
+ if part.startswith("_"):
34
+ break
35
+ public_parts.append(part)
36
+ return ".".join(public_parts) or None
File without changes
@@ -0,0 +1,311 @@
1
+ # License: MIT
2
+ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Type hints and utility functions for type checking and types.
5
+
6
+ This module provides a decorator to disable the `__init__` constructor of a class, to
7
+ force the use of a factory method to create instances. See
8
+ [`@disable_init`][frequenz.core.typing.disable_init] for more information.
9
+
10
+ It also provides a metaclass used by the decorator to disable the `__init__`:
11
+ [`NoInitConstructibleMeta`][frequenz.core.typing.NoInitConstructibleMeta]. This is
12
+ useful mostly for disabling `__init__` while having to use another metaclass too (like
13
+ [`abc.ABCMeta`][abc.ABCMeta]).
14
+ """
15
+
16
+ from collections.abc import Callable
17
+ from typing import Any, NoReturn, TypeVar, cast, overload
18
+
19
+ TypeT = TypeVar("TypeT", bound=type)
20
+ """A type variable that is bound to a type."""
21
+
22
+
23
+ @overload
24
+ def disable_init(
25
+ cls: None = None,
26
+ *,
27
+ error: Exception | None = None,
28
+ ) -> Callable[[TypeT], TypeT]: ...
29
+
30
+
31
+ @overload
32
+ def disable_init(cls: TypeT) -> TypeT: ...
33
+
34
+
35
+ def disable_init(
36
+ cls: TypeT | None = None,
37
+ *,
38
+ error: Exception | None = None,
39
+ ) -> TypeT | Callable[[TypeT], TypeT]:
40
+ """Disable the `__init__` constructor of a class.
41
+
42
+ This decorator can be used to disable the `__init__` constructor of a class. It is
43
+ intended to be used with classes that don't provide a default constructor and
44
+ require the use of a factory method to create instances.
45
+
46
+ When marking a class with this decorator, the class cannot be even declared with a
47
+ `__init__` method, as it will raise a `TypeError` when the class is created, as soon
48
+ as the class is parsed by the Python interpreter. It will also raise a `TypeError`
49
+ when the `__init__` method is called.
50
+
51
+ To create an instance you must provide a factory method, using `__new__`.
52
+
53
+ Warning:
54
+ This decorator will use a custom metaclass to disable the `__init__` constructor
55
+ of the class, so if your class already uses a custom metaclass, you should be
56
+ aware of potential conflicts. See
57
+ [`NoInitConstructibleMeta`][frequenz.core.typing.NoInitConstructibleMeta] for an
58
+ example on how to use more than one metaclass.
59
+
60
+ It is also recommended to apply this decorator only to classes inheriting from
61
+ `object` directly (i.e. not explicitly inheriting from any other classes), as
62
+ things can also get tricky when applying the constructor to a sub-class for the
63
+ first time.
64
+
65
+ Example: Basic example defining a class with a factory method
66
+ To be able to type hint the class correctly, you can declare the instance
67
+ attributes in the class body, and then use a factory method to create instances.
68
+
69
+ ```python
70
+ from typing import Self
71
+
72
+ @disable_init
73
+ class MyClass:
74
+ value: int
75
+
76
+ @classmethod
77
+ def new(cls, value: int = 1) -> Self:
78
+ self = cls.__new__(cls)
79
+ self.value = value
80
+ return self
81
+
82
+ instance = MyClass.new()
83
+
84
+ # Calling the default constructor (__init__) will raise a TypeError
85
+ try:
86
+ instance = MyClass()
87
+ except TypeError as e:
88
+ print(e)
89
+ ```
90
+
91
+ Example: Class wrongly providing an `__init__` constructor
92
+ ```python
93
+ try:
94
+ @disable_init
95
+ class MyClass:
96
+ def __init__(self) -> None:
97
+ pass
98
+ except TypeError as e:
99
+ assert isinstance(e, TypeError)
100
+ print(e)
101
+ ```
102
+
103
+ Example: Using a custom error message when the default constructor is called
104
+ ```python
105
+ from typing import Self
106
+
107
+ class NoInitError(TypeError):
108
+ def __init__(self) -> None:
109
+ super().__init__("Please create instances of MyClass using MyClass.new()")
110
+
111
+ @disable_init(error=NoInitError())
112
+ class MyClass:
113
+ @classmethod
114
+ def new(cls) -> Self:
115
+ return cls.__new__(cls)
116
+
117
+ try:
118
+ instance = MyClass()
119
+ except NoInitError as e:
120
+ assert str(e) == "Please create instances of MyClass using MyClass.new()"
121
+ print(e)
122
+ ```
123
+
124
+ Args:
125
+ cls: The class to be decorated.
126
+ error: The error to raise if __init__ is called, if `None` a default
127
+ [TypeError][] will be raised.
128
+
129
+ Returns:
130
+ A decorator that disables the `__init__` constructor of `cls`.
131
+ """
132
+
133
+ def decorator(inner_cls: TypeT) -> TypeT:
134
+ return cast(
135
+ TypeT,
136
+ NoInitConstructibleMeta(
137
+ inner_cls.__name__,
138
+ inner_cls.__bases__,
139
+ dict(inner_cls.__dict__),
140
+ no_init_constructible_error=error,
141
+ ),
142
+ )
143
+
144
+ if cls is None:
145
+ return decorator
146
+ return decorator(cls)
147
+
148
+
149
+ class NoInitConstructibleMeta(type):
150
+ """A metaclass that disables the `__init__` constructor.
151
+
152
+ This metaclass can be used to disable the `__init__` constructor of a class. It is
153
+ intended to be used with classes that don't provide a default constructor and
154
+ require the use of a factory method to create instances.
155
+
156
+ When marking a class using this metaclass, the class cannot be even declared with a
157
+ `__init__` method, as it will raise a `TypeError` when the class is created, as soon
158
+ as the class is parsed by the Python interpreter. It will also raise a `TypeError`
159
+ when the `__init__` method is called.
160
+
161
+ To create an instance you must provide a factory method, using `__new__`.
162
+
163
+ Warning:
164
+ It is also recommended to apply this metaclass only to classes inheriting from
165
+ `object` directly (i.e. not explicitly inheriting from any other classes), as
166
+ things can also get tricky when applying the constructor to a sub-class for the
167
+ first time.
168
+
169
+ Example: Basic example defining a class with a factory method
170
+ To be able to type hint the class correctly, you can declare the instance
171
+ attributes in the class body, and then use a factory method to create instances.
172
+
173
+ ```python
174
+ from typing import Self
175
+
176
+ class MyClass(metaclass=NoInitConstructibleMeta):
177
+ value: int
178
+
179
+ @classmethod
180
+ def new(cls, value: int = 1) -> Self:
181
+ self = cls.__new__(cls)
182
+ self.value = value
183
+ return self
184
+
185
+ instance = MyClass.new()
186
+
187
+ # Calling the default constructor (__init__) will raise a TypeError
188
+ try:
189
+ instance = MyClass()
190
+ except TypeError as e:
191
+ print(e)
192
+ ```
193
+
194
+ Hint:
195
+ The [`@disable_init`][frequenz.core.typing.disable_init] decorator is a more
196
+ convenient way to use this metaclass.
197
+
198
+ Example: Example combining with other metaclass
199
+ A typical case where you might want this is to combine with
200
+ [`abc.ABCMeta`][abc.ABCMeta] to create an abstract class that doesn't provide a
201
+ default constructor.
202
+
203
+ ```python
204
+ from abc import ABCMeta, abstractmethod
205
+ from typing import Self
206
+
207
+ class NoInitConstructibleABCMeta(ABCMeta, NoInitConstructibleMeta):
208
+ pass
209
+
210
+ class MyAbstractClass(metaclas=NoInitConstructibleABCMeta):
211
+ @abstractmethod
212
+ def do_something(self) -> None:
213
+ ...
214
+
215
+ class MyClass(MyAbstractClass):
216
+ value: int
217
+
218
+ @classmethod
219
+ def new(cls, value: int = 1) -> Self:
220
+ self = cls.__new__(cls)
221
+ self.value = value
222
+ return self
223
+
224
+ def do_something(self) -> None:
225
+ print("Doing something")
226
+
227
+ instance = MyClass.new()
228
+ instance.do_something()
229
+
230
+ # Calling the default constructor (__init__) will raise a TypeError
231
+ try:
232
+ instance = MyClass()
233
+ except TypeError as e:
234
+ print(e)
235
+ ```
236
+
237
+ """
238
+
239
+ # We need to use noqa here because pydoclint can't figure out that
240
+ # _get_no_init_constructible_error() returns a TypeError.
241
+ def __new__( # noqa: DOC503
242
+ mcs,
243
+ name: str,
244
+ bases: tuple[type, ...],
245
+ namespace: dict[str, Any],
246
+ **kwargs: Any,
247
+ ) -> type:
248
+ """Create a new class with a disabled __init__ constructor.
249
+
250
+ Args:
251
+ name: The name of the new class.
252
+ bases: The base classes of the new class.
253
+ namespace: The namespace of the new class.
254
+ **kwargs: Additional keyword arguments.
255
+
256
+ Returns:
257
+ The new class with a disabled __init__ constructor.
258
+
259
+ Raises:
260
+ TypeError: If the class provides a default constructor.
261
+ """
262
+ if "__init__" in namespace:
263
+ raise _get_no_init_constructible_error(name, bases, **kwargs)
264
+ return super().__new__(mcs, name, bases, namespace)
265
+
266
+ def __init__(
267
+ cls,
268
+ name: str,
269
+ bases: tuple[type, ...],
270
+ namespace: dict[str, Any],
271
+ **kwargs: Any,
272
+ ) -> None:
273
+ """Initialize the new class."""
274
+ super().__init__(name, bases, namespace)
275
+ cls._no_init_constructible_error = kwargs.get("no_init_constructible_error")
276
+
277
+ # We need to use noqa here because pydoclint can't figure out that
278
+ # _get_no_init_constructible_error() returns a TypeError.
279
+ def __call__(cls, *args: Any, **kwargs: Any) -> NoReturn: # noqa: DOC503
280
+ """Raise an error when the __init__ constructor is called.
281
+
282
+ Args:
283
+ *args: ignored positional arguments.
284
+ **kwargs: ignored keyword arguments.
285
+
286
+ Raises:
287
+ TypeError: Always.
288
+ """
289
+ raise _get_no_init_constructible_error(
290
+ cls.__name__,
291
+ cls.__bases__,
292
+ no_init_constructible_error=cls._no_init_constructible_error,
293
+ )
294
+
295
+
296
+ def _get_no_init_constructible_error(
297
+ name: str, bases: tuple[type, ...], **kwargs: Any
298
+ ) -> Exception:
299
+ error = kwargs.get("no_init_constructible_error")
300
+ if error is None:
301
+ for base in bases:
302
+ if attr := getattr(base, "_no_init_constructible_error", None):
303
+ error = attr
304
+ break
305
+ else:
306
+ error = TypeError(
307
+ f"{name} doesn't provide a default constructor, you must use a "
308
+ "factory method to create instances."
309
+ )
310
+ assert isinstance(error, Exception)
311
+ return error
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: frequenz-core
3
+ Version: 1.0.0
4
+ Summary: Core utilities to complement Python's standard library
5
+ Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
6
+ License: MIT
7
+ Project-URL: Documentation, https://frequenz-floss.github.io/frequenz-core-python/
8
+ Project-URL: Changelog, https://github.com/frequenz-floss/frequenz-core-python/releases
9
+ Project-URL: Issues, https://github.com/frequenz-floss/frequenz-core-python/issues
10
+ Project-URL: Repository, https://github.com/frequenz-floss/frequenz-core-python
11
+ Project-URL: Support, https://github.com/frequenz-floss/frequenz-core-python/discussions/categories/support
12
+ Keywords: asyncio,collections,core,datetime,frequenz,lib,library,math,python,stdlib,typing
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: <4,>=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: typing-extensions<5,>=4.13.0
24
+ Provides-Extra: dev-flake8
25
+ Requires-Dist: flake8==7.2.0; extra == "dev-flake8"
26
+ Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
27
+ Requires-Dist: flake8-pyproject==1.2.3; extra == "dev-flake8"
28
+ Requires-Dist: pydoclint==0.6.6; extra == "dev-flake8"
29
+ Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
30
+ Provides-Extra: dev-formatting
31
+ Requires-Dist: black==25.1.0; extra == "dev-formatting"
32
+ Requires-Dist: isort==6.0.1; extra == "dev-formatting"
33
+ Provides-Extra: dev-mkdocs
34
+ Requires-Dist: Markdown==3.8; extra == "dev-mkdocs"
35
+ Requires-Dist: black==25.1.0; extra == "dev-mkdocs"
36
+ Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
37
+ Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
38
+ Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
39
+ Requires-Dist: mkdocs-macros-plugin==1.3.7; extra == "dev-mkdocs"
40
+ Requires-Dist: mkdocs-material==9.6.14; extra == "dev-mkdocs"
41
+ Requires-Dist: mkdocstrings[python]==0.29.1; extra == "dev-mkdocs"
42
+ Requires-Dist: mkdocstrings-python==1.16.11; extra == "dev-mkdocs"
43
+ Requires-Dist: frequenz-repo-config[lib]==0.13.4; extra == "dev-mkdocs"
44
+ Provides-Extra: dev-mypy
45
+ Requires-Dist: mypy==1.16.0; extra == "dev-mypy"
46
+ Requires-Dist: types-Markdown==3.8.0.20250415; extra == "dev-mypy"
47
+ Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
48
+ Provides-Extra: dev-noxfile
49
+ Requires-Dist: nox==2025.5.1; extra == "dev-noxfile"
50
+ Requires-Dist: frequenz-repo-config[lib]==0.13.4; extra == "dev-noxfile"
51
+ Provides-Extra: dev-pylint
52
+ Requires-Dist: frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
53
+ Provides-Extra: dev-pytest
54
+ Requires-Dist: pytest==8.3.5; extra == "dev-pytest"
55
+ Requires-Dist: pylint==3.3.7; extra == "dev-pytest"
56
+ Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.13.4; extra == "dev-pytest"
57
+ Requires-Dist: pytest-mock==3.14.1; extra == "dev-pytest"
58
+ Requires-Dist: pytest-asyncio==0.26.0; extra == "dev-pytest"
59
+ Requires-Dist: async-solipsism==0.7; extra == "dev-pytest"
60
+ Requires-Dist: hypothesis==6.132.0; extra == "dev-pytest"
61
+ Provides-Extra: dev
62
+ Requires-Dist: frequenz-core[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
63
+ Dynamic: license-file
64
+
65
+ # Frequenz Core Library
66
+
67
+ [![Build Status](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml/badge.svg)](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml)
68
+ [![PyPI Package](https://img.shields.io/pypi/v/frequenz-core)](https://pypi.org/project/frequenz-core/)
69
+ [![Docs](https://img.shields.io/badge/docs-latest-informational)](https://frequenz-floss.github.io/frequenz-core-python/)
70
+
71
+ ## Introduction
72
+
73
+ Core utilities to complement Python's standard library.
74
+
75
+ ## Documentation
76
+
77
+ For information on how to use this library, please refer to the
78
+ [documentation](https://frequenz-floss.github.io/frequenz-core-python/).
79
+
80
+ ## Supported Platforms
81
+
82
+ The following platforms are officially supported (tested):
83
+
84
+ - **Python:** 3.11
85
+ - **Operating System:** Ubuntu Linux 20.04
86
+ - **Architectures:** amd64, arm64
87
+
88
+ ## Contributing
89
+
90
+ If you want to know how to build this project and contribute to it, please
91
+ check out the [Contributing Guide](CONTRIBUTING.md).
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ RELEASE_NOTES.md
5
+ pyproject.toml
6
+ src/frequenz/core/__init__.py
7
+ src/frequenz/core/conftest.py
8
+ src/frequenz/core/datetime.py
9
+ src/frequenz/core/id.py
10
+ src/frequenz/core/math.py
11
+ src/frequenz/core/module.py
12
+ src/frequenz/core/py.typed
13
+ src/frequenz/core/typing.py
14
+ src/frequenz_core.egg-info/PKG-INFO
15
+ src/frequenz_core.egg-info/SOURCES.txt
16
+ src/frequenz_core.egg-info/dependency_links.txt
17
+ src/frequenz_core.egg-info/requires.txt
18
+ src/frequenz_core.egg-info/top_level.txt
@@ -0,0 +1,48 @@
1
+ typing-extensions<5,>=4.13.0
2
+
3
+ [dev]
4
+ frequenz-core[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]
5
+
6
+ [dev-flake8]
7
+ flake8==7.2.0
8
+ flake8-docstrings==1.7.0
9
+ flake8-pyproject==1.2.3
10
+ pydoclint==0.6.6
11
+ pydocstyle==6.3.0
12
+
13
+ [dev-formatting]
14
+ black==25.1.0
15
+ isort==6.0.1
16
+
17
+ [dev-mkdocs]
18
+ Markdown==3.8
19
+ black==25.1.0
20
+ mike==2.1.3
21
+ mkdocs-gen-files==0.5.0
22
+ mkdocs-literate-nav==0.6.2
23
+ mkdocs-macros-plugin==1.3.7
24
+ mkdocs-material==9.6.14
25
+ mkdocstrings[python]==0.29.1
26
+ mkdocstrings-python==1.16.11
27
+ frequenz-repo-config[lib]==0.13.4
28
+
29
+ [dev-mypy]
30
+ mypy==1.16.0
31
+ types-Markdown==3.8.0.20250415
32
+ frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]
33
+
34
+ [dev-noxfile]
35
+ nox==2025.5.1
36
+ frequenz-repo-config[lib]==0.13.4
37
+
38
+ [dev-pylint]
39
+ frequenz-core[dev-mkdocs,dev-noxfile,dev-pytest]
40
+
41
+ [dev-pytest]
42
+ pytest==8.3.5
43
+ pylint==3.3.7
44
+ frequenz-repo-config[extra-lint-examples]==0.13.4
45
+ pytest-mock==3.14.1
46
+ pytest-asyncio==0.26.0
47
+ async-solipsism==0.7
48
+ hypothesis==6.132.0