frequenz-quantities 1.0.0__py3-none-any.whl

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,271 @@
1
+ # License: All rights reserved
2
+ # Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
+
4
+ """Custom marshmallow fields and schema.
5
+
6
+ This module provides custom marshmallow fields for quantities and
7
+ a [QuantitySchema][frequenz.quantities.experimental.marshmallow.QuantitySchema] class to
8
+ be used as base schema for dataclasses containing quantities.
9
+
10
+ Danger:
11
+ This module contains experimental features for which the API is not yet stable.
12
+
13
+ Any module or class in this package may be removed or changed in a future release,
14
+ even in minor or patch releases.
15
+ """
16
+
17
+ from typing import Any, Type
18
+
19
+ from marshmallow import Schema, ValidationError, fields
20
+
21
+ from .._apparent_power import ApparentPower
22
+ from .._current import Current
23
+ from .._energy import Energy
24
+ from .._frequency import Frequency
25
+ from .._percentage import Percentage
26
+ from .._power import Power
27
+ from .._quantity import Quantity
28
+ from .._reactive_power import ReactivePower
29
+ from .._temperature import Temperature
30
+ from .._voltage import Voltage
31
+
32
+
33
+ class _QuantityField(fields.Field):
34
+ """Custom field for Quantity objects supporting per-field serialization configuration.
35
+
36
+ This class handles serialization and deserialization of ALL Quantity
37
+ subclasses.
38
+ The specific Quantity subclass is determined by the field_type attribute.
39
+
40
+ * Deserialization auto-detects the type of deserialization (float or string)
41
+ based on the input type.
42
+ * Serialization uses either the schema's default or the per-field
43
+ configuration found in the metadata.
44
+
45
+ We need distinct QuantityField subclasses for each Quantity subclass, so
46
+ they can be used in the TYPE_MAPPING in the `QuantitySchema`.
47
+ Which means this class is not intended to be used directly.
48
+
49
+ Instead, we use the specific QuantityField subclasses for each Quantity.
50
+ Each field subclass simply sets the field_type attribute to the corresponding
51
+ Quantity subclass.
52
+
53
+ Those subclasses are generated and stored in the QUANTITY_FIELD_CLASSES
54
+ mapping and are used for the TYPE_MAPPING in the `QuantitySchema`.
55
+ """
56
+
57
+ field_type: Type[Quantity] | None = None
58
+ """The specific Quantity subclass."""
59
+
60
+ def _serialize(
61
+ self, value: Quantity, attr: str | None, obj: Any, **kwargs: Any
62
+ ) -> Any:
63
+ """Serialize the Quantity object based on per-field configuration."""
64
+ if self.field_type is None or not issubclass(self.field_type, Quantity):
65
+ raise TypeError(
66
+ "field_type must be set to a Quantity subclass in the subclass."
67
+ )
68
+
69
+ assert self.parent is not None
70
+
71
+ # Determine the serialization format
72
+ serialize_as_string = self.metadata.get(
73
+ "serialize_as_string",
74
+ self.parent.context.get("serialize_as_string_default", False),
75
+ )
76
+
77
+ if serialize_as_string:
78
+ # Use the Quantity's native string representation (includes unit)
79
+ return str(value)
80
+
81
+ # Serialize as float using the Quantity's base value
82
+ return value.base_value
83
+
84
+ def _deserialize(
85
+ self, value: Any, attr: str | None, data: Any, **kwargs: Any
86
+ ) -> Quantity:
87
+ """Deserialize the Quantity object from float or string."""
88
+ if self.field_type is None or not issubclass(self.field_type, Quantity):
89
+ raise TypeError(
90
+ "field_type must be set to a Quantity subclass in the subclass."
91
+ )
92
+
93
+ if isinstance(value, str):
94
+ # Use the Quantity's from_string method
95
+ try:
96
+ return self.field_type.from_string(value)
97
+ except Exception as error: # pylint: disable=broad-except
98
+ raise ValidationError(str(error)) from error
99
+ if isinstance(value, (float, int)):
100
+ try:
101
+ # Use `_new` method for creating instance from base value
102
+ return self.field_type._new( # pylint: disable=protected-access
103
+ float(value)
104
+ )
105
+ except Exception as error: # pylint: disable=broad-except
106
+ raise ValidationError(str(error)) from error
107
+
108
+ raise ValidationError("Invalid input type for QuantityField.")
109
+
110
+
111
+ _QUANTITY_SUBCLASSES = [
112
+ ApparentPower,
113
+ Current,
114
+ Energy,
115
+ Frequency,
116
+ Percentage,
117
+ Power,
118
+ ReactivePower,
119
+ Temperature,
120
+ Voltage,
121
+ ]
122
+
123
+
124
+ class ApparentPowerField(_QuantityField):
125
+ """Custom field for ApparentPower objects."""
126
+
127
+ field_type = ApparentPower
128
+
129
+
130
+ class CurrentField(_QuantityField):
131
+ """Custom field for Current objects."""
132
+
133
+ field_type = Current
134
+
135
+
136
+ class EnergyField(_QuantityField):
137
+ """Custom field for Energy objects."""
138
+
139
+ field_type = Energy
140
+
141
+
142
+ class FrequencyField(_QuantityField):
143
+ """Custom field for Frequency objects."""
144
+
145
+ field_type = Frequency
146
+
147
+
148
+ class PercentageField(_QuantityField):
149
+ """Custom field for Percentage objects."""
150
+
151
+ field_type = Percentage
152
+
153
+
154
+ class PowerField(_QuantityField):
155
+ """Custom field for Power objects."""
156
+
157
+ field_type = Power
158
+
159
+
160
+ class ReactivePowerField(_QuantityField):
161
+ """Custom field for ReactivePower objects."""
162
+
163
+ field_type = ReactivePower
164
+
165
+
166
+ class TemperatureField(_QuantityField):
167
+ """Custom field for Temperature objects."""
168
+
169
+ field_type = Temperature
170
+
171
+
172
+ class VoltageField(_QuantityField):
173
+ """Custom field for Voltage objects."""
174
+
175
+ field_type = Voltage
176
+
177
+
178
+ QUANTITY_FIELD_CLASSES: dict[type[Quantity], type[fields.Field]] = {
179
+ ApparentPower: ApparentPowerField,
180
+ Current: CurrentField,
181
+ Energy: EnergyField,
182
+ Frequency: FrequencyField,
183
+ Percentage: PercentageField,
184
+ Power: PowerField,
185
+ ReactivePower: ReactivePowerField,
186
+ Temperature: TemperatureField,
187
+ Voltage: VoltageField,
188
+ }
189
+ """Mapping of Quantity subclasses to their corresponding QuantityField subclasses.
190
+
191
+ This mapping is used in the `QuantitySchema` to determine the correct field
192
+ class for each Quantity subclass.
193
+
194
+ The keys are Quantity subclasses (e.g., Percentage, Energy) and the values are
195
+ the corresponding QuantityField subclasses.
196
+ """
197
+
198
+
199
+ class QuantitySchema(Schema):
200
+ """A schema for quantities.
201
+
202
+ Example usage:
203
+
204
+ ```python
205
+ from dataclasses import dataclass, field
206
+ from marshmallow_dataclass import class_schema
207
+ from marshmallow.validate import Range
208
+ from frequenz.quantities import Percentage
209
+ from frequenz.quantities.experimental.marshmallow import QuantitySchema
210
+ from typing import cast
211
+
212
+ @dataclass
213
+ class Config:
214
+ percentage_always_as_string: Percentage = field(
215
+ default_factory=lambda: Percentage.from_percent(25.0),
216
+ metadata={
217
+ "metadata": {
218
+ "description": "A percentage field",
219
+ },
220
+ "validate": Range(Percentage.zero(), Percentage.from_percent(100.0)),
221
+ "serialize_as_string": True,
222
+ },
223
+ )
224
+
225
+ percentage_always_as_float: Percentage = field(
226
+ default_factory=lambda: Percentage.from_percent(25.0),
227
+ metadata={
228
+ "metadata": {
229
+ "description": "A percentage field",
230
+ },
231
+ "validate": Range(Percentage.zero(), Percentage.from_percent(100.0)),
232
+ "serialize_as_string": False,
233
+ },
234
+ )
235
+
236
+ percentage_serialized_as_schema_default: Percentage = field(
237
+ default_factory=lambda: Percentage.from_percent(25.0),
238
+ metadata={
239
+ "metadata": {
240
+ "description": "A percentage field",
241
+ },
242
+ "validate": Range(Percentage.zero(), Percentage.from_percent(100.0)),
243
+ },
244
+ )
245
+
246
+ @classmethod
247
+ def load(cls, config: dict[str, Any]) -> "Config":
248
+ schema = class_schema(cls, base_schema=QuantitySchema)(
249
+ serialize_as_string_default=True # type: ignore[call-arg]
250
+ )
251
+ return cast(Config, schema.load(config))
252
+ ```
253
+ """
254
+
255
+ TYPE_MAPPING: dict[type[Quantity], type[fields.Field]] = QUANTITY_FIELD_CLASSES
256
+
257
+ def __init__(
258
+ self, *args: Any, serialize_as_string_default: bool = False, **kwargs: Any
259
+ ) -> None:
260
+ """
261
+ Initialize the schema with a default serialization format.
262
+
263
+ Args:
264
+ *args: Additional positional arguments.
265
+ serialize_as_string_default: Default serialization format for quantities.
266
+ If True, quantities are serialized as strings with units.
267
+ If False, quantities are serialized as floats.
268
+ **kwargs: Additional keyword arguments.
269
+ """
270
+ super().__init__(*args, **kwargs)
271
+ self.context["serialize_as_string_default"] = serialize_as_string_default
File without changes
@@ -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,103 @@
1
+ Metadata-Version: 2.1
2
+ Name: frequenz-quantities
3
+ Version: 1.0.0
4
+ Summary: Types for holding quantities with units
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-quantities-python/
8
+ Project-URL: Changelog, https://github.com/frequenz-floss/frequenz-quantities-python/releases
9
+ Project-URL: Issues, https://github.com/frequenz-floss/frequenz-quantities-python/issues
10
+ Project-URL: Repository, https://github.com/frequenz-floss/frequenz-quantities-python
11
+ Project-URL: Support, https://github.com/frequenz-floss/frequenz-quantities-python/discussions/categories/support
12
+ Keywords: frequenz,python,lib,library,quantities,unit,conversion
13
+ Classifier: Development Status :: 3 - Alpha
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.5.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: frequenz-quantities[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest,marshmallow]; extra == "dev"
26
+ Provides-Extra: dev-flake8
27
+ Requires-Dist: flake8==7.1.1; extra == "dev-flake8"
28
+ Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
29
+ Requires-Dist: flake8-pyproject==1.2.3; extra == "dev-flake8"
30
+ Requires-Dist: pydoclint==0.5.9; extra == "dev-flake8"
31
+ Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
32
+ Provides-Extra: dev-formatting
33
+ Requires-Dist: black==24.10.0; extra == "dev-formatting"
34
+ Requires-Dist: isort==5.13.2; extra == "dev-formatting"
35
+ Provides-Extra: dev-mkdocs
36
+ Requires-Dist: Markdown==3.7; extra == "dev-mkdocs"
37
+ Requires-Dist: black==24.10.0; extra == "dev-mkdocs"
38
+ Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
39
+ Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
40
+ Requires-Dist: mkdocs-literate-nav==0.6.1; extra == "dev-mkdocs"
41
+ Requires-Dist: mkdocs-macros-plugin==1.3.7; extra == "dev-mkdocs"
42
+ Requires-Dist: mkdocs-material==9.5.43; extra == "dev-mkdocs"
43
+ Requires-Dist: mkdocstrings[python]==0.26.2; extra == "dev-mkdocs"
44
+ Requires-Dist: mkdocstrings-python==1.12.2; extra == "dev-mkdocs"
45
+ Requires-Dist: frequenz-repo-config[lib]==0.10.0; extra == "dev-mkdocs"
46
+ Provides-Extra: dev-mypy
47
+ Requires-Dist: mypy==1.13.0; extra == "dev-mypy"
48
+ Requires-Dist: types-Markdown==3.7.0.20240822; extra == "dev-mypy"
49
+ Requires-Dist: frequenz-quantities[dev-mkdocs,dev-noxfile,dev-pytest,marshmallow]; extra == "dev-mypy"
50
+ Provides-Extra: dev-noxfile
51
+ Requires-Dist: nox==2024.10.9; extra == "dev-noxfile"
52
+ Requires-Dist: frequenz-repo-config[lib]==0.10.0; extra == "dev-noxfile"
53
+ Provides-Extra: dev-pylint
54
+ Requires-Dist: frequenz-quantities[dev-mkdocs,dev-noxfile,dev-pytest,marshmallow]; extra == "dev-pylint"
55
+ Provides-Extra: dev-pytest
56
+ Requires-Dist: pytest==8.3.3; extra == "dev-pytest"
57
+ Requires-Dist: pylint==3.3.1; extra == "dev-pytest"
58
+ Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.10.0; extra == "dev-pytest"
59
+ Requires-Dist: pytest-mock==3.14.0; extra == "dev-pytest"
60
+ Requires-Dist: pytest-asyncio==0.24.0; extra == "dev-pytest"
61
+ Requires-Dist: async-solipsism==0.7; extra == "dev-pytest"
62
+ Requires-Dist: hypothesis==6.116.0; extra == "dev-pytest"
63
+ Requires-Dist: frequenz-quantities[marshmallow]; extra == "dev-pytest"
64
+ Provides-Extra: marshmallow
65
+ Requires-Dist: marshmallow<4,>=3.0.0; extra == "marshmallow"
66
+ Requires-Dist: marshmallow-dataclass<9,>=8.0.0; extra == "marshmallow"
67
+
68
+ # Frequenz Quantities Library
69
+
70
+ [![Build Status](https://github.com/frequenz-floss/frequenz-quantities-python/actions/workflows/ci.yaml/badge.svg)](https://github.com/frequenz-floss/frequenz-quantities-python/actions/workflows/ci.yaml)
71
+ [![PyPI Package](https://img.shields.io/pypi/v/frequenz-quantities)](https://pypi.org/project/frequenz-quantities/)
72
+ [![Docs](https://img.shields.io/badge/docs-latest-informational)](https://frequenz-floss.github.io/frequenz-quantities-python/)
73
+
74
+ ## Introduction
75
+
76
+ This library provide types for holding quantities with units. The main goal is
77
+ to avoid mistakes while working with different types of quantities, for example
78
+ avoiding adding a length to a time.
79
+
80
+ It also prevents mistakes when operating between the same quantity but in
81
+ different units, like adding a power in Joules to a power in Watts without
82
+ converting one of them.
83
+
84
+ Quantities store the value in a base unit, and then provide methods to get that
85
+ quantity as a particular unit.
86
+
87
+ ## Documentation
88
+
89
+ For more information on how to use this library and examples, please check the
90
+ [Documentation website](https://frequenz-floss.github.io/frequenz-quantities-python/).
91
+
92
+ ## Supported Platforms
93
+
94
+ The following platforms are officially supported (tested):
95
+
96
+ - **Python:** 3.11
97
+ - **Operating System:** Ubuntu Linux 20.04
98
+ - **Architectures:** amd64, arm64
99
+
100
+ ## Contributing
101
+
102
+ If you want to know how to build this project and contribute to it, please
103
+ check out the [Contributing Guide](CONTRIBUTING.md).
@@ -0,0 +1,20 @@
1
+ frequenz/quantities/__init__.py,sha256=WCmf6AdoCd6qD25704053TCIWLVkbgT_EgAjBeOWsqY,4420
2
+ frequenz/quantities/_apparent_power.py,sha256=XYWhcAhYIU1a_ciLXwvUPaXTmpSSWsDP3C-W8kpQ4y0,6988
3
+ frequenz/quantities/_current.py,sha256=9y4v9AeZfr4cCkzFLbSzhzh7ZPXje7i5-V6nf0qdIgs,3573
4
+ frequenz/quantities/_energy.py,sha256=jfp2ZMb2bJV3etAZwNP7r3eWw0zcp1IQOI0hpTxVDFI,5274
5
+ frequenz/quantities/_frequency.py,sha256=Y_ljIdd-gBFbyBIFHhPeVeD2JQrPTwOeoHI79B7B-ys,2933
6
+ frequenz/quantities/_percentage.py,sha256=vdf7eBbr5EEHKj-0aPVLL6TSjE6OnFcHGN22knFTrlI,1775
7
+ frequenz/quantities/_power.py,sha256=KtT5hED-BzlGqFgKDZktE8UkudLV5khfBZ80zu0b2Vo,7074
8
+ frequenz/quantities/_quantity.py,sha256=sfYAc5hYRlkVRQ5AAgt-79x2-eZwYN1DrlxVrGxJEWw,17134
9
+ frequenz/quantities/_reactive_power.py,sha256=UiOKvY8oPW_7H2FFCE47FGyPIm-TcimfihK76fSUPFc,7203
10
+ frequenz/quantities/_temperature.py,sha256=PlQnavtvBe395ZmsgHTedh520pOAd9-jSRHkqfjv8-k,868
11
+ frequenz/quantities/_voltage.py,sha256=WuYLfC3FsWdWrHMfqxFYTAJmP1CdIFpPVeeGHlOV-eo,4057
12
+ frequenz/quantities/conftest.py,sha256=kxmvkzTdvGfh7SiDINIFX0FG9PU0EoKROl9YY75zN8w,409
13
+ frequenz/quantities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ frequenz/quantities/experimental/__init__.py,sha256=nYOmkSXnHz9kqOf1QwawV0xFqRbBwjgKwGqbxFIVGeI,350
15
+ frequenz/quantities/experimental/marshmallow.py,sha256=P9P7GUVf9uE6fZZuPFxzWvsxeyh-bbPegQPG5OZMX5M,8742
16
+ frequenz_quantities-1.0.0.dist-info/LICENSE,sha256=zt0sW1KvE_KWE2ILOabrQYlfOoP0zUZXC3xCLrzGpIA,1089
17
+ frequenz_quantities-1.0.0.dist-info/METADATA,sha256=B9YoeLZd2CqsOS5L3kVt37vPA5y8MLnmhBsMPRGoTDs,5162
18
+ frequenz_quantities-1.0.0.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
19
+ frequenz_quantities-1.0.0.dist-info/top_level.txt,sha256=x08GRcWytsyKXa2Ayme9e5pg3L5Kcq6lw_BaQmToMO4,9
20
+ frequenz_quantities-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.45.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ frequenz