aegis-stack 0.1.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.

Potentially problematic release.


This version of aegis-stack might be problematic. Click here for more details.

Files changed (103) hide show
  1. aegis/__init__.py +5 -0
  2. aegis/__main__.py +374 -0
  3. aegis/core/CLAUDE.md +365 -0
  4. aegis/core/__init__.py +6 -0
  5. aegis/core/components.py +115 -0
  6. aegis/core/dependency_resolver.py +119 -0
  7. aegis/core/template_generator.py +163 -0
  8. aegis/templates/CLAUDE.md +306 -0
  9. aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
  10. aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
  11. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
  12. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
  13. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
  14. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  15. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
  16. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
  17. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
  18. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
  19. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
  20. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
  21. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
  22. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
  23. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
  24. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
  25. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
  26. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
  27. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
  28. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
  29. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
  30. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
  31. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
  32. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
  33. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
  34. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
  35. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
  36. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
  37. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
  38. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
  39. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
  40. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
  41. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
  42. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
  43. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
  44. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
  45. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
  46. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
  47. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
  48. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
  49. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
  50. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
  51. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
  52. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
  53. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
  54. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
  55. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
  56. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
  57. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
  58. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
  59. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
  60. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
  61. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
  62. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
  63. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
  64. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
  65. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
  66. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
  67. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
  68. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
  69. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
  70. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
  71. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
  72. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
  73. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
  74. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
  75. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
  76. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
  77. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
  78. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
  79. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
  80. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
  81. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
  82. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
  83. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
  84. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
  85. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
  86. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
  87. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
  88. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
  89. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
  90. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
  91. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
  92. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
  93. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
  94. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
  95. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
  96. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
  97. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
  98. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
  99. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
  100. aegis_stack-0.1.0.dist-info/METADATA +114 -0
  101. aegis_stack-0.1.0.dist-info/RECORD +103 -0
  102. aegis_stack-0.1.0.dist-info/WHEEL +4 -0
  103. aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,269 @@
1
+ """
2
+ Pydantic models for load test data structures.
3
+
4
+ Provides type safety and validation for load test configurations,
5
+ results, and analysis data.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
11
+
12
+ from app.components.worker.constants import LoadTestTypes
13
+ from app.core.config import get_load_test_queue
14
+
15
+
16
+ class LoadTestError(Exception):
17
+ """Custom exception for load test operations."""
18
+ pass
19
+
20
+
21
+ class LoadTestConfiguration(BaseModel):
22
+ """Load test configuration with validation and defaults."""
23
+
24
+ num_tasks: int = Field(
25
+ default=100, ge=10, le=10000, description="Number of tasks to spawn"
26
+ )
27
+ task_type: LoadTestTypes = Field(
28
+ default=LoadTestTypes.CPU_INTENSIVE, description="Type of load test to run"
29
+ )
30
+ batch_size: int = Field(default=10, ge=1, le=100, description="Tasks per batch")
31
+ delay_ms: int = Field(
32
+ default=0, ge=0, le=5000, description="Delay between batches (ms)"
33
+ )
34
+ target_queue: str | None = Field(
35
+ default=None, description="Target queue for testing"
36
+ )
37
+
38
+ @field_validator('target_queue')
39
+ @classmethod
40
+ def set_default_queue(cls, v: str | None) -> str:
41
+ """Set default queue if not specified."""
42
+ return v if v is not None else get_load_test_queue()
43
+
44
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
45
+ """Convert configuration to dictionary for task enqueueing."""
46
+ data = super().model_dump(**kwargs)
47
+ # Convert enum to string value for task enqueueing
48
+ data["task_type"] = self.task_type.value
49
+ return data
50
+
51
+
52
+ class LoadTestMetrics(BaseModel):
53
+ """Metrics from load test execution."""
54
+
55
+ tasks_sent: int = Field(..., ge=0, description="Total tasks enqueued")
56
+ tasks_completed: int = Field(..., ge=0, description="Successfully completed tasks")
57
+ tasks_failed: int = Field(0, ge=0, description="Failed tasks")
58
+ total_duration_seconds: float = Field(..., ge=0, description="Total test duration")
59
+ overall_throughput: float = Field(
60
+ 0, ge=0, description="Overall throughput (tasks/sec)"
61
+ )
62
+ failure_rate_percent: float = Field(
63
+ 0, ge=0, le=100, description="Failure rate percentage"
64
+ )
65
+ completion_percentage: float = Field(
66
+ 0, ge=0, le=100, description="Completion percentage"
67
+ )
68
+ average_throughput_per_second: float = Field(
69
+ 0, ge=0, description="Average throughput"
70
+ )
71
+ monitor_duration_seconds: float = Field(0, ge=0, description="Monitoring duration")
72
+
73
+ @field_validator("tasks_completed")
74
+ @classmethod
75
+ def completed_not_exceed_sent(cls, v: int, info: ValidationInfo) -> int:
76
+ """Ensure completed tasks don't exceed sent tasks."""
77
+ if info.data and "tasks_sent" in info.data and v > info.data["tasks_sent"]:
78
+ raise ValueError(
79
+ f"Completed tasks ({v}) cannot exceed sent tasks "
80
+ f"({info.data['tasks_sent']})"
81
+ )
82
+ return v
83
+
84
+ @field_validator("tasks_failed")
85
+ @classmethod
86
+ def failed_not_exceed_sent(cls, v: int, info: ValidationInfo) -> int:
87
+ """Ensure failed tasks don't exceed sent tasks."""
88
+ if info.data and "tasks_sent" in info.data and v > info.data["tasks_sent"]:
89
+ raise ValueError(
90
+ f"Failed tasks ({v}) cannot exceed sent tasks "
91
+ f"({info.data['tasks_sent']})"
92
+ )
93
+ return v
94
+
95
+ @field_validator("failure_rate_percent")
96
+ @classmethod
97
+ def validate_failure_rate_consistency(cls, v: float, info: ValidationInfo) -> float:
98
+ """Ensure failure rate percentage matches task counts."""
99
+ if info.data and "tasks_sent" in info.data and "tasks_failed" in info.data:
100
+ tasks_sent = info.data["tasks_sent"]
101
+ tasks_failed = info.data["tasks_failed"]
102
+ if tasks_sent > 0:
103
+ calculated_rate = (tasks_failed / tasks_sent) * 100
104
+ # Allow small floating point differences (within 0.1%)
105
+ if abs(v - calculated_rate) > 0.1:
106
+ raise ValueError(
107
+ f"Failure rate {v}% doesn't match task counts "
108
+ f"({tasks_failed}/{tasks_sent} = {calculated_rate:.1f}%)"
109
+ )
110
+ return v
111
+
112
+
113
+ class PerformanceAnalysis(BaseModel):
114
+ """Performance analysis results."""
115
+
116
+ throughput_rating: str = Field(
117
+ ...,
118
+ pattern=r"^(unknown|poor|fair|good|excellent)$",
119
+ description="Throughput performance rating"
120
+ )
121
+ efficiency_rating: str = Field(
122
+ ...,
123
+ pattern=r"^(unknown|poor|fair|good|excellent)$",
124
+ description="Task completion efficiency"
125
+ )
126
+ queue_pressure: str = Field(
127
+ ...,
128
+ pattern=r"^(unknown|low|moderate|high)$",
129
+ description="Queue saturation level"
130
+ )
131
+
132
+
133
+ class ValidationStatus(BaseModel):
134
+ """Test execution validation status."""
135
+
136
+ test_type_verified: bool = Field(
137
+ default=False, description="Test type executed correctly"
138
+ )
139
+ expected_metrics_present: bool = Field(
140
+ default=False, description="Expected metrics are present"
141
+ )
142
+ performance_signature_match: str = Field(
143
+ default="unknown",
144
+ pattern=r"^(unknown|verified|partial|failed)$",
145
+ description="Performance matches expected patterns"
146
+ )
147
+ issues: list[str] = Field(default_factory=list, description="Validation issues")
148
+
149
+
150
+ class TestTypeInfo(BaseModel):
151
+ """Information about a specific test type."""
152
+
153
+ name: str = Field(..., description="Human-readable test name")
154
+ description: str = Field(..., description="Test description")
155
+ expected_metrics: list[str] = Field(..., description="Expected result metrics")
156
+ performance_signature: str = Field(..., description="Expected performance pattern")
157
+ typical_duration_ms: str = Field(..., description="Typical execution time")
158
+ concurrency_impact: str = Field(..., description="Concurrency characteristics")
159
+ validation_keys: list[str] = Field(..., description="Keys for result validation")
160
+
161
+
162
+ class LoadTestAnalysis(BaseModel):
163
+ """Complete load test analysis."""
164
+
165
+ test_type_info: TestTypeInfo = Field(..., description="Test type information")
166
+ performance_analysis: PerformanceAnalysis = Field(
167
+ ..., description="Performance analysis"
168
+ )
169
+ validation_status: ValidationStatus = Field(..., description="Validation results")
170
+ recommendations: list[str] = Field(..., description="Improvement recommendations")
171
+
172
+
173
+ class LoadTestResult(BaseModel):
174
+ """Complete load test result with analysis."""
175
+
176
+ task: str = Field(default="load_test_orchestrator", description="Task name")
177
+ status: str = Field(
178
+ ...,
179
+ pattern=r"^(completed|failed|timed_out)$",
180
+ description="Test execution status"
181
+ )
182
+ test_id: str = Field(..., description="Unique test identifier")
183
+ configuration: LoadTestConfiguration = Field(..., description="Test configuration")
184
+ metrics: LoadTestMetrics = Field(..., description="Execution metrics")
185
+ start_time: str | None = Field(None, description="Test start time")
186
+ end_time: str | None = Field(None, description="Test end time")
187
+ task_ids: list[str] = Field(default_factory=list, description="Individual task IDs")
188
+ error: str | None = Field(None, description="Error message if failed")
189
+ analysis: LoadTestAnalysis | None = Field(
190
+ None, description="Performance analysis"
191
+ )
192
+
193
+ @model_validator(mode='after')
194
+ def validate_status_consistency(self) -> 'LoadTestResult':
195
+ """Validate status consistency with error field."""
196
+ if self.status == 'failed' and not self.error:
197
+ raise ValueError("Failed status requires error message")
198
+ return self
199
+
200
+
201
+ class OrchestratorRawResult(BaseModel):
202
+ """Raw orchestrator result format for transformation."""
203
+
204
+ test_id: str = Field(..., description="Test identifier")
205
+ task_type: str = Field(..., description="Task type executed")
206
+ tasks_sent: int = Field(..., description="Tasks enqueued")
207
+ tasks_completed: int = Field(0, description="Successfully completed")
208
+ tasks_failed: int = Field(0, description="Failed tasks")
209
+ total_duration_seconds: float = Field(..., description="Total duration")
210
+ overall_throughput_per_second: float = Field(0, description="Overall throughput")
211
+ failure_rate_percent: float = Field(0, description="Failure rate")
212
+ completion_percentage: float = Field(0, description="Completion rate")
213
+ average_throughput_per_second: float = Field(0, description="Average throughput")
214
+ monitor_duration_seconds: float = Field(0, description="Monitor duration")
215
+ batch_size: int = Field(1, description="Batch size used")
216
+ delay_ms: int = Field(0, description="Delay between batches")
217
+ target_queue: str = Field(..., description="Target queue")
218
+ start_time: str | None = Field(None, description="Start time")
219
+ end_time: str | None = Field(None, description="End time")
220
+ task_ids: list[str] = Field(default_factory=list, description="Task IDs")
221
+
222
+ def to_load_test_result(self) -> LoadTestResult:
223
+ """Transform to standard LoadTestResult format."""
224
+ configuration = LoadTestConfiguration(
225
+ task_type=LoadTestTypes(self.task_type),
226
+ num_tasks=self.tasks_sent,
227
+ batch_size=self.batch_size,
228
+ delay_ms=self.delay_ms,
229
+ target_queue=self.target_queue,
230
+ )
231
+
232
+ metrics = LoadTestMetrics(
233
+ tasks_sent=self.tasks_sent,
234
+ tasks_completed=self.tasks_completed,
235
+ tasks_failed=self.tasks_failed,
236
+ total_duration_seconds=self.total_duration_seconds,
237
+ overall_throughput=self.overall_throughput_per_second,
238
+ failure_rate_percent=self.failure_rate_percent,
239
+ completion_percentage=self.completion_percentage,
240
+ average_throughput_per_second=self.average_throughput_per_second,
241
+ monitor_duration_seconds=self.monitor_duration_seconds,
242
+ )
243
+
244
+ return LoadTestResult(
245
+ status="completed",
246
+ test_id=self.test_id,
247
+ configuration=configuration,
248
+ metrics=metrics,
249
+ start_time=self.start_time,
250
+ end_time=self.end_time,
251
+ task_ids=self.task_ids,
252
+ error=None,
253
+ analysis=None,
254
+ )
255
+
256
+
257
+ class LoadTestErrorModel(BaseModel):
258
+ """Load test error result with partial information."""
259
+
260
+ task: str = Field(default="load_test_orchestrator", description="Task name")
261
+ status: str = Field(
262
+ ...,
263
+ pattern=r"^(failed|timed_out)$",
264
+ description="Error status"
265
+ )
266
+ test_id: str = Field(..., description="Unique test identifier")
267
+ error: str = Field(..., description="Error message")
268
+ partial_info: str | None = Field(None, description="Partial completion info")
269
+ tasks_sent: int | None = Field(None, ge=0, description="Tasks that were sent")
@@ -0,0 +1,15 @@
1
+ """
2
+ Shared domain models and utilities.
3
+
4
+ Common types and models used across multiple domains.
5
+ """
6
+
7
+ from .models import (
8
+ BaseResponse,
9
+ ErrorResponse,
10
+ )
11
+
12
+ __all__ = [
13
+ "BaseResponse",
14
+ "ErrorResponse",
15
+ ]
@@ -0,0 +1,26 @@
1
+ """
2
+ Shared domain models.
3
+
4
+ Common Pydantic models used across multiple domains and services.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class BaseResponse(BaseModel):
13
+ """Base API response model."""
14
+
15
+ success: bool = Field(..., description="Whether operation was successful")
16
+ message: str = Field(..., description="Response message")
17
+ data: dict[str, Any] | list[Any] | None = Field(None, description="Response data")
18
+
19
+
20
+ class ErrorResponse(BaseModel):
21
+ """Standard error response model."""
22
+
23
+ error: str = Field(..., description="Error message")
24
+ status: str = Field(default="error", description="Status indicator")
25
+ details: dict[str, Any] | None = Field(None, description="Additional error details")
26
+ timestamp: str | None = Field(None, description="ISO timestamp when error occurred")
@@ -0,0 +1,52 @@
1
+ """
2
+ System domain - health monitoring, alerts, and system management.
3
+
4
+ This domain provides functions for:
5
+ - System health checking and monitoring
6
+ - Component status validation
7
+ - Alert management and notifications
8
+ - System resource monitoring
9
+
10
+ All functions use Pydantic models for type safety and validation.
11
+ """
12
+
13
+ from .alerts import (
14
+ send_alert,
15
+ send_critical_alert,
16
+ send_health_alert,
17
+ )
18
+ from .health import (
19
+ check_system_status,
20
+ get_system_status,
21
+ is_system_healthy,
22
+ register_health_check,
23
+ )
24
+ from .models import (
25
+ Alert,
26
+ AlertSeverity,
27
+ ComponentStatus,
28
+ ComponentStatusType,
29
+ DetailedHealthResponse,
30
+ HealthResponse,
31
+ SystemStatus,
32
+ )
33
+
34
+ __all__ = [
35
+ # Health functions
36
+ "check_system_status",
37
+ "get_system_status",
38
+ "is_system_healthy",
39
+ "register_health_check",
40
+ # Alert functions
41
+ "send_alert",
42
+ "send_critical_alert",
43
+ "send_health_alert",
44
+ # Models
45
+ "Alert",
46
+ "AlertSeverity",
47
+ "ComponentStatus",
48
+ "ComponentStatusType",
49
+ "SystemStatus",
50
+ "HealthResponse",
51
+ "DetailedHealthResponse",
52
+ ]
@@ -0,0 +1,94 @@
1
+ """
2
+ System alert management functions.
3
+
4
+ Pure functions for sending alerts, managing notifications, and rate limiting.
5
+ All functions use Pydantic models for type safety and validation.
6
+ """
7
+
8
+ from datetime import datetime
9
+ from typing import TYPE_CHECKING
10
+
11
+ from app.core.log import logger
12
+
13
+ from .models import Alert, SystemStatus, alert_severity
14
+
15
+ if TYPE_CHECKING:
16
+ pass
17
+
18
+
19
+ # Global state for alert rate limiting
20
+ _last_alerts: dict[str, datetime] = {}
21
+ _rate_limit_seconds = 300 # 5 minutes between similar alerts
22
+
23
+
24
+ def _should_send_alert(alert_key: str) -> bool:
25
+ """Check if we should send this alert based on rate limiting."""
26
+ if alert_key not in _last_alerts:
27
+ return True
28
+
29
+ time_since_last = datetime.utcnow() - _last_alerts[alert_key]
30
+ return time_since_last.total_seconds() > _rate_limit_seconds
31
+
32
+
33
+ async def send_alert(alert: Alert) -> None:
34
+ """Send an alert through configured channels."""
35
+ alert_key = f"{alert.severity}:{alert.title}"
36
+
37
+ if not _should_send_alert(alert_key):
38
+ logger.debug(f"Rate limiting alert: {alert_key}")
39
+ return
40
+
41
+ _last_alerts[alert_key] = datetime.utcnow()
42
+
43
+ # Log-based alerting (always available)
44
+ log_level = {
45
+ alert_severity.INFO: logger.info,
46
+ alert_severity.WARNING: logger.warning,
47
+ alert_severity.ERROR: logger.error,
48
+ alert_severity.CRITICAL: logger.critical,
49
+ }.get(alert.severity, logger.info)
50
+
51
+ log_level(
52
+ f"🚨 ALERT [{alert.severity.upper()}]: {alert.title}",
53
+ extra={
54
+ "alert_message": alert.message,
55
+ "alert_metadata": alert.metadata,
56
+ "alert_timestamp": alert.timestamp.isoformat(),
57
+ },
58
+ )
59
+
60
+ # TODO: Add integrations for:
61
+ # - Slack/Discord webhooks
62
+ # - Email notifications
63
+ # - PagerDuty/Opsgenie
64
+ # - Custom webhook endpoints
65
+
66
+
67
+ async def send_health_alert(status: SystemStatus) -> None:
68
+ """Send health-related alerts."""
69
+ if not status.overall_healthy:
70
+ alert = Alert(
71
+ severity=alert_severity.WARNING,
72
+ title="System Health Issues Detected",
73
+ message=(
74
+ f"{len(status.unhealthy_components)} components unhealthy: "
75
+ f"{', '.join(status.unhealthy_components)}"
76
+ ),
77
+ timestamp=status.timestamp,
78
+ metadata={
79
+ "unhealthy_components": status.unhealthy_components,
80
+ "health_percentage": status.health_percentage,
81
+ },
82
+ )
83
+ await send_alert(alert)
84
+
85
+
86
+ async def send_critical_alert(title: str, message: str) -> None:
87
+ """Send a critical system alert."""
88
+ alert = Alert(
89
+ severity=alert_severity.CRITICAL,
90
+ title=title,
91
+ message=message,
92
+ timestamp=datetime.utcnow(),
93
+ )
94
+ await send_alert(alert)