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,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template generation and context building for Aegis Stack projects.
|
|
3
|
+
|
|
4
|
+
This module handles the generation of cookiecutter context and manages
|
|
5
|
+
the template rendering process based on selected components.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .components import COMPONENTS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TemplateGenerator:
|
|
15
|
+
"""Handles template context generation for cookiecutter."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, project_name: str, selected_components: list[str]):
|
|
18
|
+
"""
|
|
19
|
+
Initialize template generator.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
project_name: Name of the project being generated
|
|
23
|
+
selected_components: List of component names to include
|
|
24
|
+
"""
|
|
25
|
+
self.project_name = project_name
|
|
26
|
+
self.project_slug = project_name.lower().replace(" ", "-").replace("_", "-")
|
|
27
|
+
# Always include core components
|
|
28
|
+
all_components = ["backend", "frontend"] + selected_components
|
|
29
|
+
# Remove duplicates, preserve order
|
|
30
|
+
self.components = list(dict.fromkeys(all_components))
|
|
31
|
+
self.component_specs = {
|
|
32
|
+
name: COMPONENTS[name] for name in self.components if name in COMPONENTS
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def get_template_context(self) -> dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Generate cookiecutter context from components.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dictionary containing all template variables
|
|
41
|
+
"""
|
|
42
|
+
# Store the originally selected components (without core)
|
|
43
|
+
selected_only = [c for c in self.components if c not in ["backend", "frontend"]]
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"project_name": self.project_name,
|
|
47
|
+
"project_slug": self.project_slug,
|
|
48
|
+
# Component flags for template conditionals - cookiecutter needs yes/no
|
|
49
|
+
"include_redis": "yes" if "redis" in self.components else "no",
|
|
50
|
+
"include_worker": "yes" if "worker" in self.components else "no",
|
|
51
|
+
"include_scheduler": "yes" if "scheduler" in self.components else "no",
|
|
52
|
+
"include_database": "yes" if "database" in self.components else "no",
|
|
53
|
+
# Derived flags for template logic
|
|
54
|
+
"has_background_infrastructure": any(
|
|
55
|
+
name in self.components for name in ["worker", "scheduler"]
|
|
56
|
+
),
|
|
57
|
+
"needs_redis": "redis" in self.components,
|
|
58
|
+
# Dependency lists for templates
|
|
59
|
+
"selected_components": selected_only, # Original selection for context
|
|
60
|
+
"docker_services": self._get_docker_services(),
|
|
61
|
+
"pyproject_dependencies": self._get_pyproject_deps(),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def _get_docker_services(self) -> list[str]:
|
|
65
|
+
"""
|
|
66
|
+
Collect all docker services needed.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of docker service names
|
|
70
|
+
"""
|
|
71
|
+
services = []
|
|
72
|
+
for component_name in self.components:
|
|
73
|
+
if component_name in self.component_specs:
|
|
74
|
+
spec = self.component_specs[component_name]
|
|
75
|
+
if spec.docker_services:
|
|
76
|
+
services.extend(spec.docker_services)
|
|
77
|
+
return list(dict.fromkeys(services)) # Preserve order, remove duplicates
|
|
78
|
+
|
|
79
|
+
def _get_pyproject_deps(self) -> list[str]:
|
|
80
|
+
"""
|
|
81
|
+
Collect all Python dependencies.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Sorted list of Python package dependencies
|
|
85
|
+
"""
|
|
86
|
+
deps = []
|
|
87
|
+
for component_name in self.components:
|
|
88
|
+
if component_name in self.component_specs:
|
|
89
|
+
spec = self.component_specs[component_name]
|
|
90
|
+
if spec.pyproject_deps:
|
|
91
|
+
deps.extend(spec.pyproject_deps)
|
|
92
|
+
return sorted(set(deps)) # Sort and deduplicate
|
|
93
|
+
|
|
94
|
+
def get_template_files(self) -> list[str]:
|
|
95
|
+
"""
|
|
96
|
+
Get list of template files that should be included.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of template file paths
|
|
100
|
+
"""
|
|
101
|
+
files = []
|
|
102
|
+
for component_name in self.components:
|
|
103
|
+
if component_name in self.component_specs:
|
|
104
|
+
spec = self.component_specs[component_name]
|
|
105
|
+
if spec.template_files:
|
|
106
|
+
files.extend(spec.template_files)
|
|
107
|
+
return list(dict.fromkeys(files)) # Preserve order, remove duplicates
|
|
108
|
+
|
|
109
|
+
def get_entrypoints(self) -> list[str]:
|
|
110
|
+
"""
|
|
111
|
+
Get list of entrypoints that will be created.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of entrypoint file paths
|
|
115
|
+
"""
|
|
116
|
+
entrypoints = ["app/entrypoints/webserver.py"] # Always included
|
|
117
|
+
|
|
118
|
+
# Check component specs for actual entrypoint files
|
|
119
|
+
for component_name in self.components:
|
|
120
|
+
if component_name in self.component_specs:
|
|
121
|
+
spec = self.component_specs[component_name]
|
|
122
|
+
if spec.template_files:
|
|
123
|
+
for template_file in spec.template_files:
|
|
124
|
+
if (
|
|
125
|
+
template_file.startswith("app/entrypoints/")
|
|
126
|
+
and template_file not in entrypoints
|
|
127
|
+
):
|
|
128
|
+
entrypoints.append(template_file)
|
|
129
|
+
|
|
130
|
+
return entrypoints
|
|
131
|
+
|
|
132
|
+
def get_worker_queues(self) -> list[str]:
|
|
133
|
+
"""
|
|
134
|
+
Get list of worker queue files that will be created.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of worker queue file paths
|
|
138
|
+
"""
|
|
139
|
+
queues: list[str] = []
|
|
140
|
+
|
|
141
|
+
# Only check if worker component is included
|
|
142
|
+
if "worker" not in self.components:
|
|
143
|
+
return queues
|
|
144
|
+
|
|
145
|
+
# Discover queue files from the template directory
|
|
146
|
+
template_root = (
|
|
147
|
+
Path(__file__).parent.parent / "templates" / "cookiecutter-aegis-project"
|
|
148
|
+
)
|
|
149
|
+
worker_queues_dir = (
|
|
150
|
+
template_root
|
|
151
|
+
/ "{{cookiecutter.project_slug}}"
|
|
152
|
+
/ "app"
|
|
153
|
+
/ "components"
|
|
154
|
+
/ "worker"
|
|
155
|
+
/ "queues"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if worker_queues_dir.exists():
|
|
159
|
+
for queue_file in worker_queues_dir.glob("*.py"):
|
|
160
|
+
if queue_file.stem != "__init__":
|
|
161
|
+
queues.append(f"app/components/worker/queues/{queue_file.name}")
|
|
162
|
+
|
|
163
|
+
return sorted(queues)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Template Development Guide
|
|
2
|
+
|
|
3
|
+
This guide covers template development patterns for Aegis Stack's Cookiecutter templates.
|
|
4
|
+
|
|
5
|
+
## Template Architecture
|
|
6
|
+
|
|
7
|
+
### Template Structure
|
|
8
|
+
```
|
|
9
|
+
aegis/templates/cookiecutter-aegis-project/
|
|
10
|
+
├── cookiecutter.json # Template variables
|
|
11
|
+
├── hooks/
|
|
12
|
+
│ └── post_gen_project.py # Template processing logic
|
|
13
|
+
└── {{cookiecutter.project_slug}}/ # Generated project structure
|
|
14
|
+
├── app/
|
|
15
|
+
│ ├── components/
|
|
16
|
+
│ │ ├── backend/ # Always included
|
|
17
|
+
│ │ ├── frontend/ # Always included
|
|
18
|
+
│ │ ├── scheduler/ # Optional component
|
|
19
|
+
│ │ └── worker/ # Optional component
|
|
20
|
+
│ ├── core/ # Framework utilities
|
|
21
|
+
│ ├── entrypoints/ # Execution modes
|
|
22
|
+
│ ├── integrations/ # App composition
|
|
23
|
+
│ └── services/ # Business logic (empty)
|
|
24
|
+
├── tests/
|
|
25
|
+
├── docker-compose.yml.j2 # Conditional services
|
|
26
|
+
├── Dockerfile.j2 # Conditional entrypoints
|
|
27
|
+
├── pyproject.toml.j2 # Dependencies and configuration
|
|
28
|
+
└── scripts/entrypoint.sh.j2 # Runtime dispatch
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Template Processing Flow
|
|
32
|
+
1. **Cookiecutter generates** base project structure using `cookiecutter.json`
|
|
33
|
+
2. **Post-generation hook** (`hooks/post_gen_project.py`) processes `.j2` files with Jinja2
|
|
34
|
+
3. **Component selection** includes/excludes files based on user choices
|
|
35
|
+
4. **Auto-formatting** runs `make fix` on generated project
|
|
36
|
+
5. **Cleanup** removes unused template files and `.j2` originals
|
|
37
|
+
|
|
38
|
+
## Cookiecutter Variables
|
|
39
|
+
|
|
40
|
+
### Core Variables (cookiecutter.json)
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"project_name": "My Aegis Project",
|
|
44
|
+
"project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '-')|replace('_', '-') }}",
|
|
45
|
+
"project_description": "A production-ready Python application",
|
|
46
|
+
"author_name": "Your Name",
|
|
47
|
+
"author_email": "your.email@example.com",
|
|
48
|
+
"version": "0.1.0",
|
|
49
|
+
"python_version": "3.11",
|
|
50
|
+
"include_scheduler": "no",
|
|
51
|
+
"include_worker": "no"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Variable Usage in Templates
|
|
56
|
+
```jinja2
|
|
57
|
+
# In any .j2 file
|
|
58
|
+
{{ cookiecutter.project_name }} # "My Aegis Project"
|
|
59
|
+
{{ cookiecutter.project_slug }} # "my-aegis-project"
|
|
60
|
+
{{ cookiecutter.project_description }} # Description text
|
|
61
|
+
{{ cookiecutter.author_name }} # Author info
|
|
62
|
+
{{ cookiecutter.include_scheduler }} # "yes" or "no"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Jinja2 Template Patterns
|
|
66
|
+
|
|
67
|
+
### Conditional Content
|
|
68
|
+
```jinja2
|
|
69
|
+
{% if cookiecutter.include_scheduler == "yes" %}
|
|
70
|
+
# Scheduler-specific content
|
|
71
|
+
{% endif %}
|
|
72
|
+
|
|
73
|
+
{% if cookiecutter.include_worker == "yes" %}
|
|
74
|
+
# Worker-specific content
|
|
75
|
+
{% endif %}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Conditional Files
|
|
79
|
+
File names can be conditional:
|
|
80
|
+
```
|
|
81
|
+
{% if cookiecutter.include_scheduler == "yes" %}scheduler.py{% endif %}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Variable Substitution in Code
|
|
85
|
+
```python
|
|
86
|
+
# In .j2 files
|
|
87
|
+
CLI_NAME = "{{ cookiecutter.project_slug }}"
|
|
88
|
+
PROJECT_NAME = "{{ cookiecutter.project_name }}"
|
|
89
|
+
VERSION = "{{ cookiecutter.version }}"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Dependencies Based on Components
|
|
93
|
+
```toml
|
|
94
|
+
# pyproject.toml.j2
|
|
95
|
+
dependencies = [
|
|
96
|
+
"fastapi>=0.116.1",
|
|
97
|
+
"flet>=0.28.3",
|
|
98
|
+
{% if cookiecutter.include_scheduler == "yes" %}
|
|
99
|
+
"apscheduler>=3.10.0",
|
|
100
|
+
{% endif %}
|
|
101
|
+
{% if cookiecutter.include_worker == "yes" %}
|
|
102
|
+
"arq>=0.26.1",
|
|
103
|
+
"redis>=5.2.1",
|
|
104
|
+
{% endif %}
|
|
105
|
+
]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Post-Generation Hook Patterns
|
|
109
|
+
|
|
110
|
+
### Hook Responsibilities
|
|
111
|
+
The `hooks/post_gen_project.py` script:
|
|
112
|
+
1. **Processes .j2 files** - Renders Jinja2 templates with cookiecutter context
|
|
113
|
+
2. **Removes unused files** - Deletes component files when components not selected
|
|
114
|
+
3. **Cleans up directories** - Removes empty directories after file cleanup
|
|
115
|
+
4. **Auto-formats code** - Runs `make fix` to ensure generated code is clean
|
|
116
|
+
|
|
117
|
+
### Adding New Component Logic
|
|
118
|
+
```python
|
|
119
|
+
# In hooks/post_gen_project.py
|
|
120
|
+
if "{{ cookiecutter.include_new_component }}" != "yes":
|
|
121
|
+
# Remove component-specific files
|
|
122
|
+
remove_dir("app/components/new_component")
|
|
123
|
+
remove_file("app/entrypoints/new_component.py")
|
|
124
|
+
remove_file("tests/components/test_new_component.py")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### File Removal Patterns
|
|
128
|
+
```python
|
|
129
|
+
# Remove individual files
|
|
130
|
+
remove_file("app/components/scheduler.py")
|
|
131
|
+
remove_file("tests/components/test_scheduler.py")
|
|
132
|
+
|
|
133
|
+
# Remove entire directories
|
|
134
|
+
remove_dir("app/components/worker")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Template Development Workflow
|
|
138
|
+
|
|
139
|
+
### CRITICAL: Never Edit Generated Projects
|
|
140
|
+
**Always follow this pattern:**
|
|
141
|
+
|
|
142
|
+
1. **Edit template files** in `aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/`
|
|
143
|
+
2. **Test template changes**: `make test-template`
|
|
144
|
+
3. **If tests fail**: Fix the **template files** (step 1), never the generated projects
|
|
145
|
+
4. **Repeat** until tests pass
|
|
146
|
+
5. **Clean up**: `make clean-test-projects`
|
|
147
|
+
|
|
148
|
+
### Adding New Template Files
|
|
149
|
+
```bash
|
|
150
|
+
# 1. Create template file
|
|
151
|
+
vim aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/new_component.py
|
|
152
|
+
|
|
153
|
+
# 2. If using variables, make it a .j2 file
|
|
154
|
+
mv app/components/new_component.py app/components/new_component.py.j2
|
|
155
|
+
|
|
156
|
+
# 3. Add conditional logic to hook if needed
|
|
157
|
+
vim hooks/post_gen_project.py
|
|
158
|
+
|
|
159
|
+
# 4. Test the changes
|
|
160
|
+
make test-template
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Modifying Existing Templates
|
|
164
|
+
```bash
|
|
165
|
+
# 1. Find the template file
|
|
166
|
+
find aegis/templates/ -name "*.py" -o -name "*.j2" | grep component_name
|
|
167
|
+
|
|
168
|
+
# 2. Edit the template
|
|
169
|
+
vim aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/path/to/file.j2
|
|
170
|
+
|
|
171
|
+
# 3. Test immediately
|
|
172
|
+
make test-template-quick
|
|
173
|
+
|
|
174
|
+
# 4. Full validation
|
|
175
|
+
make test-template
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Template Testing Integration
|
|
179
|
+
|
|
180
|
+
### Template Validation Process
|
|
181
|
+
When you run `make test-template`:
|
|
182
|
+
1. **Generates fresh project** using current templates
|
|
183
|
+
2. **Processes .j2 files** through post-generation hook
|
|
184
|
+
3. **Installs dependencies** in generated project
|
|
185
|
+
4. **Runs quality checks** (lint, typecheck, tests)
|
|
186
|
+
5. **Tests CLI installation** and functionality
|
|
187
|
+
|
|
188
|
+
### Template-Specific Test Commands
|
|
189
|
+
```bash
|
|
190
|
+
make test-template # Test basic project generation
|
|
191
|
+
make test-template-with-components # Test with scheduler component
|
|
192
|
+
make test-template-worker # Test worker component
|
|
193
|
+
make test-template-full # Test all components
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Auto-Fixing in Templates
|
|
197
|
+
The template system automatically:
|
|
198
|
+
- **Fixes linting issues** in generated code
|
|
199
|
+
- **Formats code** with ruff
|
|
200
|
+
- **Ensures proper imports** and structure
|
|
201
|
+
- **Validates type annotations**
|
|
202
|
+
|
|
203
|
+
## Common Template Patterns
|
|
204
|
+
|
|
205
|
+
### Configuration Management
|
|
206
|
+
```python
|
|
207
|
+
# Use in templates for environment-dependent values
|
|
208
|
+
from app.core.config import settings
|
|
209
|
+
|
|
210
|
+
# Template generates proper imports
|
|
211
|
+
DATABASE_URL = settings.DATABASE_URL
|
|
212
|
+
REDIS_URL = settings.REDIS_URL
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Component Registration
|
|
216
|
+
```python
|
|
217
|
+
# Backend component registration
|
|
218
|
+
# In app/components/backend/startup/component_health.py.j2
|
|
219
|
+
{% if cookiecutter.include_worker == "yes" %}
|
|
220
|
+
from app.components.worker.health import register_worker_health_checks
|
|
221
|
+
{% endif %}
|
|
222
|
+
|
|
223
|
+
async def register_component_health_checks() -> None:
|
|
224
|
+
"""Register health checks for all enabled components."""
|
|
225
|
+
{% if cookiecutter.include_worker == "yes" %}
|
|
226
|
+
register_worker_health_checks()
|
|
227
|
+
{% endif %}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Docker Service Configuration
|
|
231
|
+
```yaml
|
|
232
|
+
# docker-compose.yml.j2
|
|
233
|
+
services:
|
|
234
|
+
webserver:
|
|
235
|
+
# Always included
|
|
236
|
+
|
|
237
|
+
{% if cookiecutter.include_worker == "yes" %}
|
|
238
|
+
worker-system:
|
|
239
|
+
build: .
|
|
240
|
+
command: ["worker-system"]
|
|
241
|
+
depends_on:
|
|
242
|
+
- redis
|
|
243
|
+
{% endif %}
|
|
244
|
+
|
|
245
|
+
{% if cookiecutter.include_scheduler == "yes" %}
|
|
246
|
+
scheduler:
|
|
247
|
+
build: .
|
|
248
|
+
command: ["scheduler"]
|
|
249
|
+
{% endif %}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Template Debugging
|
|
253
|
+
|
|
254
|
+
### Common Template Issues
|
|
255
|
+
- **Jinja2 syntax errors** - Check bracket matching, endif statements
|
|
256
|
+
- **Missing cookiecutter variables** - Verify variable names in cookiecutter.json
|
|
257
|
+
- **Conditional logic errors** - Test with different component combinations
|
|
258
|
+
- **File path issues** - Ensure proper directory structure
|
|
259
|
+
|
|
260
|
+
### Debugging Template Generation
|
|
261
|
+
```bash
|
|
262
|
+
# Generate project manually for debugging
|
|
263
|
+
uv run aegis init debug-project --output-dir ../debug --force --yes
|
|
264
|
+
|
|
265
|
+
# Check generated files
|
|
266
|
+
ls -la ../debug-project/
|
|
267
|
+
|
|
268
|
+
# Look for remaining .j2 files (should be none)
|
|
269
|
+
find ../debug-project/ -name "*.j2"
|
|
270
|
+
|
|
271
|
+
# Check variable substitution
|
|
272
|
+
grep -r "cookiecutter\." ../debug-project/ || echo "No unreplaced variables"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Testing Individual Components
|
|
276
|
+
```bash
|
|
277
|
+
# Test specific component combinations
|
|
278
|
+
make test-template-worker # Just worker component
|
|
279
|
+
make test-template-with-components # Just scheduler component
|
|
280
|
+
make test-template-full # All components
|
|
281
|
+
|
|
282
|
+
# Clean up between tests
|
|
283
|
+
make clean-test-projects
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Template Quality Standards
|
|
287
|
+
|
|
288
|
+
### Code Generation Requirements
|
|
289
|
+
- **No .j2 files** remain in generated projects
|
|
290
|
+
- **All variables replaced** - no `{{ cookiecutter.* }}` in final code
|
|
291
|
+
- **Proper imports** - only import what's needed based on components
|
|
292
|
+
- **Type annotations** - all generated code must be properly typed
|
|
293
|
+
- **Linting passes** - generated code passes ruff checks
|
|
294
|
+
- **Tests included** - component tests generated with components
|
|
295
|
+
|
|
296
|
+
### Component Isolation
|
|
297
|
+
- **Independent components** - each component can be enabled/disabled
|
|
298
|
+
- **Clean dependencies** - components only depend on what they need
|
|
299
|
+
- **Proper cleanup** - unused files removed when components disabled
|
|
300
|
+
- **No broken imports** - imports only exist when dependencies available
|
|
301
|
+
|
|
302
|
+
### File Organization
|
|
303
|
+
- **Consistent structure** - follow established patterns
|
|
304
|
+
- **Logical grouping** - related files in same directories
|
|
305
|
+
- **Clear naming** - descriptive file and directory names
|
|
306
|
+
- **Proper permissions** - executable files marked as executable
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_name": "My Aegis Stack Project",
|
|
3
|
+
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-').replace('_', '-') }}",
|
|
4
|
+
"project_description": "A production-ready async Python application built with Aegis Stack",
|
|
5
|
+
"author_name": "Your Name",
|
|
6
|
+
"author_email": "your.email@example.com",
|
|
7
|
+
"github_username": "your-username",
|
|
8
|
+
"version": "0.1.0",
|
|
9
|
+
"python_version": "3.11",
|
|
10
|
+
|
|
11
|
+
"_comment_components": "Component selection - these will be set by our CLI",
|
|
12
|
+
"include_scheduler": "no",
|
|
13
|
+
"include_redis": "no",
|
|
14
|
+
"include_worker": "no",
|
|
15
|
+
"include_database": "no",
|
|
16
|
+
"include_cache": "no",
|
|
17
|
+
|
|
18
|
+
"_comment_internal": "Internal variables for template logic",
|
|
19
|
+
"_has_additional_components": "{% if cookiecutter.include_scheduler == 'yes' or cookiecutter.include_redis == 'yes' or cookiecutter.include_worker == 'yes' or cookiecutter.include_database == 'yes' or cookiecutter.include_cache == 'yes' %}yes{% else %}no{% endif %}",
|
|
20
|
+
|
|
21
|
+
"_comment_dependencies": "Component-specific dependencies",
|
|
22
|
+
"_scheduler_deps": "{% if cookiecutter.include_scheduler == 'yes' %}apscheduler>=3.10.0{% endif %}",
|
|
23
|
+
"_redis_deps": "{% if cookiecutter.include_redis == 'yes' %}redis>=5.0.0{% endif %}",
|
|
24
|
+
"_worker_deps": "{% if cookiecutter.include_worker == 'yes' %}arq>=0.25.0{% endif %}",
|
|
25
|
+
"_database_deps": "{% if cookiecutter.include_database == 'yes' %}sqlmodel>=0.0.14,sqlalchemy>=2.0.0,aiosqlite>=0.19.0{% endif %}",
|
|
26
|
+
"_cache_deps": "{% if cookiecutter.include_cache == 'yes' %}redis[hiredis]>=5.0.0{% endif %}"
|
|
27
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from jinja2 import Environment
|
|
8
|
+
|
|
9
|
+
PROJECT_DIRECTORY = os.path.realpath(os.path.curdir)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def remove_file(filepath):
|
|
13
|
+
"""Removes a file from the generated project."""
|
|
14
|
+
full_path = os.path.join(PROJECT_DIRECTORY, filepath)
|
|
15
|
+
if os.path.exists(full_path):
|
|
16
|
+
os.remove(full_path)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def remove_dir(dirpath):
|
|
20
|
+
"""Removes a directory from the generated project."""
|
|
21
|
+
full_path = os.path.join(PROJECT_DIRECTORY, dirpath)
|
|
22
|
+
if os.path.exists(full_path):
|
|
23
|
+
shutil.rmtree(full_path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def process_j2_templates():
|
|
27
|
+
"""
|
|
28
|
+
Process all .j2 template files in the generated project.
|
|
29
|
+
Renders them with cookiecutter context and removes the .j2 originals.
|
|
30
|
+
Returns list of output files that were created.
|
|
31
|
+
"""
|
|
32
|
+
# Cookiecutter context variables - these template strings are processed
|
|
33
|
+
# by cookiecutter before this hook runs, so they contain actual values
|
|
34
|
+
context = {
|
|
35
|
+
"cookiecutter": {
|
|
36
|
+
"project_name": "{{ cookiecutter.project_name }}",
|
|
37
|
+
"project_slug": "{{ cookiecutter.project_slug }}",
|
|
38
|
+
"project_description": "{{ cookiecutter.project_description }}",
|
|
39
|
+
"author_name": "{{ cookiecutter.author_name }}",
|
|
40
|
+
"author_email": "{{ cookiecutter.author_email }}",
|
|
41
|
+
"version": "{{ cookiecutter.version }}",
|
|
42
|
+
"python_version": "{{ cookiecutter.python_version }}",
|
|
43
|
+
"include_redis": "{{ cookiecutter.include_redis }}",
|
|
44
|
+
"include_scheduler": "{{ cookiecutter.include_scheduler }}",
|
|
45
|
+
"include_worker": "{{ cookiecutter.include_worker }}",
|
|
46
|
+
"include_database": "{{ cookiecutter.include_database }}",
|
|
47
|
+
"include_cache": "{{ cookiecutter.include_cache }}",
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Find all .j2 files in the project
|
|
52
|
+
project_path = Path(PROJECT_DIRECTORY)
|
|
53
|
+
j2_files = list(project_path.rglob("*.j2"))
|
|
54
|
+
processed_files = []
|
|
55
|
+
|
|
56
|
+
for j2_file in j2_files:
|
|
57
|
+
# Read the template content
|
|
58
|
+
with open(j2_file, encoding="utf-8") as f:
|
|
59
|
+
template_content = f.read()
|
|
60
|
+
|
|
61
|
+
# Create Jinja2 environment and render the template
|
|
62
|
+
env = Environment()
|
|
63
|
+
template = env.from_string(template_content)
|
|
64
|
+
rendered_content = template.render(context)
|
|
65
|
+
|
|
66
|
+
# Write the rendered content to the final file (without .j2 extension)
|
|
67
|
+
output_file = j2_file.with_suffix("")
|
|
68
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
69
|
+
f.write(rendered_content)
|
|
70
|
+
|
|
71
|
+
# Remove the original .j2 file
|
|
72
|
+
j2_file.unlink()
|
|
73
|
+
|
|
74
|
+
# Track processed files for later reporting
|
|
75
|
+
processed_files.append(output_file)
|
|
76
|
+
|
|
77
|
+
# Ensure file ends with newline
|
|
78
|
+
if not rendered_content.endswith("\n"):
|
|
79
|
+
with open(output_file, "a", encoding="utf-8") as f:
|
|
80
|
+
f.write("\n")
|
|
81
|
+
|
|
82
|
+
return processed_files
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def run_auto_formatting():
|
|
86
|
+
"""
|
|
87
|
+
Auto-format generated code by calling make fix.
|
|
88
|
+
Fixes linting issues and formats code for consistency.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
print("🎨 Auto-formatting generated code...")
|
|
92
|
+
|
|
93
|
+
# Call make fix to auto-format the generated project
|
|
94
|
+
result = subprocess.run(
|
|
95
|
+
["make", "fix"],
|
|
96
|
+
cwd=PROJECT_DIRECTORY,
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
timeout=60, # Don't hang forever
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if result.returncode == 0:
|
|
103
|
+
print("✅ Code formatting completed successfully")
|
|
104
|
+
else:
|
|
105
|
+
print(
|
|
106
|
+
"⚠️ Some formatting issues detected, but project created successfully"
|
|
107
|
+
)
|
|
108
|
+
print("💡 Run 'make fix' manually to resolve remaining issues")
|
|
109
|
+
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
print("💡 Run 'make fix' to format code when ready")
|
|
112
|
+
except subprocess.TimeoutExpired:
|
|
113
|
+
print("⚠️ Formatting timeout - run 'make fix' manually when ready")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"⚠️ Auto-formatting skipped: {e}")
|
|
116
|
+
print("💡 Run 'make fix' manually to format code")
|
|
117
|
+
# Don't fail project generation due to formatting issues
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
"""
|
|
122
|
+
Runs the post-generation cleanup to remove files for unselected
|
|
123
|
+
components and process template files.
|
|
124
|
+
"""
|
|
125
|
+
# Process .j2 template files first
|
|
126
|
+
processed_files = process_j2_templates()
|
|
127
|
+
|
|
128
|
+
# Remove components not selected
|
|
129
|
+
if "{{ cookiecutter.include_scheduler }}" != "yes":
|
|
130
|
+
# Remove scheduler-specific files
|
|
131
|
+
remove_file("app/entrypoints/scheduler.py")
|
|
132
|
+
remove_dir("app/components/scheduler")
|
|
133
|
+
remove_file("tests/components/test_scheduler.py")
|
|
134
|
+
remove_file("docs/components/scheduler.md")
|
|
135
|
+
|
|
136
|
+
if "{{ cookiecutter.include_worker }}" != "yes":
|
|
137
|
+
# Remove worker-specific files
|
|
138
|
+
remove_dir("app/components/worker")
|
|
139
|
+
remove_file("app/cli/load_test.py")
|
|
140
|
+
remove_file("app/services/load_test.py")
|
|
141
|
+
remove_file("app/services/load_test_models.py")
|
|
142
|
+
remove_file("tests/services/test_load_test_models.py")
|
|
143
|
+
remove_file("tests/services/test_load_test_service.py")
|
|
144
|
+
remove_file("tests/services/test_worker_health_registration.py")
|
|
145
|
+
|
|
146
|
+
if "{{ cookiecutter.include_database }}" != "yes":
|
|
147
|
+
remove_file("app/core/db.py")
|
|
148
|
+
|
|
149
|
+
if "{{ cookiecutter.include_cache }}" != "yes":
|
|
150
|
+
# remove_file("app/services/cache_service.py")
|
|
151
|
+
pass # Placeholder for cache component
|
|
152
|
+
|
|
153
|
+
# Clean up empty docs/components directory if no components selected
|
|
154
|
+
if (
|
|
155
|
+
"{{ cookiecutter.include_scheduler }}" != "yes"
|
|
156
|
+
and "{{ cookiecutter.include_worker }}" != "yes"
|
|
157
|
+
and "{{ cookiecutter.include_database }}" != "yes"
|
|
158
|
+
and "{{ cookiecutter.include_cache }}" != "yes"
|
|
159
|
+
):
|
|
160
|
+
remove_dir("docs/components")
|
|
161
|
+
|
|
162
|
+
# Print only templates that survived cleanup
|
|
163
|
+
for file_path in processed_files:
|
|
164
|
+
if file_path.exists():
|
|
165
|
+
print(f"Processed template: {file_path.name}")
|
|
166
|
+
|
|
167
|
+
# Run auto-formatting after all processing is complete
|
|
168
|
+
run_auto_formatting()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
main()
|