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.
- frequenz_core-1.0.0/LICENSE +21 -0
- frequenz_core-1.0.0/MANIFEST.in +13 -0
- frequenz_core-1.0.0/PKG-INFO +91 -0
- frequenz_core-1.0.0/README.md +27 -0
- frequenz_core-1.0.0/RELEASE_NOTES.md +14 -0
- frequenz_core-1.0.0/pyproject.toml +181 -0
- frequenz_core-1.0.0/setup.cfg +4 -0
- frequenz_core-1.0.0/src/frequenz/core/__init__.py +4 -0
- frequenz_core-1.0.0/src/frequenz/core/conftest.py +13 -0
- frequenz_core-1.0.0/src/frequenz/core/datetime.py +10 -0
- frequenz_core-1.0.0/src/frequenz/core/id.py +194 -0
- frequenz_core-1.0.0/src/frequenz/core/math.py +116 -0
- frequenz_core-1.0.0/src/frequenz/core/module.py +36 -0
- frequenz_core-1.0.0/src/frequenz/core/py.typed +0 -0
- frequenz_core-1.0.0/src/frequenz/core/typing.py +311 -0
- frequenz_core-1.0.0/src/frequenz_core.egg-info/PKG-INFO +91 -0
- frequenz_core-1.0.0/src/frequenz_core.egg-info/SOURCES.txt +18 -0
- frequenz_core-1.0.0/src/frequenz_core.egg-info/dependency_links.txt +1 -0
- frequenz_core-1.0.0/src/frequenz_core.egg-info/requires.txt +48 -0
- frequenz_core-1.0.0/src/frequenz_core.egg-info/top_level.txt +1 -0
|
@@ -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
|
+
[](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml)
|
|
68
|
+
[](https://pypi.org/project/frequenz-core/)
|
|
69
|
+
[](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
|
+
[](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml)
|
|
4
|
+
[](https://pypi.org/project/frequenz-core/)
|
|
5
|
+
[](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,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
|
+
[](https://github.com/frequenz-floss/frequenz-core-python/actions/workflows/ci.yaml)
|
|
68
|
+
[](https://pypi.org/project/frequenz-core/)
|
|
69
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
frequenz
|