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
aegis/core/CLAUDE.md
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# CLI Core Development Guide
|
|
2
|
+
|
|
3
|
+
This guide covers CLI development patterns for Aegis Stack's core command system.
|
|
4
|
+
|
|
5
|
+
## Component System Architecture
|
|
6
|
+
|
|
7
|
+
### Component Registry (`components.py`)
|
|
8
|
+
The component system uses a centralized registry with typed component specifications:
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
@dataclass
|
|
12
|
+
class ComponentSpec:
|
|
13
|
+
"""Specification for a component."""
|
|
14
|
+
name: str
|
|
15
|
+
description: str
|
|
16
|
+
type: ComponentType
|
|
17
|
+
requires: list[str] = field(default_factory=list)
|
|
18
|
+
recommends: list[str] = field(default_factory=list)
|
|
19
|
+
conflicts: list[str] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
# Component registry
|
|
22
|
+
COMPONENTS: dict[str, ComponentSpec] = {
|
|
23
|
+
"redis": ComponentSpec(
|
|
24
|
+
name="redis",
|
|
25
|
+
description="Redis server for caching and message queues",
|
|
26
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
27
|
+
),
|
|
28
|
+
"worker": ComponentSpec(
|
|
29
|
+
name="worker",
|
|
30
|
+
description="Background task processing with arq",
|
|
31
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
32
|
+
requires=["redis"],
|
|
33
|
+
),
|
|
34
|
+
"scheduler": ComponentSpec(
|
|
35
|
+
name="scheduler",
|
|
36
|
+
description="Scheduled task execution with APScheduler",
|
|
37
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
38
|
+
),
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Component Types
|
|
43
|
+
```python
|
|
44
|
+
class ComponentType(Enum):
|
|
45
|
+
"""Component types for organization."""
|
|
46
|
+
INFRASTRUCTURE = "infrastructure" # Redis, workers, databases
|
|
47
|
+
SERVICE = "service" # Future: API services, microservices
|
|
48
|
+
INTEGRATION = "integration" # Future: External integrations
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Dependency Resolution Patterns
|
|
52
|
+
|
|
53
|
+
### Dependency Resolver (`dependency_resolver.py`)
|
|
54
|
+
The dependency resolver handles component relationships and validation:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
class DependencyResolver:
|
|
58
|
+
"""Resolves component dependencies and validates selections."""
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def validate_components(components: list[str]) -> list[str]:
|
|
62
|
+
"""Validate component names and return errors."""
|
|
63
|
+
errors = []
|
|
64
|
+
for component in components:
|
|
65
|
+
if component not in COMPONENTS:
|
|
66
|
+
available = list(COMPONENTS.keys())
|
|
67
|
+
# Suggest similar components
|
|
68
|
+
suggestion = DependencyResolver._suggest_component(component, available)
|
|
69
|
+
if suggestion:
|
|
70
|
+
errors.append(f"Unknown component '{component}'. Did you mean '{suggestion}'?")
|
|
71
|
+
else:
|
|
72
|
+
errors.append(f"Unknown component '{component}'. Available: {', '.join(available)}")
|
|
73
|
+
return errors
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def resolve_dependencies(selected: list[str]) -> list[str]:
|
|
77
|
+
"""Resolve all dependencies for selected components."""
|
|
78
|
+
resolved = set(selected)
|
|
79
|
+
|
|
80
|
+
# Add required dependencies
|
|
81
|
+
queue = list(selected)
|
|
82
|
+
while queue:
|
|
83
|
+
component = queue.pop(0)
|
|
84
|
+
if component in COMPONENTS:
|
|
85
|
+
for required in COMPONENTS[component].requires:
|
|
86
|
+
if required not in resolved:
|
|
87
|
+
resolved.add(required)
|
|
88
|
+
queue.append(required)
|
|
89
|
+
|
|
90
|
+
return sorted(resolved)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Validation Patterns
|
|
94
|
+
```python
|
|
95
|
+
def validate_project_name(project_name: str) -> None:
|
|
96
|
+
"""Validate project name and raise typer.Exit if invalid."""
|
|
97
|
+
import re
|
|
98
|
+
|
|
99
|
+
# Check for invalid characters
|
|
100
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", project_name):
|
|
101
|
+
typer.echo("❌ Invalid project name. Only letters, numbers, hyphens, and underscores are allowed.", err=True)
|
|
102
|
+
raise typer.Exit(1)
|
|
103
|
+
|
|
104
|
+
# Check for reserved names
|
|
105
|
+
reserved_names = {"aegis", "aegis-stack"}
|
|
106
|
+
if project_name.lower() in reserved_names:
|
|
107
|
+
typer.echo(f"❌ '{project_name}' is a reserved name.", err=True)
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
|
|
110
|
+
# Check length limit
|
|
111
|
+
if len(project_name) > 50:
|
|
112
|
+
typer.echo("❌ Project name too long. Maximum 50 characters allowed.", err=True)
|
|
113
|
+
raise typer.Exit(1)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Template Generation Logic
|
|
117
|
+
|
|
118
|
+
### Template Generator (`template_generator.py`)
|
|
119
|
+
The template generator converts component selections into cookiecutter context:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
class TemplateGenerator:
|
|
123
|
+
"""Generates template context from component selections."""
|
|
124
|
+
|
|
125
|
+
def __init__(self, project_name: str, components: list[str]):
|
|
126
|
+
self.project_name = project_name
|
|
127
|
+
self.project_slug = self._generate_slug(project_name)
|
|
128
|
+
self.components = components
|
|
129
|
+
|
|
130
|
+
def get_template_context(self) -> dict[str, Any]:
|
|
131
|
+
"""Generate cookiecutter context from component selections."""
|
|
132
|
+
return {
|
|
133
|
+
"project_name": self.project_name,
|
|
134
|
+
"project_slug": self.project_slug,
|
|
135
|
+
"project_description": f"A production-ready Python application",
|
|
136
|
+
"author_name": "Developer",
|
|
137
|
+
"author_email": "dev@example.com",
|
|
138
|
+
"version": "0.1.0",
|
|
139
|
+
"python_version": "3.11",
|
|
140
|
+
"include_scheduler": "yes" if "scheduler" in self.components else "no",
|
|
141
|
+
"include_worker": "yes" if "worker" in self.components else "no",
|
|
142
|
+
"include_database": "yes" if "database" in self.components else "no",
|
|
143
|
+
"include_cache": "yes" if "cache" in self.components else "no",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def get_template_files(self) -> list[str]:
|
|
147
|
+
"""Get list of template files that will be generated."""
|
|
148
|
+
files = []
|
|
149
|
+
|
|
150
|
+
# Core files (always included)
|
|
151
|
+
files.extend([
|
|
152
|
+
"app/components/backend/main.py",
|
|
153
|
+
"app/components/frontend/main.py",
|
|
154
|
+
"app/core/config.py",
|
|
155
|
+
"app/integrations/main.py",
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
# Component-specific files
|
|
159
|
+
if "scheduler" in self.components:
|
|
160
|
+
files.extend([
|
|
161
|
+
"app/components/scheduler/main.py",
|
|
162
|
+
"app/entrypoints/scheduler.py",
|
|
163
|
+
"tests/components/test_scheduler.py",
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
if "worker" in self.components:
|
|
167
|
+
files.extend([
|
|
168
|
+
"app/components/worker/queues/system.py",
|
|
169
|
+
"app/components/worker/tasks/system_tasks.py",
|
|
170
|
+
"app/services/load_test.py",
|
|
171
|
+
"tests/services/test_worker_health_registration.py",
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
return sorted(files)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## CLI Command Structure
|
|
178
|
+
|
|
179
|
+
### Command Definitions (`__main__.py`)
|
|
180
|
+
Commands use Typer with proper validation and error handling:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
@app.command()
|
|
184
|
+
def init(
|
|
185
|
+
project_name: str = typer.Argument(..., help="Name of the new Aegis Stack project to create"),
|
|
186
|
+
components: str | None = typer.Option(
|
|
187
|
+
None,
|
|
188
|
+
"--components", "-c",
|
|
189
|
+
callback=validate_and_resolve_components,
|
|
190
|
+
help="Comma-separated list of components (redis,worker,scheduler)",
|
|
191
|
+
),
|
|
192
|
+
interactive: bool = typer.Option(
|
|
193
|
+
True, "--interactive/--no-interactive", "-i/-ni",
|
|
194
|
+
help="Use interactive component selection"
|
|
195
|
+
),
|
|
196
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing directory if it exists"),
|
|
197
|
+
output_dir: str | None = typer.Option(None, "--output-dir", "-o", help="Directory to create the project in"),
|
|
198
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Initialize a new Aegis Stack project with battle-tested component combinations."""
|
|
201
|
+
|
|
202
|
+
# Validate project name first
|
|
203
|
+
validate_project_name(project_name)
|
|
204
|
+
|
|
205
|
+
# Show project configuration
|
|
206
|
+
show_project_configuration(project_name, components, output_dir)
|
|
207
|
+
|
|
208
|
+
# Confirm before proceeding
|
|
209
|
+
if not yes and not typer.confirm("🚀 Create this project?"):
|
|
210
|
+
typer.echo("❌ Project creation cancelled")
|
|
211
|
+
raise typer.Exit(0)
|
|
212
|
+
|
|
213
|
+
# Create project using cookiecutter
|
|
214
|
+
create_project_with_cookiecutter(project_name, components, output_dir, force)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Interactive Selection Patterns
|
|
218
|
+
```python
|
|
219
|
+
def interactive_component_selection() -> list[str]:
|
|
220
|
+
"""Interactive component selection with dependency awareness."""
|
|
221
|
+
|
|
222
|
+
typer.echo("🎯 Component Selection")
|
|
223
|
+
typer.echo("=" * 40)
|
|
224
|
+
typer.echo("✅ Core components (backend + frontend) included automatically\\n")
|
|
225
|
+
|
|
226
|
+
selected = []
|
|
227
|
+
|
|
228
|
+
# Infrastructure components
|
|
229
|
+
typer.echo("🏗️ Infrastructure Components:")
|
|
230
|
+
if typer.confirm(" Add Redis (caching, message queues)?"):
|
|
231
|
+
selected.append("redis")
|
|
232
|
+
|
|
233
|
+
if "redis" in selected:
|
|
234
|
+
if typer.confirm(" Add worker infrastructure (background tasks)?"):
|
|
235
|
+
selected.append("worker")
|
|
236
|
+
else:
|
|
237
|
+
if typer.confirm(" Add worker infrastructure? (will auto-add Redis)"):
|
|
238
|
+
selected.extend(["redis", "worker"])
|
|
239
|
+
|
|
240
|
+
if typer.confirm(" Add scheduler infrastructure (scheduled tasks)?"):
|
|
241
|
+
selected.append("scheduler")
|
|
242
|
+
|
|
243
|
+
return selected
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## CLI Error Handling Patterns
|
|
247
|
+
|
|
248
|
+
### Validation Callbacks
|
|
249
|
+
```python
|
|
250
|
+
def validate_and_resolve_components(
|
|
251
|
+
ctx: typer.Context, param: typer.CallbackParam, value: str | None
|
|
252
|
+
) -> list[str] | None:
|
|
253
|
+
"""Validate and resolve component dependencies."""
|
|
254
|
+
if not value:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
# Parse comma-separated string
|
|
258
|
+
components_raw = [c.strip() for c in value.split(",")]
|
|
259
|
+
|
|
260
|
+
# Check for empty components
|
|
261
|
+
if any(not c for c in components_raw):
|
|
262
|
+
typer.echo("❌ Empty component name is not allowed", err=True)
|
|
263
|
+
raise typer.Exit(1)
|
|
264
|
+
|
|
265
|
+
selected = [c for c in components_raw if c]
|
|
266
|
+
|
|
267
|
+
# Validate components exist
|
|
268
|
+
errors = DependencyResolver.validate_components(selected)
|
|
269
|
+
if errors:
|
|
270
|
+
for error in errors:
|
|
271
|
+
typer.echo(f"❌ {error}", err=True)
|
|
272
|
+
raise typer.Exit(1)
|
|
273
|
+
|
|
274
|
+
# Resolve dependencies
|
|
275
|
+
resolved = DependencyResolver.resolve_dependencies(selected)
|
|
276
|
+
|
|
277
|
+
# Show dependency resolution
|
|
278
|
+
auto_added = DependencyResolver.get_missing_dependencies(selected)
|
|
279
|
+
if auto_added:
|
|
280
|
+
typer.echo(f"📦 Auto-added dependencies: {', '.join(auto_added)}")
|
|
281
|
+
|
|
282
|
+
return resolved
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Error Message Quality
|
|
286
|
+
```python
|
|
287
|
+
def show_helpful_error(component: str, available: list[str]) -> None:
|
|
288
|
+
"""Show helpful error message with suggestions."""
|
|
289
|
+
# Suggest similar components using fuzzy matching
|
|
290
|
+
suggestion = find_closest_match(component, available)
|
|
291
|
+
|
|
292
|
+
if suggestion:
|
|
293
|
+
typer.echo(f"❌ Unknown component '{component}'. Did you mean '{suggestion}'?", err=True)
|
|
294
|
+
else:
|
|
295
|
+
typer.echo(f"❌ Unknown component '{component}'.", err=True)
|
|
296
|
+
|
|
297
|
+
typer.echo(f" Available components: {', '.join(available)}", err=True)
|
|
298
|
+
typer.echo(" Use 'aegis components' to see detailed information", err=True)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## CLI Development Best Practices
|
|
302
|
+
|
|
303
|
+
### Command Design
|
|
304
|
+
1. **Clear naming** - Use descriptive command and option names
|
|
305
|
+
2. **Helpful descriptions** - Provide clear help text for all commands
|
|
306
|
+
3. **Sensible defaults** - Choose good default values for options
|
|
307
|
+
4. **Progressive disclosure** - Show basic options first, advanced options with help
|
|
308
|
+
5. **Consistent patterns** - Use similar patterns across commands
|
|
309
|
+
|
|
310
|
+
### Validation Strategy
|
|
311
|
+
1. **Early validation** - Validate inputs as early as possible
|
|
312
|
+
2. **Clear error messages** - Provide actionable error messages
|
|
313
|
+
3. **Helpful suggestions** - Suggest corrections when possible
|
|
314
|
+
4. **Context-aware errors** - Show relevant information in errors
|
|
315
|
+
5. **Graceful degradation** - Handle edge cases gracefully
|
|
316
|
+
|
|
317
|
+
### User Experience
|
|
318
|
+
1. **Interactive guidance** - Provide interactive help when possible
|
|
319
|
+
2. **Visual feedback** - Use emojis and formatting for clarity
|
|
320
|
+
3. **Progress indication** - Show progress for long-running operations
|
|
321
|
+
4. **Confirmation prompts** - Ask before destructive operations
|
|
322
|
+
5. **Escape hatches** - Provide ways to cancel or undo operations
|
|
323
|
+
|
|
324
|
+
### Code Organization
|
|
325
|
+
1. **Separation of concerns** - Keep CLI logic separate from business logic
|
|
326
|
+
2. **Reusable functions** - Extract common patterns into functions
|
|
327
|
+
3. **Type safety** - Use proper type hints throughout
|
|
328
|
+
4. **Error handling** - Handle all possible error conditions
|
|
329
|
+
5. **Testing support** - Design code to be easily testable
|
|
330
|
+
|
|
331
|
+
## CLI Testing Patterns
|
|
332
|
+
|
|
333
|
+
### Command Testing
|
|
334
|
+
```python
|
|
335
|
+
def test_component_validation():
|
|
336
|
+
"""Test component validation logic."""
|
|
337
|
+
# Valid components
|
|
338
|
+
assert validate_components(["worker", "scheduler"]) == []
|
|
339
|
+
|
|
340
|
+
# Invalid components
|
|
341
|
+
errors = validate_components(["invalid"])
|
|
342
|
+
assert "Unknown component 'invalid'" in errors[0]
|
|
343
|
+
|
|
344
|
+
# Suggestions
|
|
345
|
+
errors = validate_components(["schedul"])
|
|
346
|
+
assert "Did you mean 'scheduler'?" in errors[0]
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Integration Testing
|
|
350
|
+
```python
|
|
351
|
+
def test_project_generation():
|
|
352
|
+
"""Test full project generation workflow."""
|
|
353
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
354
|
+
result = run_aegis_init(
|
|
355
|
+
"test-project",
|
|
356
|
+
["worker"],
|
|
357
|
+
Path(temp_dir)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
assert result.success
|
|
361
|
+
assert (result.project_path / "app" / "components" / "worker").exists()
|
|
362
|
+
assert (result.project_path / "docker-compose.yml").exists()
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
This approach ensures the CLI is maintainable, user-friendly, and follows established patterns for command-line tool development.
|
aegis/core/__init__.py
ADDED
aegis/core/components.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component registry and specifications for Aegis Stack.
|
|
3
|
+
|
|
4
|
+
This module defines all available components, their dependencies, and metadata
|
|
5
|
+
used for project generation and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ComponentType(Enum):
|
|
13
|
+
"""Component type classifications."""
|
|
14
|
+
|
|
15
|
+
CORE = "core" # Always included (backend, frontend)
|
|
16
|
+
INFRASTRUCTURE = "infra" # Redis, workers - foundation for services to use
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ComponentSpec:
|
|
21
|
+
"""Specification for a single component."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
type: ComponentType
|
|
25
|
+
description: str
|
|
26
|
+
requires: list[str] | None = None # Hard dependencies
|
|
27
|
+
recommends: list[str] | None = None # Soft dependencies
|
|
28
|
+
conflicts: list[str] | None = None # Mutual exclusions
|
|
29
|
+
docker_services: list[str] | None = None
|
|
30
|
+
pyproject_deps: list[str] | None = None
|
|
31
|
+
template_files: list[str] | None = None
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
"""Ensure all list fields are initialized."""
|
|
35
|
+
if self.requires is None:
|
|
36
|
+
self.requires = []
|
|
37
|
+
if self.recommends is None:
|
|
38
|
+
self.recommends = []
|
|
39
|
+
if self.conflicts is None:
|
|
40
|
+
self.conflicts = []
|
|
41
|
+
if self.docker_services is None:
|
|
42
|
+
self.docker_services = []
|
|
43
|
+
if self.pyproject_deps is None:
|
|
44
|
+
self.pyproject_deps = []
|
|
45
|
+
if self.template_files is None:
|
|
46
|
+
self.template_files = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Component registry - single source of truth
|
|
50
|
+
COMPONENTS: dict[str, ComponentSpec] = {
|
|
51
|
+
"backend": ComponentSpec(
|
|
52
|
+
name="backend",
|
|
53
|
+
type=ComponentType.CORE,
|
|
54
|
+
description="FastAPI backend server",
|
|
55
|
+
pyproject_deps=["fastapi==0.116.1", "uvicorn==0.35.0"],
|
|
56
|
+
template_files=["app/components/backend/"],
|
|
57
|
+
),
|
|
58
|
+
"frontend": ComponentSpec(
|
|
59
|
+
name="frontend",
|
|
60
|
+
type=ComponentType.CORE,
|
|
61
|
+
description="Flet frontend interface",
|
|
62
|
+
pyproject_deps=["flet==0.28.3"],
|
|
63
|
+
template_files=["app/components/frontend/"],
|
|
64
|
+
),
|
|
65
|
+
"redis": ComponentSpec(
|
|
66
|
+
name="redis",
|
|
67
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
68
|
+
description="Redis cache and message broker",
|
|
69
|
+
docker_services=["redis"],
|
|
70
|
+
pyproject_deps=["redis==5.0.8"],
|
|
71
|
+
),
|
|
72
|
+
"worker": ComponentSpec(
|
|
73
|
+
name="worker",
|
|
74
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
75
|
+
description="Background task processing infrastructure with arq",
|
|
76
|
+
requires=["redis"], # Hard dependency
|
|
77
|
+
pyproject_deps=["arq==0.25.0"],
|
|
78
|
+
docker_services=["worker-system", "worker-load-test"],
|
|
79
|
+
template_files=["app/components/worker/"],
|
|
80
|
+
),
|
|
81
|
+
"scheduler": ComponentSpec(
|
|
82
|
+
name="scheduler",
|
|
83
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
84
|
+
description="Scheduled task execution infrastructure",
|
|
85
|
+
pyproject_deps=["apscheduler==3.10.4"],
|
|
86
|
+
docker_services=["scheduler"],
|
|
87
|
+
template_files=["app/components/scheduler.py", "app/entrypoints/scheduler.py"],
|
|
88
|
+
),
|
|
89
|
+
"database": ComponentSpec(
|
|
90
|
+
name="database",
|
|
91
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
92
|
+
description="SQLite database with SQLModel ORM",
|
|
93
|
+
pyproject_deps=["sqlmodel>=0.0.14", "sqlalchemy>=2.0.0", "aiosqlite>=0.19.0"],
|
|
94
|
+
template_files=["app/core/db.py"],
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_component(name: str) -> ComponentSpec:
|
|
100
|
+
"""Get component specification by name."""
|
|
101
|
+
if name not in COMPONENTS:
|
|
102
|
+
raise ValueError(f"Unknown component: {name}")
|
|
103
|
+
return COMPONENTS[name]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_components_by_type(component_type: ComponentType) -> dict[str, ComponentSpec]:
|
|
107
|
+
"""Get all components of a specific type."""
|
|
108
|
+
return {
|
|
109
|
+
name: spec for name, spec in COMPONENTS.items() if spec.type == component_type
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def list_available_components() -> list[str]:
|
|
114
|
+
"""Get list of all available component names."""
|
|
115
|
+
return list(COMPONENTS.keys())
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component dependency resolution for Aegis Stack.
|
|
3
|
+
|
|
4
|
+
This module handles dependency resolution, validation, and recommendations
|
|
5
|
+
for component selection during project generation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .components import COMPONENTS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DependencyResolver:
|
|
12
|
+
"""Handles component dependency resolution and validation."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def resolve_dependencies(selected_components: list[str]) -> list[str]:
|
|
16
|
+
"""
|
|
17
|
+
Resolve all dependencies and return final component list.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
selected_components: List of component names selected by user
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Complete list of components including dependencies
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If any selected components are invalid
|
|
27
|
+
"""
|
|
28
|
+
# Validate all components first
|
|
29
|
+
errors = DependencyResolver.validate_components(selected_components)
|
|
30
|
+
if errors:
|
|
31
|
+
raise ValueError(f"Invalid components: {'; '.join(errors)}")
|
|
32
|
+
|
|
33
|
+
resolved = set(selected_components)
|
|
34
|
+
|
|
35
|
+
# Resolve hard dependencies recursively
|
|
36
|
+
while True:
|
|
37
|
+
before_size = len(resolved)
|
|
38
|
+
for component_name in list(resolved):
|
|
39
|
+
if component_name not in COMPONENTS:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
component = COMPONENTS[component_name]
|
|
43
|
+
if component.requires:
|
|
44
|
+
resolved.update(component.requires)
|
|
45
|
+
|
|
46
|
+
if len(resolved) == before_size:
|
|
47
|
+
break # No new dependencies added
|
|
48
|
+
|
|
49
|
+
return sorted(resolved)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def validate_components(components: list[str]) -> list[str]:
|
|
53
|
+
"""
|
|
54
|
+
Validate component selection and return errors.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
components: List of component names to validate
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of error messages (empty if valid)
|
|
61
|
+
"""
|
|
62
|
+
errors = []
|
|
63
|
+
|
|
64
|
+
for component in components:
|
|
65
|
+
if component not in COMPONENTS:
|
|
66
|
+
errors.append(f"Unknown component: {component}")
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
spec = COMPONENTS[component]
|
|
70
|
+
|
|
71
|
+
# Check conflicts
|
|
72
|
+
if spec.conflicts:
|
|
73
|
+
for conflict in spec.conflicts:
|
|
74
|
+
if conflict in components:
|
|
75
|
+
errors.append(
|
|
76
|
+
f"Component '{component}' conflicts with '{conflict}'"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return errors
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def get_recommendations(selected_components: list[str]) -> list[str]:
|
|
83
|
+
"""
|
|
84
|
+
Get recommended components based on selection.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
selected_components: List of already selected components
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of recommended component names not already selected
|
|
91
|
+
"""
|
|
92
|
+
recommendations = set()
|
|
93
|
+
|
|
94
|
+
for component_name in selected_components:
|
|
95
|
+
if component_name not in COMPONENTS:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
component = COMPONENTS[component_name]
|
|
99
|
+
if component.recommends:
|
|
100
|
+
for rec in component.recommends:
|
|
101
|
+
if rec not in selected_components:
|
|
102
|
+
recommendations.add(rec)
|
|
103
|
+
|
|
104
|
+
return sorted(recommendations)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def get_missing_dependencies(selected_components: list[str]) -> list[str]:
|
|
108
|
+
"""
|
|
109
|
+
Get dependencies that would be auto-added.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
selected_components: List of user-selected components
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of dependencies that would be automatically added
|
|
116
|
+
"""
|
|
117
|
+
resolved = DependencyResolver.resolve_dependencies(selected_components)
|
|
118
|
+
auto_added = set(resolved) - set(selected_components)
|
|
119
|
+
return sorted(auto_added)
|