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.
Files changed (70) hide show
  1. {ucon-0.6.3 → ucon-0.6.5}/PKG-INFO +1 -1
  2. ucon-0.6.5/tests/ucon/test_mcp_server.py +118 -0
  3. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_unit_parsing.py +22 -0
  4. {ucon-0.6.3 → ucon-0.6.5}/ucon/mcp/server.py +5 -10
  5. {ucon-0.6.3 → ucon-0.6.5}/ucon/units.py +1 -0
  6. {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/PKG-INFO +1 -1
  7. {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/SOURCES.txt +1 -0
  8. {ucon-0.6.3 → ucon-0.6.5}/.github/workflows/publish.yaml +0 -0
  9. {ucon-0.6.3 → ucon-0.6.5}/.github/workflows/tests.yaml +0 -0
  10. {ucon-0.6.3 → ucon-0.6.5}/.gitignore +0 -0
  11. {ucon-0.6.3 → ucon-0.6.5}/LICENSE +0 -0
  12. {ucon-0.6.3 → ucon-0.6.5}/Makefile +0 -0
  13. {ucon-0.6.3 → ucon-0.6.5}/NOTICE +0 -0
  14. {ucon-0.6.3 → ucon-0.6.5}/README.md +0 -0
  15. {ucon-0.6.3 → ucon-0.6.5}/ROADMAP.md +0 -0
  16. {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/001-unity-distance-metric-for-nearest-scale.md +0 -0
  17. {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/002-composite-units.md +0 -0
  18. {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/003-composable-unit-algebra.md +0 -0
  19. {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/004-unit-algebra-naming.md +0 -0
  20. {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/005-pseudo-dimension-tuple-values.md +0 -0
  21. {ucon-0.6.3 → ucon-0.6.5}/docs/decisions/006-pydantic-integration-pattern.md +0 -0
  22. {ucon-0.6.3 → ucon-0.6.5}/docs/examples/basis-transform-fantasy-units.md +0 -0
  23. {ucon-0.6.3 → ucon-0.6.5}/docs/explainers/exponent-scale-relationship.md +0 -0
  24. {ucon-0.6.3 → ucon-0.6.5}/docs/explainers/type-operation-matrix.md +0 -0
  25. {ucon-0.6.3 → ucon-0.6.5}/docs/explainers/why-algebraic-closure-matters.md +0 -0
  26. {ucon-0.6.3 → ucon-0.6.5}/docs/explainers/why-type-safety-matters.md +0 -0
  27. {ucon-0.6.3 → ucon-0.6.5}/docs/proposals/interface-unifying-the-value-layer.md +0 -0
  28. {ucon-0.6.3 → ucon-0.6.5}/docs/proposals/project_unified-algebraic-core.md +0 -0
  29. {ucon-0.6.3 → ucon-0.6.5}/docs/proposals/support-for-fractional-exponents.md +0 -0
  30. {ucon-0.6.3 → ucon-0.6.5}/docs/proposals/unified-unit-presentation.md +0 -0
  31. {ucon-0.6.3 → ucon-0.6.5}/pyproject.toml +0 -0
  32. {ucon-0.6.3 → ucon-0.6.5}/requirements.txt +0 -0
  33. {ucon-0.6.3 → ucon-0.6.5}/setup.cfg +0 -0
  34. {ucon-0.6.3 → ucon-0.6.5}/setup.py +0 -0
  35. {ucon-0.6.3 → ucon-0.6.5}/tests/__init__.py +0 -0
  36. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/__init__.py +0 -0
  37. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/conversion/__init__.py +0 -0
  38. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/conversion/test_graph.py +0 -0
  39. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/conversion/test_map.py +0 -0
  40. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/mcp/__init__.py +0 -0
  41. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/mcp/test_server.py +0 -0
  42. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_algebra.py +0 -0
  43. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_basis_transform.py +0 -0
  44. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_core.py +0 -0
  45. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_default_graph_conversions.py +0 -0
  46. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_dimensionless_units.py +0 -0
  47. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_graph_basis_transform.py +0 -0
  48. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_logmap.py +0 -0
  49. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_nines.py +0 -0
  50. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_pickle.py +0 -0
  51. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_pydantic.py +0 -0
  52. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_quantity.py +0 -0
  53. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_rebased_unit.py +0 -0
  54. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_uncertainty.py +0 -0
  55. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_unit_system.py +0 -0
  56. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_units.py +0 -0
  57. {ucon-0.6.3 → ucon-0.6.5}/tests/ucon/test_vector_fraction.py +0 -0
  58. {ucon-0.6.3 → ucon-0.6.5}/ucon/__init__.py +0 -0
  59. {ucon-0.6.3 → ucon-0.6.5}/ucon/algebra.py +0 -0
  60. {ucon-0.6.3 → ucon-0.6.5}/ucon/core.py +0 -0
  61. {ucon-0.6.3 → ucon-0.6.5}/ucon/graph.py +0 -0
  62. {ucon-0.6.3 → ucon-0.6.5}/ucon/maps.py +0 -0
  63. {ucon-0.6.3 → ucon-0.6.5}/ucon/mcp/__init__.py +0 -0
  64. {ucon-0.6.3 → ucon-0.6.5}/ucon/pydantic.py +0 -0
  65. {ucon-0.6.3 → ucon-0.6.5}/ucon/quantity.py +0 -0
  66. {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/dependency_links.txt +0 -0
  67. {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/entry_points.txt +0 -0
  68. {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/requires.txt +0 -0
  69. {ucon-0.6.3 → ucon-0.6.5}/ucon.egg-info/top_level.txt +0 -0
  70. {ucon-0.6.3 → ucon-0.6.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucon
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
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
@@ -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
- unit_str = None
95
- dim_name = "none"
96
-
97
- if result.unit:
98
- if isinstance(result.unit, UnitProduct):
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]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucon
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
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
@@ -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
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