GeneralManager 0.17.0__py3-none-any.whl → 0.19.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.
Files changed (68) hide show
  1. general_manager/__init__.py +11 -1
  2. general_manager/_types/api.py +0 -1
  3. general_manager/_types/bucket.py +0 -1
  4. general_manager/_types/cache.py +0 -1
  5. general_manager/_types/factory.py +0 -1
  6. general_manager/_types/general_manager.py +0 -1
  7. general_manager/_types/interface.py +0 -1
  8. general_manager/_types/manager.py +0 -1
  9. general_manager/_types/measurement.py +0 -1
  10. general_manager/_types/permission.py +0 -1
  11. general_manager/_types/rule.py +0 -1
  12. general_manager/_types/utils.py +0 -1
  13. general_manager/api/__init__.py +13 -1
  14. general_manager/api/graphql.py +356 -221
  15. general_manager/api/graphql_subscription_consumer.py +81 -78
  16. general_manager/api/mutation.py +85 -23
  17. general_manager/api/property.py +39 -13
  18. general_manager/apps.py +188 -47
  19. general_manager/bucket/__init__.py +10 -1
  20. general_manager/bucket/calculationBucket.py +155 -53
  21. general_manager/bucket/databaseBucket.py +157 -45
  22. general_manager/bucket/groupBucket.py +133 -44
  23. general_manager/cache/__init__.py +10 -1
  24. general_manager/cache/cacheDecorator.py +3 -0
  25. general_manager/cache/dependencyIndex.py +143 -45
  26. general_manager/cache/signals.py +9 -2
  27. general_manager/factory/__init__.py +10 -1
  28. general_manager/factory/autoFactory.py +55 -13
  29. general_manager/factory/factories.py +110 -40
  30. general_manager/factory/factoryMethods.py +122 -34
  31. general_manager/interface/__init__.py +10 -1
  32. general_manager/interface/baseInterface.py +129 -36
  33. general_manager/interface/calculationInterface.py +35 -18
  34. general_manager/interface/databaseBasedInterface.py +71 -45
  35. general_manager/interface/databaseInterface.py +96 -38
  36. general_manager/interface/models.py +5 -5
  37. general_manager/interface/readOnlyInterface.py +94 -20
  38. general_manager/manager/__init__.py +10 -1
  39. general_manager/manager/generalManager.py +25 -16
  40. general_manager/manager/groupManager.py +20 -6
  41. general_manager/manager/meta.py +84 -16
  42. general_manager/measurement/__init__.py +10 -1
  43. general_manager/measurement/measurement.py +289 -95
  44. general_manager/measurement/measurementField.py +42 -31
  45. general_manager/permission/__init__.py +10 -1
  46. general_manager/permission/basePermission.py +120 -38
  47. general_manager/permission/managerBasedPermission.py +72 -21
  48. general_manager/permission/mutationPermission.py +14 -9
  49. general_manager/permission/permissionChecks.py +14 -12
  50. general_manager/permission/permissionDataManager.py +24 -11
  51. general_manager/permission/utils.py +34 -6
  52. general_manager/public_api_registry.py +36 -10
  53. general_manager/rule/__init__.py +10 -1
  54. general_manager/rule/handler.py +133 -44
  55. general_manager/rule/rule.py +178 -39
  56. general_manager/utils/__init__.py +10 -1
  57. general_manager/utils/argsToKwargs.py +34 -9
  58. general_manager/utils/filterParser.py +22 -7
  59. general_manager/utils/formatString.py +1 -0
  60. general_manager/utils/pathMapping.py +23 -15
  61. general_manager/utils/public_api.py +33 -2
  62. general_manager/utils/testing.py +31 -33
  63. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/METADATA +3 -1
  64. generalmanager-0.19.0.dist-info/RECORD +77 -0
  65. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/licenses/LICENSE +1 -1
  66. generalmanager-0.17.0.dist-info/RECORD +0 -77
  67. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/WHEEL +0 -0
  68. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,7 @@ from pint.facets.plain import PlainQuantity
12
12
  getcontext().prec = 28
13
13
 
14
14
  # Create a new UnitRegistry
15
- ureg = pint.UnitRegistry(auto_reduce_dimensions=True)
15
+ ureg = pint.UnitRegistry(auto_reduce_dimensions=True) # type: ignore
16
16
 
17
17
  # Define currency units
18
18
  currency_units = ["EUR", "USD", "GBP", "JPY", "CHF", "AUD", "CAD"]
@@ -21,26 +21,177 @@ for currency in currency_units:
21
21
  ureg.define(f"{currency} = [{currency}]")
22
22
 
23
23
 
24
+ class InvalidMeasurementInitializationError(ValueError):
25
+ """Raised when a measurement cannot be constructed from the provided value."""
26
+
27
+ def __init__(self) -> None:
28
+ """
29
+ Exception raised when a Measurement cannot be constructed from the provided value.
30
+
31
+ This error indicates the initializer received a value that is not a Decimal, float, int, or otherwise compatible numeric type suitable for constructing a Measurement.
32
+ """
33
+ super().__init__("Value must be a Decimal, float, int or compatible.")
34
+
35
+
36
+ class InvalidDimensionlessValueError(ValueError):
37
+ """Raised when parsing a dimensionless measurement with an invalid value."""
38
+
39
+ def __init__(self) -> None:
40
+ """
41
+ Initialize the exception indicating an invalid or malformed dimensionless measurement value.
42
+
43
+ The exception carries a default message: "Invalid value for dimensionless measurement."
44
+ """
45
+ super().__init__("Invalid value for dimensionless measurement.")
46
+
47
+
48
+ class InvalidMeasurementStringError(ValueError):
49
+ """Raised when a measurement string is not in the expected format."""
50
+
51
+ def __init__(self) -> None:
52
+ """
53
+ Exception raised when a measurement string is not in the expected "<value> <unit>" format.
54
+
55
+ Initializes the exception with the default message: "String must be in the format 'value unit'."
56
+ """
57
+ super().__init__("String must be in the format 'value unit'.")
58
+
59
+
60
+ class MissingExchangeRateError(ValueError):
61
+ """Raised when a currency conversion lacks a required exchange rate."""
62
+
63
+ def __init__(self) -> None:
64
+ """
65
+ Exception raised when a currency-to-currency conversion is attempted without an exchange rate.
66
+
67
+ This exception indicates that an explicit exchange rate is required to convert between two different currency units.
68
+ """
69
+ super().__init__("Conversion between currencies requires an exchange rate.")
70
+
71
+
72
+ class MeasurementOperandTypeError(TypeError):
73
+ """Raised when arithmetic operations receive non-measurement operands."""
74
+
75
+ def __init__(self, operation: str) -> None:
76
+ """
77
+ Create an exception indicating an arithmetic operation was attempted with a non-Measurement operand.
78
+
79
+ Parameters:
80
+ operation (str): The name of the operation (e.g., '+', '-', '*', '/') used to format the exception message.
81
+ """
82
+ super().__init__(f"{operation} is only allowed between Measurement instances.")
83
+
84
+
85
+ class CurrencyMismatchError(ValueError):
86
+ """Raised when performing arithmetic between mismatched currencies."""
87
+
88
+ def __init__(self, operation: str) -> None:
89
+ """
90
+ Initialize the exception with a message describing the attempted currency operation that is disallowed.
91
+
92
+ Parameters:
93
+ operation (str): Name of the attempted operation (e.g., "add", "divide") used to construct the error message.
94
+ """
95
+ super().__init__(f"{operation} between different currencies is not allowed.")
96
+
97
+
98
+ class IncompatibleUnitsError(ValueError):
99
+ """Raised when operations involve incompatible physical units."""
100
+
101
+ def __init__(self, operation: str) -> None:
102
+ """
103
+ Initialize the exception indicating that two units are incompatible for a given operation.
104
+
105
+ Parameters:
106
+ operation (str): Name or description of the operation that failed due to incompatible units (e.g., 'addition', 'comparison').
107
+ """
108
+ super().__init__(f"Units are not compatible for {operation}.")
109
+
110
+
111
+ class MixedUnitOperationError(TypeError):
112
+ """Raised when mixing currency and physical units in arithmetic."""
113
+
114
+ def __init__(self, operation: str) -> None:
115
+ """
116
+ Create a MixedUnitOperationError indicating an attempted operation mixing currency and physical units.
117
+
118
+ Parameters:
119
+ operation (str): The name of the attempted operation (e.g., "addition", "multiplication"); used to build the exception message.
120
+ """
121
+ super().__init__(
122
+ f"{operation} between currency and physical unit is not allowed."
123
+ )
124
+
125
+
126
+ class CurrencyScalarOperationError(TypeError):
127
+ """Raised when multiplication/division uses unsupported currency operands."""
128
+
129
+ def __init__(self, operation: str) -> None:
130
+ """
131
+ Exception raised when attempting an arithmetic operation between two currency amounts that is not allowed.
132
+
133
+ Parameters:
134
+ operation (str): The name of the attempted operation (e.g., "multiplication", "division"); used to compose the exception message.
135
+ """
136
+ super().__init__(f"{operation} between two currency amounts is not allowed.")
137
+
138
+
139
+ class MeasurementScalarTypeError(TypeError):
140
+ """Raised when operations expect a measurement or numeric operand."""
141
+
142
+ def __init__(self, operation: str) -> None:
143
+ """
144
+ Initialize the exception indicating an invalid operand type for the specified operation.
145
+
146
+ Parameters:
147
+ operation (str): Name of the operation that only accepts Measurement or numeric operands; used to construct the exception message.
148
+ """
149
+ super().__init__(
150
+ f"{operation} is only allowed with Measurement or numeric values."
151
+ )
152
+
153
+
154
+ class UnsupportedComparisonError(TypeError):
155
+ """Raised when comparing measurements with non-measurement types."""
156
+
157
+ def __init__(self) -> None:
158
+ """
159
+ Initialize the exception with a fixed message indicating comparisons require Measurement instances.
160
+
161
+ This constructor sets the exception's message to "Comparison is only allowed between Measurement instances."
162
+ """
163
+ super().__init__("Comparison is only allowed between Measurement instances.")
164
+
165
+
166
+ class IncomparableMeasurementError(ValueError):
167
+ """Raised when measurements of different dimensions are compared."""
168
+
169
+ def __init__(self) -> None:
170
+ """
171
+ Raised when attempting to compare two measurements whose units belong to different physical dimensions (for example, length vs mass), indicating they are not comparable.
172
+ """
173
+ super().__init__("Cannot compare measurements with different dimensions.")
174
+
175
+
24
176
  class Measurement:
25
177
  def __init__(self, value: Decimal | float | int | str, unit: str) -> None:
26
178
  """
27
- Create a measurement from a numeric value and a unit string.
179
+ Create a Measurement from a numeric value and a unit label.
28
180
 
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.
181
+ Converts the provided numeric-like value to a Decimal and constructs the internal quantity using the given unit.
32
182
 
33
- Returns:
34
- None
183
+ Parameters:
184
+ value (Decimal | float | int | str): Numeric value to use as the measurement magnitude; strings and numeric types are coerced to Decimal.
185
+ unit (str): Unit label registered in the module's unit registry (currency codes or physical unit names).
35
186
 
36
187
  Raises:
37
- ValueError: If the numeric value cannot be converted to `Decimal`.
188
+ InvalidMeasurementInitializationError: If `value` cannot be converted to a Decimal.
38
189
  """
39
190
  if not isinstance(value, (Decimal, float, int)):
40
191
  try:
41
192
  value = Decimal(str(value))
42
- except Exception:
43
- raise ValueError("Value must be a Decimal, float, int or compatible.")
193
+ except (InvalidOperation, TypeError, ValueError) as error:
194
+ raise InvalidMeasurementInitializationError() from error
44
195
  if not isinstance(value, Decimal):
45
196
  value = Decimal(str(value))
46
197
  self.__quantity = ureg.Quantity(self.formatDecimal(value), unit)
@@ -105,26 +256,28 @@ class Measurement:
105
256
  @classmethod
106
257
  def from_string(cls, value: str) -> Measurement:
107
258
  """
108
- Create a measurement from a textual representation of magnitude and unit.
259
+ Parse a textual representation into a Measurement.
109
260
 
110
261
  Parameters:
111
- value (str): String formatted as `"<number> <unit>"`; a single numeric value is treated as dimensionless.
262
+ value (str): A string in the form "<number> <unit>" or a single numeric token for a dimensionless value.
112
263
 
113
264
  Returns:
114
- Measurement: Measurement parsed from the provided string.
265
+ Measurement: Measurement constructed from the parsed magnitude and unit.
115
266
 
116
267
  Raises:
117
- ValueError: If the string lacks a unit, has too many tokens, or contains a non-numeric magnitude.
268
+ InvalidDimensionlessValueError: If a single-token input cannot be parsed as a number.
269
+ InvalidMeasurementStringError: If the string does not contain exactly one or two space-separated tokens.
270
+ InvalidMeasurementInitializationError: If constructing the Measurement from the parsed parts fails.
118
271
  """
119
272
  splitted = value.split(" ")
120
273
  if len(splitted) == 1:
121
274
  # If only one part, assume it's a dimensionless value
122
275
  try:
123
276
  return cls(Decimal(splitted[0]), "dimensionless")
124
- except InvalidOperation:
125
- raise ValueError("Invalid value for dimensionless measurement.")
277
+ except InvalidOperation as error:
278
+ raise InvalidDimensionlessValueError() from error
126
279
  if len(splitted) != 2:
127
- raise ValueError("String must be in the format 'value unit'.")
280
+ raise InvalidMeasurementStringError()
128
281
  value, unit = splitted
129
282
  return cls(value, unit)
130
283
 
@@ -154,31 +307,27 @@ class Measurement:
154
307
  exchange_rate: float | None = None,
155
308
  ) -> Measurement:
156
309
  """
157
- Convert this measurement to a specified target unit, supporting both currency and physical unit conversions.
158
-
159
- For currency conversions between different currencies, an explicit exchange rate must be provided; if converting to the same currency, the original measurement is returned. For physical units, standard unit conversion is performed using the unit registry.
310
+ Convert this measurement to the specified target unit, handling currency conversions when applicable.
160
311
 
161
312
  Parameters:
162
- target_unit (str): The unit to convert to.
163
- exchange_rate (float, optional): Required for currency conversion between different currencies.
313
+ target_unit (str): Unit label or currency code to convert the measurement into.
314
+ exchange_rate (float | None): Exchange rate to use when converting between different currencies; ignored for same-currency conversions and physical-unit conversions.
164
315
 
165
316
  Returns:
166
- Measurement: The converted measurement in the target unit.
317
+ Measurement: The measurement expressed in the target unit.
167
318
 
168
319
  Raises:
169
- ValueError: If converting between different currencies without an exchange rate.
320
+ MissingExchangeRateError: If converting between two different currencies without providing an exchange rate.
170
321
  """
171
322
  if self.is_currency():
172
- if self.unit == ureg(target_unit):
323
+ if str(self.unit) == str(target_unit):
173
324
  return self # Same currency, no conversion needed
174
325
  elif exchange_rate is not None:
175
326
  # Convert using the provided exchange rate
176
327
  value = self.magnitude * Decimal(str(exchange_rate))
177
328
  return Measurement(value, target_unit)
178
329
  else:
179
- raise ValueError(
180
- "Conversion between currencies requires an exchange rate."
181
- )
330
+ raise MissingExchangeRateError()
182
331
  else:
183
332
  # Standard conversion for physical units
184
333
  converted_quantity: pint.Quantity = self.quantity.to(target_unit) # type: ignore
@@ -197,102 +346,99 @@ class Measurement:
197
346
 
198
347
  def __add__(self, other: Any) -> Measurement:
199
348
  """
200
- Add another measurement while enforcing currency and dimensional rules.
349
+ Return the sum of this Measurement and another Measurement while enforcing currency and dimensional rules.
350
+
351
+ If both operands are currency units their currency codes must match. If both are physical units their dimensionalities must match. Mixing currency and physical units is not permitted.
201
352
 
202
353
  Parameters:
203
- other (Any): Measurement or compatible value used as the addend.
354
+ other (Measurement): The addend measurement.
204
355
 
205
356
  Returns:
206
- Measurement: Measurement representing the sum.
357
+ Measurement: A new Measurement representing the sum.
207
358
 
208
359
  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.
360
+ MeasurementOperandTypeError: If `other` is not a Measurement.
361
+ CurrencyMismatchError: If both operands are currencies with different currency codes.
362
+ IncompatibleUnitsError: If both operands are physical units but have different dimensionalities or the result cannot be represented as a pint.Quantity.
363
+ MixedUnitOperationError: If one operand is a currency and the other is a physical unit.
211
364
  """
212
365
  if not isinstance(other, Measurement):
213
- raise TypeError("Addition is only allowed between Measurement instances.")
366
+ raise MeasurementOperandTypeError("Addition")
214
367
  if self.is_currency() and other.is_currency():
215
368
  # Both are currencies
216
369
  if self.unit != other.unit:
217
- raise ValueError(
218
- "Addition between different currencies is not allowed."
219
- )
370
+ raise CurrencyMismatchError("Addition")
220
371
  result_quantity = self.quantity + other.quantity
221
372
  if not isinstance(result_quantity, pint.Quantity):
222
- raise ValueError("Units are not compatible for addition.")
373
+ raise IncompatibleUnitsError("addition")
223
374
  return Measurement(
224
375
  Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
225
376
  )
226
377
  elif not self.is_currency() and not other.is_currency():
227
378
  # Both are physical units
228
379
  if self.quantity.dimensionality != other.quantity.dimensionality:
229
- raise ValueError("Units are not compatible for addition.")
380
+ raise IncompatibleUnitsError("addition")
230
381
  result_quantity = self.quantity + other.quantity
231
382
  if not isinstance(result_quantity, pint.Quantity):
232
- raise ValueError("Units are not compatible for addition.")
383
+ raise IncompatibleUnitsError("addition")
233
384
  return Measurement(
234
385
  Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
235
386
  )
236
387
  else:
237
- raise TypeError(
238
- "Addition between currency and physical unit is not allowed."
239
- )
388
+ raise MixedUnitOperationError("Addition")
240
389
 
241
390
  def __sub__(self, other: Any) -> Measurement:
242
391
  """
243
- Subtract another measurement while enforcing unit compatibility.
392
+ Subtract another Measurement from this one, enforcing currency and unit compatibility.
393
+
394
+ Performs subtraction for two currency Measurements only when they share the same currency code, or for two physical Measurements only when they have the same dimensionality; mixing currency and physical units is disallowed.
244
395
 
245
396
  Parameters:
246
- other (Any): Measurement or compatible value that should be subtracted.
397
+ other (Measurement): The measurement to subtract from this measurement.
247
398
 
248
399
  Returns:
249
- Measurement: Measurement representing the difference.
400
+ Measurement: A new Measurement representing the difference.
250
401
 
251
402
  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.
403
+ MeasurementOperandTypeError: If `other` is not a Measurement.
404
+ CurrencyMismatchError: If both operands are currencies but use different currency codes.
405
+ IncompatibleUnitsError: If both operands are physical units but have incompatible dimensionality.
406
+ MixedUnitOperationError: If one operand is a currency and the other is a physical unit.
254
407
  """
255
408
  if not isinstance(other, Measurement):
256
- raise TypeError(
257
- "Subtraction is only allowed between Measurement instances."
258
- )
409
+ raise MeasurementOperandTypeError("Subtraction")
259
410
  if self.is_currency() and other.is_currency():
260
411
  # Both are currencies
261
412
  if self.unit != other.unit:
262
- raise ValueError(
263
- "Subtraction between different currencies is not allowed."
264
- )
413
+ raise CurrencyMismatchError("Subtraction")
265
414
  result_quantity = self.quantity - other.quantity
266
415
  return Measurement(Decimal(str(result_quantity.magnitude)), str(self.unit))
267
416
  elif not self.is_currency() and not other.is_currency():
268
417
  # Both are physical units
269
418
  if self.quantity.dimensionality != other.quantity.dimensionality:
270
- raise ValueError("Units are not compatible for subtraction.")
419
+ raise IncompatibleUnitsError("subtraction")
271
420
  result_quantity = self.quantity - other.quantity
272
421
  return Measurement(Decimal(str(result_quantity.magnitude)), str(self.unit))
273
422
  else:
274
- raise TypeError(
275
- "Subtraction between currency and physical unit is not allowed."
276
- )
423
+ raise MixedUnitOperationError("Subtraction")
277
424
 
278
425
  def __mul__(self, other: Any) -> Measurement:
279
426
  """
280
- Multiply the measurement by another measurement or scalar.
427
+ Multiply this measurement by another measurement or by a numeric scalar.
281
428
 
282
429
  Parameters:
283
- other (Any): Measurement or numeric value used as the multiplier.
430
+ other (Measurement | Decimal | float | int): The multiplier. When a Measurement is provided, units are combined according to unit algebra; when a numeric scalar is provided, the magnitude is scaled and the unit is preserved.
284
431
 
285
432
  Returns:
286
- Measurement: Product expressed as a measurement.
433
+ Measurement: The product as a Measurement with the resulting magnitude and unit.
287
434
 
288
435
  Raises:
289
- TypeError: If multiplying two currency amounts or using an unsupported type.
436
+ CurrencyScalarOperationError: If both operands are currency measurements (multiplying two currencies is not allowed).
437
+ MeasurementScalarTypeError: If `other` is not a Measurement or a supported numeric type.
290
438
  """
291
439
  if isinstance(other, Measurement):
292
440
  if self.is_currency() and other.is_currency():
293
- raise TypeError(
294
- "Multiplication between two currency amounts is not allowed."
295
- )
441
+ raise CurrencyScalarOperationError("Multiplication")
296
442
  result_quantity = self.quantity * other.quantity
297
443
  return Measurement(
298
444
  Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
@@ -303,28 +449,25 @@ class Measurement:
303
449
  result_quantity = self.quantity * other
304
450
  return Measurement(Decimal(str(result_quantity.magnitude)), str(self.unit))
305
451
  else:
306
- raise TypeError(
307
- "Multiplication is only allowed with Measurement or numeric values."
308
- )
452
+ raise MeasurementScalarTypeError("Multiplication")
309
453
 
310
454
  def __truediv__(self, other: Any) -> Measurement:
311
455
  """
312
- Divide the measurement by another measurement or scalar value.
456
+ Divide this measurement by another measurement or by a numeric scalar.
313
457
 
314
458
  Parameters:
315
- other (Any): Measurement or numeric divisor.
459
+ other (Measurement | Decimal | float | int): The divisor; when a Measurement, must be compatible (currencies require same unit).
316
460
 
317
461
  Returns:
318
- Measurement: Quotient expressed as a measurement.
462
+ Measurement: The quotient as a new Measurement. If `other` is a Measurement the result carries the derived units; if `other` is a scalar the result retains this measurement's unit.
319
463
 
320
464
  Raises:
321
- TypeError: If dividing currency amounts with different units or using an unsupported type.
465
+ CurrencyMismatchError: If both operands are currencies with different units.
466
+ MeasurementScalarTypeError: If `other` is not a Measurement or a numeric type.
322
467
  """
323
468
  if isinstance(other, Measurement):
324
469
  if self.is_currency() and other.is_currency() and self.unit != other.unit:
325
- raise TypeError(
326
- "Division between two different currency amounts is not allowed."
327
- )
470
+ raise CurrencyMismatchError("Division")
328
471
  result_quantity = self.quantity / other.quantity
329
472
  return Measurement(
330
473
  Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
@@ -335,16 +478,14 @@ class Measurement:
335
478
  result_quantity = self.quantity / other
336
479
  return Measurement(Decimal(str(result_quantity.magnitude)), str(self.unit))
337
480
  else:
338
- raise TypeError(
339
- "Division is only allowed with Measurement or numeric values."
340
- )
481
+ raise MeasurementScalarTypeError("Division")
341
482
 
342
483
  def __str__(self) -> str:
343
484
  """
344
- Format the measurement as a string, including the unit when present.
485
+ Return a human-readable string of the measurement, including its unit when not dimensionless.
345
486
 
346
487
  Returns:
347
- str: Text representation of the magnitude and unit.
488
+ A string formatted as "<magnitude> <unit>" for measurements with a unit, or as "<magnitude>" for dimensionless measurements.
348
489
  """
349
490
  if not str(self.unit) == "dimensionless":
350
491
  return f"{self.magnitude} {self.unit}"
@@ -361,49 +502,102 @@ class Measurement:
361
502
 
362
503
  def _compare(self, other: Any, operation: Callable[..., bool]) -> bool:
363
504
  """
364
- Normalise operands into comparable measurements before applying a comparison.
505
+ Compare this measurement to another value by normalizing both to the same unit and applying a comparison operation.
365
506
 
366
507
  Parameters:
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.
508
+ other (Any): A Measurement instance or a string parseable by Measurement.from_string; empty or null-like values return False.
509
+ operation (Callable[..., bool]): A callable that accepts two magnitudes (self and other, after unit normalization) and returns a boolean result.
369
510
 
370
511
  Returns:
371
- bool: Outcome of the supplied comparison function.
512
+ bool: Result of applying `operation` to the two magnitudes; `False` for empty/null-like `other`.
372
513
 
373
514
  Raises:
374
- TypeError: If `other` cannot be interpreted as a measurement.
375
- ValueError: If the operands use incompatible dimensions.
515
+ UnsupportedComparisonError: If `other` cannot be interpreted as a Measurement.
516
+ IncomparableMeasurementError: If the two measurements have incompatible dimensions and cannot be converted to the same unit.
376
517
  """
377
518
  if other is None or other in ("", [], (), {}):
378
519
  return False
379
520
  if isinstance(other, str):
380
521
  other = Measurement.from_string(other)
381
522
 
382
- # Überprüfen, ob `other` ein Measurement-Objekt ist
383
523
  if not isinstance(other, Measurement):
384
- raise TypeError("Comparison is only allowed between Measurement instances.")
524
+ raise UnsupportedComparisonError()
385
525
  try:
386
- # Convert `other` to the same units as `self`
387
526
  other_converted: pint.Quantity = other.quantity.to(self.unit) # type: ignore
388
- # Apply the comparison operation
389
527
  return operation(self.magnitude, other_converted.magnitude)
390
- except pint.DimensionalityError:
391
- raise ValueError("Cannot compare measurements with different dimensions.")
528
+ except pint.DimensionalityError as error:
529
+ raise IncomparableMeasurementError() from error
392
530
 
393
531
  def __radd__(self, other: Any) -> Measurement:
394
532
  """
395
- Support sum() by treating zero as a neutral element.
533
+ Allow right-side addition so sum() treats 0 as the neutral element.
396
534
 
397
535
  Parameters:
398
- other (Any): Left operand supplied by Python's arithmetic machinery.
536
+ other (Any): Left operand supplied by Python's arithmetic machinery; typically 0 when used with sum().
399
537
 
400
538
  Returns:
401
- Measurement: Either `self` or the result of addition.
539
+ Measurement: `self` if `other` is 0, otherwise the result of adding `other` to `self`.
402
540
  """
403
541
  if other == 0:
404
542
  return self
405
543
  return self.__add__(other)
406
544
 
545
+ def __rsub__(self, other: Any) -> Measurement:
546
+ """
547
+ Support right-side subtraction.
548
+
549
+ Parameters:
550
+ other (Any): Left operand supplied by Python's arithmetic machinery; typically 0 when used with sum().
551
+
552
+ Returns:
553
+ Measurement: Result of subtracting `self` from `other`.
554
+
555
+ Raises:
556
+ TypeError: If `other` is not a Measurement instance.
557
+ """
558
+ if other == 0:
559
+ return self * -1
560
+ if not isinstance(other, Measurement):
561
+ raise MeasurementOperandTypeError("Subtraction")
562
+ return other.__sub__(self)
563
+
564
+ def __rmul__(self, other: Any) -> Measurement:
565
+ """
566
+ Support right-side multiplication.
567
+
568
+ Parameters:
569
+ other (Any): Left operand supplied by Python's arithmetic machinery.
570
+
571
+ Returns:
572
+ Measurement: Result of multiplying `other` by `self`.
573
+ """
574
+ return self.__mul__(other)
575
+
576
+ def __rtruediv__(self, other: Any) -> Measurement:
577
+ """
578
+ Support right-side division.
579
+
580
+ Parameters:
581
+ other (Any): Left operand supplied by Python's arithmetic machinery.
582
+
583
+ Returns:
584
+ Measurement: Result of dividing `other` by `self`.
585
+
586
+ Raises:
587
+ TypeError: If `other` is not a Measurement instance.
588
+ """
589
+ if isinstance(other, (Decimal, float, int)):
590
+ if not isinstance(other, Decimal):
591
+ other = Decimal(str(other))
592
+ result_quantity = other / self.quantity
593
+ return Measurement(
594
+ Decimal(str(result_quantity.magnitude)), str(result_quantity.units)
595
+ )
596
+
597
+ if not isinstance(other, Measurement):
598
+ raise MeasurementOperandTypeError("Division")
599
+ return other.__truediv__(self)
600
+
407
601
  # Comparison Operators
408
602
  def __eq__(self, other: Any) -> bool:
409
603
  return self._compare(other, eq)