aegis-stack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (103) hide show
  1. aegis/__init__.py +5 -0
  2. aegis/__main__.py +374 -0
  3. aegis/core/CLAUDE.md +365 -0
  4. aegis/core/__init__.py +6 -0
  5. aegis/core/components.py +115 -0
  6. aegis/core/dependency_resolver.py +119 -0
  7. aegis/core/template_generator.py +163 -0
  8. aegis/templates/CLAUDE.md +306 -0
  9. aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
  10. aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
  11. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
  12. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
  13. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
  14. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  15. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
  16. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
  17. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
  18. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
  19. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
  20. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
  21. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
  22. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
  23. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
  24. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
  25. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
  26. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
  27. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
  28. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
  29. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
  30. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
  31. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
  32. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
  33. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
  34. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
  35. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
  36. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
  37. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
  38. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
  39. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
  40. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
  41. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
  42. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
  43. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
  44. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
  45. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
  46. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
  47. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
  48. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
  49. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
  50. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
  51. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
  52. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
  53. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
  54. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
  55. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
  56. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
  57. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
  58. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
  59. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
  60. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
  61. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
  62. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
  63. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
  64. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
  65. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
  66. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
  67. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
  68. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
  69. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
  70. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
  71. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
  72. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
  73. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
  74. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
  75. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
  76. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
  77. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
  78. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
  79. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
  80. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
  81. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
  82. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
  83. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
  84. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
  85. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
  86. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
  87. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
  88. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
  89. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
  90. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
  91. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
  92. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
  93. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
  94. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
  95. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
  96. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
  97. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
  98. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
  99. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
  100. aegis_stack-0.1.0.dist-info/METADATA +114 -0
  101. aegis_stack-0.1.0.dist-info/RECORD +103 -0
  102. aegis_stack-0.1.0.dist-info/WHEEL +4 -0
  103. aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,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()