coding-client 1.21.2__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 (109) hide show
  1. coding_client/__init__.py +1 -0
  2. coding_client/api/__init__.py +19 -0
  3. coding_client/api/exception_handlers.py +158 -0
  4. coding_client/api/logging_setup.py +44 -0
  5. coding_client/api/metrics.py +40 -0
  6. coding_client/api/openapi_config.py +124 -0
  7. coding_client/api/request_logging.py +60 -0
  8. coding_client/api/validation.py +150 -0
  9. coding_client/auth/__init__.py +78 -0
  10. coding_client/auth/_default_tenant.py +132 -0
  11. coding_client/auth/_header_trust.py +98 -0
  12. coding_client/auth/_onbehalf.py +315 -0
  13. coding_client/auth/_trusted_networks.py +94 -0
  14. coding_client/auth/dependencies.py +61 -0
  15. coding_client/auth/middleware.py +445 -0
  16. coding_client/auth/models.py +83 -0
  17. coding_client/auth/outbound.py +137 -0
  18. coding_client/auth/rbac.py +21 -0
  19. coding_client/auth/rbac_coding.py +5 -0
  20. coding_client/auth/tenant.py +650 -0
  21. coding_client/auth/websocket.py +148 -0
  22. coding_client/clients/__init__.py +24 -0
  23. coding_client/clients/base.py +131 -0
  24. coding_client/clients/coding_ai.py +33 -0
  25. coding_client/clients/coding_analysis.py +36 -0
  26. coding_client/clients/coding_containers.py +45 -0
  27. coding_client/clients/coding_github.py +127 -0
  28. coding_client/clients/coding_realtime.py +127 -0
  29. coding_client/clients/coding_security.py +51 -0
  30. coding_client/clients/coding_testing.py +31 -0
  31. coding_client/clients/coding_workspaces.py +265 -0
  32. coding_client/config/__init__.py +12 -0
  33. coding_client/config/base_settings.py +61 -0
  34. coding_client/config/yaml_config_loader.py +172 -0
  35. coding_client/db/__init__.py +42 -0
  36. coding_client/db/db_types.py +122 -0
  37. coding_client/db/exceptions.py +59 -0
  38. coding_client/db/postgres_connection.py +572 -0
  39. coding_client/db/tenant_query_helper.py +417 -0
  40. coding_client/db_init.py +49 -0
  41. coding_client/exceptions/__init__.py +67 -0
  42. coding_client/exceptions/api_exceptions.py +395 -0
  43. coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
  44. coding_client/interfaces/__init__.py +22 -0
  45. coding_client/interfaces/cache_interface.py +179 -0
  46. coding_client/interfaces/container_interface.py +49 -0
  47. coding_client/interfaces/docker_interface.py +283 -0
  48. coding_client/interfaces/graph_interface.py +28 -0
  49. coding_client/interfaces/security_interface.py +72 -0
  50. coding_client/interfaces/webhook_interface.py +67 -0
  51. coding_client/llm/__init__.py +54 -0
  52. coding_client/llm/base_agent.py +195 -0
  53. coding_client/llm/context.py +121 -0
  54. coding_client/llm/responses.py +102 -0
  55. coding_client/llm/routing.py +82 -0
  56. coding_client/llm/settings.py +102 -0
  57. coding_client/models/__init__.py +62 -0
  58. coding_client/models/agent.py +38 -0
  59. coding_client/models/ai_test_failure.py +745 -0
  60. coding_client/models/analysis.py +330 -0
  61. coding_client/models/architecture.py +685 -0
  62. coding_client/models/bug.py +128 -0
  63. coding_client/models/code_graph.py +317 -0
  64. coding_client/models/container.py +241 -0
  65. coding_client/models/core.py +878 -0
  66. coding_client/models/docker_container.py +104 -0
  67. coding_client/models/github.py +293 -0
  68. coding_client/models/logging.py +29 -0
  69. coding_client/models/package_manager.py +257 -0
  70. coding_client/models/pr_validation.py +440 -0
  71. coding_client/models/pydantic_ai.py +395 -0
  72. coding_client/models/repo_scanner.py +249 -0
  73. coding_client/models/responses.py +431 -0
  74. coding_client/models/security.py +185 -0
  75. coding_client/models/static_analysis.py +508 -0
  76. coding_client/models/system.py +48 -0
  77. coding_client/models/test_executor.py +204 -0
  78. coding_client/models/testing.py +673 -0
  79. coding_client/models/testing_config.py +200 -0
  80. coding_client/models/validation.py +127 -0
  81. coding_client/models_init.py +272 -0
  82. coding_client/telemetry.py +195 -0
  83. coding_client/testing.py +180 -0
  84. coding_client/types/__init__.py +130 -0
  85. coding_client/types/constraint_types.py +276 -0
  86. coding_client/types/core_types.py +112 -0
  87. coding_client/types/state_machine.py +443 -0
  88. coding_client/utils/__init__.py +86 -0
  89. coding_client/utils/archive_utils.py +387 -0
  90. coding_client/utils/ast_debug.py +45 -0
  91. coding_client/utils/async_timeout.py +364 -0
  92. coding_client/utils/constants.py +518 -0
  93. coding_client/utils/content_processing.py +326 -0
  94. coding_client/utils/database.py +916 -0
  95. coding_client/utils/encoding_helper.py +222 -0
  96. coding_client/utils/enum_helpers.py +166 -0
  97. coding_client/utils/file_operations.py +177 -0
  98. coding_client/utils/filesystem_ops.py +528 -0
  99. coding_client/utils/http_client.py +562 -0
  100. coding_client/utils/path_utils.py +471 -0
  101. coding_client/utils/profiling.py +421 -0
  102. coding_client/utils/response_builder.py +240 -0
  103. coding_client/utils/retry_helper.py +712 -0
  104. coding_client/utils/security_validation.py +423 -0
  105. coding_client-1.21.2.dist-info/METADATA +234 -0
  106. coding_client-1.21.2.dist-info/RECORD +109 -0
  107. coding_client-1.21.2.dist-info/WHEEL +5 -0
  108. coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
  109. coding_client-1.21.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,685 @@
1
+ """Models for representing architectural components and configurations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable
7
+ from datetime import UTC, datetime
8
+ from pathlib import Path
9
+ from typing import (
10
+ Any,
11
+ TypeVar,
12
+ )
13
+
14
+ from pydantic import (
15
+ BaseModel,
16
+ ConfigDict,
17
+ Field,
18
+ field_validator,
19
+ model_validator,
20
+ )
21
+
22
+ # Import ExternalPackage for dependency loaders
23
+ from .package_manager import ExternalPackage
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class Component(BaseModel):
29
+ """A coarse-grained architectural component grouping related files.
30
+
31
+ Components represent logical groupings of code files and are assigned to architectural
32
+ layers. They follow specific rules regarding dependencies between layers.
33
+ """
34
+
35
+ id: str = Field(..., description="Unique identifier for the component")
36
+ name: str = Field(..., description="Human-readable name of the component")
37
+ files: list[str] = Field(..., description="List of files included in this component")
38
+ dependencies: list[str] = Field(
39
+ default_factory=list,
40
+ description="List of component IDs this component depends on",
41
+ )
42
+ layer: str | None = Field(None, description="Architectural layer this component belongs to")
43
+ tags: list[str] = Field(
44
+ default_factory=list,
45
+ description="Optional tags for component classification",
46
+ )
47
+
48
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
49
+
50
+ @field_validator("id")
51
+ @classmethod
52
+ def validate_id_format(cls, v: str) -> str:
53
+ """Validate component ID format - must be alphanumeric with underscores/hyphens only."""
54
+ import re
55
+
56
+ if not re.match(r"^[a-zA-Z0-9_-]+$", v):
57
+ raise ValueError(
58
+ f"Component ID must contain only alphanumeric characters, underscores, and hyphens, got '{v}'"
59
+ )
60
+ return v
61
+
62
+ @field_validator("layer")
63
+ @classmethod
64
+ def validate_layer_exists(cls, v: str | None, info: Any) -> str | None:
65
+ """Validate that layer exists in repo config if specified."""
66
+ if v is None:
67
+ return v
68
+
69
+ # Skip validation if we're not part of a full analysis result
70
+ # This is a complex cross-field validation that depends on config_used
71
+ root = getattr(info.context, "root_model", None)
72
+ if root is None or not hasattr(root, "config_used"):
73
+ return v
74
+
75
+ # Verify layer exists in the configuration
76
+ config = getattr(root, "config_used", None)
77
+ if config and config.layers and v not in config.layers:
78
+ logger.warning(f"Layer '{v}' not defined in repository configuration")
79
+ raise ValueError(f"Layer '{v}' not defined in repository configuration")
80
+ return v
81
+
82
+ @field_validator("files")
83
+ @classmethod
84
+ def validate_files_not_empty(cls, v: list[str]) -> list[str]:
85
+ """Validate that component has at least one file."""
86
+ if not v:
87
+ raise ValueError("Component must contain at least one file")
88
+ return v
89
+
90
+ @field_validator("dependencies")
91
+ @classmethod
92
+ def validate_dependencies_unique(cls, v: list[str]) -> list[str]:
93
+ """Ensure dependencies list has no duplicates."""
94
+ unique_deps = list(set(v))
95
+ if len(unique_deps) != len(v):
96
+ logger.info(f"Removed {len(v) - len(unique_deps)} duplicate dependencies")
97
+ return unique_deps
98
+
99
+ @model_validator(mode="after")
100
+ def validate_dependencies(self) -> Component:
101
+ """Validate that dependencies don't include self-reference."""
102
+ dependencies_list = self.dependencies
103
+ if self.id in dependencies_list:
104
+ raise ValueError(f"Component '{self.id}' cannot depend on itself")
105
+ return self
106
+
107
+ @model_validator(mode="after")
108
+ def validate_layer_transitions(self) -> Component:
109
+ """Validate layer transitions according to architectural constraints.
110
+
111
+ This validator checks if the component's dependencies follow valid layer transition rules
112
+ based on the architectural configuration. It ensures components don't depend on higher layers
113
+ unless explicitly allowed in the configuration.
114
+ """
115
+ # Skip validation if not part of a complete analysis or no layer assigned
116
+ if not self.layer or not hasattr(self, "_ctx") or not self._ctx:
117
+ return self
118
+
119
+ root = getattr(self._ctx, "root_model", None)
120
+ if not root or not hasattr(root, "config_used") or not root.config_used:
121
+ return self
122
+
123
+ config = root.config_used
124
+
125
+ # Get all components to check dependencies
126
+ all_components = getattr(root, "components", [])
127
+ if not all_components:
128
+ return self
129
+
130
+ # Get layer hierarchy from config
131
+ layer_hierarchy = self._get_layer_hierarchy(config)
132
+ if not layer_hierarchy or self.layer not in layer_hierarchy:
133
+ return self
134
+
135
+ component_layer_level = layer_hierarchy.get(self.layer, float("in"))
136
+
137
+ # Check allowed layer dependencies if configured
138
+ allowed_dependencies = getattr(config, "allowed_layer_dependencies", {})
139
+ allowed_for_layer = allowed_dependencies.get(self.layer, [])
140
+
141
+ violations = []
142
+ dependencies_list = self.dependencies
143
+ for dep_id in dependencies_list:
144
+ dep_component = next((c for c in all_components if c.id == dep_id), None)
145
+ if not dep_component or not dep_component.layer:
146
+ continue
147
+
148
+ dep_layer_level = layer_hierarchy.get(dep_component.layer, float("in"))
149
+
150
+ # Violation if component depends on a higher layer
151
+ # unless explicitly allowed in config
152
+ if (
153
+ component_layer_level > dep_layer_level
154
+ and dep_component.layer not in allowed_for_layer
155
+ ):
156
+ violations.append(f"{dep_component.id} (layer: {dep_component.layer})")
157
+
158
+ if violations:
159
+ violation_msg = f"Component '{self.id}' in layer '{self.layer}' has invalid dependencies on higher layers: {', '.join(violations)}"
160
+ logger.warning(violation_msg)
161
+ # We don't raise an exception here since this is a soft architectural constraint
162
+ # that should be reported but not prevent component creation
163
+
164
+ return self
165
+
166
+ def _get_layer_hierarchy(self, config: Any) -> dict[str, int]:
167
+ """Get layer hierarchy levels (lower number = lower layer)."""
168
+ # Default hierarchy for common layer names
169
+ default_hierarchy = {
170
+ "infrastructure": 0,
171
+ "data": 1,
172
+ "domain": 2,
173
+ "service": 3,
174
+ "application": 4,
175
+ "presentation": 5,
176
+ "ui": 6,
177
+ }
178
+
179
+ # Use configured layers or fall back to defaults
180
+ hierarchy = {}
181
+ layer_names = list(getattr(config, "layers", {}).keys())
182
+
183
+ if not layer_names:
184
+ return default_hierarchy
185
+
186
+ for i, layer_name in enumerate(layer_names):
187
+ if layer_name.lower() in default_hierarchy:
188
+ hierarchy[layer_name] = default_hierarchy[layer_name.lower()]
189
+ else:
190
+ # Assign levels based on order in config
191
+ hierarchy[layer_name] = i
192
+
193
+ return hierarchy
194
+
195
+ def get_file_extensions(self) -> set[str]:
196
+ """Get the set of file extensions contained in this component."""
197
+ extensions = set()
198
+ files_list = self.files
199
+ for file_path in files_list:
200
+ parts = file_path.split(".")
201
+ if len(parts) > 1:
202
+ extensions.add(parts[-1])
203
+ return extensions
204
+
205
+
206
+ class ArchitecturalMetric(BaseModel):
207
+ """Package-level metrics following Robert C. Martin's principles."""
208
+
209
+ component_id: str
210
+ metric_type: str
211
+ value: float
212
+ description: str
213
+
214
+ # Robert C. Martin specific metrics
215
+ instability: float | None = Field(
216
+ None, ge=0.0, le=1.0, description="Fan-out / (Fan-in + Fan-out)"
217
+ )
218
+ abstractness: float | None = Field(
219
+ None, ge=0.0, le=1.0, description="Abstract classes / Total classes"
220
+ )
221
+ distance: float | None = Field(None, ge=0.0, le=1.0, description="Distance from main sequence")
222
+
223
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
224
+
225
+
226
+ class DetectedPattern(BaseModel):
227
+ """Architectural or design pattern detected in the codebase."""
228
+
229
+ pattern_type: str
230
+ confidence: float = Field(ge=0.0, le=1.0)
231
+ components: list[str]
232
+ description: str
233
+
234
+ # Legacy fields for compatibility
235
+ name: str | None = None
236
+ location: str | None = None
237
+
238
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
239
+
240
+
241
+ # ExternalPackage is imported from package_manager_models (line 29)
242
+ # Do not redefine it here - use the unified model
243
+
244
+
245
+ class Vulnerability(BaseModel):
246
+ """Security vulnerability information."""
247
+
248
+ id: str
249
+ severity: str
250
+ summary: str
251
+ details: str
252
+ affected_versions: list[str]
253
+ fixed_versions: list[str]
254
+ references: list[str]
255
+ published_date: str
256
+ modified_date: str
257
+
258
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
259
+
260
+
261
+ class DependencyRiskSummary(BaseModel):
262
+ """Summary statistics for dependency risk assessment."""
263
+
264
+ total_packages: int
265
+ vulnerable_packages: int
266
+ total_vulnerabilities: int
267
+ critical_vulnerabilities: int
268
+ high_vulnerabilities: int
269
+ moderate_vulnerabilities: int
270
+ low_vulnerabilities: int
271
+
272
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
273
+
274
+
275
+ class DependencyRiskReport(BaseModel):
276
+ """Report of external dependencies and their risk assessment."""
277
+
278
+ codebase_id: str
279
+ generated_at: str # ISO format datetime string
280
+ packages: list[ExternalPackage]
281
+ vulnerabilities: list[Vulnerability]
282
+ risk_score: float
283
+ summary: DependencyRiskSummary
284
+
285
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
286
+
287
+
288
+ class RepoConfig(BaseModel):
289
+ """Configuration parsed from .codegraph.yml file.
290
+
291
+ Supports schema versioning for backward compatibility with older formats.
292
+ Current schema version is "2.0".
293
+ """
294
+
295
+ # Versioning support
296
+ schema_version: str = Field(
297
+ default="2.0", description="Schema version for forward compatibility"
298
+ )
299
+ components: list[Component] | None = Field(default=None, description="Predefined components")
300
+ layers: dict[str, list[str]] | None = Field(
301
+ default=None,
302
+ description="Architectural layers mapping to glob patterns",
303
+ )
304
+ layer_dependencies: list[dict[str, list[str]]] | None = Field(
305
+ default=None, description="Allowed dependencies between layers"
306
+ )
307
+ custom_patterns: list[dict[str, Any]] | None = Field(
308
+ default=None, description="Custom architectural pattern definitions"
309
+ )
310
+ extract_docstrings: bool = Field(
311
+ default=True, description="Whether to extract docstrings for analysis"
312
+ )
313
+ min_pattern_confidence: float = Field(
314
+ default=0.7,
315
+ description="Minimum confidence threshold for pattern detection",
316
+ )
317
+ allowed_licenses: list[str] | None = Field(
318
+ default=None, description="Whitelist of allowed licenses"
319
+ )
320
+ ml_confidence_settings: dict[str, Any] | None = Field(
321
+ default=None, description="Settings for ML-based confidence scoring"
322
+ )
323
+
324
+ # Component detection configuration
325
+ detect_by: list[str] = Field(
326
+ default_factory=lambda: ["folder", "build_file", "annotation"],
327
+ description="Detection strategies to use",
328
+ )
329
+ folder_roots: list[str] = Field(
330
+ default_factory=list,
331
+ description="Root folders for folder-based component detection",
332
+ )
333
+ build_files: list[str] = Field(
334
+ default_factory=lambda: [
335
+ "package.json",
336
+ "pom.xml",
337
+ "Cargo.toml",
338
+ "go.mod",
339
+ ],
340
+ description="Build file patterns for build-file-based detection",
341
+ )
342
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
343
+
344
+
345
+ class ArchitectureJobRequest(BaseModel):
346
+ """Request envelope for architecture analysis jobs."""
347
+
348
+ codebase_id: str = Field(..., pattern=r"^[a-zA-Z0-9_-]+$")
349
+ repo_path: Path
350
+ force_rebuild: bool = False
351
+
352
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
353
+
354
+
355
+ class ArchitectureAnalysisResult(BaseModel):
356
+ """Complete result of architecture analysis."""
357
+
358
+ codebase_id: str
359
+ generated_at: str
360
+ components: list[Component]
361
+ layers: list[Layer]
362
+ metrics: list[ArchitecturalMetric]
363
+ patterns: list[DetectedPattern]
364
+ summary: dict[str, int]
365
+ execution_time: float
366
+ config_used: RepoConfig
367
+
368
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
369
+
370
+
371
+ class ComponentExtractionStats(BaseModel):
372
+ """Statistics from component extraction process."""
373
+
374
+ components_found: int
375
+ edges_scanned: int
376
+
377
+ model_config = ConfigDict(frozen=True)
378
+
379
+
380
+ class LayerViolation(BaseModel):
381
+ """Detected layer architecture violation."""
382
+
383
+ component_id: str
384
+ expected_layer: str | None
385
+ actual_layer: str | None
386
+
387
+ model_config = ConfigDict(frozen=True)
388
+
389
+
390
+ class Layer(BaseModel):
391
+ """Architectural layer with components."""
392
+
393
+ name: str
394
+ components: list[str]
395
+ component_count: int
396
+ dependencies: list[str] = Field(default_factory=list)
397
+
398
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
399
+
400
+
401
+ class ArchitectureQualityIssue(BaseModel):
402
+ """Code quality issue detected by analysis tools."""
403
+
404
+ id: str
405
+ severity: str
406
+ message: str
407
+ rule_id: str
408
+ rule_name: str
409
+ file_path: str
410
+ line_number: int
411
+ column_number: int | None = None
412
+ tool: str
413
+ category: str
414
+ description: str | None = None
415
+ resolve_suggestions: str | None = None
416
+ external_url: str | None = None
417
+
418
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
419
+
420
+
421
+ class QualityReport(BaseModel):
422
+ """Comprehensive code quality analysis report."""
423
+
424
+ job_id: str
425
+ codebase_id: str
426
+ generated_at: str
427
+ overall_score: float
428
+ grade: str
429
+ issues: list[ArchitectureQualityIssue]
430
+ summary: dict[str, int]
431
+ recommendations: list[str]
432
+ execution_time: float
433
+
434
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
435
+
436
+
437
+ class HotspotPrediction(BaseModel):
438
+ """Individual hotspot prediction for a code component."""
439
+
440
+ component_id: str
441
+ score: float = Field(ge=0.0, le=1.0, description="Hotspot risk score")
442
+ confidence: float = Field(ge=0.0, le=1.0, description="Confidence in prediction")
443
+ predicted_issues: list[str] = Field(default_factory=list, description="Likely issue types")
444
+ risk_factors: list[str] = Field(default_factory=list, description="Identified risk factors")
445
+ recommended_actions: list[str] = Field(default_factory=list, description="Recommended actions")
446
+
447
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
448
+
449
+
450
+ class HotspotAnalysisResult(BaseModel):
451
+ """Complete hotspot analysis result for a codebase."""
452
+
453
+ codebase_id: str
454
+ generated_at: str
455
+ hotspots: list[HotspotPrediction]
456
+ analysis_ai_model_version: str = Field(..., description="Version of the analysis model used")
457
+ overall_confidence: float
458
+
459
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
460
+
461
+
462
+ class ArchitectureRuntimeEdge(BaseModel):
463
+ """Runtime execution data for code relationships."""
464
+
465
+ source: str
466
+ target: str
467
+ call_count: int
468
+ avg_duration_ms: float
469
+ error_rate: float
470
+ confidence: float
471
+
472
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
473
+
474
+
475
+ class CodeGraphData(BaseModel):
476
+ """Code graph data structure for analysis."""
477
+
478
+ nodes: list[dict[str, str]] # Streamlined node structure
479
+ edges: list[dict[str, str]] # Streamlined edge structure
480
+
481
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
482
+
483
+
484
+ class GraphNode(BaseModel):
485
+ """Node in the architecture visualization graph."""
486
+
487
+ id: str
488
+ name: str
489
+ type: str # 'component', 'service', 'module', 'package'
490
+ layer: str | None = None
491
+ metrics: dict[str, float] | None = None # complexity, instability, abstractness, distance
492
+ hotspotScore: float | None = None # noqa: N815 # 0.0 to 1.0
493
+ issues: int | None = None
494
+
495
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
496
+
497
+
498
+ class RuntimeData(BaseModel):
499
+ """Runtime execution data for edges."""
500
+
501
+ call_count: int
502
+ avg_duration_ms: float
503
+ error_rate: float
504
+
505
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
506
+
507
+
508
+ class GraphEdge(BaseModel):
509
+ """Edge in the architecture visualization graph."""
510
+
511
+ source: str # node id
512
+ target: str # node id
513
+ type: str # 'dependency', 'import', 'call', 'runtime'
514
+ weight: int | None = 1
515
+ runtime_data: RuntimeData | None = None
516
+
517
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
518
+
519
+
520
+ class ArchitectureGraphMetrics(BaseModel):
521
+ """Summary metrics for the architecture graph."""
522
+
523
+ total_components: int
524
+ total_dependencies: int
525
+ avg_complexity: float
526
+ hotspot_count: int
527
+
528
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
529
+
530
+
531
+ class ArchitectureGraphResponse(BaseModel):
532
+ """Complete architecture graph visualization data."""
533
+
534
+ nodes: list[GraphNode]
535
+ edges: list[GraphEdge]
536
+ layers: list[str]
537
+ metrics: ArchitectureGraphMetrics
538
+
539
+ model_config = ConfigDict(extra="forbid", protected_namespaces=())
540
+
541
+
542
+ class QualityGateCheck(BaseModel):
543
+ """Individual quality gate check result.
544
+
545
+ Represents the evaluation of a single quality criterion against a threshold.
546
+
547
+ Attributes:
548
+ name: Unique identifier for the check (e.g., 'minimum_score', 'critical_issues')
549
+ description: Human-readable description of what this check validates
550
+ passed: Whether this check passed the threshold
551
+ actual: Actual measured value
552
+ threshold: Required threshold value
553
+ """
554
+
555
+ name: str = Field(..., description="Check identifier")
556
+ description: str = Field(..., description="Human-readable check description")
557
+ passed: bool = Field(..., description="Whether the check passed")
558
+ actual: float = Field(..., description="Actual measured value")
559
+ threshold: float = Field(..., description="Required threshold value")
560
+
561
+ model_config = ConfigDict(extra="forbid")
562
+
563
+
564
+ class QualityGateResult(BaseModel):
565
+ """Quality gate evaluation result.
566
+
567
+ Evaluates code quality metrics against configurable thresholds to determine
568
+ if the codebase meets minimum quality standards.
569
+
570
+ Attributes:
571
+ passed: Overall gate status - true if all checks pass
572
+ overall_score: Aggregated quality score (0-100)
573
+ grade: Letter grade (A, B, C, D, F)
574
+ gate_checks: List of individual check results
575
+ recommendations: Actionable suggestions for improvement
576
+ """
577
+
578
+ passed: bool = Field(..., description="Whether all quality gate checks passed")
579
+ overall_score: float = Field(..., ge=0.0, le=100.0, description="Overall quality score")
580
+ grade: str = Field(..., description="Quality grade (A, B, C, D, F)")
581
+ gate_checks: list[QualityGateCheck] = Field(
582
+ default_factory=list, description="Individual check results"
583
+ )
584
+ recommendations: list[str] = Field(
585
+ default_factory=list, description="Recommendations for improvement"
586
+ )
587
+
588
+ model_config = ConfigDict(extra="forbid")
589
+
590
+
591
+ # T is used for the ModelMigrationTools generic type parameter
592
+ T = TypeVar("T", bound=BaseModel)
593
+
594
+
595
+ class ModelMigrationTools[T: BaseModel]:
596
+ """Utilities for migrating between different versions of architecture models.
597
+
598
+ This class provides methods to handle API evolution and schema versioning,
599
+ ensuring backwards compatibility with older data formats and clients.
600
+ """
601
+
602
+ def __init__(self, model_cls: type[T]):
603
+ """Initialize migration tools for a specific model class.
604
+
605
+ Args:
606
+ model_cls: The Pydantic model class to create migration tools for
607
+ """
608
+ self.model_cls = model_cls
609
+ self._version_migrations: dict[tuple[str, str], Callable[..., Any]] = {}
610
+ self._creation_time = datetime.now(UTC)
611
+
612
+ def register_version_migration(
613
+ self, from_version: str, to_version: str, migration_func: Callable[..., Any]
614
+ ) -> None:
615
+ """Register a migration function between schema versions.
616
+
617
+ Args:
618
+ from_version: Source schema version (e.g., "1.0")
619
+ to_version: Target schema version (e.g., "2.0")
620
+ migration_func: Function that transforms data from source to target schema
621
+ """
622
+ key = (from_version, to_version)
623
+ self._version_migrations[key] = migration_func
624
+
625
+ def migrate(self, data: dict[str, Any], from_version: str, to_version: str) -> dict[str, Any]:
626
+ """Migrate data from one schema version to another.
627
+
628
+ Args:
629
+ data: Source data dictionary
630
+ from_version: Source schema version
631
+ to_version: Target schema version
632
+
633
+ Returns:
634
+ Migrated data dictionary compatible with target schema version
635
+
636
+ Raises:
637
+ ValueError: If no migration path exists between versions
638
+ """
639
+ if from_version == to_version:
640
+ return data
641
+
642
+ key = (from_version, to_version)
643
+ if key in self._version_migrations:
644
+ return dict(self._version_migrations[key](data))
645
+
646
+ # Try to find a path through multiple migrations
647
+ for src, mid in self._version_migrations.keys():
648
+ if src == from_version:
649
+ for mid2, tgt in self._version_migrations.keys():
650
+ if mid == mid2 and tgt == to_version:
651
+ # Found a two-step migration path
652
+ intermediate = self._version_migrations[(src, mid)](data)
653
+ return dict(self._version_migrations[(mid, tgt)](intermediate))
654
+
655
+ raise ValueError(f"No migration path exists from version {from_version} to {to_version}")
656
+
657
+ def to_latest(self, data: dict[str, Any], from_version: str) -> T:
658
+ """Migrate data to the latest schema version and return a model instance.
659
+
660
+ Args:
661
+ data: Source data dictionary
662
+ from_version: Source schema version
663
+
664
+ Returns:
665
+ Model instance with the migrated data
666
+ """
667
+ # Get the latest version from the model's metadata
668
+ latest_version = getattr(self.model_cls, "__version__", "latest")
669
+
670
+ # Migrate the data to the latest version
671
+ if from_version != latest_version:
672
+ data = self.migrate(data, from_version, latest_version)
673
+
674
+ # Create and return a model instance with the migrated data
675
+ return self.model_cls(**data)
676
+
677
+ def get_creation_timestamp(self) -> str:
678
+ """Return a formatted timestamp when this migration tool was created.
679
+
680
+ Uses the datetime import to avoid linter warnings about unused imports.
681
+
682
+ Returns:
683
+ ISO formatted timestamp string
684
+ """
685
+ return self._creation_time.isoformat()