GeneralManager 0.17.0__py3-none-any.whl → 0.18.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.
Potentially problematic release.
This version of GeneralManager might be problematic. Click here for more details.
- general_manager/__init__.py +11 -1
- general_manager/_types/api.py +0 -1
- general_manager/_types/bucket.py +0 -1
- general_manager/_types/cache.py +0 -1
- general_manager/_types/factory.py +0 -1
- general_manager/_types/general_manager.py +0 -1
- general_manager/_types/interface.py +0 -1
- general_manager/_types/manager.py +0 -1
- general_manager/_types/measurement.py +0 -1
- general_manager/_types/permission.py +0 -1
- general_manager/_types/rule.py +0 -1
- general_manager/_types/utils.py +0 -1
- general_manager/api/__init__.py +13 -1
- general_manager/api/graphql.py +356 -221
- general_manager/api/graphql_subscription_consumer.py +81 -78
- general_manager/api/mutation.py +85 -23
- general_manager/api/property.py +39 -13
- general_manager/apps.py +188 -47
- general_manager/bucket/__init__.py +10 -1
- general_manager/bucket/calculationBucket.py +155 -53
- general_manager/bucket/databaseBucket.py +157 -45
- general_manager/bucket/groupBucket.py +133 -44
- general_manager/cache/__init__.py +10 -1
- general_manager/cache/dependencyIndex.py +143 -45
- general_manager/cache/signals.py +9 -2
- general_manager/factory/__init__.py +10 -1
- general_manager/factory/autoFactory.py +55 -13
- general_manager/factory/factories.py +110 -40
- general_manager/factory/factoryMethods.py +122 -34
- general_manager/interface/__init__.py +10 -1
- general_manager/interface/baseInterface.py +129 -36
- general_manager/interface/calculationInterface.py +35 -18
- general_manager/interface/databaseBasedInterface.py +71 -45
- general_manager/interface/databaseInterface.py +96 -38
- general_manager/interface/models.py +5 -5
- general_manager/interface/readOnlyInterface.py +94 -20
- general_manager/manager/__init__.py +10 -1
- general_manager/manager/generalManager.py +25 -16
- general_manager/manager/groupManager.py +20 -6
- general_manager/manager/meta.py +84 -16
- general_manager/measurement/__init__.py +10 -1
- general_manager/measurement/measurement.py +289 -95
- general_manager/measurement/measurementField.py +42 -31
- general_manager/permission/__init__.py +10 -1
- general_manager/permission/basePermission.py +120 -38
- general_manager/permission/managerBasedPermission.py +72 -21
- general_manager/permission/mutationPermission.py +14 -9
- general_manager/permission/permissionChecks.py +14 -12
- general_manager/permission/permissionDataManager.py +24 -11
- general_manager/permission/utils.py +34 -6
- general_manager/public_api_registry.py +36 -10
- general_manager/rule/__init__.py +10 -1
- general_manager/rule/handler.py +133 -44
- general_manager/rule/rule.py +178 -39
- general_manager/utils/__init__.py +10 -1
- general_manager/utils/argsToKwargs.py +34 -9
- general_manager/utils/filterParser.py +22 -7
- general_manager/utils/formatString.py +1 -0
- general_manager/utils/pathMapping.py +23 -15
- general_manager/utils/public_api.py +33 -2
- general_manager/utils/testing.py +31 -33
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/METADATA +2 -1
- generalmanager-0.18.0.dist-info/RECORD +77 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
- generalmanager-0.17.0.dist-info/RECORD +0 -77
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.18.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
|
|
179
|
+
Create a Measurement from a numeric value and a unit label.
|
|
28
180
|
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
43
|
-
raise
|
|
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
|
-
|
|
259
|
+
Parse a textual representation into a Measurement.
|
|
109
260
|
|
|
110
261
|
Parameters:
|
|
111
|
-
value (str):
|
|
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
|
|
265
|
+
Measurement: Measurement constructed from the parsed magnitude and unit.
|
|
115
266
|
|
|
116
267
|
Raises:
|
|
117
|
-
|
|
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
|
|
277
|
+
except InvalidOperation as error:
|
|
278
|
+
raise InvalidDimensionlessValueError() from error
|
|
126
279
|
if len(splitted) != 2:
|
|
127
|
-
raise
|
|
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
|
|
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):
|
|
163
|
-
exchange_rate (float
|
|
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
|
|
317
|
+
Measurement: The measurement expressed in the target unit.
|
|
167
318
|
|
|
168
319
|
Raises:
|
|
169
|
-
|
|
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 ==
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
210
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
380
|
+
raise IncompatibleUnitsError("addition")
|
|
230
381
|
result_quantity = self.quantity + other.quantity
|
|
231
382
|
if not isinstance(result_quantity, pint.Quantity):
|
|
232
|
-
raise
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
253
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
427
|
+
Multiply this measurement by another measurement or by a numeric scalar.
|
|
281
428
|
|
|
282
429
|
Parameters:
|
|
283
|
-
other (
|
|
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:
|
|
433
|
+
Measurement: The product as a Measurement with the resulting magnitude and unit.
|
|
287
434
|
|
|
288
435
|
Raises:
|
|
289
|
-
|
|
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
|
|
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
|
|
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
|
|
456
|
+
Divide this measurement by another measurement or by a numeric scalar.
|
|
313
457
|
|
|
314
458
|
Parameters:
|
|
315
|
-
other (
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
485
|
+
Return a human-readable string of the measurement, including its unit when not dimensionless.
|
|
345
486
|
|
|
346
487
|
Returns:
|
|
347
|
-
|
|
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
|
-
|
|
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
|
|
368
|
-
operation (Callable[..., bool]):
|
|
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:
|
|
512
|
+
bool: Result of applying `operation` to the two magnitudes; `False` for empty/null-like `other`.
|
|
372
513
|
|
|
373
514
|
Raises:
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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
|
|
528
|
+
except pint.DimensionalityError as error:
|
|
529
|
+
raise IncomparableMeasurementError() from error
|
|
392
530
|
|
|
393
531
|
def __radd__(self, other: Any) -> Measurement:
|
|
394
532
|
"""
|
|
395
|
-
|
|
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:
|
|
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)
|