truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.0__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.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Version Constraint Parsing and Matching.
|
|
2
|
+
|
|
3
|
+
This module provides version constraint parsing for common formats:
|
|
4
|
+
- Exact: 1.2.3 or =1.2.3
|
|
5
|
+
- Greater than: >1.2.3, >=1.2.3
|
|
6
|
+
- Less than: <1.2.3, <=1.2.3
|
|
7
|
+
- Caret: ^1.2.3 (compatible with 1.x.x)
|
|
8
|
+
- Tilde: ~1.2.3 (compatible with 1.2.x)
|
|
9
|
+
- Range: >=1.0.0 <2.0.0
|
|
10
|
+
- Wildcard: 1.2.*, 1.x
|
|
11
|
+
- Or: 1.2.3 || 2.0.0
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .semver import Version, parse_version
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConstraintOp(str, Enum):
|
|
25
|
+
"""Version constraint operators."""
|
|
26
|
+
|
|
27
|
+
EQ = "=" # Exact match
|
|
28
|
+
GT = ">" # Greater than
|
|
29
|
+
GTE = ">=" # Greater than or equal
|
|
30
|
+
LT = "<" # Less than
|
|
31
|
+
LTE = "<=" # Less than or equal
|
|
32
|
+
CARET = "^" # Caret range
|
|
33
|
+
TILDE = "~" # Tilde range
|
|
34
|
+
ANY = "*" # Any version
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class VersionConstraint:
|
|
39
|
+
"""A single version constraint.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
op: Constraint operator.
|
|
43
|
+
version: Target version.
|
|
44
|
+
original: Original constraint string.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
op: ConstraintOp
|
|
48
|
+
version: Version | None
|
|
49
|
+
original: str = ""
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
"""Return string representation."""
|
|
53
|
+
if self.original:
|
|
54
|
+
return self.original
|
|
55
|
+
if self.op == ConstraintOp.ANY:
|
|
56
|
+
return "*"
|
|
57
|
+
if self.version:
|
|
58
|
+
return f"{self.op.value}{self.version}"
|
|
59
|
+
return self.op.value
|
|
60
|
+
|
|
61
|
+
def matches(self, version: Version | str) -> bool:
|
|
62
|
+
"""Check if a version matches this constraint.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
version: Version to check.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if version matches constraint.
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(version, str):
|
|
71
|
+
version = parse_version(version)
|
|
72
|
+
|
|
73
|
+
if self.op == ConstraintOp.ANY:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
if self.version is None:
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
if self.op == ConstraintOp.EQ:
|
|
80
|
+
return version == self.version
|
|
81
|
+
|
|
82
|
+
if self.op == ConstraintOp.GT:
|
|
83
|
+
return version > self.version
|
|
84
|
+
|
|
85
|
+
if self.op == ConstraintOp.GTE:
|
|
86
|
+
return version >= self.version
|
|
87
|
+
|
|
88
|
+
if self.op == ConstraintOp.LT:
|
|
89
|
+
return version < self.version
|
|
90
|
+
|
|
91
|
+
if self.op == ConstraintOp.LTE:
|
|
92
|
+
return version <= self.version
|
|
93
|
+
|
|
94
|
+
if self.op == ConstraintOp.CARET:
|
|
95
|
+
return self._matches_caret(version)
|
|
96
|
+
|
|
97
|
+
if self.op == ConstraintOp.TILDE:
|
|
98
|
+
return self._matches_tilde(version)
|
|
99
|
+
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def _matches_caret(self, version: Version) -> bool:
|
|
103
|
+
"""Check caret constraint (^).
|
|
104
|
+
|
|
105
|
+
^1.2.3 := >=1.2.3 <2.0.0
|
|
106
|
+
^0.2.3 := >=0.2.3 <0.3.0
|
|
107
|
+
^0.0.3 := >=0.0.3 <0.0.4
|
|
108
|
+
"""
|
|
109
|
+
if self.version is None:
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
if version < self.version:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
if self.version.major != 0:
|
|
116
|
+
# ^1.2.3: allow 1.x.x
|
|
117
|
+
return version.major == self.version.major
|
|
118
|
+
|
|
119
|
+
if self.version.minor != 0:
|
|
120
|
+
# ^0.2.3: allow 0.2.x
|
|
121
|
+
return (
|
|
122
|
+
version.major == self.version.major
|
|
123
|
+
and version.minor == self.version.minor
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# ^0.0.3: only exact patch
|
|
127
|
+
return (
|
|
128
|
+
version.major == self.version.major
|
|
129
|
+
and version.minor == self.version.minor
|
|
130
|
+
and version.patch == self.version.patch
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _matches_tilde(self, version: Version) -> bool:
|
|
134
|
+
"""Check tilde constraint (~).
|
|
135
|
+
|
|
136
|
+
~1.2.3 := >=1.2.3 <1.3.0
|
|
137
|
+
~1.2 := >=1.2.0 <1.3.0
|
|
138
|
+
~1 := >=1.0.0 <2.0.0
|
|
139
|
+
"""
|
|
140
|
+
if self.version is None:
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
if version < self.version:
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Allow same major.minor, any patch
|
|
147
|
+
return (
|
|
148
|
+
version.major == self.version.major
|
|
149
|
+
and version.minor == self.version.minor
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> dict[str, Any]:
|
|
153
|
+
"""Convert to dictionary."""
|
|
154
|
+
return {
|
|
155
|
+
"op": self.op.value,
|
|
156
|
+
"version": self.version.to_dict() if self.version else None,
|
|
157
|
+
"original": self.original,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class VersionRange:
|
|
163
|
+
"""A version range consisting of multiple constraints.
|
|
164
|
+
|
|
165
|
+
Constraints are ANDed together within a range.
|
|
166
|
+
Multiple ranges can be ORed.
|
|
167
|
+
|
|
168
|
+
Attributes:
|
|
169
|
+
constraints: List of constraints (ANDed).
|
|
170
|
+
original: Original range string.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
constraints: list[VersionConstraint]
|
|
174
|
+
original: str = ""
|
|
175
|
+
|
|
176
|
+
def __str__(self) -> str:
|
|
177
|
+
"""Return string representation."""
|
|
178
|
+
if self.original:
|
|
179
|
+
return self.original
|
|
180
|
+
return " ".join(str(c) for c in self.constraints)
|
|
181
|
+
|
|
182
|
+
def matches(self, version: Version | str) -> bool:
|
|
183
|
+
"""Check if a version matches all constraints in this range.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
version: Version to check.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if version matches all constraints.
|
|
190
|
+
"""
|
|
191
|
+
if isinstance(version, str):
|
|
192
|
+
version = parse_version(version)
|
|
193
|
+
|
|
194
|
+
return all(c.matches(version) for c in self.constraints)
|
|
195
|
+
|
|
196
|
+
def to_dict(self) -> dict[str, Any]:
|
|
197
|
+
"""Convert to dictionary."""
|
|
198
|
+
return {
|
|
199
|
+
"constraints": [c.to_dict() for c in self.constraints],
|
|
200
|
+
"original": self.original,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Constraint parsing patterns
|
|
205
|
+
CONSTRAINT_PATTERNS = [
|
|
206
|
+
# Caret range: ^1.2.3
|
|
207
|
+
(r"^\^(.+)$", ConstraintOp.CARET),
|
|
208
|
+
# Tilde range: ~1.2.3
|
|
209
|
+
(r"^~(.+)$", ConstraintOp.TILDE),
|
|
210
|
+
# Greater than or equal: >=1.2.3
|
|
211
|
+
(r"^>=(.+)$", ConstraintOp.GTE),
|
|
212
|
+
# Less than or equal: <=1.2.3
|
|
213
|
+
(r"^<=(.+)$", ConstraintOp.LTE),
|
|
214
|
+
# Greater than: >1.2.3
|
|
215
|
+
(r"^>(.+)$", ConstraintOp.GT),
|
|
216
|
+
# Less than: <1.2.3
|
|
217
|
+
(r"^<(.+)$", ConstraintOp.LT),
|
|
218
|
+
# Exact: =1.2.3
|
|
219
|
+
(r"^=(.+)$", ConstraintOp.EQ),
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _parse_single_constraint(constraint_str: str) -> VersionConstraint:
|
|
224
|
+
"""Parse a single constraint string.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
constraint_str: Constraint string.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
VersionConstraint object.
|
|
231
|
+
"""
|
|
232
|
+
constraint_str = constraint_str.strip()
|
|
233
|
+
|
|
234
|
+
if not constraint_str or constraint_str == "*":
|
|
235
|
+
return VersionConstraint(ConstraintOp.ANY, None, constraint_str)
|
|
236
|
+
|
|
237
|
+
# Handle wildcard versions
|
|
238
|
+
if constraint_str.endswith(".*") or constraint_str.endswith(".x"):
|
|
239
|
+
# 1.2.* -> ^1.2.0, 1.x -> ^1.0.0
|
|
240
|
+
parts = constraint_str.replace(".*", "").replace(".x", "").split(".")
|
|
241
|
+
if len(parts) == 1:
|
|
242
|
+
version = parse_version(f"{parts[0]}.0.0")
|
|
243
|
+
return VersionConstraint(ConstraintOp.CARET, version, constraint_str)
|
|
244
|
+
elif len(parts) == 2:
|
|
245
|
+
version = parse_version(f"{parts[0]}.{parts[1]}.0")
|
|
246
|
+
return VersionConstraint(ConstraintOp.TILDE, version, constraint_str)
|
|
247
|
+
else:
|
|
248
|
+
version = parse_version(".".join(parts[:3]))
|
|
249
|
+
return VersionConstraint(ConstraintOp.TILDE, version, constraint_str)
|
|
250
|
+
|
|
251
|
+
# Try each pattern
|
|
252
|
+
for pattern, op in CONSTRAINT_PATTERNS:
|
|
253
|
+
match = re.match(pattern, constraint_str)
|
|
254
|
+
if match:
|
|
255
|
+
version = parse_version(match.group(1).strip())
|
|
256
|
+
return VersionConstraint(op, version, constraint_str)
|
|
257
|
+
|
|
258
|
+
# No operator - treat as exact match
|
|
259
|
+
version = parse_version(constraint_str)
|
|
260
|
+
return VersionConstraint(ConstraintOp.EQ, version, constraint_str)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def parse_constraint(constraint_str: str) -> list[VersionRange]:
|
|
264
|
+
"""Parse a constraint string into version ranges.
|
|
265
|
+
|
|
266
|
+
Supports:
|
|
267
|
+
- Single constraint: "^1.2.3"
|
|
268
|
+
- Range with AND: ">=1.0.0 <2.0.0"
|
|
269
|
+
- Multiple ranges with OR: "^1.2.3 || ^2.0.0"
|
|
270
|
+
- Hyphen range: "1.0.0 - 2.0.0"
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
constraint_str: Constraint string.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of VersionRange objects (ORed together).
|
|
277
|
+
"""
|
|
278
|
+
if not constraint_str or constraint_str.strip() == "*":
|
|
279
|
+
return [VersionRange([VersionConstraint(ConstraintOp.ANY, None, "*")], "*")]
|
|
280
|
+
|
|
281
|
+
# Split by OR operator
|
|
282
|
+
or_parts = re.split(r"\s*\|\|\s*", constraint_str)
|
|
283
|
+
ranges = []
|
|
284
|
+
|
|
285
|
+
for or_part in or_parts:
|
|
286
|
+
or_part = or_part.strip()
|
|
287
|
+
if not or_part:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
# Check for hyphen range: 1.0.0 - 2.0.0
|
|
291
|
+
hyphen_match = re.match(r"^([^\s]+)\s+-\s+([^\s]+)$", or_part)
|
|
292
|
+
if hyphen_match:
|
|
293
|
+
min_version = parse_version(hyphen_match.group(1))
|
|
294
|
+
max_version = parse_version(hyphen_match.group(2))
|
|
295
|
+
constraints = [
|
|
296
|
+
VersionConstraint(ConstraintOp.GTE, min_version, f">={min_version}"),
|
|
297
|
+
VersionConstraint(ConstraintOp.LTE, max_version, f"<={max_version}"),
|
|
298
|
+
]
|
|
299
|
+
ranges.append(VersionRange(constraints, or_part))
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# Split by whitespace for AND constraints
|
|
303
|
+
and_parts = or_part.split()
|
|
304
|
+
constraints = []
|
|
305
|
+
|
|
306
|
+
for and_part in and_parts:
|
|
307
|
+
and_part = and_part.strip()
|
|
308
|
+
if and_part:
|
|
309
|
+
constraints.append(_parse_single_constraint(and_part))
|
|
310
|
+
|
|
311
|
+
if constraints:
|
|
312
|
+
ranges.append(VersionRange(constraints, or_part))
|
|
313
|
+
|
|
314
|
+
return ranges or [VersionRange([VersionConstraint(ConstraintOp.ANY, None, "*")], "*")]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def satisfies(version: Version | str, constraint_str: str) -> bool:
|
|
318
|
+
"""Check if a version satisfies a constraint.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
version: Version to check.
|
|
322
|
+
constraint_str: Constraint string.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
True if version satisfies constraint.
|
|
326
|
+
"""
|
|
327
|
+
if isinstance(version, str):
|
|
328
|
+
version = parse_version(version)
|
|
329
|
+
|
|
330
|
+
ranges = parse_constraint(constraint_str)
|
|
331
|
+
# Version must match at least one range (OR)
|
|
332
|
+
return any(r.matches(version) for r in ranges)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def find_best_version(
|
|
336
|
+
available_versions: list[str | Version],
|
|
337
|
+
constraint_str: str,
|
|
338
|
+
prefer_stable: bool = True,
|
|
339
|
+
) -> Version | None:
|
|
340
|
+
"""Find the best version that satisfies a constraint.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
available_versions: List of available versions.
|
|
344
|
+
constraint_str: Constraint string.
|
|
345
|
+
prefer_stable: Prefer stable versions over pre-releases.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Best matching version, or None if no match.
|
|
349
|
+
"""
|
|
350
|
+
# Parse versions
|
|
351
|
+
versions = []
|
|
352
|
+
for v in available_versions:
|
|
353
|
+
if isinstance(v, str):
|
|
354
|
+
try:
|
|
355
|
+
versions.append(parse_version(v))
|
|
356
|
+
except ValueError:
|
|
357
|
+
continue
|
|
358
|
+
else:
|
|
359
|
+
versions.append(v)
|
|
360
|
+
|
|
361
|
+
# Filter by constraint
|
|
362
|
+
ranges = parse_constraint(constraint_str)
|
|
363
|
+
matching = [v for v in versions if any(r.matches(v) for r in ranges)]
|
|
364
|
+
|
|
365
|
+
if not matching:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
# Sort by version (descending)
|
|
369
|
+
matching.sort(reverse=True)
|
|
370
|
+
|
|
371
|
+
if prefer_stable:
|
|
372
|
+
# Prefer stable versions
|
|
373
|
+
stable = [v for v in matching if not v.is_prerelease()]
|
|
374
|
+
if stable:
|
|
375
|
+
return stable[0]
|
|
376
|
+
|
|
377
|
+
return matching[0]
|