python-units 0.1.3__py3-none-any.whl → 0.2.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,109 @@
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
+ Unit = Quantity
57
+
58
+ __all__ = [
59
+ "Dimension",
60
+ "DimensionSystem",
61
+ "Quantity",
62
+ "Unit",
63
+ "UnitsError",
64
+ "InvalidUnitError",
65
+ "InvalidValueError",
66
+ "UnitCompatibilityError",
67
+ "UnitOperandError",
68
+ "BaseUnit",
69
+ "CustomUnitBase",
70
+ "DerivedUnit",
71
+ "SIUnit",
72
+ "ampere",
73
+ "becquerel",
74
+ "candela",
75
+ "complex_quantity",
76
+ "complex_unit",
77
+ "coulomb",
78
+ "degree_celcius",
79
+ "farad",
80
+ "float_quantity",
81
+ "float_unit",
82
+ "gray",
83
+ "henry",
84
+ "hertz",
85
+ "int_quantity",
86
+ "int_unit",
87
+ "joule",
88
+ "katal",
89
+ "kelvin",
90
+ "kilogram",
91
+ "long_quantity",
92
+ "long_unit",
93
+ "lumen",
94
+ "lux",
95
+ "metre",
96
+ "mole",
97
+ "newton",
98
+ "ohm",
99
+ "pascal",
100
+ "radian",
101
+ "second",
102
+ "siemens",
103
+ "sievert",
104
+ "steradian",
105
+ "tesla",
106
+ "volt",
107
+ "watt",
108
+ "weber",
109
+ ]
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/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,216 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Quantity type and quantity operations."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from core.errors import InvalidValueError, UnitCompatibilityError, UnitOperandError
7
+ from core.unit_definitions import BaseUnit, SIUnit, clone_unit
8
+ from models.dimension import SI_DIMENSION_SYSTEM
9
+ from utils.numbers import Scalar, is_number, is_real_number, validate_numeric_value
10
+
11
+
12
+ def require_quantity_operand(operand: object, operation: str) -> None:
13
+ """Raise when an operand is not a quantity."""
14
+ if not isinstance(operand, Quantity):
15
+ raise UnitOperandError(
16
+ "unsupported operand for {}: {}".format(operation, type(operand).__name__)
17
+ )
18
+
19
+
20
+ class Quantity:
21
+ """A numeric value coupled to a unit definition."""
22
+
23
+ def __init__(self, value: Scalar, unit: BaseUnit | None = None) -> None:
24
+ validate_numeric_value(value)
25
+ self._value = value
26
+ self._unit = clone_unit(unit)
27
+
28
+ @property
29
+ def value(self) -> Scalar:
30
+ """Return the numeric value for the quantity."""
31
+ return self._value
32
+
33
+ @value.setter
34
+ def value(self, value: Scalar) -> None:
35
+ validate_numeric_value(value)
36
+ self._value = value
37
+
38
+ @property
39
+ def unit(self) -> BaseUnit:
40
+ """Return the unit definition for the quantity."""
41
+ return self._unit
42
+
43
+ @unit.setter
44
+ def unit(self, unit: BaseUnit | None) -> None:
45
+ self._unit = clone_unit(unit)
46
+
47
+ @property
48
+ def is_unitless(self) -> bool:
49
+ """Return ``True`` when the quantity is dimensionless."""
50
+ return all(exponent == 0 for exponent in self.unit.unit_dict.values())
51
+
52
+ @property
53
+ def full_units(self) -> str:
54
+ """Render derived units in their SI decomposition."""
55
+ if self.unit.dimension.system != SI_DIMENSION_SYSTEM:
56
+ return str(self)
57
+ if not self.is_unitless and not isinstance(self.unit, SIUnit):
58
+ return "{} {}".format(self.value, self.unit.full_units).strip()
59
+ return str(self)
60
+
61
+ def _dimensionless_unit(self) -> SIUnit:
62
+ return SIUnit()
63
+
64
+ def _require_compatible_quantity(self, quantity2: object, operation: str) -> None:
65
+ require_quantity_operand(quantity2, operation)
66
+ if self.unit != quantity2.unit:
67
+ raise UnitCompatibilityError(
68
+ "units mismatch: {} and {}".format(self.unit, quantity2.unit)
69
+ )
70
+
71
+ def _require_real_scalar(self, value: object, operation: str) -> None:
72
+ if not is_real_number(value):
73
+ raise UnitOperandError(
74
+ "unsupported scalar for {}: {}".format(operation, type(value).__name__)
75
+ )
76
+
77
+ def _require_numeric_scalar(self, value: object, operation: str) -> None:
78
+ if not is_number(value):
79
+ raise UnitOperandError(
80
+ "unsupported scalar for {}: {}".format(operation, type(value).__name__)
81
+ )
82
+
83
+ def __add__(self, quantity2: object) -> "Quantity":
84
+ self._require_compatible_quantity(quantity2, "addition")
85
+ return self.__class__(self.value + quantity2.value, self.unit)
86
+
87
+ def __radd__(self, quantity2: object) -> "Quantity":
88
+ return self.__add__(quantity2)
89
+
90
+ def __sub__(self, quantity2: object) -> "Quantity":
91
+ self._require_compatible_quantity(quantity2, "subtraction")
92
+ return self.__class__(self.value - quantity2.value, self.unit)
93
+
94
+ def __rsub__(self, quantity2: object) -> "Quantity":
95
+ self._require_compatible_quantity(quantity2, "subtraction")
96
+ return self.__class__(quantity2.value - self.value, self.unit)
97
+
98
+ def __mul__(self, quantity2: object) -> "Quantity":
99
+ if isinstance(quantity2, Quantity):
100
+ return self.__class__(self.value * quantity2.value, self.unit * quantity2.unit)
101
+ if isinstance(quantity2, BaseUnit):
102
+ return self.__class__(self.value, self.unit * quantity2)
103
+ self._require_numeric_scalar(quantity2, "multiplication")
104
+ return self.__class__(self.value * quantity2, self.unit)
105
+
106
+ def __rmul__(self, quantity2: object) -> "Quantity":
107
+ return self.__mul__(quantity2)
108
+
109
+ def __truediv__(self, quantity2: object) -> "Quantity":
110
+ if isinstance(quantity2, Quantity):
111
+ return self.__class__(self.value / quantity2.value, self.unit / quantity2.unit)
112
+ if isinstance(quantity2, BaseUnit):
113
+ return self.__class__(self.value, self.unit / quantity2)
114
+ self._require_numeric_scalar(quantity2, "division")
115
+ return self.__class__(self.value / quantity2, self.unit)
116
+
117
+ def __rtruediv__(self, quantity2: object) -> "Quantity":
118
+ if isinstance(quantity2, Quantity):
119
+ return self.__class__(quantity2.value / self.value, quantity2.unit / self.unit)
120
+ self._require_numeric_scalar(quantity2, "division")
121
+ return self.__class__(quantity2 / self.value, self._dimensionless_unit() / self.unit)
122
+
123
+ def __floordiv__(self, quantity2: object) -> "Quantity":
124
+ if isinstance(quantity2, Quantity):
125
+ return self.__class__(self.value // quantity2.value, self.unit / quantity2.unit)
126
+ self._require_real_scalar(quantity2, "floor division")
127
+ return self.__class__(self.value // quantity2, self.unit)
128
+
129
+ def __rfloordiv__(self, quantity2: object) -> "Quantity":
130
+ if isinstance(quantity2, Quantity):
131
+ return self.__class__(quantity2.value // self.value, quantity2.unit / self.unit)
132
+ self._require_real_scalar(quantity2, "floor division")
133
+ return self.__class__(quantity2 // self.value, self._dimensionless_unit() / self.unit)
134
+
135
+ def __mod__(self, quantity2: object) -> "Quantity":
136
+ if isinstance(quantity2, Quantity):
137
+ self._require_compatible_quantity(quantity2, "modulo")
138
+ return self.__class__(self.value % quantity2.value, self.unit)
139
+ self._require_real_scalar(quantity2, "modulo")
140
+ return self.__class__(self.value % quantity2, self.unit)
141
+
142
+ def __rmod__(self, quantity2: object) -> "Quantity":
143
+ if isinstance(quantity2, Quantity):
144
+ self._require_compatible_quantity(quantity2, "modulo")
145
+ return self.__class__(quantity2.value % self.value, self.unit)
146
+ self._require_real_scalar(quantity2, "modulo")
147
+ return self.__class__(quantity2 % self.value, self._dimensionless_unit() / self.unit)
148
+
149
+ def __divmod__(self, quantity2: object) -> tuple["Quantity", "Quantity"]:
150
+ return self.__floordiv__(quantity2), self.__mod__(quantity2)
151
+
152
+ def __rdivmod__(self, quantity2: object) -> tuple["Quantity", "Quantity"]:
153
+ return self.__rfloordiv__(quantity2), self.__rmod__(quantity2)
154
+
155
+ def __pow__(self, exponent: object) -> "Quantity":
156
+ self._require_numeric_scalar(exponent, "power")
157
+ if isinstance(exponent, complex):
158
+ raise UnitOperandError("unsupported scalar for power: complex")
159
+ if not self.is_unitless and (not isinstance(exponent, int) or isinstance(exponent, bool)):
160
+ raise UnitOperandError(
161
+ "unsupported scalar for power: {}".format(type(exponent).__name__)
162
+ )
163
+ if self.is_unitless:
164
+ return self.__class__(self.value ** exponent, self.unit)
165
+ return self.__class__(self.value ** exponent, self.unit ** exponent)
166
+
167
+ def __neg__(self) -> "Quantity":
168
+ return self.__class__(-self.value, self.unit)
169
+
170
+ def __pos__(self) -> "Quantity":
171
+ return self.__class__(+self.value, self.unit)
172
+
173
+ def __abs__(self) -> "Quantity":
174
+ return self.__class__(abs(self.value), self.unit)
175
+
176
+ def __complex__(self) -> complex:
177
+ raise TypeError("invalid conversion from Quantity object to complex")
178
+
179
+ def __int__(self) -> int:
180
+ raise TypeError("invalid conversion from Quantity object to int")
181
+
182
+ def __float__(self) -> float:
183
+ raise TypeError("invalid conversion from Quantity object to float")
184
+
185
+ def __str__(self) -> str:
186
+ return "{} {}".format(self.value, self.unit).strip()
187
+
188
+
189
+ def int_quantity(quantity: Quantity) -> Quantity:
190
+ """Convert a quantity value to int while preserving its unit."""
191
+ require_quantity_operand(quantity, "int conversion")
192
+ return Quantity(int(quantity.value), quantity.unit)
193
+
194
+
195
+ def float_quantity(quantity: Quantity) -> Quantity:
196
+ """Convert a quantity value to float while preserving its unit."""
197
+ require_quantity_operand(quantity, "float conversion")
198
+ return Quantity(float(quantity.value), quantity.unit)
199
+
200
+
201
+ def long_quantity(quantity: Quantity) -> Quantity:
202
+ """Legacy compatibility helper equivalent to ``int_quantity``."""
203
+ require_quantity_operand(quantity, "long conversion")
204
+ return Quantity(int(quantity.value), quantity.unit)
205
+
206
+
207
+ def complex_quantity(quantity: Quantity) -> Quantity:
208
+ """Convert a quantity value to complex while preserving its unit."""
209
+ require_quantity_operand(quantity, "complex conversion")
210
+ return Quantity(complex(quantity.value), quantity.unit)
211
+
212
+
213
+ int_unit = int_quantity
214
+ float_unit = float_quantity
215
+ long_unit = long_quantity
216
+ complex_unit = complex_quantity