python-units 0.1.3__py3-none-any.whl → 0.3.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.
adapters/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Adapter-layer package reserved for external integrations."""
api/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Curated API exports for the units package."""
2
+
3
+ from .public import *
api/public.py ADDED
@@ -0,0 +1,110 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Public export surface for the units package."""
3
+
4
+ from api.si import (
5
+ ampere,
6
+ becquerel,
7
+ candela,
8
+ coulomb,
9
+ degree_celcius,
10
+ farad,
11
+ gray,
12
+ henry,
13
+ hertz,
14
+ joule,
15
+ katal,
16
+ kelvin,
17
+ kilogram,
18
+ lumen,
19
+ lux,
20
+ metre,
21
+ mole,
22
+ newton,
23
+ ohm,
24
+ pascal,
25
+ radian,
26
+ second,
27
+ siemens,
28
+ sievert,
29
+ steradian,
30
+ tesla,
31
+ volt,
32
+ watt,
33
+ weber,
34
+ )
35
+ from core.errors import (
36
+ InvalidUnitError,
37
+ InvalidValueError,
38
+ UnitCompatibilityError,
39
+ UnitOperandError,
40
+ UnitsError,
41
+ )
42
+ from core.quantity import (
43
+ Quantity,
44
+ complex_quantity,
45
+ complex_unit,
46
+ float_quantity,
47
+ float_unit,
48
+ int_quantity,
49
+ int_unit,
50
+ long_quantity,
51
+ long_unit,
52
+ )
53
+ from core.unit_definitions import BaseUnit, CustomUnitBase, DerivedUnit, SIUnit
54
+ from models.dimension import Dimension, DimensionSystem
55
+
56
+
57
+ Unit = Quantity
58
+
59
+ __all__ = [
60
+ "Dimension",
61
+ "DimensionSystem",
62
+ "Quantity",
63
+ "Unit",
64
+ "UnitsError",
65
+ "InvalidUnitError",
66
+ "InvalidValueError",
67
+ "UnitCompatibilityError",
68
+ "UnitOperandError",
69
+ "BaseUnit",
70
+ "CustomUnitBase",
71
+ "DerivedUnit",
72
+ "SIUnit",
73
+ "ampere",
74
+ "becquerel",
75
+ "candela",
76
+ "complex_quantity",
77
+ "complex_unit",
78
+ "coulomb",
79
+ "degree_celcius",
80
+ "farad",
81
+ "float_quantity",
82
+ "float_unit",
83
+ "gray",
84
+ "henry",
85
+ "hertz",
86
+ "int_quantity",
87
+ "int_unit",
88
+ "joule",
89
+ "katal",
90
+ "kelvin",
91
+ "kilogram",
92
+ "long_quantity",
93
+ "long_unit",
94
+ "lumen",
95
+ "lux",
96
+ "metre",
97
+ "mole",
98
+ "newton",
99
+ "ohm",
100
+ "pascal",
101
+ "radian",
102
+ "second",
103
+ "siemens",
104
+ "sievert",
105
+ "steradian",
106
+ "tesla",
107
+ "volt",
108
+ "watt",
109
+ "weber",
110
+ ]
api/si.py ADDED
@@ -0,0 +1,88 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Predefined SI base units and common derived units."""
3
+
4
+ from core.unit_definitions import DerivedUnit, SIUnit, register_canonical_unit
5
+
6
+ ampere = SIUnit.define("A")
7
+ candela = SIUnit.define("cd")
8
+ kelvin = SIUnit.define("K")
9
+ kilogram = SIUnit.define("kg")
10
+ metre = SIUnit.define("m")
11
+ mole = SIUnit.define("mol")
12
+ second = SIUnit.define("s")
13
+
14
+ for unit in (ampere, candela, kelvin, kilogram, metre, mole, second):
15
+ register_canonical_unit(unit)
16
+
17
+ radian = DerivedUnit.define("rad", metre / metre)
18
+ steradian = DerivedUnit.define("sr", metre * metre / metre / metre)
19
+ hertz = DerivedUnit.define("Hz", SIUnit() / second)
20
+ newton = DerivedUnit.define("N", kilogram * metre / second / second)
21
+ pascal = DerivedUnit.define("Pa", newton / metre / metre)
22
+ joule = DerivedUnit.define("J", newton * metre)
23
+ watt = DerivedUnit.define("W", joule / second)
24
+ coulomb = DerivedUnit.define("C", second * ampere)
25
+ volt = DerivedUnit.define("V", watt / ampere)
26
+ farad = DerivedUnit.define("F", coulomb / volt)
27
+ ohm = DerivedUnit.define("Ω", volt / ampere)
28
+ siemens = DerivedUnit.define("S", ampere / volt)
29
+ weber = DerivedUnit.define("Wb", volt * second)
30
+ tesla = DerivedUnit.define("T", weber / metre / metre)
31
+ henry = DerivedUnit.define("H", weber / ampere)
32
+ degree_celcius = DerivedUnit.define("°C", kelvin)
33
+ lumen = DerivedUnit.define("lm", candela * steradian)
34
+ lux = DerivedUnit.define("lx", lumen / metre / metre)
35
+ becquerel = DerivedUnit.define("Bq", SIUnit() / second)
36
+ gray = DerivedUnit.define("Gy", joule / kilogram)
37
+ sievert = DerivedUnit.define("Sv", joule / kilogram)
38
+ katal = DerivedUnit.define("kat", mole / second)
39
+
40
+ for unit in (
41
+ newton,
42
+ pascal,
43
+ joule,
44
+ watt,
45
+ coulomb,
46
+ volt,
47
+ farad,
48
+ ohm,
49
+ siemens,
50
+ weber,
51
+ tesla,
52
+ henry,
53
+ lumen,
54
+ lux,
55
+ ):
56
+ register_canonical_unit(unit)
57
+
58
+ __all__ = [
59
+ "ampere",
60
+ "becquerel",
61
+ "candela",
62
+ "coulomb",
63
+ "degree_celcius",
64
+ "farad",
65
+ "gray",
66
+ "henry",
67
+ "hertz",
68
+ "joule",
69
+ "katal",
70
+ "kelvin",
71
+ "kilogram",
72
+ "lumen",
73
+ "lux",
74
+ "metre",
75
+ "mole",
76
+ "newton",
77
+ "ohm",
78
+ "pascal",
79
+ "radian",
80
+ "second",
81
+ "siemens",
82
+ "sievert",
83
+ "steradian",
84
+ "tesla",
85
+ "volt",
86
+ "watt",
87
+ "weber",
88
+ ]
core/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """Core unit and quantity logic."""
2
+
3
+ from .errors import (
4
+ InvalidUnitError,
5
+ InvalidValueError,
6
+ UnitCompatibilityError,
7
+ UnitOperandError,
8
+ UnitsError,
9
+ )
10
+ from .quantity import (
11
+ Quantity,
12
+ complex_quantity,
13
+ complex_unit,
14
+ float_quantity,
15
+ float_unit,
16
+ int_quantity,
17
+ int_unit,
18
+ long_quantity,
19
+ long_unit,
20
+ )
21
+ from .unit_definitions import BaseUnit, CustomUnitBase, DerivedUnit, SIUnit
22
+
23
+ __all__ = [
24
+ "BaseUnit",
25
+ "CustomUnitBase",
26
+ "DerivedUnit",
27
+ "InvalidUnitError",
28
+ "InvalidValueError",
29
+ "Quantity",
30
+ "SIUnit",
31
+ "UnitCompatibilityError",
32
+ "UnitOperandError",
33
+ "UnitsError",
34
+ "complex_quantity",
35
+ "complex_unit",
36
+ "float_quantity",
37
+ "float_unit",
38
+ "int_quantity",
39
+ "int_unit",
40
+ "long_quantity",
41
+ "long_unit",
42
+ ]
core/deprecations.py ADDED
@@ -0,0 +1,62 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Deprecation helpers for compatibility APIs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import warnings
7
+ from collections.abc import Callable
8
+ from typing import TypeVar
9
+
10
+
11
+ _T = TypeVar("_T")
12
+
13
+
14
+ def warn_legacy_api(name: str, replacement: str, removal_version: str) -> None:
15
+ """
16
+ Emit a deprecation warning for a legacy public API.
17
+
18
+ Args:
19
+ name: Deprecated API name.
20
+ replacement: Preferred API name or usage pattern.
21
+ removal_version: Planned removal release.
22
+
23
+ Returns:
24
+ None.
25
+
26
+ Raises:
27
+ None.
28
+ """
29
+ warnings.warn(
30
+ "{} is deprecated; use {} instead. It is scheduled for removal in {}.".format(
31
+ name,
32
+ replacement,
33
+ removal_version,
34
+ ),
35
+ DeprecationWarning,
36
+ stacklevel=4,
37
+ )
38
+
39
+
40
+ def deprecated_call(
41
+ name: str,
42
+ replacement: str,
43
+ removal_version: str,
44
+ callback: Callable[[], _T],
45
+ ) -> _T:
46
+ """
47
+ Warn for a deprecated API call and return the callback result.
48
+
49
+ Args:
50
+ name: Deprecated API name.
51
+ replacement: Preferred API name or usage pattern.
52
+ removal_version: Planned removal release.
53
+ callback: Zero-argument callable that performs the actual work.
54
+
55
+ Returns:
56
+ The callback result.
57
+
58
+ Raises:
59
+ Any exception raised by ``callback``.
60
+ """
61
+ warn_legacy_api(name, replacement, removal_version)
62
+ return callback()
core/errors.py ADDED
@@ -0,0 +1,22 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Domain exceptions for the units package."""
3
+
4
+
5
+ class UnitsError(Exception):
6
+ """Base exception for unit-related failures."""
7
+
8
+
9
+ class InvalidValueError(UnitsError):
10
+ """Raised when a numeric value is invalid for a quantity."""
11
+
12
+
13
+ class InvalidUnitError(UnitsError):
14
+ """Raised when a provided unit object or unit definition is invalid."""
15
+
16
+
17
+ class UnitCompatibilityError(UnitsError):
18
+ """Raised when an operation requires compatible units but they differ."""
19
+
20
+
21
+ class UnitOperandError(UnitsError):
22
+ """Raised when an operation is attempted with an unsupported operand."""
core/quantity.py ADDED
@@ -0,0 +1,255 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Quantity type and quantity operations."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from core.deprecations import deprecated_call
7
+ from core.errors import InvalidValueError, UnitCompatibilityError, UnitOperandError
8
+ from core.unit_definitions import BaseUnit, SIUnit, clone_unit
9
+ from models.dimension import SI_DIMENSION_SYSTEM
10
+ from utils.numbers import Scalar, is_number, is_real_number, validate_numeric_value
11
+
12
+
13
+ def require_quantity_operand(operand: object, operation: str) -> None:
14
+ """Raise when an operand is not a quantity."""
15
+ if not isinstance(operand, Quantity):
16
+ raise UnitOperandError(
17
+ "unsupported operand for {}: {}".format(operation, type(operand).__name__)
18
+ )
19
+
20
+
21
+ class Quantity:
22
+ """A numeric value coupled to a unit definition."""
23
+
24
+ def __init__(self, value: Scalar, unit: BaseUnit | None = None) -> None:
25
+ validate_numeric_value(value)
26
+ self._value = value
27
+ self._unit = clone_unit(unit)
28
+
29
+ @property
30
+ def value(self) -> Scalar:
31
+ """Return the numeric value for the quantity."""
32
+ return self._value
33
+
34
+ @value.setter
35
+ def value(self, value: Scalar) -> None:
36
+ validate_numeric_value(value)
37
+ self._value = value
38
+
39
+ @property
40
+ def unit(self) -> BaseUnit:
41
+ """Return the unit definition for the quantity."""
42
+ return self._unit
43
+
44
+ @unit.setter
45
+ def unit(self, unit: BaseUnit | None) -> None:
46
+ self._unit = clone_unit(unit)
47
+
48
+ @property
49
+ def is_unitless(self) -> bool:
50
+ """Return ``True`` when the quantity is dimensionless."""
51
+ return all(exponent == 0 for exponent in self.unit.unit_dict.values())
52
+
53
+ @property
54
+ def full_units(self) -> str:
55
+ """Render derived units in their SI decomposition."""
56
+ if self.unit.dimension.system != SI_DIMENSION_SYSTEM:
57
+ return str(self)
58
+ if not self.is_unitless and not isinstance(self.unit, SIUnit):
59
+ return "{} {}".format(self.value, self.unit.full_units).strip()
60
+ return str(self)
61
+
62
+ def _dimensionless_unit(self) -> SIUnit:
63
+ return SIUnit()
64
+
65
+ def _require_compatible_quantity(self, quantity2: object, operation: str) -> None:
66
+ require_quantity_operand(quantity2, operation)
67
+ if self.unit != quantity2.unit:
68
+ raise UnitCompatibilityError(
69
+ "units mismatch: {} and {}".format(self.unit, quantity2.unit)
70
+ )
71
+
72
+ def _require_real_scalar(self, value: object, operation: str) -> None:
73
+ if not is_real_number(value):
74
+ raise UnitOperandError(
75
+ "unsupported scalar for {}: {}".format(operation, type(value).__name__)
76
+ )
77
+
78
+ def _require_numeric_scalar(self, value: object, operation: str) -> None:
79
+ if not is_number(value):
80
+ raise UnitOperandError(
81
+ "unsupported scalar for {}: {}".format(operation, type(value).__name__)
82
+ )
83
+
84
+ def __add__(self, quantity2: object) -> "Quantity":
85
+ self._require_compatible_quantity(quantity2, "addition")
86
+ return self.__class__(self.value + quantity2.value, self.unit)
87
+
88
+ def __radd__(self, quantity2: object) -> "Quantity":
89
+ return self.__add__(quantity2)
90
+
91
+ def __sub__(self, quantity2: object) -> "Quantity":
92
+ self._require_compatible_quantity(quantity2, "subtraction")
93
+ return self.__class__(self.value - quantity2.value, self.unit)
94
+
95
+ def __rsub__(self, quantity2: object) -> "Quantity":
96
+ self._require_compatible_quantity(quantity2, "subtraction")
97
+ return self.__class__(quantity2.value - self.value, self.unit)
98
+
99
+ def __mul__(self, quantity2: object) -> "Quantity":
100
+ if isinstance(quantity2, Quantity):
101
+ return self.__class__(self.value * quantity2.value, self.unit * quantity2.unit)
102
+ if isinstance(quantity2, BaseUnit):
103
+ return self.__class__(self.value, self.unit * quantity2)
104
+ self._require_numeric_scalar(quantity2, "multiplication")
105
+ return self.__class__(self.value * quantity2, self.unit)
106
+
107
+ def __rmul__(self, quantity2: object) -> "Quantity":
108
+ return self.__mul__(quantity2)
109
+
110
+ def __truediv__(self, quantity2: object) -> "Quantity":
111
+ if isinstance(quantity2, Quantity):
112
+ return self.__class__(self.value / quantity2.value, self.unit / quantity2.unit)
113
+ if isinstance(quantity2, BaseUnit):
114
+ return self.__class__(self.value, self.unit / quantity2)
115
+ self._require_numeric_scalar(quantity2, "division")
116
+ return self.__class__(self.value / quantity2, self.unit)
117
+
118
+ def __rtruediv__(self, quantity2: object) -> "Quantity":
119
+ if isinstance(quantity2, Quantity):
120
+ return self.__class__(quantity2.value / self.value, quantity2.unit / self.unit)
121
+ self._require_numeric_scalar(quantity2, "division")
122
+ return self.__class__(quantity2 / self.value, self._dimensionless_unit() / self.unit)
123
+
124
+ def __floordiv__(self, quantity2: object) -> "Quantity":
125
+ if isinstance(quantity2, Quantity):
126
+ return self.__class__(self.value // quantity2.value, self.unit / quantity2.unit)
127
+ self._require_real_scalar(quantity2, "floor division")
128
+ return self.__class__(self.value // quantity2, self.unit)
129
+
130
+ def __rfloordiv__(self, quantity2: object) -> "Quantity":
131
+ if isinstance(quantity2, Quantity):
132
+ return self.__class__(quantity2.value // self.value, quantity2.unit / self.unit)
133
+ self._require_real_scalar(quantity2, "floor division")
134
+ return self.__class__(quantity2 // self.value, self._dimensionless_unit() / self.unit)
135
+
136
+ def __mod__(self, quantity2: object) -> "Quantity":
137
+ if isinstance(quantity2, Quantity):
138
+ self._require_compatible_quantity(quantity2, "modulo")
139
+ return self.__class__(self.value % quantity2.value, self.unit)
140
+ self._require_real_scalar(quantity2, "modulo")
141
+ return self.__class__(self.value % quantity2, self.unit)
142
+
143
+ def __rmod__(self, quantity2: object) -> "Quantity":
144
+ if isinstance(quantity2, Quantity):
145
+ self._require_compatible_quantity(quantity2, "modulo")
146
+ return self.__class__(quantity2.value % self.value, self.unit)
147
+ self._require_real_scalar(quantity2, "modulo")
148
+ return self.__class__(quantity2 % self.value, self._dimensionless_unit() / self.unit)
149
+
150
+ def __divmod__(self, quantity2: object) -> tuple["Quantity", "Quantity"]:
151
+ return self.__floordiv__(quantity2), self.__mod__(quantity2)
152
+
153
+ def __rdivmod__(self, quantity2: object) -> tuple["Quantity", "Quantity"]:
154
+ return self.__rfloordiv__(quantity2), self.__rmod__(quantity2)
155
+
156
+ def __pow__(self, exponent: object) -> "Quantity":
157
+ self._require_numeric_scalar(exponent, "power")
158
+ if isinstance(exponent, complex):
159
+ raise UnitOperandError("unsupported scalar for power: complex")
160
+ if not self.is_unitless and (not isinstance(exponent, int) or isinstance(exponent, bool)):
161
+ raise UnitOperandError(
162
+ "unsupported scalar for power: {}".format(type(exponent).__name__)
163
+ )
164
+ if self.is_unitless:
165
+ return self.__class__(self.value ** exponent, self.unit)
166
+ return self.__class__(self.value ** exponent, self.unit ** exponent)
167
+
168
+ def __neg__(self) -> "Quantity":
169
+ return self.__class__(-self.value, self.unit)
170
+
171
+ def __pos__(self) -> "Quantity":
172
+ return self.__class__(+self.value, self.unit)
173
+
174
+ def __abs__(self) -> "Quantity":
175
+ return self.__class__(abs(self.value), self.unit)
176
+
177
+ def __complex__(self) -> complex:
178
+ raise TypeError("invalid conversion from Quantity object to complex")
179
+
180
+ def __int__(self) -> int:
181
+ raise TypeError("invalid conversion from Quantity object to int")
182
+
183
+ def __float__(self) -> float:
184
+ raise TypeError("invalid conversion from Quantity object to float")
185
+
186
+ def __str__(self) -> str:
187
+ return "{} {}".format(self.value, self.unit).strip()
188
+
189
+
190
+ def int_quantity(quantity: Quantity) -> Quantity:
191
+ """Convert a quantity value to int while preserving its unit."""
192
+ require_quantity_operand(quantity, "int conversion")
193
+ return Quantity(int(quantity.value), quantity.unit)
194
+
195
+
196
+ def float_quantity(quantity: Quantity) -> Quantity:
197
+ """Convert a quantity value to float while preserving its unit."""
198
+ require_quantity_operand(quantity, "float conversion")
199
+ return Quantity(float(quantity.value), quantity.unit)
200
+
201
+
202
+ def long_quantity(quantity: Quantity) -> Quantity:
203
+ """Legacy compatibility helper equivalent to ``int_quantity``."""
204
+ return deprecated_call(
205
+ "long_quantity",
206
+ "int_quantity",
207
+ "1.0.0",
208
+ lambda: int_quantity(quantity),
209
+ )
210
+
211
+
212
+ def complex_quantity(quantity: Quantity) -> Quantity:
213
+ """Convert a quantity value to complex while preserving its unit."""
214
+ require_quantity_operand(quantity, "complex conversion")
215
+ return Quantity(complex(quantity.value), quantity.unit)
216
+
217
+
218
+ def int_unit(quantity: Quantity) -> Quantity:
219
+ """Legacy compatibility helper equivalent to ``int_quantity``."""
220
+ return deprecated_call(
221
+ "int_unit",
222
+ "int_quantity",
223
+ "1.0.0",
224
+ lambda: int_quantity(quantity),
225
+ )
226
+
227
+
228
+ def float_unit(quantity: Quantity) -> Quantity:
229
+ """Legacy compatibility helper equivalent to ``float_quantity``."""
230
+ return deprecated_call(
231
+ "float_unit",
232
+ "float_quantity",
233
+ "1.0.0",
234
+ lambda: float_quantity(quantity),
235
+ )
236
+
237
+
238
+ def long_unit(quantity: Quantity) -> Quantity:
239
+ """Legacy compatibility helper equivalent to ``int_quantity``."""
240
+ return deprecated_call(
241
+ "long_unit",
242
+ "int_quantity",
243
+ "1.0.0",
244
+ lambda: int_quantity(quantity),
245
+ )
246
+
247
+
248
+ def complex_unit(quantity: Quantity) -> Quantity:
249
+ """Legacy compatibility helper equivalent to ``complex_quantity``."""
250
+ return deprecated_call(
251
+ "complex_unit",
252
+ "complex_quantity",
253
+ "1.0.0",
254
+ lambda: complex_quantity(quantity),
255
+ )