ucon 0.5.1__tar.gz → 0.6.0__tar.gz

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 (73) hide show
  1. {ucon-0.5.1 → ucon-0.6.0}/.github/workflows/publish.yaml +5 -5
  2. {ucon-0.5.1 → ucon-0.6.0}/.github/workflows/tests.yaml +4 -6
  3. ucon-0.6.0/Makefile +129 -0
  4. {ucon-0.5.1/ucon.egg-info → ucon-0.6.0}/PKG-INFO +88 -31
  5. ucon-0.5.1/PKG-INFO → ucon-0.6.0/README.md +75 -58
  6. {ucon-0.5.1 → ucon-0.6.0}/ROADMAP.md +51 -36
  7. ucon-0.5.1/docs/decisions/unity-distance-metric-for-nearest-scale.md → ucon-0.6.0/docs/decisions/001-unity-distance-metric-for-nearest-scale.md +35 -17
  8. ucon-0.6.0/docs/decisions/002-composite-units.md +116 -0
  9. ucon-0.6.0/docs/decisions/003-composable-unit-algebra.md +123 -0
  10. ucon-0.5.1/docs/decisions/unit-algebra-naming.md → ucon-0.6.0/docs/decisions/004-unit-algebra-naming.md +49 -48
  11. ucon-0.5.1/docs/decisions/pseudo-dimension-tuple-values.md → ucon-0.6.0/docs/decisions/005-pseudo-dimension-tuple-values.md +39 -75
  12. ucon-0.6.0/docs/decisions/006-pydantic-integration-pattern.md +92 -0
  13. ucon-0.6.0/docs/examples/basis-transform-fantasy-units.md +89 -0
  14. ucon-0.6.0/pyproject.toml +86 -0
  15. ucon-0.6.0/requirements.txt +1 -0
  16. {ucon-0.5.1 → ucon-0.6.0}/setup.py +9 -0
  17. ucon-0.6.0/tests/ucon/mcp/__init__.py +1 -0
  18. ucon-0.6.0/tests/ucon/mcp/test_server.py +351 -0
  19. ucon-0.6.0/tests/ucon/test_basis_transform.py +521 -0
  20. ucon-0.6.0/tests/ucon/test_graph_basis_transform.py +263 -0
  21. ucon-0.6.0/tests/ucon/test_pickle.py +197 -0
  22. ucon-0.6.0/tests/ucon/test_pydantic.py +350 -0
  23. ucon-0.6.0/tests/ucon/test_rebased_unit.py +184 -0
  24. ucon-0.6.0/tests/ucon/test_unit_parsing.py +290 -0
  25. ucon-0.6.0/tests/ucon/test_unit_system.py +174 -0
  26. ucon-0.6.0/tests/ucon/test_vector_fraction.py +185 -0
  27. {ucon-0.5.1 → ucon-0.6.0}/ucon/__init__.py +24 -3
  28. {ucon-0.5.1 → ucon-0.6.0}/ucon/algebra.py +36 -14
  29. {ucon-0.5.1 → ucon-0.6.0}/ucon/core.py +414 -2
  30. {ucon-0.5.1 → ucon-0.6.0}/ucon/graph.py +167 -10
  31. ucon-0.6.0/ucon/mcp/__init__.py +8 -0
  32. ucon-0.6.0/ucon/mcp/server.py +250 -0
  33. ucon-0.6.0/ucon/pydantic.py +199 -0
  34. ucon-0.6.0/ucon/units.py +434 -0
  35. ucon-0.5.1/README.md → ucon-0.6.0/ucon.egg-info/PKG-INFO +115 -21
  36. {ucon-0.5.1 → ucon-0.6.0}/ucon.egg-info/SOURCES.txt +26 -7
  37. ucon-0.6.0/ucon.egg-info/entry_points.txt +2 -0
  38. ucon-0.6.0/ucon.egg-info/requires.txt +11 -0
  39. ucon-0.6.0/ucon.egg-info/top_level.txt +1 -0
  40. ucon-0.6.0/uv.lock +1603 -0
  41. ucon-0.5.1/docs/decisions/composable-unit-algebra.md +0 -241
  42. ucon-0.5.1/docs/decisions/composite-units.md +0 -160
  43. ucon-0.5.1/noxfile.py +0 -132
  44. ucon-0.5.1/requirements.txt +0 -2
  45. ucon-0.5.1/ucon/units.py +0 -159
  46. ucon-0.5.1/ucon.egg-info/top_level.txt +0 -2
  47. {ucon-0.5.1 → ucon-0.6.0}/.gitignore +0 -0
  48. {ucon-0.5.1 → ucon-0.6.0}/LICENSE +0 -0
  49. {ucon-0.5.1 → ucon-0.6.0}/NOTICE +0 -0
  50. {ucon-0.5.1 → ucon-0.6.0}/docs/explainers/exponent-scale-relationship.md +0 -0
  51. {ucon-0.5.1 → ucon-0.6.0}/docs/explainers/type-operation-matrix.md +0 -0
  52. {ucon-0.5.1 → ucon-0.6.0}/docs/explainers/why-algebraic-closure-matters.md +0 -0
  53. {ucon-0.5.1 → ucon-0.6.0}/docs/explainers/why-type-safety-matters.md +0 -0
  54. {ucon-0.5.1 → ucon-0.6.0}/docs/proposals/interface-unifying-the-value-layer.md +0 -0
  55. {ucon-0.5.1 → ucon-0.6.0}/docs/proposals/project_unified-algebraic-core.md +0 -0
  56. {ucon-0.5.1 → ucon-0.6.0}/docs/proposals/support-for-fractional-exponents.md +0 -0
  57. {ucon-0.5.1 → ucon-0.6.0}/docs/proposals/unified-unit-presentation.md +0 -0
  58. {ucon-0.5.1 → ucon-0.6.0}/setup.cfg +0 -0
  59. {ucon-0.5.1 → ucon-0.6.0}/tests/__init__.py +0 -0
  60. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/__init__.py +0 -0
  61. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/conversion/__init__.py +0 -0
  62. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/conversion/test_graph.py +0 -0
  63. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/conversion/test_map.py +0 -0
  64. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_algebra.py +0 -0
  65. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_core.py +0 -0
  66. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_default_graph_conversions.py +0 -0
  67. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_dimensionless_units.py +0 -0
  68. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_quantity.py +0 -0
  69. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_uncertainty.py +0 -0
  70. {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_units.py +0 -0
  71. {ucon-0.5.1 → ucon-0.6.0}/ucon/maps.py +0 -0
  72. {ucon-0.5.1 → ucon-0.6.0}/ucon/quantity.py +0 -0
  73. {ucon-0.5.1 → ucon-0.6.0}/ucon.egg-info/dependency_links.txt +0 -0
@@ -16,16 +16,16 @@ jobs:
16
16
  fetch-depth: 0 # needed for ancestry check
17
17
  ref: ${{ github.ref_name }}
18
18
 
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v5
21
+
19
22
  - name: Set up Python
20
23
  uses: actions/setup-python@v5
21
24
  with:
22
25
  python-version: '3.14'
23
26
 
24
- - name: Install dependencies
25
- run: pip install -r requirements.txt
26
-
27
27
  - name: Build distribution 📦
28
- run: PYTHONWARNINGS=ignore LOCAL_VERSION_SCHEME=true nox -s build
28
+ run: LOCAL_VERSION_SCHEME=true make build
29
29
 
30
30
  - name: Publish to Test PyPI
31
31
  if: github.ref == 'refs/heads/main'
@@ -39,7 +39,7 @@ jobs:
39
39
  - name: Verify tag is on mainline
40
40
  if: startsWith(github.ref, 'refs/tags/')
41
41
  run: |
42
- git fetch origin main --depth=1
42
+ git fetch origin main --unshallow 2>/dev/null || git fetch origin main
43
43
  TAG=${GITHUB_REF#refs/tags/}
44
44
  TAG_COMMIT=$(git rev-list -n 1 "$TAG")
45
45
  if ! git merge-base --is-ancestor "$TAG_COMMIT" origin/main; then
@@ -26,18 +26,16 @@ jobs:
26
26
  - name: Checkout code
27
27
  uses: actions/checkout@v5
28
28
 
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@v5
31
+
29
32
  - name: Set up Python ${{ matrix.python-version }}
30
33
  uses: actions/setup-python@v6
31
34
  with:
32
35
  python-version: ${{ matrix.python-version }}
33
36
 
34
- - name: Install dependencies
35
- run: |
36
- python -m pip install --upgrade pip
37
- pip install -r requirements.txt
38
-
39
37
  - name: Run tests
40
- run: nox -s test
38
+ run: make test PYTHON=${{ matrix.python-version }}
41
39
 
42
40
  - name: Upload coverage to Codecov
43
41
  if: success() && github.repository_owner == 'withtwoemms'
ucon-0.6.0/Makefile ADDED
@@ -0,0 +1,129 @@
1
+ # © 2025 The Radiativity Company
2
+ # Licensed under the Apache License, Version 2.0
3
+ # See the LICENSE file for details.
4
+
5
+ # --- Configuration ---
6
+ PROJECTNAME := ucon
7
+ PYTHON ?= 3.12
8
+ SUPPORTED_PYTHONS := 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14
9
+ UV_VENV ?= .${PROJECTNAME}-${PYTHON}
10
+ UV_INSTALLED := .uv-installed
11
+ DEPS_INSTALLED := ${UV_VENV}/.deps-installed
12
+ TESTDIR := tests/
13
+ TESTNAME ?=
14
+ COVERAGE ?= true
15
+
16
+ # --- Color Setup ---
17
+ GREEN := \033[0;32m
18
+ CYAN := \033[0;36m
19
+ YELLOW := \033[1;33m
20
+ RESET := \033[0m
21
+
22
+ # --- Help Command ---
23
+ .PHONY: help
24
+ help:
25
+ @echo "\n${YELLOW}ucon Development Commands${RESET}\n"
26
+ @echo " ${CYAN}install${RESET} - Install package with all extras"
27
+ @echo " ${CYAN}install-test${RESET} - Install with test dependencies only"
28
+ @echo " ${CYAN}test${RESET} - Run tests (PYTHON=X.Y for specific version)"
29
+ @echo " ${CYAN}test-all${RESET} - Run tests across all supported Python versions"
30
+ @echo " ${CYAN}coverage${RESET} - Generate coverage report"
31
+ @echo " ${CYAN}build${RESET} - Build source and wheel distributions"
32
+ @echo " ${CYAN}venv${RESET} - Create virtual environment"
33
+ @echo " ${CYAN}clean${RESET} - Remove build artifacts and caches"
34
+ @echo ""
35
+ @echo "${YELLOW}Variables:${RESET}\n"
36
+ @echo " PYTHON=${PYTHON} - Python version for test target"
37
+ @echo " UV_VENV=${UV_VENV} - Path to virtual environment"
38
+ @echo " TESTNAME= - Specific test to run (e.g., tests.ucon.test_core)"
39
+ @echo " COVERAGE=${COVERAGE} - Enable coverage (true/false)"
40
+ @echo ""
41
+
42
+ # --- uv Installation ---
43
+ ${UV_INSTALLED}:
44
+ @command -v uv >/dev/null 2>&1 || { \
45
+ echo "${GREEN}Installing uv...${RESET}"; \
46
+ curl -LsSf https://astral.sh/uv/install.sh | sh; \
47
+ }
48
+ @touch ${UV_INSTALLED}
49
+
50
+ # --- Virtual Environment ---
51
+ ${UV_VENV}: ${UV_INSTALLED}
52
+ @echo "${GREEN}Creating virtual environment at ${UV_VENV}...${RESET}"
53
+ @uv venv --python ${PYTHON} ${UV_VENV}
54
+
55
+ ${DEPS_INSTALLED}: pyproject.toml uv.lock | ${UV_VENV}
56
+ @echo "${GREEN}Syncing dependencies into ${UV_VENV}...${RESET}"
57
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test --extra pydantic --extra mcp
58
+ @touch ${DEPS_INSTALLED}
59
+
60
+ .PHONY: venv
61
+ venv: ${DEPS_INSTALLED}
62
+ @echo "${CYAN}Virtual environment ready at ${UV_VENV}${RESET}"
63
+ @echo "${CYAN}Activate with:${RESET} source ${UV_VENV}/bin/activate"
64
+
65
+ # --- Installation ---
66
+ .PHONY: install-test
67
+ install-test: ${UV_VENV}
68
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test
69
+
70
+ .PHONY: install
71
+ install: ${UV_VENV}
72
+ @echo "${GREEN}Installing with all extras into ${UV_VENV}...${RESET}"
73
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test --extra pydantic --extra mcp
74
+
75
+ # --- Testing ---
76
+ .PHONY: test
77
+ test: ${DEPS_INSTALLED}
78
+ @echo "${GREEN}Running tests with Python ${PYTHON}...${RESET}"
79
+ ifeq ($(COVERAGE),true)
80
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage run --source=ucon --branch \
81
+ --omit="**/tests/*,**/site-packages/*.py,setup.py" \
82
+ -m unittest $(if $(TESTNAME),$(TESTNAME),discover --start-directory ${TESTDIR} --top-level-directory .)
83
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage report -m
84
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage xml
85
+ else
86
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} -m unittest \
87
+ $(if $(TESTNAME),$(TESTNAME),discover --start-directory ${TESTDIR} --top-level-directory .)
88
+ endif
89
+
90
+ .PHONY: test-all
91
+ test-all: ${UV_INSTALLED}
92
+ @echo "${GREEN}Running tests across all supported Python versions...${RESET}"
93
+ @for pyver in $(SUPPORTED_PYTHONS); do \
94
+ echo "\n${CYAN}=== Python $$pyver ===${RESET}"; \
95
+ uv run --python $$pyver -m unittest discover \
96
+ --start-directory ${TESTDIR} --top-level-directory . \
97
+ || echo "${YELLOW}Python $$pyver: FAILED or not available${RESET}"; \
98
+ done
99
+
100
+ # --- Coverage ---
101
+ .PHONY: coverage
102
+ coverage: ${DEPS_INSTALLED}
103
+ @echo "${GREEN}Generating coverage report...${RESET}"
104
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage report -m
105
+ @UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage html
106
+ @echo "${CYAN}HTML report at htmlcov/index.html${RESET}"
107
+
108
+ # --- Building ---
109
+ .PHONY: build
110
+ build: ${UV_INSTALLED}
111
+ @echo "${GREEN}Building distributions...${RESET}"
112
+ @uv build
113
+ @echo "${CYAN}Distributions at dist/${RESET}"
114
+
115
+ # --- Cleaning ---
116
+ .PHONY: clean
117
+ clean:
118
+ @echo "${GREEN}Cleaning build artifacts...${RESET}"
119
+ @rm -rf dist/ build/ *.egg-info/
120
+ @rm -rf ${UV_VENV} ${DEPS_INSTALLED} ${UV_INSTALLED}
121
+ @rm -rf .uv_cache .pytest_cache htmlcov/
122
+ @rm -f coverage.xml .coverage
123
+ @find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
124
+ @echo "${CYAN}Clean complete.${RESET}"
125
+
126
+ .PHONY: clean-all
127
+ clean-all: clean
128
+ @echo "${YELLOW}Removing uv.lock...${RESET}"
129
+ @rm -f uv.lock
@@ -1,18 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucon
3
- Version: 0.5.1
4
- Summary: a tool for dimensional analysis: a "Unit CONverter"
3
+ Version: 0.6.0
4
+ Summary: A tool for dimensional analysis: a 'Unit CONverter'
5
5
  Home-page: https://github.com/withtwoemms/ucon
6
6
  Author: Emmanuel I. Obi
7
+ Author-email: "Emmanuel I. Obi" <withtwoemms@gmail.com>
7
8
  Maintainer: Emmanuel I. Obi
8
- Maintainer-email: withtwoemms@gmail.com
9
+ Maintainer-email: "Emmanuel I. Obi" <withtwoemms@gmail.com>
9
10
  License: Apache-2.0
11
+ Project-URL: Homepage, https://github.com/withtwoemms/ucon
12
+ Project-URL: Repository, https://github.com/withtwoemms/ucon
10
13
  Classifier: Development Status :: 4 - Beta
11
14
  Classifier: Intended Audience :: Developers
12
15
  Classifier: Intended Audience :: Education
13
16
  Classifier: Intended Audience :: Science/Research
14
17
  Classifier: Topic :: Software Development :: Build Tools
15
- Classifier: License :: OSI Approved :: Apache Software License
16
18
  Classifier: Programming Language :: Python :: 3.7
17
19
  Classifier: Programming Language :: Python :: 3.8
18
20
  Classifier: Programming Language :: Python :: 3.9
@@ -21,19 +23,20 @@ Classifier: Programming Language :: Python :: 3.11
21
23
  Classifier: Programming Language :: Python :: 3.12
22
24
  Classifier: Programming Language :: Python :: 3.13
23
25
  Classifier: Programming Language :: Python :: 3.14
26
+ Requires-Python: >=3.7
24
27
  Description-Content-Type: text/markdown
25
28
  License-File: LICENSE
26
29
  License-File: NOTICE
30
+ Provides-Extra: test
31
+ Requires-Dist: coverage[toml]>=5.5; extra == "test"
32
+ Provides-Extra: pydantic
33
+ Requires-Dist: pydantic>=2.0; extra == "pydantic"
34
+ Provides-Extra: mcp
35
+ Requires-Dist: mcp>=1.0; python_version >= "3.10" and extra == "mcp"
27
36
  Dynamic: author
28
- Dynamic: classifier
29
- Dynamic: description
30
- Dynamic: description-content-type
31
37
  Dynamic: home-page
32
- Dynamic: license
33
38
  Dynamic: license-file
34
39
  Dynamic: maintainer
35
- Dynamic: maintainer-email
36
- Dynamic: summary
37
40
 
38
41
  <table>
39
42
  <tr>
@@ -68,6 +71,7 @@ It combines **units**, **scales**, and **dimensions** into a composable algebra
68
71
  - Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, etc.)
69
72
  - Pseudo-dimensions for angles, solid angles, and ratios with semantic isolation
70
73
  - Uncertainty propagation through arithmetic and conversions
74
+ - Pydantic v2 integration for API validation and JSON serialization
71
75
  - A clean foundation for physics, chemistry, data modeling, and beyond
72
76
 
73
77
  Think of it as **`decimal.Decimal` for the physical world** — precise, predictable, and type-safe.
@@ -92,7 +96,11 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
92
96
  | **`Ratio`** | `ucon.core` | Represents the division of two `Number` objects; captures relationships between quantities. | Expressing rates, densities, efficiencies (e.g., energy / time = power, length / time = velocity). |
93
97
  | **`Map`** hierarchy | `ucon.maps` | Composable conversion morphisms: `LinearMap`, `AffineMap`, `ComposedMap`. | Defining conversion functions between units (e.g., meter→foot, celsius→kelvin). |
94
98
  | **`ConversionGraph`** | `ucon.graph` | Registry of unit conversion edges with BFS path composition. | Converting between units via `Number.to(target)`; managing default and custom graphs. |
99
+ | **`UnitSystem`** | `ucon.core` | Named mapping from dimensions to base units (e.g., SI, Imperial). | Defining coherent unit systems; grouping base units by dimension. |
100
+ | **`BasisTransform`** | `ucon.core` | Matrix-based transformation between dimensional exponent spaces. | Converting between incompatible dimensional structures; exact arithmetic with `Fraction`. |
101
+ | **`RebasedUnit`** | `ucon.core` | A unit rebased to another system's dimension, preserving provenance. | Cross-basis conversions; tracking original unit through basis changes. |
95
102
  | **`units` module** | `ucon.units` | Defines canonical unit instances (SI, imperial, information, and derived units). | Quick access to standard physical units (`units.meter`, `units.foot`, `units.byte`, etc.). |
103
+ | **`pydantic` module** | `ucon.pydantic` | Pydantic v2 integration with `Number` type for model validation and JSON serialization. | Using `Number` in Pydantic models; API request/response validation; JSON round-trip serialization. |
96
104
 
97
105
  ### Under the Hood
98
106
 
@@ -139,35 +147,40 @@ Simple:
139
147
  pip install ucon
140
148
  ```
141
149
 
150
+ With Pydantic v2 support:
151
+ ```bash
152
+ pip install ucon[pydantic]
153
+ ```
154
+
142
155
  ## Usage
143
156
 
144
- This sort of dimensional analysis:
157
+ ### Quantities and Arithmetic
158
+
159
+ Dimensional analysis like this:
145
160
  ```
146
161
  2 mL bromine | 3.119 g bromine
147
162
  --------------x----------------- #=> 6.238 g bromine
148
163
  1 | 1 mL bromine
149
164
  ```
150
- becomes straightforward when you define a measurement:
165
+ becomes straightforward:
151
166
  ```python
152
167
  from ucon import Number, Scale, units
153
168
  from ucon.quantity import Ratio
154
169
 
155
- # Two milliliters of bromine
156
170
  mL = Scale.milli * units.liter
157
171
  two_mL_bromine = Number(quantity=2, unit=mL)
158
172
 
159
- # Density of bromine: 3.119 g/mL
160
173
  bromine_density = Ratio(
161
174
  numerator=Number(unit=units.gram, quantity=3.119),
162
175
  denominator=Number(unit=mL),
163
176
  )
164
177
 
165
- # Multiply to find mass
166
178
  grams_bromine = bromine_density.evaluate() * two_mL_bromine
167
179
  print(grams_bromine) # <6.238 g>
168
180
  ```
169
181
 
170
- Scale prefixes compose naturally:
182
+ ### Scale Prefixes
183
+
171
184
  ```python
172
185
  km = Scale.kilo * units.meter # UnitProduct with kilo-scaled meter
173
186
  mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
@@ -175,12 +188,12 @@ mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
175
188
  print(km.shorthand) # 'km'
176
189
  print(mg.shorthand) # 'mg'
177
190
 
178
- # Scale arithmetic
179
191
  print(km.fold_scale()) # 1000.0
180
192
  print(mg.fold_scale()) # 0.001
181
193
  ```
182
194
 
183
- Units are callable for ergonomic quantity construction:
195
+ ### Callable Units and Conversion
196
+
184
197
  ```python
185
198
  from ucon import units, Scale
186
199
 
@@ -199,16 +212,16 @@ distance_mi = distance.to(units.mile)
199
212
  print(distance_mi) # <3.107... mi>
200
213
  ```
201
214
 
202
- Dimensionless units have semantic isolation — angles, solid angles, and ratios are distinct:
215
+ ### Dimensionless Units
216
+
217
+ Angles, solid angles, and ratios are semantically isolated:
203
218
  ```python
204
219
  import math
205
220
  from ucon import units
206
221
 
207
- # Angle conversions
208
222
  angle = units.radian(math.pi)
209
223
  print(angle.to(units.degree)) # <180.0 deg>
210
224
 
211
- # Ratio conversions
212
225
  ratio = units.percent(50)
213
226
  print(ratio.to(units.ppm)) # <500000.0 ppm>
214
227
 
@@ -216,25 +229,61 @@ print(ratio.to(units.ppm)) # <500000.0 ppm>
216
229
  units.radian(1).to(units.percent) # raises ConversionNotFound
217
230
  ```
218
231
 
219
- Uncertainty propagates through arithmetic and conversions:
232
+ ### Uncertainty Propagation
233
+
220
234
  ```python
221
235
  from ucon import units
222
236
 
223
- # Measurements with uncertainty
224
237
  length = units.meter(1.234, uncertainty=0.005)
225
238
  width = units.meter(0.567, uncertainty=0.003)
226
239
 
227
240
  print(length) # <1.234 ± 0.005 m>
228
241
 
229
- # Uncertainty propagates through arithmetic (quadrature)
242
+ # Propagates through arithmetic (quadrature)
230
243
  area = length * width
231
244
  print(area) # <0.699678 ± 0.00424... m²>
232
245
 
233
- # Uncertainty propagates through conversion
246
+ # Propagates through conversion
234
247
  length_ft = length.to(units.foot)
235
248
  print(length_ft) # <4.048... ± 0.0164... ft>
236
249
  ```
237
250
 
251
+ ### Pydantic Integration
252
+
253
+ ```python
254
+ from pydantic import BaseModel
255
+ from ucon.pydantic import Number
256
+ from ucon import units
257
+
258
+ class Measurement(BaseModel):
259
+ value: Number
260
+
261
+ # From JSON/dict input
262
+ m = Measurement(value={"quantity": 5, "unit": "km"})
263
+ print(m.value) # <5 km>
264
+
265
+ # From Number instance
266
+ m2 = Measurement(value=units.meter(10))
267
+
268
+ # Serialize to JSON
269
+ print(m.model_dump_json())
270
+ # {"value": {"quantity": 5.0, "unit": "km", "uncertainty": null}}
271
+
272
+ # Supports both Unicode and ASCII unit notation
273
+ m3 = Measurement(value={"quantity": 9.8, "unit": "m/s^2"}) # ASCII
274
+ m4 = Measurement(value={"quantity": 9.8, "unit": "m/s²"}) # Unicode
275
+ ```
276
+
277
+ **Design notes:**
278
+ - **Serialization format**: Units serialize as human-readable shorthand strings (`"km"`, `"m/s^2"`) rather than structured dicts, aligning with how scientists express units.
279
+ - **Parsing priority**: When parsing `"kg"`, ucon returns `Scale.kilo * gram` rather than looking up a `kilogram` Unit, ensuring consistent round-trip serialization and avoiding redundant unit definitions.
280
+
281
+ ### Custom Unit Systems
282
+
283
+ `BasisTransform` enables conversions between incompatible dimensional structures (e.g., fantasy game physics, CGS units, domain-specific systems).
284
+
285
+ See full example: [docs/examples/basis-transform-fantasy-units.md](./docs/examples/basis-transform-fantasy-units.md)
286
+
238
287
  ---
239
288
 
240
289
  ## Roadmap Highlights
@@ -245,8 +294,9 @@ print(length_ft) # <4.048... ± 0.0164... ft>
245
294
  | **0.4.x** | Conversion System | `ConversionGraph`, `Number.to()`, callable units | ✅ Complete |
246
295
  | **0.5.0** | Dimensionless Units | Pseudo-dimensions for angle, solid angle, ratio | ✅ Complete |
247
296
  | **0.5.x** | Uncertainty | Propagation through arithmetic and conversions | ✅ Complete |
248
- | **0.5.x** | Unit Systems | `BasisMap`, `UnitSystem` | 🚧 In Progress |
249
- | **0.7.x** | Pydantic Integration | Type-safe quantity validation | Planned |
297
+ | **0.5.x** | Unit Systems | `BasisTransform`, `UnitSystem`, cross-basis conversion | Complete |
298
+ | **0.6.x** | Pydantic Integration | Type-safe quantity validation, JSON serialization | Complete |
299
+ | **0.7.x** | NumPy Arrays | Vectorized conversion and arithmetic | ⏳ Planned |
250
300
 
251
301
  See full roadmap: [ROADMAP.md](./ROADMAP.md)
252
302
 
@@ -255,14 +305,21 @@ See full roadmap: [ROADMAP.md](./ROADMAP.md)
255
305
  ## Contributing
256
306
 
257
307
  Contributions, issues, and pull requests are welcome!
258
- Ensure `nox` is installed.
308
+
309
+ Set up your development environment:
310
+ ```bash
311
+ make venv
312
+ source .ucon-3.12/bin/activate
259
313
  ```
260
- pip install -r requirements.txt
314
+
315
+ Run the test suite before committing:
316
+ ```bash
317
+ make test
261
318
  ```
262
- Then run the full test suite (against all supported python versions) before committing:
263
319
 
320
+ Run tests across all supported Python versions:
264
321
  ```bash
265
- nox -s test
322
+ make test-all
266
323
  ```
267
324
  ---
268
325
 
@@ -1,40 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: ucon
3
- Version: 0.5.1
4
- Summary: a tool for dimensional analysis: a "Unit CONverter"
5
- Home-page: https://github.com/withtwoemms/ucon
6
- Author: Emmanuel I. Obi
7
- Maintainer: Emmanuel I. Obi
8
- Maintainer-email: withtwoemms@gmail.com
9
- License: Apache-2.0
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Intended Audience :: Education
13
- Classifier: Intended Audience :: Science/Research
14
- Classifier: Topic :: Software Development :: Build Tools
15
- Classifier: License :: OSI Approved :: Apache Software License
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
- Classifier: Programming Language :: Python :: 3.10
20
- Classifier: Programming Language :: Python :: 3.11
21
- Classifier: Programming Language :: Python :: 3.12
22
- Classifier: Programming Language :: Python :: 3.13
23
- Classifier: Programming Language :: Python :: 3.14
24
- Description-Content-Type: text/markdown
25
- License-File: LICENSE
26
- License-File: NOTICE
27
- Dynamic: author
28
- Dynamic: classifier
29
- Dynamic: description
30
- Dynamic: description-content-type
31
- Dynamic: home-page
32
- Dynamic: license
33
- Dynamic: license-file
34
- Dynamic: maintainer
35
- Dynamic: maintainer-email
36
- Dynamic: summary
37
-
38
1
  <table>
39
2
  <tr>
40
3
  <td width="200">
@@ -68,6 +31,7 @@ It combines **units**, **scales**, and **dimensions** into a composable algebra
68
31
  - Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, etc.)
69
32
  - Pseudo-dimensions for angles, solid angles, and ratios with semantic isolation
70
33
  - Uncertainty propagation through arithmetic and conversions
34
+ - Pydantic v2 integration for API validation and JSON serialization
71
35
  - A clean foundation for physics, chemistry, data modeling, and beyond
72
36
 
73
37
  Think of it as **`decimal.Decimal` for the physical world** — precise, predictable, and type-safe.
@@ -92,7 +56,11 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
92
56
  | **`Ratio`** | `ucon.core` | Represents the division of two `Number` objects; captures relationships between quantities. | Expressing rates, densities, efficiencies (e.g., energy / time = power, length / time = velocity). |
93
57
  | **`Map`** hierarchy | `ucon.maps` | Composable conversion morphisms: `LinearMap`, `AffineMap`, `ComposedMap`. | Defining conversion functions between units (e.g., meter→foot, celsius→kelvin). |
94
58
  | **`ConversionGraph`** | `ucon.graph` | Registry of unit conversion edges with BFS path composition. | Converting between units via `Number.to(target)`; managing default and custom graphs. |
59
+ | **`UnitSystem`** | `ucon.core` | Named mapping from dimensions to base units (e.g., SI, Imperial). | Defining coherent unit systems; grouping base units by dimension. |
60
+ | **`BasisTransform`** | `ucon.core` | Matrix-based transformation between dimensional exponent spaces. | Converting between incompatible dimensional structures; exact arithmetic with `Fraction`. |
61
+ | **`RebasedUnit`** | `ucon.core` | A unit rebased to another system's dimension, preserving provenance. | Cross-basis conversions; tracking original unit through basis changes. |
95
62
  | **`units` module** | `ucon.units` | Defines canonical unit instances (SI, imperial, information, and derived units). | Quick access to standard physical units (`units.meter`, `units.foot`, `units.byte`, etc.). |
63
+ | **`pydantic` module** | `ucon.pydantic` | Pydantic v2 integration with `Number` type for model validation and JSON serialization. | Using `Number` in Pydantic models; API request/response validation; JSON round-trip serialization. |
96
64
 
97
65
  ### Under the Hood
98
66
 
@@ -139,35 +107,40 @@ Simple:
139
107
  pip install ucon
140
108
  ```
141
109
 
110
+ With Pydantic v2 support:
111
+ ```bash
112
+ pip install ucon[pydantic]
113
+ ```
114
+
142
115
  ## Usage
143
116
 
144
- This sort of dimensional analysis:
117
+ ### Quantities and Arithmetic
118
+
119
+ Dimensional analysis like this:
145
120
  ```
146
121
  2 mL bromine | 3.119 g bromine
147
122
  --------------x----------------- #=> 6.238 g bromine
148
123
  1 | 1 mL bromine
149
124
  ```
150
- becomes straightforward when you define a measurement:
125
+ becomes straightforward:
151
126
  ```python
152
127
  from ucon import Number, Scale, units
153
128
  from ucon.quantity import Ratio
154
129
 
155
- # Two milliliters of bromine
156
130
  mL = Scale.milli * units.liter
157
131
  two_mL_bromine = Number(quantity=2, unit=mL)
158
132
 
159
- # Density of bromine: 3.119 g/mL
160
133
  bromine_density = Ratio(
161
134
  numerator=Number(unit=units.gram, quantity=3.119),
162
135
  denominator=Number(unit=mL),
163
136
  )
164
137
 
165
- # Multiply to find mass
166
138
  grams_bromine = bromine_density.evaluate() * two_mL_bromine
167
139
  print(grams_bromine) # <6.238 g>
168
140
  ```
169
141
 
170
- Scale prefixes compose naturally:
142
+ ### Scale Prefixes
143
+
171
144
  ```python
172
145
  km = Scale.kilo * units.meter # UnitProduct with kilo-scaled meter
173
146
  mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
@@ -175,12 +148,12 @@ mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
175
148
  print(km.shorthand) # 'km'
176
149
  print(mg.shorthand) # 'mg'
177
150
 
178
- # Scale arithmetic
179
151
  print(km.fold_scale()) # 1000.0
180
152
  print(mg.fold_scale()) # 0.001
181
153
  ```
182
154
 
183
- Units are callable for ergonomic quantity construction:
155
+ ### Callable Units and Conversion
156
+
184
157
  ```python
185
158
  from ucon import units, Scale
186
159
 
@@ -199,16 +172,16 @@ distance_mi = distance.to(units.mile)
199
172
  print(distance_mi) # <3.107... mi>
200
173
  ```
201
174
 
202
- Dimensionless units have semantic isolation — angles, solid angles, and ratios are distinct:
175
+ ### Dimensionless Units
176
+
177
+ Angles, solid angles, and ratios are semantically isolated:
203
178
  ```python
204
179
  import math
205
180
  from ucon import units
206
181
 
207
- # Angle conversions
208
182
  angle = units.radian(math.pi)
209
183
  print(angle.to(units.degree)) # <180.0 deg>
210
184
 
211
- # Ratio conversions
212
185
  ratio = units.percent(50)
213
186
  print(ratio.to(units.ppm)) # <500000.0 ppm>
214
187
 
@@ -216,25 +189,61 @@ print(ratio.to(units.ppm)) # <500000.0 ppm>
216
189
  units.radian(1).to(units.percent) # raises ConversionNotFound
217
190
  ```
218
191
 
219
- Uncertainty propagates through arithmetic and conversions:
192
+ ### Uncertainty Propagation
193
+
220
194
  ```python
221
195
  from ucon import units
222
196
 
223
- # Measurements with uncertainty
224
197
  length = units.meter(1.234, uncertainty=0.005)
225
198
  width = units.meter(0.567, uncertainty=0.003)
226
199
 
227
200
  print(length) # <1.234 ± 0.005 m>
228
201
 
229
- # Uncertainty propagates through arithmetic (quadrature)
202
+ # Propagates through arithmetic (quadrature)
230
203
  area = length * width
231
204
  print(area) # <0.699678 ± 0.00424... m²>
232
205
 
233
- # Uncertainty propagates through conversion
206
+ # Propagates through conversion
234
207
  length_ft = length.to(units.foot)
235
208
  print(length_ft) # <4.048... ± 0.0164... ft>
236
209
  ```
237
210
 
211
+ ### Pydantic Integration
212
+
213
+ ```python
214
+ from pydantic import BaseModel
215
+ from ucon.pydantic import Number
216
+ from ucon import units
217
+
218
+ class Measurement(BaseModel):
219
+ value: Number
220
+
221
+ # From JSON/dict input
222
+ m = Measurement(value={"quantity": 5, "unit": "km"})
223
+ print(m.value) # <5 km>
224
+
225
+ # From Number instance
226
+ m2 = Measurement(value=units.meter(10))
227
+
228
+ # Serialize to JSON
229
+ print(m.model_dump_json())
230
+ # {"value": {"quantity": 5.0, "unit": "km", "uncertainty": null}}
231
+
232
+ # Supports both Unicode and ASCII unit notation
233
+ m3 = Measurement(value={"quantity": 9.8, "unit": "m/s^2"}) # ASCII
234
+ m4 = Measurement(value={"quantity": 9.8, "unit": "m/s²"}) # Unicode
235
+ ```
236
+
237
+ **Design notes:**
238
+ - **Serialization format**: Units serialize as human-readable shorthand strings (`"km"`, `"m/s^2"`) rather than structured dicts, aligning with how scientists express units.
239
+ - **Parsing priority**: When parsing `"kg"`, ucon returns `Scale.kilo * gram` rather than looking up a `kilogram` Unit, ensuring consistent round-trip serialization and avoiding redundant unit definitions.
240
+
241
+ ### Custom Unit Systems
242
+
243
+ `BasisTransform` enables conversions between incompatible dimensional structures (e.g., fantasy game physics, CGS units, domain-specific systems).
244
+
245
+ See full example: [docs/examples/basis-transform-fantasy-units.md](./docs/examples/basis-transform-fantasy-units.md)
246
+
238
247
  ---
239
248
 
240
249
  ## Roadmap Highlights
@@ -245,8 +254,9 @@ print(length_ft) # <4.048... ± 0.0164... ft>
245
254
  | **0.4.x** | Conversion System | `ConversionGraph`, `Number.to()`, callable units | ✅ Complete |
246
255
  | **0.5.0** | Dimensionless Units | Pseudo-dimensions for angle, solid angle, ratio | ✅ Complete |
247
256
  | **0.5.x** | Uncertainty | Propagation through arithmetic and conversions | ✅ Complete |
248
- | **0.5.x** | Unit Systems | `BasisMap`, `UnitSystem` | 🚧 In Progress |
249
- | **0.7.x** | Pydantic Integration | Type-safe quantity validation | Planned |
257
+ | **0.5.x** | Unit Systems | `BasisTransform`, `UnitSystem`, cross-basis conversion | Complete |
258
+ | **0.6.x** | Pydantic Integration | Type-safe quantity validation, JSON serialization | Complete |
259
+ | **0.7.x** | NumPy Arrays | Vectorized conversion and arithmetic | ⏳ Planned |
250
260
 
251
261
  See full roadmap: [ROADMAP.md](./ROADMAP.md)
252
262
 
@@ -255,14 +265,21 @@ See full roadmap: [ROADMAP.md](./ROADMAP.md)
255
265
  ## Contributing
256
266
 
257
267
  Contributions, issues, and pull requests are welcome!
258
- Ensure `nox` is installed.
268
+
269
+ Set up your development environment:
270
+ ```bash
271
+ make venv
272
+ source .ucon-3.12/bin/activate
259
273
  ```
260
- pip install -r requirements.txt
274
+
275
+ Run the test suite before committing:
276
+ ```bash
277
+ make test
261
278
  ```
262
- Then run the full test suite (against all supported python versions) before committing:
263
279
 
280
+ Run tests across all supported Python versions:
264
281
  ```bash
265
- nox -s test
282
+ make test-all
266
283
  ```
267
284
  ---
268
285