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.
- aegis/__init__.py +5 -0
- aegis/__main__.py +374 -0
- aegis/core/CLAUDE.md +365 -0
- aegis/core/__init__.py +6 -0
- aegis/core/components.py +115 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/template_generator.py +163 -0
- aegis/templates/CLAUDE.md +306 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis_stack-0.1.0.dist-info/METADATA +114 -0
- aegis_stack-0.1.0.dist-info/RECORD +103 -0
- aegis_stack-0.1.0.dist-info/WHEEL +4 -0
- aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System domain Pydantic models.
|
|
3
|
+
|
|
4
|
+
Type-safe data models for system health monitoring, alerts, and status reporting.
|
|
5
|
+
All models provide runtime validation and automatic FastAPI integration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field, computed_field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ComponentStatusType(str, Enum):
|
|
16
|
+
"""Component status levels."""
|
|
17
|
+
HEALTHY = "healthy"
|
|
18
|
+
INFO = "info"
|
|
19
|
+
WARNING = "warning"
|
|
20
|
+
UNHEALTHY = "unhealthy"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ComponentStatus(BaseModel):
|
|
24
|
+
"""Status of a single system component."""
|
|
25
|
+
|
|
26
|
+
name: str = Field(..., description="Component name")
|
|
27
|
+
status: ComponentStatusType = Field(
|
|
28
|
+
default=ComponentStatusType.HEALTHY,
|
|
29
|
+
description="Detailed status level (healthy/info/warning/unhealthy)"
|
|
30
|
+
)
|
|
31
|
+
message: str = Field(..., description="Status message or error description")
|
|
32
|
+
response_time_ms: float | None = Field(
|
|
33
|
+
None, description="Response time in milliseconds"
|
|
34
|
+
)
|
|
35
|
+
metadata: dict[str, Any] = Field(
|
|
36
|
+
default_factory=dict, description="Additional component metadata"
|
|
37
|
+
)
|
|
38
|
+
sub_components: dict[str, "ComponentStatus"] = Field(
|
|
39
|
+
default_factory=dict,
|
|
40
|
+
description="Sub-components (e.g., system metrics under backend)"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@computed_field # type: ignore[prop-decorator]
|
|
44
|
+
@property
|
|
45
|
+
def healthy(self) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Component is healthy if status is not UNHEALTHY.
|
|
48
|
+
|
|
49
|
+
WARNING and INFO statuses are considered healthy (functional but with concerns),
|
|
50
|
+
only UNHEALTHY is considered not healthy (non-functional).
|
|
51
|
+
"""
|
|
52
|
+
return self.status != ComponentStatusType.UNHEALTHY
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SystemStatus(BaseModel):
|
|
56
|
+
"""Overall system status with component details."""
|
|
57
|
+
|
|
58
|
+
components: dict[str, ComponentStatus] = Field(
|
|
59
|
+
..., description="Component status by name"
|
|
60
|
+
)
|
|
61
|
+
overall_healthy: bool = Field(..., description="Whether all components are healthy")
|
|
62
|
+
timestamp: datetime = Field(..., description="When status was generated")
|
|
63
|
+
system_info: dict[str, Any] = Field(
|
|
64
|
+
default_factory=dict, description="System information"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _get_all_components_flat(self) -> list[tuple[str, ComponentStatus]]:
|
|
68
|
+
"""Get all components including sub-components as a flat list."""
|
|
69
|
+
all_components = []
|
|
70
|
+
for name, component in self.components.items():
|
|
71
|
+
all_components.append((name, component))
|
|
72
|
+
# Add sub-components with parent.child naming
|
|
73
|
+
for sub_name, sub_component in component.sub_components.items():
|
|
74
|
+
all_components.append((f"{name}.{sub_name}", sub_component))
|
|
75
|
+
return all_components
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def healthy_components(self) -> list[str]:
|
|
79
|
+
"""List of healthy component names (including sub-components)."""
|
|
80
|
+
return [
|
|
81
|
+
name for name, component in self._get_all_components_flat()
|
|
82
|
+
if component.healthy
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def unhealthy_components(self) -> list[str]:
|
|
87
|
+
"""List of unhealthy component names (including sub-components)."""
|
|
88
|
+
return [
|
|
89
|
+
name for name, component in self._get_all_components_flat()
|
|
90
|
+
if not component.healthy
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def health_percentage(self) -> float:
|
|
95
|
+
"""Percentage of healthy components (including sub-components)."""
|
|
96
|
+
all_components = self._get_all_components_flat()
|
|
97
|
+
if not all_components:
|
|
98
|
+
return 100.0
|
|
99
|
+
healthy_count = len([c for _, c in all_components if c.healthy])
|
|
100
|
+
return (healthy_count / len(all_components)) * 100
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def total_components(self) -> int:
|
|
104
|
+
"""Total number of top-level components."""
|
|
105
|
+
return len(self.components)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def healthy_top_level_components(self) -> list[str]:
|
|
109
|
+
"""List of healthy top-level component names."""
|
|
110
|
+
return [name for name, comp in self.components.items() if comp.healthy]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class HealthResponse(BaseModel):
|
|
114
|
+
"""Basic health check API response."""
|
|
115
|
+
|
|
116
|
+
healthy: bool = Field(..., description="Whether system is healthy")
|
|
117
|
+
status: str = Field(..., description="Overall status text")
|
|
118
|
+
components: dict[str, ComponentStatus] = Field(
|
|
119
|
+
default_factory=dict, description="Component statuses"
|
|
120
|
+
)
|
|
121
|
+
timestamp: str = Field(..., description="ISO timestamp when checked")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class DetailedHealthResponse(BaseModel):
|
|
125
|
+
"""Detailed health check API response with system information."""
|
|
126
|
+
|
|
127
|
+
healthy: bool = Field(..., description="Whether system is healthy")
|
|
128
|
+
status: str = Field(..., description="Overall status text")
|
|
129
|
+
service: str = Field(..., description="Service name")
|
|
130
|
+
version: str = Field(..., description="Service version")
|
|
131
|
+
components: dict[str, ComponentStatus] = Field(
|
|
132
|
+
..., description="Component statuses"
|
|
133
|
+
)
|
|
134
|
+
system_info: dict[str, Any] = Field(..., description="System information")
|
|
135
|
+
timestamp: str = Field(..., description="ISO timestamp when checked")
|
|
136
|
+
healthy_components: list[str] = Field(
|
|
137
|
+
..., description="List of healthy component names"
|
|
138
|
+
)
|
|
139
|
+
unhealthy_components: list[str] = Field(
|
|
140
|
+
..., description="List of unhealthy component names"
|
|
141
|
+
)
|
|
142
|
+
health_percentage: float = Field(
|
|
143
|
+
..., description="Percentage of healthy components"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AlertSeverity(BaseModel):
|
|
148
|
+
"""Alert severity levels as constants."""
|
|
149
|
+
|
|
150
|
+
INFO: str = Field(default="info", description="Informational alert")
|
|
151
|
+
WARNING: str = Field(default="warning", description="Warning alert")
|
|
152
|
+
ERROR: str = Field(default="error", description="Error alert")
|
|
153
|
+
CRITICAL: str = Field(default="critical", description="Critical alert")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Alert(BaseModel):
|
|
157
|
+
"""System alert model."""
|
|
158
|
+
|
|
159
|
+
severity: str = Field(..., description="Alert severity level")
|
|
160
|
+
title: str = Field(..., min_length=1, description="Alert title")
|
|
161
|
+
message: str = Field(..., min_length=1, description="Alert message")
|
|
162
|
+
timestamp: datetime = Field(..., description="When alert was created")
|
|
163
|
+
metadata: dict[str, Any] = Field(
|
|
164
|
+
default_factory=dict, description="Additional alert data"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Singleton for alert severity constants
|
|
169
|
+
alert_severity = AlertSeverity()
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared UI helpers for status presentation across CLI and frontend.
|
|
3
|
+
|
|
4
|
+
Provides a single source of truth for mapping ComponentStatusType to
|
|
5
|
+
icons and semantic colors, so CLI and dashboard remain consistent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .models import ComponentStatusType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_status_icon(status: ComponentStatusType) -> str:
|
|
12
|
+
"""Return the display icon for a given status.
|
|
13
|
+
|
|
14
|
+
Note: INFO icon intentionally includes a trailing space for terminal alignment.
|
|
15
|
+
"""
|
|
16
|
+
if status == ComponentStatusType.HEALTHY:
|
|
17
|
+
return "✅"
|
|
18
|
+
if status == ComponentStatusType.INFO:
|
|
19
|
+
return "ℹ️ "
|
|
20
|
+
if status == ComponentStatusType.WARNING:
|
|
21
|
+
return "⚠️"
|
|
22
|
+
if status == ComponentStatusType.UNHEALTHY:
|
|
23
|
+
return "❌"
|
|
24
|
+
return "❓"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_status_color_name(status: ComponentStatusType) -> str:
|
|
28
|
+
"""Return a semantic color name for a given status (CLI friendly).
|
|
29
|
+
|
|
30
|
+
Frontend can adapt these semantic names to theme-specific colors.
|
|
31
|
+
"""
|
|
32
|
+
if status == ComponentStatusType.HEALTHY:
|
|
33
|
+
return "green"
|
|
34
|
+
if status == ComponentStatusType.INFO:
|
|
35
|
+
return "blue"
|
|
36
|
+
if status == ComponentStatusType.WARNING:
|
|
37
|
+
return "yellow"
|
|
38
|
+
if status == ComponentStatusType.UNHEALTHY:
|
|
39
|
+
return "red"
|
|
40
|
+
return "white"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_component_label(component_name: str) -> str:
|
|
44
|
+
"""Map component keys to user-facing labels (brand or friendly name)."""
|
|
45
|
+
mapping = {
|
|
46
|
+
"backend": "FastAPI",
|
|
47
|
+
"frontend": "Flet",
|
|
48
|
+
"cache": "Redis",
|
|
49
|
+
"worker": "arq",
|
|
50
|
+
"scheduler": "APScheduler",
|
|
51
|
+
}
|
|
52
|
+
return mapping.get(component_name, component_name.title())
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
|
|
2
|
+
x-app: &app
|
|
3
|
+
image: ${AEGIS_STACK_TAG:-aegis-stack:latest}
|
|
4
|
+
build:
|
|
5
|
+
context: .
|
|
6
|
+
args:
|
|
7
|
+
- AEGIS_STACK_VERSION=${AEGIS_STACK_VERSION:-dev}
|
|
8
|
+
- PORT=${PORT:-8000}
|
|
9
|
+
env_file:
|
|
10
|
+
- ${AEGIS_STACK_ENV_FILE:-.env}
|
|
11
|
+
volumes:
|
|
12
|
+
- .:/code
|
|
13
|
+
# Exclude .venv to prevent host/container virtual environment conflicts
|
|
14
|
+
- /code/.venv
|
|
15
|
+
environment:
|
|
16
|
+
# Docker container marker for entrypoint script
|
|
17
|
+
- DOCKER_CONTAINER=1
|
|
18
|
+
- PORT=${PORT:-8000}
|
|
19
|
+
labels:
|
|
20
|
+
logging: "aegis-stack"
|
|
21
|
+
|
|
22
|
+
services:
|
|
23
|
+
# DEVELOPMENT PROFILE
|
|
24
|
+
|
|
25
|
+
webserver:
|
|
26
|
+
<<: *app
|
|
27
|
+
command:
|
|
28
|
+
- webserver
|
|
29
|
+
ports:
|
|
30
|
+
- "${PORT:-8000}:${PORT:-8000}"
|
|
31
|
+
{%- if cookiecutter.include_redis == "yes" %}
|
|
32
|
+
depends_on:
|
|
33
|
+
redis:
|
|
34
|
+
condition: service_healthy
|
|
35
|
+
environment:
|
|
36
|
+
# Redis connection for health checks and task enqueueing
|
|
37
|
+
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
|
38
|
+
{%- endif %}
|
|
39
|
+
healthcheck:
|
|
40
|
+
test: ["CMD", "curl", "-f", "http://localhost:${PORT:-8000}/health/"]
|
|
41
|
+
interval: 30s
|
|
42
|
+
timeout: 10s
|
|
43
|
+
retries: 3
|
|
44
|
+
start_period: 40s
|
|
45
|
+
restart: unless-stopped
|
|
46
|
+
deploy:
|
|
47
|
+
resources:
|
|
48
|
+
limits:
|
|
49
|
+
memory: 512M
|
|
50
|
+
cpus: '0.5'
|
|
51
|
+
reservations:
|
|
52
|
+
memory: 256M
|
|
53
|
+
cpus: '0.25'
|
|
54
|
+
profiles:
|
|
55
|
+
- dev
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
{%- if cookiecutter.include_scheduler == "yes" %}
|
|
59
|
+
# Scheduler service (runs scheduler tasks independently)
|
|
60
|
+
scheduler:
|
|
61
|
+
<<: *app
|
|
62
|
+
command:
|
|
63
|
+
- scheduler
|
|
64
|
+
{%- if cookiecutter.include_redis == "yes" %}
|
|
65
|
+
depends_on:
|
|
66
|
+
redis:
|
|
67
|
+
condition: service_healthy
|
|
68
|
+
environment:
|
|
69
|
+
# Redis connection for future scheduler integration
|
|
70
|
+
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
|
71
|
+
{%- endif %}
|
|
72
|
+
restart: unless-stopped
|
|
73
|
+
deploy:
|
|
74
|
+
resources:
|
|
75
|
+
limits:
|
|
76
|
+
memory: 256M
|
|
77
|
+
cpus: '0.25'
|
|
78
|
+
reservations:
|
|
79
|
+
memory: 128M
|
|
80
|
+
cpus: '0.1'
|
|
81
|
+
profiles:
|
|
82
|
+
- dev
|
|
83
|
+
- prod
|
|
84
|
+
{%- endif %}
|
|
85
|
+
|
|
86
|
+
{%- if cookiecutter.include_worker == "yes" %}
|
|
87
|
+
# Load Test Worker (high-performance load testing tasks)
|
|
88
|
+
worker-load-test:
|
|
89
|
+
<<: *app
|
|
90
|
+
command:
|
|
91
|
+
- worker
|
|
92
|
+
- load_test
|
|
93
|
+
depends_on:
|
|
94
|
+
redis:
|
|
95
|
+
condition: service_healthy
|
|
96
|
+
restart: unless-stopped
|
|
97
|
+
tty: true
|
|
98
|
+
environment:
|
|
99
|
+
# Load test worker configuration (high concurrency)
|
|
100
|
+
- WORKER_QUEUE_TYPE=load_test
|
|
101
|
+
- WORKER_CONCURRENCY=${LOAD_TEST_WORKER_CONCURRENCY:-20}
|
|
102
|
+
- WORKER_TIMEOUT_SECONDS=${LOAD_TEST_WORKER_TIMEOUT:-60}
|
|
103
|
+
- WORKER_MAX_TRIES=${LOAD_TEST_WORKER_MAX_TRIES:-1}
|
|
104
|
+
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
|
105
|
+
# Docker file watching fix - force polling for volume mounts
|
|
106
|
+
- WATCHFILES_FORCE_POLLING=true
|
|
107
|
+
- WATCHFILES_POLL_DELAY_MS=800
|
|
108
|
+
deploy:
|
|
109
|
+
resources:
|
|
110
|
+
limits:
|
|
111
|
+
memory: 1G
|
|
112
|
+
cpus: '1.0'
|
|
113
|
+
reservations:
|
|
114
|
+
memory: 512M
|
|
115
|
+
cpus: '0.5'
|
|
116
|
+
profiles:
|
|
117
|
+
- dev
|
|
118
|
+
- prod
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# System Worker (maintenance and administrative tasks)
|
|
122
|
+
worker-system:
|
|
123
|
+
<<: *app
|
|
124
|
+
command:
|
|
125
|
+
- worker
|
|
126
|
+
- system
|
|
127
|
+
depends_on:
|
|
128
|
+
redis:
|
|
129
|
+
condition: service_healthy
|
|
130
|
+
restart: unless-stopped
|
|
131
|
+
tty: true
|
|
132
|
+
environment:
|
|
133
|
+
# System worker configuration (low concurrency, high reliability)
|
|
134
|
+
- WORKER_QUEUE_TYPE=system
|
|
135
|
+
- WORKER_CONCURRENCY=${SYSTEM_WORKER_CONCURRENCY:-2}
|
|
136
|
+
- WORKER_TIMEOUT_SECONDS=${SYSTEM_WORKER_TIMEOUT:-1800}
|
|
137
|
+
- WORKER_MAX_TRIES=${SYSTEM_WORKER_MAX_TRIES:-5}
|
|
138
|
+
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
|
139
|
+
# Docker file watching fix - force polling for volume mounts
|
|
140
|
+
- WATCHFILES_FORCE_POLLING=true
|
|
141
|
+
- WATCHFILES_POLL_DELAY_MS=800
|
|
142
|
+
deploy:
|
|
143
|
+
resources:
|
|
144
|
+
limits:
|
|
145
|
+
memory: 256M
|
|
146
|
+
cpus: '0.25'
|
|
147
|
+
reservations:
|
|
148
|
+
memory: 128M
|
|
149
|
+
cpus: '0.1'
|
|
150
|
+
profiles:
|
|
151
|
+
- dev
|
|
152
|
+
- prod
|
|
153
|
+
{%- endif %}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
{%- if cookiecutter.include_redis == "yes" %}
|
|
157
|
+
# REDIS SERVICE
|
|
158
|
+
|
|
159
|
+
redis:
|
|
160
|
+
image: redis:7-alpine
|
|
161
|
+
ports:
|
|
162
|
+
- "6379:6379"
|
|
163
|
+
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
|
|
164
|
+
healthcheck:
|
|
165
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
166
|
+
interval: 10s
|
|
167
|
+
timeout: 5s
|
|
168
|
+
retries: 3
|
|
169
|
+
start_period: 10s
|
|
170
|
+
restart: unless-stopped
|
|
171
|
+
deploy:
|
|
172
|
+
resources:
|
|
173
|
+
limits:
|
|
174
|
+
memory: 512M
|
|
175
|
+
cpus: '0.25'
|
|
176
|
+
reservations:
|
|
177
|
+
memory: 256M
|
|
178
|
+
cpus: '0.1'
|
|
179
|
+
profiles:
|
|
180
|
+
- dev
|
|
181
|
+
{%- endif %}
|
|
182
|
+
|
|
183
|
+
# TEST PROFILE
|
|
184
|
+
|
|
185
|
+
test_runner:
|
|
186
|
+
<<: *app
|
|
187
|
+
command:
|
|
188
|
+
- test
|
|
189
|
+
env_file:
|
|
190
|
+
- ${AEGIS_STACK_ENV_FILE:-.env}
|
|
191
|
+
profiles:
|
|
192
|
+
- test
|
|
193
|
+
|
|
194
|
+
volumes:
|
|
195
|
+
aegis-data:
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
{{ cookiecutter.project_name }} provides a FastAPI-based REST API with automatic documentation.
|
|
4
|
+
|
|
5
|
+
## Interactive Documentation
|
|
6
|
+
|
|
7
|
+
When running the application, interactive API documentation is available at:
|
|
8
|
+
|
|
9
|
+
- **Swagger UI**: [http://localhost:8000/docs](http://localhost:8000/docs)
|
|
10
|
+
- **ReDoc**: [http://localhost:8000/redoc](http://localhost:8000/redoc)
|
|
11
|
+
- **OpenAPI Schema**: [http://localhost:8000/openapi.json](http://localhost:8000/openapi.json)
|
|
12
|
+
|
|
13
|
+
## Health Endpoints
|
|
14
|
+
|
|
15
|
+
### Basic Health Check
|
|
16
|
+
|
|
17
|
+
**GET** `/health/`
|
|
18
|
+
|
|
19
|
+
Returns basic health status of the application.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl http://localhost:8000/health/
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Response:**
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"healthy": true,
|
|
29
|
+
"status": "healthy",
|
|
30
|
+
"components": {},
|
|
31
|
+
"timestamp": "2024-01-01T00:00:00Z"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Detailed Health Check
|
|
36
|
+
|
|
37
|
+
**GET** `/health/detailed`
|
|
38
|
+
|
|
39
|
+
Returns comprehensive health information including system metrics.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl http://localhost:8000/health/detailed
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Response:**
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"healthy": true,
|
|
49
|
+
"status": "healthy",
|
|
50
|
+
"components": {
|
|
51
|
+
"system": {
|
|
52
|
+
"status": "healthy",
|
|
53
|
+
"cpu_percent": 15.2,
|
|
54
|
+
"memory_percent": 45.8,
|
|
55
|
+
"disk_percent": 32.1,
|
|
56
|
+
"response_time_ms": 2.1
|
|
57
|
+
}{% if cookiecutter.include_scheduler == "yes" %},
|
|
58
|
+
"scheduler": {
|
|
59
|
+
"status": "healthy",
|
|
60
|
+
"active_jobs": 2,
|
|
61
|
+
"next_run": "2024-01-01T02:00:00Z",
|
|
62
|
+
"response_time_ms": 1.5
|
|
63
|
+
}{% endif %}
|
|
64
|
+
},
|
|
65
|
+
"timestamp": "2024-01-01T00:00:00Z",
|
|
66
|
+
"uptime_seconds": 3600
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Authentication
|
|
71
|
+
|
|
72
|
+
Currently, {{ cookiecutter.project_name }} does not implement authentication.
|
|
73
|
+
|
|
74
|
+
To add authentication:
|
|
75
|
+
|
|
76
|
+
1. **Install dependencies:**
|
|
77
|
+
```bash
|
|
78
|
+
uv add python-jose[cryptography] passlib[bcrypt]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
2. **Add authentication routes:**
|
|
82
|
+
```python
|
|
83
|
+
# app/components/backend/api/auth.py
|
|
84
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
85
|
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
86
|
+
|
|
87
|
+
router = APIRouter()
|
|
88
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
89
|
+
|
|
90
|
+
@router.post("/token")
|
|
91
|
+
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
|
92
|
+
# Implement login logic
|
|
93
|
+
pass
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
3. **Register in routing:**
|
|
97
|
+
```python
|
|
98
|
+
# app/components/backend/api/routing.py
|
|
99
|
+
from app.components.backend.api import auth
|
|
100
|
+
|
|
101
|
+
def include_routers(app: FastAPI) -> None:
|
|
102
|
+
app.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## CORS Configuration
|
|
106
|
+
|
|
107
|
+
CORS is pre-configured to allow all origins in development. For production, update the CORS middleware in `app/components/backend/middleware/cors.py`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
111
|
+
|
|
112
|
+
app.add_middleware(
|
|
113
|
+
CORSMiddleware,
|
|
114
|
+
allow_origins=["https://yourdomain.com"], # Update for production
|
|
115
|
+
allow_credentials=True,
|
|
116
|
+
allow_methods=["*"],
|
|
117
|
+
allow_headers=["*"],
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Error Handling
|
|
122
|
+
|
|
123
|
+
The API includes standard HTTP error responses:
|
|
124
|
+
|
|
125
|
+
- **400 Bad Request**: Invalid request data
|
|
126
|
+
- **404 Not Found**: Resource not found
|
|
127
|
+
- **422 Unprocessable Entity**: Validation errors
|
|
128
|
+
- **500 Internal Server Error**: Server errors
|
|
129
|
+
|
|
130
|
+
Custom error handlers can be added in `app/components/backend/main.py`:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from fastapi import HTTPException, Request
|
|
134
|
+
from fastapi.responses import JSONResponse
|
|
135
|
+
|
|
136
|
+
@app.exception_handler(404)
|
|
137
|
+
async def not_found_handler(request: Request, exc: HTTPException):
|
|
138
|
+
return JSONResponse(
|
|
139
|
+
status_code=404,
|
|
140
|
+
content={"message": "Resource not found"}
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Adding Custom Endpoints
|
|
145
|
+
|
|
146
|
+
### 1. Create Router Module
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# app/components/backend/api/my_api.py
|
|
150
|
+
from fastapi import APIRouter, HTTPException
|
|
151
|
+
from app.services.my_service import get_data, create_item
|
|
152
|
+
|
|
153
|
+
router = APIRouter()
|
|
154
|
+
|
|
155
|
+
@router.get("/items")
|
|
156
|
+
async def list_items():
|
|
157
|
+
"""Get all items."""
|
|
158
|
+
items = await get_data()
|
|
159
|
+
return {"items": items}
|
|
160
|
+
|
|
161
|
+
@router.post("/items")
|
|
162
|
+
async def create_item_endpoint(name: str):
|
|
163
|
+
"""Create a new item."""
|
|
164
|
+
try:
|
|
165
|
+
item = await create_item(name)
|
|
166
|
+
return {"item": item}
|
|
167
|
+
except ValueError as e:
|
|
168
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 2. Register Router
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# app/components/backend/api/routing.py
|
|
175
|
+
from app.components.backend.api import my_api
|
|
176
|
+
|
|
177
|
+
def include_routers(app: FastAPI) -> None:
|
|
178
|
+
app.include_router(my_api.router, prefix="/api", tags=["items"])
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 3. Add Tests
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# tests/api/test_my_api.py
|
|
185
|
+
def test_list_items(client):
|
|
186
|
+
response = client.get("/api/items")
|
|
187
|
+
assert response.status_code == 200
|
|
188
|
+
assert "items" in response.json()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The API will automatically include your endpoints in the interactive documentation at `/docs`.
|