GeneralManager 0.14.1__py3-none-any.whl → 0.15.1__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.
Files changed (62) hide show
  1. general_manager/__init__.py +49 -0
  2. general_manager/api/__init__.py +36 -0
  3. general_manager/api/graphql.py +92 -43
  4. general_manager/api/mutation.py +35 -10
  5. general_manager/api/property.py +26 -3
  6. general_manager/apps.py +23 -16
  7. general_manager/bucket/__init__.py +32 -0
  8. general_manager/bucket/baseBucket.py +76 -64
  9. general_manager/bucket/calculationBucket.py +188 -108
  10. general_manager/bucket/databaseBucket.py +130 -49
  11. general_manager/bucket/groupBucket.py +113 -60
  12. general_manager/cache/__init__.py +38 -0
  13. general_manager/cache/cacheDecorator.py +29 -17
  14. general_manager/cache/cacheTracker.py +34 -15
  15. general_manager/cache/dependencyIndex.py +117 -33
  16. general_manager/cache/modelDependencyCollector.py +17 -8
  17. general_manager/cache/signals.py +17 -6
  18. general_manager/factory/__init__.py +34 -5
  19. general_manager/factory/autoFactory.py +57 -60
  20. general_manager/factory/factories.py +39 -14
  21. general_manager/factory/factoryMethods.py +38 -1
  22. general_manager/interface/__init__.py +36 -0
  23. general_manager/interface/baseInterface.py +71 -27
  24. general_manager/interface/calculationInterface.py +18 -10
  25. general_manager/interface/databaseBasedInterface.py +102 -71
  26. general_manager/interface/databaseInterface.py +66 -20
  27. general_manager/interface/models.py +10 -4
  28. general_manager/interface/readOnlyInterface.py +44 -30
  29. general_manager/manager/__init__.py +36 -3
  30. general_manager/manager/generalManager.py +73 -47
  31. general_manager/manager/groupManager.py +72 -17
  32. general_manager/manager/input.py +23 -15
  33. general_manager/manager/meta.py +53 -53
  34. general_manager/measurement/__init__.py +37 -2
  35. general_manager/measurement/measurement.py +135 -58
  36. general_manager/measurement/measurementField.py +161 -61
  37. general_manager/permission/__init__.py +32 -1
  38. general_manager/permission/basePermission.py +29 -12
  39. general_manager/permission/managerBasedPermission.py +32 -26
  40. general_manager/permission/mutationPermission.py +32 -3
  41. general_manager/permission/permissionChecks.py +9 -1
  42. general_manager/permission/permissionDataManager.py +49 -15
  43. general_manager/permission/utils.py +14 -3
  44. general_manager/rule/__init__.py +27 -1
  45. general_manager/rule/handler.py +90 -5
  46. general_manager/rule/rule.py +40 -27
  47. general_manager/utils/__init__.py +44 -2
  48. general_manager/utils/argsToKwargs.py +17 -9
  49. general_manager/utils/filterParser.py +29 -30
  50. general_manager/utils/formatString.py +2 -0
  51. general_manager/utils/jsonEncoder.py +14 -1
  52. general_manager/utils/makeCacheKey.py +18 -12
  53. general_manager/utils/noneToZero.py +8 -6
  54. general_manager/utils/pathMapping.py +92 -29
  55. general_manager/utils/public_api.py +49 -0
  56. general_manager/utils/testing.py +135 -69
  57. {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/METADATA +10 -2
  58. generalmanager-0.15.1.dist-info/RECORD +62 -0
  59. generalmanager-0.14.1.dist-info/RECORD +0 -58
  60. {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/WHEEL +0 -0
  61. {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/licenses/LICENSE +0 -0
  62. {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/top_level.txt +0 -0
@@ -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: The expected Python type for the input value.
25
- possible_values: Optional; an iterable of allowed values or a callable returning allowed values.
26
- depends_on: Optional; a list of dependency names. If not provided and possible_values is callable, dependencies are inferred from the callable's parameter names.
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
- # Verwende die angegebenen Abhängigkeiten
37
+ # Use the provided dependency list when available
34
38
  self.depends_on = depends_on
35
39
  elif callable(possible_values):
36
- # Ermittele Abhängigkeiten automatisch aus den Parametern der Funktion
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
- # Keine Abhängigkeiten
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
- Converts the input value to the type specified by this Input instance, handling special cases for dates, datetimes, GeneralManager subclasses, and Measurement types.
46
-
47
- If the value is already of the target type, it is returned unchanged. For date and datetime types, string and cross-type conversions are supported. For GeneralManager subclasses, instances are constructed from a dictionary or an ID. For Measurement, string values are parsed accordingly. Otherwise, the value is cast using the target type's constructor.
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
- The value converted to the target type, or an instance of the target type.
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:
@@ -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, Generic, TypeVar, Iterable
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
- all_classes: list[Type[GeneralManager]] = []
21
- read_only_classes: list[Type[GeneralManager]] = []
22
- pending_graphql_interfaces: list[Type[GeneralManager]] = []
23
- pending_attribute_initialization: list[Type[GeneralManager]] = []
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__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type:
29
+ def __new__(
30
+ mcs: type["GeneralManagerMeta"],
31
+ name: str,
32
+ bases: tuple[type, ...],
33
+ attrs: dict[str, Any],
34
+ ) -> type:
27
35
  """
28
- Creates a new class using the metaclass, integrating interface hooks and registering the class for attribute initialization and tracking.
36
+ Create a new GeneralManager subclass and register its interface hooks.
29
37
 
30
- If the class definition includes an `Interface` attribute, validates it as a subclass of `InterfaceBase`, applies pre- and post-creation hooks from the interface, and registers the resulting class for attribute initialization and management. Regardless of interface presence, the new class is tracked for pending GraphQL interface creation.
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
- The newly created class, potentially augmented with interface integration and registration logic.
44
+ type: Newly created class augmented with interface integration.
34
45
  """
35
46
 
36
47
  def createNewGeneralManagerClass(
37
- mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]
38
- ) -> Type[GeneralManager]:
39
- """
40
- Create a new GeneralManager class using the standard metaclass instantiation process.
41
-
42
- Returns:
43
- The newly created GeneralManager subclass.
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
- Dynamically creates and assigns property descriptors to a class for each given attribute name.
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 to create property descriptors.
83
- new_class (Type[GeneralManager]): The class to which the properties will be added.
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 desciptorMethod(attr_name: str, new_class: type):
87
- """
88
- Create a property descriptor for dynamic attribute access and callable resolution.
89
-
90
- When accessed on the class, returns the field type from the class's associated interface. When accessed on an instance, retrieves the attribute value from the instance's `_attributes` dictionary; if the value 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.
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(Generic[GeneralManagerType]):
94
- def __init__(self, attr_name: str, new_class: Type[GeneralManager]):
95
- self.attr_name = attr_name
96
- self.new_class = new_class
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: GeneralManagerType | None,
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.new_class.Interface.getFieldType(self.attr_name)
110
- attribute = instance._attributes.get(attr_name, _nonExistent)
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.attr_name} not found in {instance.__class__.__name__}"
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.attr_name}: {e}"
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, desciptorMethod(attr_name, new_class))
127
+ setattr(new_class, attr_name, descriptorMethod(attr_name, new_class))
@@ -1,2 +1,37 @@
1
- from .measurement import Measurement
2
- from .measurementField import MeasurementField
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
- Initialize a Measurement with a numeric value and unit.
26
-
27
- Converts the provided value to a Decimal and associates it with the specified unit, creating a unit-aware measurement.
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 a Decimal.
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
- Return a dictionary representing the serializable state of the measurement, including its magnitude and unit as strings.
50
+ Produce a serialisable representation of the measurement.
44
51
 
45
52
  Returns:
46
- dict: Contains 'magnitude' and 'unit' keys for serialization purposes.
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
- Restore the Measurement object from a serialized state.
63
+ Recreate the internal quantity from a serialized representation.
57
64
 
58
65
  Parameters:
59
- state (dict): Dictionary with 'magnitude' (as a string) and 'unit' (as a string) representing the measurement.
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
- Return the internal quantity as a `PlainQuantity` object from the `pint` library.
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
- Creates a Measurement instance from a string containing a numeric value and a unit.
108
+ Create a measurement from a textual representation of magnitude and unit.
84
109
 
85
- If the string contains only a value, it is treated as dimensionless. If the string contains both a value and a unit separated by a space, both are used to construct the Measurement. Raises ValueError if the format is invalid or the value cannot be parsed.
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: The constructed Measurement object.
114
+ Measurement: Measurement parsed from the provided string.
89
115
 
90
116
  Raises:
91
- ValueError: If the string format is invalid or the value cannot be parsed as a number.
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(self, target_unit: str, exchange_rate: float | None = None):
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
- Return True if the measurement's unit is one of the defined currency units.
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 this measurement to another, supporting both currency and physical units.
200
+ Add another measurement while enforcing currency and dimensional rules.
160
201
 
161
- Addition is permitted only if both operands are currencies of the same unit or both are physical units with compatible dimensions. Raises a TypeError if operands are of different types (currency vs. physical unit) or not Measurement instances, and raises a ValueError if units are incompatible.
202
+ Parameters:
203
+ other (Any): Measurement or compatible value used as the addend.
162
204
 
163
205
  Returns:
164
- Measurement: A new Measurement representing the sum.
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
- Subtracts another Measurement from this one, enforcing unit compatibility.
243
+ Subtract another measurement while enforcing unit compatibility.
198
244
 
199
- Subtraction is permitted only between two currency measurements of the same unit or two physical measurements with compatible dimensions. Raises a TypeError if the operand is not a Measurement or if subtracting between a currency and a physical unit. Raises a ValueError if subtracting different currencies or incompatible physical units.
245
+ Parameters:
246
+ other (Any): Measurement or compatible value that should be subtracted.
200
247
 
201
248
  Returns:
202
- Measurement: A new Measurement representing the result of the subtraction.
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 this measurement by another measurement or a numeric value.
230
-
231
- Multiplication between two currency measurements is not allowed. When multiplying by another measurement, the resulting measurement combines their units. When multiplying by a numeric value, only the magnitude is scaled.
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: The product as a new Measurement instance.
235
-
286
+ Measurement: Product expressed as a measurement.
287
+
236
288
  Raises:
237
- TypeError: If both operands are currency measurements, or if the operand is neither a Measurement nor a numeric value.
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 this measurement by another measurement or a numeric value.
312
+ Divide the measurement by another measurement or scalar value.
261
313
 
262
- If dividing by another `Measurement`:
263
- - Division between two *different* currencies is disallowed (raises TypeError).
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: The result of the division as a new `Measurement` instance.
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
- Return a string representation of the measurement, including the unit unless it is dimensionless.
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 string representation of the Measurement instance for debugging, showing its magnitude and unit.
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
- Compare this Measurement to another using a specified comparison operation.
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: The object to compare, which can be a Measurement instance or a string in the format "value unit".
313
- operation: A callable that takes two magnitudes and returns a boolean result.
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: The result of applying the comparison operation to the magnitudes.
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
- Return True if this measurement is greater than or equal to another measurement or compatible value.
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
- The comparison converts the other operand to this measurement's unit before evaluating. Raises TypeError if the operand is not a Measurement or convertible string, or ValueError if units are incompatible.
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
- Return a hash based on the measurement's magnitude and unit.
441
+ Compute a hash using the measurement's magnitude and unit.
366
442
 
367
- Enables Measurement instances to be used in hash-based collections.
443
+ Returns:
444
+ int: Stable hash suitable for use in dictionaries and sets.
368
445
  """
369
446
  return hash((self.magnitude, str(self.unit)))