ucon 0.6.3__tar.gz → 0.6.5__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.
- {ucon-0.6.3 → ucon-0.6.5}/PKG-INFO +1 -1
- ucon-0.6.5/tests/ucon/test_mcp_server.py +118 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_unit_parsing.py +22 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/mcp/server.py +5 -10
- {ucon-0.6.3 → ucon-0.6.5}/ucon/units.py +1 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/PKG-INFO +1 -1
- {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/SOURCES.txt +1 -0
- {ucon-0.6.3 → ucon-0.6.5}/.github/workflows/publish.yaml +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/.github/workflows/tests.yaml +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/.gitignore +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/LICENSE +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/Makefile +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/NOTICE +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/README.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ROADMAP.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/001-unity-distance-metric-for-nearest-scale.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/002-composite-units.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/003-composable-unit-algebra.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/004-unit-algebra-naming.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/005-pseudo-dimension-tuple-values.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/006-pydantic-integration-pattern.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/examples/basis-transform-fantasy-units.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/explainers/exponent-scale-relationship.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/explainers/type-operation-matrix.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/explainers/why-algebraic-closure-matters.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/explainers/why-type-safety-matters.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/proposals/interface-unifying-the-value-layer.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/proposals/project_unified-algebraic-core.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/proposals/support-for-fractional-exponents.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/docs/proposals/unified-unit-presentation.md +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/pyproject.toml +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/requirements.txt +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/setup.cfg +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/setup.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/__init__.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/__init__.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/conversion/__init__.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/conversion/test_graph.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/conversion/test_map.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/mcp/__init__.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/mcp/test_server.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_algebra.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_basis_transform.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_core.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_default_graph_conversions.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_dimensionless_units.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_graph_basis_transform.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_logmap.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_nines.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_pickle.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_pydantic.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_quantity.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_rebased_unit.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_uncertainty.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_unit_system.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_units.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_vector_fraction.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/__init__.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/algebra.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/core.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/graph.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/maps.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/mcp/__init__.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/pydantic.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon/quantity.py +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/dependency_links.txt +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/entry_points.txt +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/requires.txt +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/top_level.txt +0 -0
- {ucon-0.6.3 → ucon-0.6.5}/uv.lock +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# © 2026 The Radiativity Company
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
# See the LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Tests for MCP server tools.
|
|
7
|
+
|
|
8
|
+
Tests the convert, list_units, list_scales, check_dimensions, and
|
|
9
|
+
list_dimensions tools exposed via the MCP server.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import unittest
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from ucon.mcp.server import convert, list_units, list_scales, check_dimensions, list_dimensions
|
|
16
|
+
HAS_MCP = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_MCP = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@unittest.skipUnless(HAS_MCP, "MCP not installed")
|
|
22
|
+
class TestConvert(unittest.TestCase):
|
|
23
|
+
"""Test the convert tool."""
|
|
24
|
+
|
|
25
|
+
def test_basic_conversion(self):
|
|
26
|
+
result = convert(1000, "m", "km")
|
|
27
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=9)
|
|
28
|
+
self.assertEqual(result.unit, "km")
|
|
29
|
+
|
|
30
|
+
def test_returns_target_unit_string(self):
|
|
31
|
+
"""Convert should return the target unit string as requested."""
|
|
32
|
+
result = convert(100, "cm", "m")
|
|
33
|
+
self.assertEqual(result.unit, "m")
|
|
34
|
+
|
|
35
|
+
def test_ratio_unit_preserved(self):
|
|
36
|
+
"""Ratio units like mg/kg should preserve the unit string."""
|
|
37
|
+
result = convert(0.1, "mg/kg", "µg/kg")
|
|
38
|
+
self.assertAlmostEqual(result.quantity, 100.0, places=6)
|
|
39
|
+
self.assertEqual(result.unit, "µg/kg")
|
|
40
|
+
|
|
41
|
+
def test_medical_units(self):
|
|
42
|
+
"""Medical unit aliases should work."""
|
|
43
|
+
# mcg
|
|
44
|
+
result = convert(500, "mcg", "mg")
|
|
45
|
+
self.assertAlmostEqual(result.quantity, 0.5, places=9)
|
|
46
|
+
self.assertEqual(result.unit, "mg")
|
|
47
|
+
|
|
48
|
+
# cc
|
|
49
|
+
result = convert(5, "cc", "mL")
|
|
50
|
+
self.assertAlmostEqual(result.quantity, 5.0, places=9)
|
|
51
|
+
self.assertEqual(result.unit, "mL")
|
|
52
|
+
|
|
53
|
+
# min
|
|
54
|
+
result = convert(120, "mL/h", "mL/min")
|
|
55
|
+
self.assertAlmostEqual(result.quantity, 2.0, places=9)
|
|
56
|
+
self.assertEqual(result.unit, "mL/min")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@unittest.skipUnless(HAS_MCP, "MCP not installed")
|
|
60
|
+
class TestListUnits(unittest.TestCase):
|
|
61
|
+
"""Test the list_units tool."""
|
|
62
|
+
|
|
63
|
+
def test_returns_units(self):
|
|
64
|
+
result = list_units()
|
|
65
|
+
self.assertIsInstance(result, list)
|
|
66
|
+
self.assertGreater(len(result), 0)
|
|
67
|
+
|
|
68
|
+
def test_filter_by_dimension(self):
|
|
69
|
+
result = list_units(dimension="length")
|
|
70
|
+
self.assertGreater(len(result), 0)
|
|
71
|
+
for unit in result:
|
|
72
|
+
self.assertEqual(unit.dimension, "length")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@unittest.skipUnless(HAS_MCP, "MCP not installed")
|
|
76
|
+
class TestListScales(unittest.TestCase):
|
|
77
|
+
"""Test the list_scales tool."""
|
|
78
|
+
|
|
79
|
+
def test_returns_scales(self):
|
|
80
|
+
result = list_scales()
|
|
81
|
+
self.assertIsInstance(result, list)
|
|
82
|
+
self.assertGreater(len(result), 0)
|
|
83
|
+
|
|
84
|
+
def test_includes_kilo(self):
|
|
85
|
+
result = list_scales()
|
|
86
|
+
names = [s.name for s in result]
|
|
87
|
+
self.assertIn("kilo", names)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@unittest.skipUnless(HAS_MCP, "MCP not installed")
|
|
91
|
+
class TestCheckDimensions(unittest.TestCase):
|
|
92
|
+
"""Test the check_dimensions tool."""
|
|
93
|
+
|
|
94
|
+
def test_compatible(self):
|
|
95
|
+
result = check_dimensions("m", "ft")
|
|
96
|
+
self.assertTrue(result.compatible)
|
|
97
|
+
self.assertEqual(result.dimension_a, "length")
|
|
98
|
+
self.assertEqual(result.dimension_b, "length")
|
|
99
|
+
|
|
100
|
+
def test_incompatible(self):
|
|
101
|
+
result = check_dimensions("m", "s")
|
|
102
|
+
self.assertFalse(result.compatible)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@unittest.skipUnless(HAS_MCP, "MCP not installed")
|
|
106
|
+
class TestListDimensions(unittest.TestCase):
|
|
107
|
+
"""Test the list_dimensions tool."""
|
|
108
|
+
|
|
109
|
+
def test_returns_dimensions(self):
|
|
110
|
+
result = list_dimensions()
|
|
111
|
+
self.assertIsInstance(result, list)
|
|
112
|
+
self.assertIn("length", result)
|
|
113
|
+
self.assertIn("mass", result)
|
|
114
|
+
self.assertIn("time", result)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == '__main__':
|
|
118
|
+
unittest.main()
|
|
@@ -371,6 +371,28 @@ class TestPriorityScaledAliases(unittest.TestCase):
|
|
|
371
371
|
result = get_unit_by_name("mcg")
|
|
372
372
|
self.assertIsInstance(result, UnitProduct)
|
|
373
373
|
|
|
374
|
+
def test_cc_is_milliliter(self):
|
|
375
|
+
"""'cc' should parse as milliliter (1 cc = 1 mL)."""
|
|
376
|
+
from ucon.core import Dimension
|
|
377
|
+
result = get_unit_by_name("cc")
|
|
378
|
+
self.assertIsInstance(result, UnitProduct)
|
|
379
|
+
self.assertEqual(result.dimension, Dimension.volume)
|
|
380
|
+
self.assertAlmostEqual(result.fold_scale(), 0.001, places=10)
|
|
381
|
+
|
|
382
|
+
def test_cc_to_mL(self):
|
|
383
|
+
"""Conversion from cc to mL should be 1:1."""
|
|
384
|
+
from ucon.core import Number
|
|
385
|
+
vol = Number(5, unit=get_unit_by_name("cc"))
|
|
386
|
+
result = vol.to(get_unit_by_name("mL"))
|
|
387
|
+
self.assertAlmostEqual(result.quantity, 5.0, places=9)
|
|
388
|
+
|
|
389
|
+
def test_cc_to_L(self):
|
|
390
|
+
"""Conversion from cc to L should work."""
|
|
391
|
+
from ucon.core import Number
|
|
392
|
+
vol = Number(1000, unit=get_unit_by_name("cc"))
|
|
393
|
+
result = vol.to(get_unit_by_name("L"))
|
|
394
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=9)
|
|
395
|
+
|
|
374
396
|
|
|
375
397
|
if __name__ == '__main__':
|
|
376
398
|
unittest.main()
|
|
@@ -91,16 +91,11 @@ def convert(value: float, from_unit: str, to_unit: str) -> ConversionResult:
|
|
|
91
91
|
num = Number(quantity=value, unit=src)
|
|
92
92
|
result = num.to(dst)
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
unit_str = result.unit.shorthand
|
|
100
|
-
dim_name = result.unit.dimension.name
|
|
101
|
-
elif isinstance(result.unit, Unit):
|
|
102
|
-
unit_str = result.unit.shorthand
|
|
103
|
-
dim_name = result.unit.dimension.name
|
|
94
|
+
# Use the target unit string as output (what the user asked for).
|
|
95
|
+
# This handles cases like mg/kg → µg/kg where internal representation
|
|
96
|
+
# may lose unit info due to dimension cancellation.
|
|
97
|
+
unit_str = to_unit
|
|
98
|
+
dim_name = dst.dimension.name if hasattr(dst, 'dimension') else "none"
|
|
104
99
|
|
|
105
100
|
return ConversionResult(
|
|
106
101
|
quantity=result.quantity,
|
|
@@ -267,6 +267,7 @@ def _build_registry() -> None:
|
|
|
267
267
|
|
|
268
268
|
# Register priority scaled aliases (medical conventions)
|
|
269
269
|
_PRIORITY_SCALED_ALIASES['mcg'] = (gram, Scale.micro) # microgram
|
|
270
|
+
_PRIORITY_SCALED_ALIASES['cc'] = (liter, Scale.milli) # cubic centimeter = 1 mL
|
|
270
271
|
|
|
271
272
|
|
|
272
273
|
def _parse_exponent(s: str) -> Tuple[str, float]:
|
|
@@ -34,6 +34,7 @@ tests/ucon/test_default_graph_conversions.py
|
|
|
34
34
|
tests/ucon/test_dimensionless_units.py
|
|
35
35
|
tests/ucon/test_graph_basis_transform.py
|
|
36
36
|
tests/ucon/test_logmap.py
|
|
37
|
+
tests/ucon/test_mcp_server.py
|
|
37
38
|
tests/ucon/test_nines.py
|
|
38
39
|
tests/ucon/test_pickle.py
|
|
39
40
|
tests/ucon/test_pydantic.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ucon-0.6.3 → ucon-0.6.5}/NOTICE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|