qnty 0.1.3__py3-none-any.whl → 0.1.4__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.
@@ -0,0 +1,198 @@
1
+ """
2
+ Unit Suggestions System
3
+ ======================
4
+
5
+ Provides fuzzy string matching for unit validation errors with intelligent recommendations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from difflib import SequenceMatcher
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from ..units.registry import Registry
17
+
18
+
19
+ class UnitSuggester:
20
+ """Provides intelligent suggestions for invalid unit strings."""
21
+
22
+ __slots__ = ("_unit_names", "_unit_aliases", "_all_suggestions", "_loaded")
23
+
24
+ def __init__(self):
25
+ self._unit_names: list[str] = []
26
+ self._unit_aliases: dict[str, str] = {} # alias -> canonical_name
27
+ self._all_suggestions: list[str] = [] # All searchable strings
28
+ self._loaded = False
29
+
30
+ def _load_units(self) -> None:
31
+ """Load unit names and aliases from the unit_data.json file."""
32
+ if self._loaded:
33
+ return
34
+
35
+ try:
36
+ # Load from the unit data file used by code generation
37
+ unit_data_path = Path(__file__).parent.parent.parent.parent / "codegen" / "generators" / "data" / "unit_data.json"
38
+
39
+ if not unit_data_path.exists():
40
+ # Fallback: try to load from registry if available
41
+ self._load_from_registry()
42
+ return
43
+
44
+ with open(unit_data_path, 'r', encoding='utf-8') as f:
45
+ unit_data = json.load(f)
46
+
47
+ # Extract all unit names and aliases
48
+ for field_data in unit_data.values():
49
+ if not isinstance(field_data, dict) or 'units' not in field_data:
50
+ continue
51
+
52
+ for unit in field_data['units']:
53
+ # Add normalized name
54
+ unit_name = unit.get('normalized_name', '')
55
+ if unit_name:
56
+ self._unit_names.append(unit_name)
57
+ self._all_suggestions.append(unit_name)
58
+
59
+ # Add notation as an alias
60
+ notation = unit.get('notation', '')
61
+ if notation and notation != unit_name:
62
+ # Clean up notation (remove LaTeX formatting)
63
+ clean_notation = self._clean_notation(notation)
64
+ if clean_notation and clean_notation not in self._unit_aliases:
65
+ self._unit_aliases[clean_notation] = unit_name
66
+ self._all_suggestions.append(clean_notation)
67
+
68
+ # Add aliases
69
+ aliases = unit.get('aliases', [])
70
+ for alias in aliases:
71
+ if alias and alias not in self._unit_aliases:
72
+ self._unit_aliases[alias] = unit_name
73
+ self._all_suggestions.append(alias)
74
+
75
+ self._loaded = True
76
+
77
+ except Exception:
78
+ # Fallback to registry-based loading
79
+ self._load_from_registry()
80
+
81
+ def _load_from_registry(self) -> None:
82
+ """Fallback: load units from registry if available."""
83
+ try:
84
+ from ..units.registry import registry
85
+
86
+ if hasattr(registry, 'units'):
87
+ for unit_name in registry.units.keys():
88
+ self._unit_names.append(unit_name)
89
+ self._all_suggestions.append(unit_name)
90
+
91
+ self._loaded = True
92
+ except Exception:
93
+ # If all else fails, just mark as loaded with empty data
94
+ self._loaded = True
95
+
96
+ def _clean_notation(self, notation: str) -> str:
97
+ """Clean LaTeX and special formatting from notation strings."""
98
+ # Remove common LaTeX patterns
99
+ notation = notation.replace('\\mathrm{', '').replace('}', '')
100
+ notation = notation.replace('\\text{', '').replace('$', '')
101
+ notation = notation.replace('\\', '')
102
+ notation = notation.replace('{', '').replace('}', '')
103
+
104
+ # Remove extra spaces and common patterns
105
+ notation = notation.strip()
106
+ notation = notation.replace(' ', '')
107
+
108
+ # Skip if too long or contains special characters that make it unusable
109
+ if len(notation) > 20 or any(char in notation for char in ['(', ')', '^', '_', '/']):
110
+ return ''
111
+
112
+ return notation if notation and len(notation) <= 10 else ''
113
+
114
+ def get_suggestions(self, invalid_unit: str, max_suggestions: int = 3) -> list[str]:
115
+ """
116
+ Get fuzzy string matching suggestions for an invalid unit.
117
+
118
+ Args:
119
+ invalid_unit: The invalid unit string that was entered
120
+ max_suggestions: Maximum number of suggestions to return
121
+
122
+ Returns:
123
+ List of suggested unit strings, ordered by similarity
124
+ """
125
+ self._load_units()
126
+
127
+ if not self._all_suggestions:
128
+ return []
129
+
130
+ # Calculate similarity scores for all units
131
+ similarities = []
132
+ invalid_lower = invalid_unit.lower().strip()
133
+
134
+ for suggestion in self._all_suggestions:
135
+ suggestion_lower = suggestion.lower()
136
+
137
+ # Exact match (shouldn't happen, but just in case)
138
+ if invalid_lower == suggestion_lower:
139
+ continue
140
+
141
+ # Calculate similarity using SequenceMatcher
142
+ similarity = SequenceMatcher(None, invalid_lower, suggestion_lower).ratio()
143
+
144
+ # Bonus for starts-with matches
145
+ if suggestion_lower.startswith(invalid_lower) or invalid_lower.startswith(suggestion_lower):
146
+ similarity += 0.2
147
+
148
+ # Bonus for contains matches
149
+ if invalid_lower in suggestion_lower or suggestion_lower in invalid_lower:
150
+ similarity += 0.1
151
+
152
+ similarities.append((similarity, suggestion))
153
+
154
+ # Sort by similarity score (descending) and return top matches
155
+ similarities.sort(key=lambda x: x[0], reverse=True)
156
+
157
+ # Filter for meaningful suggestions (similarity > 0.4)
158
+ meaningful_suggestions = [
159
+ suggestion for similarity, suggestion in similarities
160
+ if similarity > 0.4
161
+ ]
162
+
163
+ return meaningful_suggestions[:max_suggestions]
164
+
165
+
166
+ class UnitValidationError(ValueError):
167
+ """Error raised when an invalid unit is provided, with suggestions."""
168
+
169
+ def __init__(self, invalid_unit: str, variable_type: str = "", suggestions: list[str] | None = None):
170
+ self.invalid_unit = invalid_unit
171
+ self.variable_type = variable_type
172
+ self.suggestions = suggestions or []
173
+
174
+ # Construct error message
175
+ if variable_type:
176
+ msg = f"Unknown unit '{invalid_unit}' for {variable_type}"
177
+ else:
178
+ msg = f"Unknown unit '{invalid_unit}'"
179
+
180
+ if self.suggestions:
181
+ msg += f". Did you mean: {', '.join(repr(s) for s in self.suggestions)}?"
182
+
183
+ super().__init__(msg)
184
+
185
+
186
+ # Global suggester instance
187
+ _suggester = UnitSuggester()
188
+
189
+
190
+ def get_unit_suggestions(invalid_unit: str, max_suggestions: int = 3) -> list[str]:
191
+ """Get unit suggestions for an invalid unit string."""
192
+ return _suggester.get_suggestions(invalid_unit, max_suggestions)
193
+
194
+
195
+ def create_unit_validation_error(invalid_unit: str, variable_type: str = "") -> UnitValidationError:
196
+ """Create a UnitValidationError with intelligent suggestions."""
197
+ suggestions = get_unit_suggestions(invalid_unit)
198
+ return UnitValidationError(invalid_unit, variable_type, suggestions)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qnty
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: High-performance unit system library for Python with dimensional safety and fast unit conversions
5
5
  License: Apache-2.0
6
6
  Keywords: units,dimensional analysis,engineering,physics,quantities,measurements
@@ -9,12 +9,12 @@ qnty/dimensions/field_dims.py,sha256=bcPu1xxPhjoNjc7TxyP_B4xKDLHKGdtNne-sCB9hz-8
9
9
  qnty/dimensions/field_dims.pyi,sha256=lk3YrH3Ovs3CJCZe5MfX334kdpmsfEql4D3fLKjuYDs,4575
10
10
  qnty/dimensions/signature.py,sha256=yk7QGejAV-TEPTqWE1Q5yV2sZA-RWGiK_rHiMT0Q2yU,4173
11
11
  qnty/equations/__init__.py,sha256=Ou5H6tTFXgVw16JYan_a4653NxroBxcnTY6YWt380Qo,108
12
- qnty/equations/equation.py,sha256=6Ot3-XhSFyxdv3hUwJvdTvB2BGZlAoXX46uok_6q-14,9041
12
+ qnty/equations/equation.py,sha256=EPw1mKTMriI94uKCqSWTPvQfcZTBLPhX-D4nDZI9vJg,10580
13
13
  qnty/equations/system.py,sha256=vMoD1iTUrAHnVFVvCUKeyNfSBfMiqpwQbfDx46kN9N8,5155
14
14
  qnty/expressions/__init__.py,sha256=DA2s7DBhVCmdUgsYSTJWObsp2DbbpFn492yr1nUTg2g,930
15
15
  qnty/expressions/formatter.py,sha256=yLGLwLYjhBvVi2Q6rfkg8pbyH0-a1Ko0AYLsqJTJf50,7806
16
16
  qnty/expressions/functions.py,sha256=ek43udfUDpThKo38rVPBYPvKfZNc9Bbs8RuL-CvQc_A,2729
17
- qnty/expressions/nodes.py,sha256=oqMFfMeeWhccv4KBHNI5qYBz2pSe6jD-OwE4P2HyG1A,28644
17
+ qnty/expressions/nodes.py,sha256=Q6DgmhP7MVUkdsUpzzp7_yjV8hegd0AwETvd4bo8nWc,29362
18
18
  qnty/expressions/types.py,sha256=eoM-IqY-k-IypRHAlRwjEtMmB6DiwX7YGot8t_vGw3o,1729
19
19
  qnty/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  qnty/extensions/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -22,18 +22,18 @@ qnty/extensions/plotting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
22
22
  qnty/extensions/reporting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  qnty/problems/__init__.py,sha256=g7zuml2IecoSwgzX6r1zZ5SlmBKFc8qqTR_cao037pc,4808
24
24
  qnty/problems/composition.py,sha256=HI5ffsF14IXRuqjM0z_O0kkpme9fPnJVdz6m4rDCJsM,44916
25
- qnty/problems/problem.py,sha256=8dg_AwX3lgN2rpa33RhhmA-AXbPg8lmSBJzeB3iXBMk,31565
25
+ qnty/problems/problem.py,sha256=xnkHsI9OcY3HJ0UxtMnMQrS6n7accl4d-5fSJQ6ITPo,31721
26
26
  qnty/problems/rules.py,sha256=NwIStAa8bocVtvzAsnPmRdC_0ENTJWyXLOoYBnkvpPA,5176
27
27
  qnty/problems/solving.py,sha256=LTI8F9ujDiSqXE9Aiz8sOgaGJNX9p7oaR5CQIZHpCY8,44315
28
28
  qnty/problems/validation.py,sha256=SmFEsgHx5XwRNlR2suOhxO-WNsOwPZhCP8wyVKYo1EE,4826
29
29
  qnty/quantities/__init__.py,sha256=K_h5v6X6-OyITSXOhbIZTDAJe6-y_7iMMDEIQ4O3luc,809
30
- qnty/quantities/base_qnty.py,sha256=QasOR4-a7gwPBvc6cLJ3ooQHmOcWbYexgtNQ9I6bXI8,32516
31
- qnty/quantities/field_converters.py,sha256=rDWttIE0lwF1doGlLG5RJTcTikYEYruMrRBMlr8fvBI,1008701
32
- qnty/quantities/field_qnty.py,sha256=ZUZ8N102WqVW0r3JZtSCbSSrP1co5r_swOBNTJJCNTk,43053
30
+ qnty/quantities/base_qnty.py,sha256=u-1kzSAup47SUMjYVvnFyfVGk6hW8eUCcOtgUeFaMtM,33776
31
+ qnty/quantities/field_converters.py,sha256=GNG1fgd4odagfAEtmhSHwG9DvX8DL4cq8DHlRYwaw3U,1008869
32
+ qnty/quantities/field_qnty.py,sha256=YZ0Gd2r5tz55E18a1_ItHke7P7VJZ15_fhI6DLoFxAI,43938
33
33
  qnty/quantities/field_setter.py,sha256=JCvRom4qvCYvgRNgFLZEuwb1PCmz0qrKzxDv0-h4RMo,449348
34
34
  qnty/quantities/field_setter.pyi,sha256=NDsOvngsfEaXczZ56CcxlCOsLFGpXbEhxaBq1qW80Us,140812
35
- qnty/quantities/field_vars.py,sha256=-wZ91NHxgRg8jJnSbggaWphRICjuqKPr6B7PVfIjk4Y,401188
36
- qnty/quantities/field_vars.pyi,sha256=nTrhUI1U_CFeVzAuYlHgp2supUvkkb0EAM-74kiJ7L4,145467
35
+ qnty/quantities/field_vars.py,sha256=Zhc7PoCqtqfmaAw-MPMiwBSPRxTcLruzo-9Krusdbsw,517162
36
+ qnty/quantities/field_vars.pyi,sha256=_O5QaeclK5sgVYUlTLFyX4whCtcNIEBk5WVd0DbAxss,146420
37
37
  qnty/solving/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  qnty/solving/manager.py,sha256=LQBMhWD3ajRYMBXkwRpkVzdo7qVEDviBAoHpjAzS-0U,3945
39
39
  qnty/solving/order.py,sha256=q1G3fMWamhmBK6vN0L2BuTqWl0aa94ZJPCWusrntcXo,15488
@@ -56,6 +56,7 @@ qnty/utils/error_handling/handlers.py,sha256=_q12co-jr4YSktRoCPpGBbh6WXEDw9MbmWx
56
56
  qnty/utils/logging.py,sha256=2H6_gSOQjxdK5024XTY3E1jGIQPE8WdalVhVBFw51OA,1143
57
57
  qnty/utils/protocols.py,sha256=c_Ya_epCm7qenAADRMZiwiQ0PdD-Z4T85b1z1YQNXAk,5247
58
58
  qnty/utils/scope_discovery.py,sha256=mQc-FHJ5-VNBzqQwiFofV-hqeF3GpLRaLlTjYDRnOqs,15184
59
- qnty-0.1.3.dist-info/METADATA,sha256=bHbY7G0DtCTsP3buOzzTusq8SEGEMwuwii6_7VAQhU4,6761
60
- qnty-0.1.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
61
- qnty-0.1.3.dist-info/RECORD,,
59
+ qnty/utils/unit_suggestions.py,sha256=V_eNGYIXayyHY7bi4tA_bDuiNwKlkbV8R2OgMMGLk_w,7774
60
+ qnty-0.1.4.dist-info/METADATA,sha256=55oIRTrDystUWnodxecG8Gbf0zx42u5wPuR00JSET_Q,6761
61
+ qnty-0.1.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
62
+ qnty-0.1.4.dist-info/RECORD,,
File without changes