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.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
  162. truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {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]