GeneralManager 0.14.0__py3-none-any.whl → 0.15.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.
- general_manager/__init__.py +49 -0
- general_manager/api/__init__.py +36 -0
- general_manager/api/graphql.py +92 -43
- general_manager/api/mutation.py +35 -10
- general_manager/api/property.py +26 -3
- general_manager/apps.py +23 -16
- general_manager/bucket/__init__.py +32 -0
- general_manager/bucket/baseBucket.py +76 -64
- general_manager/bucket/calculationBucket.py +188 -108
- general_manager/bucket/databaseBucket.py +130 -49
- general_manager/bucket/groupBucket.py +113 -60
- general_manager/cache/__init__.py +38 -0
- general_manager/cache/cacheDecorator.py +29 -17
- general_manager/cache/cacheTracker.py +34 -15
- general_manager/cache/dependencyIndex.py +117 -33
- general_manager/cache/modelDependencyCollector.py +17 -8
- general_manager/cache/signals.py +17 -6
- general_manager/factory/__init__.py +34 -5
- general_manager/factory/autoFactory.py +57 -60
- general_manager/factory/factories.py +39 -14
- general_manager/factory/factoryMethods.py +38 -1
- general_manager/interface/__init__.py +36 -0
- general_manager/interface/baseInterface.py +71 -27
- general_manager/interface/calculationInterface.py +18 -10
- general_manager/interface/databaseBasedInterface.py +102 -71
- general_manager/interface/databaseInterface.py +66 -20
- general_manager/interface/models.py +10 -4
- general_manager/interface/readOnlyInterface.py +44 -30
- general_manager/manager/__init__.py +36 -3
- general_manager/manager/generalManager.py +73 -47
- general_manager/manager/groupManager.py +72 -17
- general_manager/manager/input.py +23 -15
- general_manager/manager/meta.py +53 -53
- general_manager/measurement/__init__.py +37 -2
- general_manager/measurement/measurement.py +135 -58
- general_manager/measurement/measurementField.py +161 -61
- general_manager/permission/__init__.py +32 -1
- general_manager/permission/basePermission.py +29 -12
- general_manager/permission/managerBasedPermission.py +32 -26
- general_manager/permission/mutationPermission.py +32 -3
- general_manager/permission/permissionChecks.py +9 -1
- general_manager/permission/permissionDataManager.py +49 -15
- general_manager/permission/utils.py +14 -3
- general_manager/rule/__init__.py +27 -1
- general_manager/rule/handler.py +90 -5
- general_manager/rule/rule.py +40 -27
- general_manager/utils/__init__.py +44 -2
- general_manager/utils/argsToKwargs.py +17 -9
- general_manager/utils/filterParser.py +29 -30
- general_manager/utils/formatString.py +2 -0
- general_manager/utils/jsonEncoder.py +14 -1
- general_manager/utils/makeCacheKey.py +18 -12
- general_manager/utils/noneToZero.py +8 -6
- general_manager/utils/pathMapping.py +92 -29
- general_manager/utils/public_api.py +49 -0
- general_manager/utils/testing.py +135 -69
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/METADATA +38 -4
- generalmanager-0.15.0.dist-info/RECORD +62 -0
- generalmanager-0.15.0.dist-info/licenses/LICENSE +21 -0
- generalmanager-0.14.0.dist-info/RECORD +0 -58
- generalmanager-0.14.0.dist-info/licenses/LICENSE +0 -29
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.14.0.dist-info → generalmanager-0.15.0.dist-info}/top_level.txt +0 -0
general_manager/manager/input.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
"""Input field metadata used by GeneralManager interfaces."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
|
-
from typing import Iterable, Optional, Callable, List, TypeVar, Generic, Any
|
4
|
+
from typing import Iterable, Optional, Callable, List, TypeVar, Generic, Any, Type, cast
|
3
5
|
import inspect
|
4
6
|
|
5
7
|
from general_manager.manager.generalManager import GeneralManager
|
@@ -11,43 +13,49 @@ INPUT_TYPE = TypeVar("INPUT_TYPE", bound=type)
|
|
11
13
|
|
12
14
|
|
13
15
|
class Input(Generic[INPUT_TYPE]):
|
16
|
+
"""Descriptor describing the expected type and constraints for an interface input."""
|
17
|
+
|
14
18
|
def __init__(
|
15
19
|
self,
|
16
20
|
type: INPUT_TYPE,
|
17
21
|
possible_values: Optional[Callable | Iterable] = None,
|
18
22
|
depends_on: Optional[List[str]] = None,
|
19
|
-
):
|
23
|
+
) -> None:
|
20
24
|
"""
|
21
25
|
Create an Input specification with type information, allowed values, and dependency metadata.
|
22
|
-
|
26
|
+
|
23
27
|
Parameters:
|
24
|
-
type:
|
25
|
-
possible_values
|
26
|
-
depends_on
|
28
|
+
type (INPUT_TYPE): Expected Python type for the input value.
|
29
|
+
possible_values (Callable | Iterable | None): Allowed values as an iterable or callable returning allowed values.
|
30
|
+
depends_on (list[str] | None): Names of other inputs required for computing possible values.
|
27
31
|
"""
|
28
|
-
self.type = type
|
32
|
+
self.type: Type[Any] = cast(Type[Any], type)
|
29
33
|
self.possible_values = possible_values
|
30
34
|
self.is_manager = issubclass(type, GeneralManager)
|
31
35
|
|
32
36
|
if depends_on is not None:
|
33
|
-
#
|
37
|
+
# Use the provided dependency list when available
|
34
38
|
self.depends_on = depends_on
|
35
39
|
elif callable(possible_values):
|
36
|
-
#
|
40
|
+
# Derive dependencies automatically from the callable signature
|
37
41
|
sig = inspect.signature(possible_values)
|
38
42
|
self.depends_on = list(sig.parameters.keys())
|
39
43
|
else:
|
40
|
-
#
|
44
|
+
# Default to no dependencies when none are provided
|
41
45
|
self.depends_on = []
|
42
46
|
|
43
47
|
def cast(self, value: Any) -> Any:
|
44
48
|
"""
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
Convert a raw value to the configured input type.
|
50
|
+
|
51
|
+
Parameters:
|
52
|
+
value (Any): Raw value supplied by the caller.
|
53
|
+
|
49
54
|
Returns:
|
50
|
-
|
55
|
+
Any: Value converted to the target type.
|
56
|
+
|
57
|
+
Raises:
|
58
|
+
ValueError: If the value cannot be converted to the target type.
|
51
59
|
"""
|
52
60
|
if self.type == date:
|
53
61
|
if isinstance(value, datetime) and type(value) is not date:
|
general_manager/manager/meta.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
+
"""Metaclass infrastructure for registering GeneralManager subclasses."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
|
3
5
|
from django.conf import settings
|
4
|
-
from typing import Any, Type, TYPE_CHECKING,
|
6
|
+
from typing import Any, Type, TYPE_CHECKING, ClassVar, TypeVar, Iterable, cast
|
5
7
|
from general_manager.interface.baseInterface import InterfaceBase
|
6
8
|
|
7
9
|
if TYPE_CHECKING:
|
8
|
-
from general_manager.interface.readOnlyInterface import ReadOnlyInterface
|
9
10
|
from general_manager.manager.generalManager import GeneralManager
|
10
11
|
|
11
12
|
|
@@ -17,32 +18,40 @@ class _nonExistent:
|
|
17
18
|
|
18
19
|
|
19
20
|
class GeneralManagerMeta(type):
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
"""Metaclass responsible for wiring GeneralManager interfaces and registries."""
|
22
|
+
|
23
|
+
all_classes: ClassVar[list[Type[GeneralManager]]] = []
|
24
|
+
read_only_classes: ClassVar[list[Type[GeneralManager]]] = []
|
25
|
+
pending_graphql_interfaces: ClassVar[list[Type[GeneralManager]]] = []
|
26
|
+
pending_attribute_initialization: ClassVar[list[Type[GeneralManager]]] = []
|
24
27
|
Interface: type[InterfaceBase]
|
25
28
|
|
26
|
-
def __new__(
|
29
|
+
def __new__(
|
30
|
+
mcs: type["GeneralManagerMeta"],
|
31
|
+
name: str,
|
32
|
+
bases: tuple[type, ...],
|
33
|
+
attrs: dict[str, Any],
|
34
|
+
) -> type:
|
27
35
|
"""
|
28
|
-
|
36
|
+
Create a new GeneralManager subclass and register its interface hooks.
|
29
37
|
|
30
|
-
|
38
|
+
Parameters:
|
39
|
+
name (str): Name of the class being created.
|
40
|
+
bases (tuple[type, ...]): Base classes inherited by the new class.
|
41
|
+
attrs (dict[str, Any]): Class namespace supplied during creation.
|
31
42
|
|
32
43
|
Returns:
|
33
|
-
|
44
|
+
type: Newly created class augmented with interface integration.
|
34
45
|
"""
|
35
46
|
|
36
47
|
def createNewGeneralManagerClass(
|
37
|
-
mcs
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
"""
|
45
|
-
return super().__new__(mcs, name, bases, attrs)
|
48
|
+
mcs: type["GeneralManagerMeta"],
|
49
|
+
name: str,
|
50
|
+
bases: tuple[type, ...],
|
51
|
+
attrs: dict[str, Any],
|
52
|
+
) -> Type["GeneralManager"]:
|
53
|
+
"""Helper to instantiate the class via the default ``type.__new__``."""
|
54
|
+
return cast(Type["GeneralManager"], type.__new__(mcs, name, bases, attrs))
|
46
55
|
|
47
56
|
if "Interface" in attrs:
|
48
57
|
interface = attrs.pop("Interface")
|
@@ -68,60 +77,51 @@ class GeneralManagerMeta(type):
|
|
68
77
|
@staticmethod
|
69
78
|
def createAtPropertiesForAttributes(
|
70
79
|
attributes: Iterable[str], new_class: Type[GeneralManager]
|
71
|
-
):
|
80
|
+
) -> None:
|
72
81
|
"""
|
73
|
-
|
74
|
-
|
75
|
-
For each attribute, adds a property to the class that:
|
76
|
-
- Returns the field type from the class's interface when accessed on the class itself.
|
77
|
-
- Retrieves the value from the instance's `_attributes` dictionary when accessed on an instance.
|
78
|
-
- If the attribute value is callable, invokes it with the instance's interface and returns the result.
|
79
|
-
- Raises `AttributeError` if the attribute is missing or if an error occurs during callable invocation.
|
80
|
-
|
82
|
+
Attach descriptor-based properties for each attribute declared on the interface.
|
83
|
+
|
81
84
|
Parameters:
|
82
|
-
attributes (Iterable[str]): Names of attributes for which
|
83
|
-
new_class (Type[GeneralManager]):
|
85
|
+
attributes (Iterable[str]): Names of attributes for which descriptors are created.
|
86
|
+
new_class (Type[GeneralManager]): Class receiving the generated descriptors.
|
84
87
|
"""
|
85
88
|
|
86
|
-
def
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
"""
|
89
|
+
def descriptorMethod(
|
90
|
+
attr_name: str,
|
91
|
+
new_class: type,
|
92
|
+
) -> object:
|
93
|
+
"""Create a descriptor that resolves attribute values from the interface at runtime."""
|
92
94
|
|
93
|
-
class Descriptor
|
94
|
-
def __init__(
|
95
|
-
self
|
96
|
-
|
95
|
+
class Descriptor:
|
96
|
+
def __init__(
|
97
|
+
self, descriptor_attr_name: str, descriptor_class: Type[Any]
|
98
|
+
) -> None:
|
99
|
+
self._attr_name = descriptor_attr_name
|
100
|
+
self._class = descriptor_class
|
97
101
|
|
98
102
|
def __get__(
|
99
103
|
self,
|
100
|
-
instance:
|
104
|
+
instance: Any | None,
|
101
105
|
owner: type | None = None,
|
102
|
-
):
|
103
|
-
"""
|
104
|
-
Retrieve the value of a dynamically defined attribute from an instance or its interface.
|
105
|
-
|
106
|
-
When accessed on the class, returns the field type from the associated interface. When accessed on an instance, retrieves the attribute value from the instance's `_attributes` dictionary. If the attribute is callable, it is invoked with the instance's interface. Raises `AttributeError` if the attribute is missing or if a callable attribute raises an exception.
|
107
|
-
"""
|
106
|
+
) -> Any:
|
107
|
+
"""Return the field type on the class or the stored value on an instance."""
|
108
108
|
if instance is None:
|
109
|
-
return self.
|
110
|
-
attribute = instance._attributes.get(
|
109
|
+
return self._class.Interface.getFieldType(self._attr_name)
|
110
|
+
attribute = instance._attributes.get(self._attr_name, _nonExistent)
|
111
111
|
if attribute is _nonExistent:
|
112
112
|
raise AttributeError(
|
113
|
-
f"{self.
|
113
|
+
f"{self._attr_name} not found in {instance.__class__.__name__}"
|
114
114
|
)
|
115
115
|
if callable(attribute):
|
116
116
|
try:
|
117
117
|
attribute = attribute(instance._interface)
|
118
118
|
except Exception as e:
|
119
119
|
raise AttributeError(
|
120
|
-
f"Error calling attribute {self.
|
120
|
+
f"Error calling attribute {self._attr_name}: {e}"
|
121
121
|
) from e
|
122
122
|
return attribute
|
123
123
|
|
124
|
-
return Descriptor(attr_name, new_class)
|
124
|
+
return Descriptor(attr_name, cast(Type[Any], new_class))
|
125
125
|
|
126
126
|
for attr_name in attributes:
|
127
|
-
setattr(new_class, attr_name,
|
127
|
+
setattr(new_class, attr_name, descriptorMethod(attr_name, new_class))
|
@@ -1,2 +1,37 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
"""Public API for measurement utilities."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from general_manager.utils.public_api import build_module_dir, resolve_export
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"Measurement",
|
11
|
+
"MeasurementField",
|
12
|
+
"ureg",
|
13
|
+
"currency_units",
|
14
|
+
]
|
15
|
+
|
16
|
+
_MODULE_MAP = {
|
17
|
+
"Measurement": ("general_manager.measurement.measurement", "Measurement"),
|
18
|
+
"ureg": ("general_manager.measurement.measurement", "ureg"),
|
19
|
+
"currency_units": ("general_manager.measurement.measurement", "currency_units"),
|
20
|
+
"MeasurementField": (
|
21
|
+
"general_manager.measurement.measurementField",
|
22
|
+
"MeasurementField",
|
23
|
+
),
|
24
|
+
}
|
25
|
+
|
26
|
+
|
27
|
+
def __getattr__(name: str) -> Any:
|
28
|
+
return resolve_export(
|
29
|
+
name,
|
30
|
+
module_all=__all__,
|
31
|
+
module_map=_MODULE_MAP,
|
32
|
+
module_globals=globals(),
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
def __dir__() -> list[str]:
|
37
|
+
return build_module_dir(module_all=__all__, module_globals=globals())
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Utility types and helpers for unit-aware measurements."""
|
2
|
+
|
1
3
|
# units.py
|
2
4
|
from __future__ import annotations
|
3
5
|
from typing import Any, Callable
|
@@ -20,14 +22,19 @@ for currency in currency_units:
|
|
20
22
|
|
21
23
|
|
22
24
|
class Measurement:
|
23
|
-
def __init__(self, value: Decimal | float | int | str, unit: str):
|
25
|
+
def __init__(self, value: Decimal | float | int | str, unit: str) -> None:
|
24
26
|
"""
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
Create a measurement from a numeric value and a unit string.
|
28
|
+
|
29
|
+
Parameters:
|
30
|
+
value (Decimal | float | int | str): Numeric value, which will be coerced to `Decimal` if needed.
|
31
|
+
unit (str): Unit name registered in the unit registry, including currencies and physical units.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
None
|
35
|
+
|
29
36
|
Raises:
|
30
|
-
ValueError: If the value cannot be converted to
|
37
|
+
ValueError: If the numeric value cannot be converted to `Decimal`.
|
31
38
|
"""
|
32
39
|
if not isinstance(value, (Decimal, float, int)):
|
33
40
|
try:
|
@@ -38,12 +45,12 @@ class Measurement:
|
|
38
45
|
value = Decimal(str(value))
|
39
46
|
self.__quantity = ureg.Quantity(self.formatDecimal(value), unit)
|
40
47
|
|
41
|
-
def __getstate__(self):
|
48
|
+
def __getstate__(self) -> dict[str, str]:
|
42
49
|
"""
|
43
|
-
|
50
|
+
Produce a serialisable representation of the measurement.
|
44
51
|
|
45
52
|
Returns:
|
46
|
-
dict:
|
53
|
+
dict[str, str]: Mapping with `magnitude` and `unit` entries for pickling.
|
47
54
|
"""
|
48
55
|
state = {
|
49
56
|
"magnitude": str(self.magnitude),
|
@@ -51,12 +58,15 @@ class Measurement:
|
|
51
58
|
}
|
52
59
|
return state
|
53
60
|
|
54
|
-
def __setstate__(self, state):
|
61
|
+
def __setstate__(self, state: dict[str, str]) -> None:
|
55
62
|
"""
|
56
|
-
|
63
|
+
Recreate the internal quantity from a serialized representation.
|
57
64
|
|
58
65
|
Parameters:
|
59
|
-
state (dict):
|
66
|
+
state (dict[str, str]): Serialized state containing `magnitude` and `unit` values.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
None
|
60
70
|
"""
|
61
71
|
value = Decimal(state["magnitude"])
|
62
72
|
unit = state["unit"]
|
@@ -65,30 +75,46 @@ class Measurement:
|
|
65
75
|
@property
|
66
76
|
def quantity(self) -> PlainQuantity:
|
67
77
|
"""
|
68
|
-
|
78
|
+
Access the underlying pint quantity for advanced operations.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
PlainQuantity: Pint quantity representing the measurement value and unit.
|
69
82
|
"""
|
70
83
|
return self.__quantity
|
71
84
|
|
72
85
|
@property
|
73
86
|
def magnitude(self) -> Decimal:
|
87
|
+
"""
|
88
|
+
Fetch the numeric component of the measurement.
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
Decimal: Magnitude of the measurement in its current unit.
|
92
|
+
"""
|
74
93
|
return self.__quantity.magnitude
|
75
94
|
|
76
95
|
@property
|
77
96
|
def unit(self) -> str:
|
97
|
+
"""
|
98
|
+
Retrieve the unit label associated with the measurement.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
str: Canonical unit string as provided by the unit registry.
|
102
|
+
"""
|
78
103
|
return str(self.__quantity.units)
|
79
104
|
|
80
105
|
@classmethod
|
81
106
|
def from_string(cls, value: str) -> Measurement:
|
82
107
|
"""
|
83
|
-
|
108
|
+
Create a measurement from a textual representation of magnitude and unit.
|
84
109
|
|
85
|
-
|
110
|
+
Parameters:
|
111
|
+
value (str): String formatted as `"<number> <unit>"`; a single numeric value is treated as dimensionless.
|
86
112
|
|
87
113
|
Returns:
|
88
|
-
Measurement:
|
114
|
+
Measurement: Measurement parsed from the provided string.
|
89
115
|
|
90
116
|
Raises:
|
91
|
-
ValueError: If the string
|
117
|
+
ValueError: If the string lacks a unit, has too many tokens, or contains a non-numeric magnitude.
|
92
118
|
"""
|
93
119
|
splitted = value.split(" ")
|
94
120
|
if len(splitted) == 1:
|
@@ -104,6 +130,15 @@ class Measurement:
|
|
104
130
|
|
105
131
|
@staticmethod
|
106
132
|
def formatDecimal(value: Decimal) -> Decimal:
|
133
|
+
"""
|
134
|
+
Normalise decimals so integers have no fractional component.
|
135
|
+
|
136
|
+
Parameters:
|
137
|
+
value (Decimal): Decimal value that should be normalised.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Decimal: Normalised decimal with insignificant trailing zeros removed.
|
141
|
+
"""
|
107
142
|
value = value.normalize()
|
108
143
|
if value == value.to_integral_value():
|
109
144
|
try:
|
@@ -113,7 +148,11 @@ class Measurement:
|
|
113
148
|
else:
|
114
149
|
return value
|
115
150
|
|
116
|
-
def to(
|
151
|
+
def to(
|
152
|
+
self,
|
153
|
+
target_unit: str,
|
154
|
+
exchange_rate: float | None = None,
|
155
|
+
) -> Measurement:
|
117
156
|
"""
|
118
157
|
Convert this measurement to a specified target unit, supporting both currency and physical unit conversions.
|
119
158
|
|
@@ -147,21 +186,28 @@ class Measurement:
|
|
147
186
|
unit = str(converted_quantity.units)
|
148
187
|
return Measurement(value, unit)
|
149
188
|
|
150
|
-
def is_currency(self):
|
151
|
-
# Check if the unit is a defined currency
|
189
|
+
def is_currency(self) -> bool:
|
152
190
|
"""
|
153
|
-
|
191
|
+
Determine whether the measurement's unit represents a configured currency.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
bool: True if the unit matches one of the registered currency codes.
|
154
195
|
"""
|
155
196
|
return str(self.unit) in currency_units
|
156
197
|
|
157
198
|
def __add__(self, other: Any) -> Measurement:
|
158
199
|
"""
|
159
|
-
Add
|
200
|
+
Add another measurement while enforcing currency and dimensional rules.
|
160
201
|
|
161
|
-
|
202
|
+
Parameters:
|
203
|
+
other (Any): Measurement or compatible value used as the addend.
|
162
204
|
|
163
205
|
Returns:
|
164
|
-
Measurement:
|
206
|
+
Measurement: Measurement representing the sum.
|
207
|
+
|
208
|
+
Raises:
|
209
|
+
TypeError: If the operand is not a measurement or mixes currency with non-currency units.
|
210
|
+
ValueError: If the operands use incompatible currency codes or physical dimensions.
|
165
211
|
"""
|
166
212
|
if not isinstance(other, Measurement):
|
167
213
|
raise TypeError("Addition is only allowed between Measurement instances.")
|
@@ -194,12 +240,17 @@ class Measurement:
|
|
194
240
|
|
195
241
|
def __sub__(self, other: Any) -> Measurement:
|
196
242
|
"""
|
197
|
-
|
243
|
+
Subtract another measurement while enforcing unit compatibility.
|
198
244
|
|
199
|
-
|
245
|
+
Parameters:
|
246
|
+
other (Any): Measurement or compatible value that should be subtracted.
|
200
247
|
|
201
248
|
Returns:
|
202
|
-
Measurement:
|
249
|
+
Measurement: Measurement representing the difference.
|
250
|
+
|
251
|
+
Raises:
|
252
|
+
TypeError: If the operand is not a measurement or mixes currency with non-currency units.
|
253
|
+
ValueError: If the operands use incompatible currency codes or physical dimensions.
|
203
254
|
"""
|
204
255
|
if not isinstance(other, Measurement):
|
205
256
|
raise TypeError(
|
@@ -226,15 +277,16 @@ class Measurement:
|
|
226
277
|
|
227
278
|
def __mul__(self, other: Any) -> Measurement:
|
228
279
|
"""
|
229
|
-
Multiply
|
230
|
-
|
231
|
-
|
232
|
-
|
280
|
+
Multiply the measurement by another measurement or scalar.
|
281
|
+
|
282
|
+
Parameters:
|
283
|
+
other (Any): Measurement or numeric value used as the multiplier.
|
284
|
+
|
233
285
|
Returns:
|
234
|
-
Measurement:
|
235
|
-
|
286
|
+
Measurement: Product expressed as a measurement.
|
287
|
+
|
236
288
|
Raises:
|
237
|
-
TypeError: If
|
289
|
+
TypeError: If multiplying two currency amounts or using an unsupported type.
|
238
290
|
"""
|
239
291
|
if isinstance(other, Measurement):
|
240
292
|
if self.is_currency() and other.is_currency():
|
@@ -257,17 +309,16 @@ class Measurement:
|
|
257
309
|
|
258
310
|
def __truediv__(self, other: Any) -> Measurement:
|
259
311
|
"""
|
260
|
-
Divide
|
312
|
+
Divide the measurement by another measurement or scalar value.
|
261
313
|
|
262
|
-
|
263
|
-
|
264
|
-
- Division between the *same* currency is allowed and yields a dimensionless result.
|
265
|
-
Returns a new `Measurement` with the resulting value and unit.
|
314
|
+
Parameters:
|
315
|
+
other (Any): Measurement or numeric divisor.
|
266
316
|
|
267
|
-
Raises:
|
268
|
-
TypeError: If dividing two currency measurements with different units, or if the operand is not a `Measurement` or numeric value.
|
269
317
|
Returns:
|
270
|
-
Measurement:
|
318
|
+
Measurement: Quotient expressed as a measurement.
|
319
|
+
|
320
|
+
Raises:
|
321
|
+
TypeError: If dividing currency amounts with different units or using an unsupported type.
|
271
322
|
"""
|
272
323
|
if isinstance(other, Measurement):
|
273
324
|
if self.is_currency() and other.is_currency() and self.unit != other.unit:
|
@@ -288,32 +339,40 @@ class Measurement:
|
|
288
339
|
"Division is only allowed with Measurement or numeric values."
|
289
340
|
)
|
290
341
|
|
291
|
-
def __str__(self):
|
342
|
+
def __str__(self) -> str:
|
292
343
|
"""
|
293
|
-
|
344
|
+
Format the measurement as a string, including the unit when present.
|
345
|
+
|
346
|
+
Returns:
|
347
|
+
str: Text representation of the magnitude and unit.
|
294
348
|
"""
|
295
349
|
if not str(self.unit) == "dimensionless":
|
296
350
|
return f"{self.magnitude} {self.unit}"
|
297
351
|
return f"{self.magnitude}"
|
298
352
|
|
299
|
-
def __repr__(self):
|
353
|
+
def __repr__(self) -> str:
|
300
354
|
"""
|
301
|
-
Return a
|
355
|
+
Return a detailed representation suitable for debugging.
|
356
|
+
|
357
|
+
Returns:
|
358
|
+
str: Debug-friendly notation including magnitude and unit.
|
302
359
|
"""
|
303
360
|
return f"Measurement({self.magnitude}, '{self.unit}')"
|
304
361
|
|
305
362
|
def _compare(self, other: Any, operation: Callable[..., bool]) -> bool:
|
306
363
|
"""
|
307
|
-
|
308
|
-
|
309
|
-
If `other` is a string, it is parsed as a Measurement. Returns `False` if `other` is `None` or an empty value. Raises `TypeError` if `other` is not a Measurement or a valid string. Raises `ValueError` if the measurements have incompatible dimensions.
|
310
|
-
|
364
|
+
Normalise operands into comparable measurements before applying a comparison.
|
365
|
+
|
311
366
|
Parameters:
|
312
|
-
other:
|
313
|
-
operation:
|
314
|
-
|
367
|
+
other (Any): Measurement instance or string representation used for the comparison.
|
368
|
+
operation (Callable[..., bool]): Callable that consumes two magnitudes and returns a comparison result.
|
369
|
+
|
315
370
|
Returns:
|
316
|
-
bool:
|
371
|
+
bool: Outcome of the supplied comparison function.
|
372
|
+
|
373
|
+
Raises:
|
374
|
+
TypeError: If `other` cannot be interpreted as a measurement.
|
375
|
+
ValueError: If the operands use incompatible dimensions.
|
317
376
|
"""
|
318
377
|
if other is None or other in ("", [], (), {}):
|
319
378
|
return False
|
@@ -332,6 +391,15 @@ class Measurement:
|
|
332
391
|
raise ValueError("Cannot compare measurements with different dimensions.")
|
333
392
|
|
334
393
|
def __radd__(self, other: Any) -> Measurement:
|
394
|
+
"""
|
395
|
+
Support sum() by treating zero as a neutral element.
|
396
|
+
|
397
|
+
Parameters:
|
398
|
+
other (Any): Left operand supplied by Python's arithmetic machinery.
|
399
|
+
|
400
|
+
Returns:
|
401
|
+
Measurement: Either `self` or the result of addition.
|
402
|
+
"""
|
335
403
|
if other == 0:
|
336
404
|
return self
|
337
405
|
return self.__add__(other)
|
@@ -354,16 +422,25 @@ class Measurement:
|
|
354
422
|
|
355
423
|
def __ge__(self, other: Any) -> bool:
|
356
424
|
"""
|
357
|
-
|
425
|
+
Check whether the measurement is greater than or equal to another value.
|
426
|
+
|
427
|
+
Parameters:
|
428
|
+
other (Any): Measurement or compatible representation used in the comparison.
|
358
429
|
|
359
|
-
|
430
|
+
Returns:
|
431
|
+
bool: True when the measurement is greater than or equal to `other`.
|
432
|
+
|
433
|
+
Raises:
|
434
|
+
TypeError: If `other` cannot be interpreted as a measurement.
|
435
|
+
ValueError: If units are incompatible.
|
360
436
|
"""
|
361
437
|
return self._compare(other, ge)
|
362
438
|
|
363
439
|
def __hash__(self) -> int:
|
364
440
|
"""
|
365
|
-
|
441
|
+
Compute a hash using the measurement's magnitude and unit.
|
366
442
|
|
367
|
-
|
443
|
+
Returns:
|
444
|
+
int: Stable hash suitable for use in dictionaries and sets.
|
368
445
|
"""
|
369
446
|
return hash((self.magnitude, str(self.unit)))
|