qnty 0.1.2__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.
- qnty/equations/equation.py +29 -3
- qnty/expressions/nodes.py +21 -13
- qnty/problems/problem.py +4 -0
- qnty/quantities/base_qnty.py +38 -0
- qnty/quantities/field_converters.py +5 -2
- qnty/quantities/field_qnty.py +25 -5
- qnty/quantities/field_vars.py +6523 -1606
- qnty/quantities/field_vars.pyi +963 -107
- qnty/utils/unit_suggestions.py +198 -0
- {qnty-0.1.2.dist-info → qnty-0.1.4.dist-info}/METADATA +1 -1
- {qnty-0.1.2.dist-info → qnty-0.1.4.dist-info}/RECORD +12 -11
- {qnty-0.1.2.dist-info → qnty-0.1.4.dist-info}/WHEEL +0 -0
@@ -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
|
+
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=
|
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=
|
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=
|
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=
|
31
|
-
qnty/quantities/field_converters.py,sha256=
|
32
|
-
qnty/quantities/field_qnty.py,sha256=
|
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=
|
36
|
-
qnty/quantities/field_vars.pyi,sha256=
|
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
|
60
|
-
qnty-0.1.
|
61
|
-
qnty-0.1.
|
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
|