aegis-stack 0.2.0rc2__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.
- aegis/__init__.py +5 -0
- aegis/__main__.py +51 -0
- aegis/cli/__init__.py +6 -0
- aegis/cli/callbacks.py +114 -0
- aegis/cli/interactive.py +611 -0
- aegis/cli/utils.py +70 -0
- aegis/cli/validators.py +34 -0
- aegis/commands/__init__.py +6 -0
- aegis/commands/add.py +353 -0
- aegis/commands/add_service.py +332 -0
- aegis/commands/components.py +35 -0
- aegis/commands/init.py +370 -0
- aegis/commands/remove.py +227 -0
- aegis/commands/services.py +52 -0
- aegis/commands/update.py +252 -0
- aegis/commands/version.py +12 -0
- aegis/config/__init__.py +1 -0
- aegis/config/shared_files.py +136 -0
- aegis/core/CLAUDE.md +377 -0
- aegis/core/__init__.py +6 -0
- aegis/core/component_files.py +228 -0
- aegis/core/component_utils.py +220 -0
- aegis/core/components.py +127 -0
- aegis/core/copier_manager.py +315 -0
- aegis/core/copier_updater.py +475 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/manual_updater.py +554 -0
- aegis/core/post_gen_tasks.py +547 -0
- aegis/core/service_resolver.py +261 -0
- aegis/core/services.py +157 -0
- aegis/core/template_generator.py +266 -0
- aegis/core/version_compatibility.py +259 -0
- aegis/templates/CLAUDE.md +591 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +39 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +214 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +130 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +236 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/alembic.ini.j2 +111 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/env.py.j2 +91 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/script.py.mako +25 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/versions/001_initial_auth.py.j2 +51 -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/ai.py.j2 +700 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai_rendering.py +361 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/auth.py.j2 +253 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py.j2 +419 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py.j2 +656 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py.j2 +65 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/marko_terminal_renderer.py +489 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/tasks.py.j2 +328 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/{% if cookiecutter.include_scheduler == /"yes/" %}tasks.py{% endif %}" +340 -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/ai/__init__.py +8 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/router.py +329 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/router.py +64 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/deps.py +58 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +163 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +280 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/scheduler.py.j2 +121 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/worker.py.j2 +478 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +144 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +31 -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 +418 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/database_init.py.j2 +83 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/__init__.py +27 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/table.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/text.py +142 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/__init__.py.j2 +47 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/base_card.py +256 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/database_card.py +420 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/services_card.py +233 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py.j2 +653 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/theme.py +48 -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.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md.j2 +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 +97 -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 +55 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +49 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +44 -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 +120 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +507 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +33 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +281 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +178 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +58 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py.j2 +176 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +92 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/security.py +62 -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 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/user.py +44 -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/ai/__init__.py +8 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/config.py +130 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/conversation.py +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/health.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/models.py +229 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/providers.py +370 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/service.py +388 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/auth_service.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/health.py +164 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/user_service.py +83 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/middleware_inspector.py.j2 +223 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/models.py.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/route_inspector.py.j2 +155 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +679 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +266 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/__init__.py.j2 +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/models.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/scheduled_task_manager.py.j2 +273 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/task_monitor.py.j2 +189 -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/backup.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1333 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +243 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-dark.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-square-backup.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.env.example.j2 +64 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.gitignore +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/README.md.j2 +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/docker-compose.yml.j2 +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/pyproject.toml.j2 +120 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/uv.lock +1673 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +200 -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 +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md.j2 +621 -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 +131 -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 +93 -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_auth_endpoints.py.j2 +307 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +262 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_scheduler_endpoints.py.j2 +214 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_worker_endpoints.py.j2 +165 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_ai_rendering.py +427 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_conversation_memory.py +465 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +43 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +195 -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/ai/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/conftest.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_health.py +157 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_models.py +164 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_service.py +198 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_auth_integration.py.j2 +528 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +387 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_conversation_persistence.py +342 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +663 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +619 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +603 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_middleware_inspector.py.j2 +248 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_scheduled_task_manager.py.j2 +292 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +98 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +257 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +49 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.dockerignore +71 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.env.example.jinja +130 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.gitignore +131 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/Dockerfile +53 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/Makefile.jinja +236 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/README.md.jinja +196 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/alembic.ini.jinja +111 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/env.py.jinja +91 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/script.py.mako +25 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/versions/001_initial_auth.py.jinja +51 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/__init__.py.jinja +5 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/__init__.py.jinja +6 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai.py.jinja +700 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai_rendering.py +360 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/auth.py.jinja +253 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/health.py.jinja +419 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/load_test.py.jinja +656 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/main.py.jinja +65 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/marko_terminal_renderer.py +489 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/tasks.py.jinja +328 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/__init__.py +8 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/router.py +329 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/router.py +64 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/deps.py +58 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/health.py.jinja +163 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/models.py.jinja +280 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/routing.py.jinja +32 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/scheduler.py.jinja +121 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/worker.py.jinja +478 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/hooks.py +144 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/main.py +31 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/component_health.py.jinja +418 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/database_init.py.jinja +83 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/__init__.py +5 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/__init__.py +27 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/table.py +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/text.py +142 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/__init__.py.jinja +47 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/base_card.py +256 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/database_card.py +420 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/services_card.py +233 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/main.py.jinja +653 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/theme.py +48 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/main.py.jinja +156 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/CLAUDE.md.jinja +213 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/__init__.py +6 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/constants.py.jinja +30 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/pools.py +97 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/load_test.py +55 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/media.py +49 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/system.py +44 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/registry.py +139 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/__init__.py +120 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/load_tasks.py +507 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/simple_system_tasks.py +33 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/system_tasks.py +281 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/config.py.jinja +178 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/constants.py +58 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/db.py.jinja +176 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/log.py +92 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/security.py +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/scheduler.py.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/webserver.py +39 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/main.py +61 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/user.py +44 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/py.typed +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/__init__.py +8 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/config.py +130 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/conversation.py +213 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/health.py +96 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/models.py +229 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/providers.py.jinja +370 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/service.py +387 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/auth_service.py +40 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/health.py +162 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/user_service.py +82 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/middleware_inspector.py.jinja +223 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/models.py.jinja +70 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/route_inspector.py.jinja +155 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test.py +678 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test_models.py +265 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/__init__.py.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/models.py.jinja +119 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/scheduled_task_manager.py.jinja +273 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/task_monitor.py.jinja +189 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/__init__.py +15 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/models.py +26 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/__init__.py +52 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/alerts.py +94 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/backup.py.jinja +119 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/health.py.jinja +1333 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/models.py +243 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/ui.py +52 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57223!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57224!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57225!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57533!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57534!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57538!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57897!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57898!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57904!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58315!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58316!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58324!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58837!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58838!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58849!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/.env.example.jinja +64 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/README.md.jinja +172 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/docker-compose.yml.jinja +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/mkdocs.yml.jinja +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/pyproject.toml.jinja +120 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docker-compose.yml.jinja +200 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/api.md.jinja +191 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md.jinja +621 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/development.md.jinja +215 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/health.md.jinja +240 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/mkdocs.yml.jinja +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/pyproject.toml.jinja +131 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh +87 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh.jinja +93 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/gen_docs.py +16 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_auth_endpoints.py.jinja +307 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_health_endpoints.py.jinja +262 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_scheduler_endpoints.py.jinja +214 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_worker_endpoints.py.jinja +165 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_ai_rendering.py +427 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_conversation_memory.py +465 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/components/test_scheduler.py +43 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/conftest.py.jinja +195 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/conftest.py +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_health.py +157 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_models.py +164 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_service.py +198 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_auth_integration.py.jinja +528 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_component_integration.py.jinja +387 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_conversation_persistence.py +342 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_health_logic.py.jinja +663 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_models.py +619 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_service.py +603 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_middleware_inspector.py.jinja +248 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_scheduled_task_manager.py.jinja +292 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_system_service.py +98 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_worker_health_registration.py.jinja +257 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/test_core.py +49 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/uv.lock +1673 -0
- aegis_stack-0.2.0rc2.dist-info/METADATA +165 -0
- aegis_stack-0.2.0rc2.dist-info/RECORD +392 -0
- aegis_stack-0.2.0rc2.dist-info/WHEEL +4 -0
- aegis_stack-0.2.0rc2.dist-info/entry_points.txt +3 -0
- aegis_stack-0.2.0rc2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component name parsing utilities for Aegis Stack.
|
|
3
|
+
|
|
4
|
+
This module provides centralized utilities for parsing component names with engine
|
|
5
|
+
information (e.g., 'database[sqlite]') to eliminate code duplication and improve
|
|
6
|
+
robustness throughout the codebase.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_component_name(component: str) -> tuple[str, str | None]:
|
|
13
|
+
"""
|
|
14
|
+
Parse a component name with optional engine information.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
component: Component name like 'database[sqlite]' or 'scheduler'
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Tuple of (base_name, engine) where engine is None if not specified
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
parse_component_name('database[sqlite]') -> ('database', 'sqlite')
|
|
24
|
+
parse_component_name('scheduler') -> ('scheduler', None)
|
|
25
|
+
parse_component_name('database[]') -> ('database', None)
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If component name format is invalid
|
|
29
|
+
"""
|
|
30
|
+
if not component or not isinstance(component, str):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Component name must be a non-empty string, got: {component!r}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
component = component.strip()
|
|
36
|
+
if not component:
|
|
37
|
+
raise ValueError("Component name cannot be empty or whitespace")
|
|
38
|
+
|
|
39
|
+
# Use regex for robust parsing
|
|
40
|
+
pattern = r"^([a-zA-Z][a-zA-Z0-9_-]*?)(?:\[([a-zA-Z0-9_-]*)\])?$"
|
|
41
|
+
match = re.match(pattern, component)
|
|
42
|
+
|
|
43
|
+
if not match:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Invalid component name format: '{component}'. "
|
|
46
|
+
"Expected format: 'name' or 'name[engine]'"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
base_name, engine = match.groups()
|
|
50
|
+
|
|
51
|
+
# Handle empty engine brackets
|
|
52
|
+
if engine == "":
|
|
53
|
+
engine = None
|
|
54
|
+
|
|
55
|
+
return base_name, engine
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def extract_base_component_name(component: str) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Extract the base component name without engine information.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
component: Component name like 'database[sqlite]' or 'scheduler'
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Base component name
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
extract_base_component_name('database[sqlite]') -> 'database'
|
|
70
|
+
extract_base_component_name('scheduler') -> 'scheduler'
|
|
71
|
+
"""
|
|
72
|
+
base_name, _ = parse_component_name(component)
|
|
73
|
+
return base_name
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def extract_engine_info(component: str) -> str | None:
|
|
77
|
+
"""
|
|
78
|
+
Extract engine information from a component name.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
component: Component name like 'database[sqlite]' or 'scheduler'
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Engine name or None if not specified
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
extract_engine_info('database[sqlite]') -> 'sqlite'
|
|
88
|
+
extract_engine_info('scheduler') -> None
|
|
89
|
+
extract_engine_info('database[]') -> None
|
|
90
|
+
"""
|
|
91
|
+
_, engine = parse_component_name(component)
|
|
92
|
+
return engine
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def format_component_with_engine(base: str, engine: str | None) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Format a component name with engine information.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
base: Base component name
|
|
101
|
+
engine: Engine name or None
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Formatted component name
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
format_component_with_engine('database', 'sqlite') -> 'database[sqlite]'
|
|
108
|
+
format_component_with_engine('scheduler', None) -> 'scheduler'
|
|
109
|
+
"""
|
|
110
|
+
if not base or not isinstance(base, str):
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Base component name must be a non-empty string, got: {base!r}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
base = base.strip()
|
|
116
|
+
if not base:
|
|
117
|
+
raise ValueError("Base component name cannot be empty or whitespace")
|
|
118
|
+
|
|
119
|
+
if engine is not None:
|
|
120
|
+
if not isinstance(engine, str) or not engine.strip():
|
|
121
|
+
raise ValueError(f"Engine must be a non-empty string, got: {engine!r}")
|
|
122
|
+
return f"{base}[{engine.strip()}]"
|
|
123
|
+
|
|
124
|
+
return base
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def clean_component_names(components: list[str]) -> list[str]:
|
|
128
|
+
"""
|
|
129
|
+
Extract base component names from a list, removing engine information.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
components: List of component names (some may have engine info)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of base component names
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
clean_component_names(['redis', 'database[sqlite]']) -> ['redis', 'database']
|
|
139
|
+
"""
|
|
140
|
+
return [extract_base_component_name(comp) for comp in components]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def restore_engine_info(
|
|
144
|
+
resolved_components: list[str], original_components: list[str]
|
|
145
|
+
) -> list[str]:
|
|
146
|
+
"""
|
|
147
|
+
Restore engine information from original components to resolved components.
|
|
148
|
+
|
|
149
|
+
This function matches resolved base component names with their original
|
|
150
|
+
engine-specific versions and restores the engine information.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
resolved_components: List of resolved base component names
|
|
154
|
+
original_components: List of original components with engine info
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List with engine information restored where applicable
|
|
158
|
+
|
|
159
|
+
Examples:
|
|
160
|
+
resolved = ['redis', 'database', 'scheduler']
|
|
161
|
+
original = ['database[sqlite]', 'scheduler']
|
|
162
|
+
restore_engine_info(resolved, original)
|
|
163
|
+
-> ['redis', 'database[sqlite]', 'scheduler']
|
|
164
|
+
"""
|
|
165
|
+
# Build mapping of base names to original components with engine info
|
|
166
|
+
engine_mapping: dict[str, str] = {}
|
|
167
|
+
for orig in original_components:
|
|
168
|
+
base_name = extract_base_component_name(orig)
|
|
169
|
+
if base_name != orig: # Has engine info
|
|
170
|
+
engine_mapping[base_name] = orig
|
|
171
|
+
|
|
172
|
+
# Restore engine info where available
|
|
173
|
+
result = []
|
|
174
|
+
for comp in resolved_components:
|
|
175
|
+
if comp in engine_mapping:
|
|
176
|
+
result.append(engine_mapping[comp])
|
|
177
|
+
else:
|
|
178
|
+
result.append(comp)
|
|
179
|
+
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def find_components_with_engine(components: list[str], base_name: str) -> list[str]:
|
|
184
|
+
"""
|
|
185
|
+
Find all components that match a base name (with or without engine info).
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
components: List of component names to search
|
|
189
|
+
base_name: Base component name to match
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of matching components
|
|
193
|
+
|
|
194
|
+
Examples:
|
|
195
|
+
components = ['redis', 'database[sqlite]', 'database[postgres]', 'scheduler']
|
|
196
|
+
find_components_with_engine(components, 'database')
|
|
197
|
+
-> ['database[sqlite]', 'database[postgres]']
|
|
198
|
+
"""
|
|
199
|
+
matches = []
|
|
200
|
+
for comp in components:
|
|
201
|
+
if extract_base_component_name(comp) == base_name:
|
|
202
|
+
matches.append(comp)
|
|
203
|
+
return matches
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def has_engine_info(component: str) -> bool:
|
|
207
|
+
"""
|
|
208
|
+
Check if a component name includes engine information.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
component: Component name to check
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
True if component has engine info, False otherwise
|
|
215
|
+
|
|
216
|
+
Examples:
|
|
217
|
+
has_engine_info('database[sqlite]') -> True
|
|
218
|
+
has_engine_info('scheduler') -> False
|
|
219
|
+
"""
|
|
220
|
+
return extract_engine_info(component) is not None
|
aegis/core/components.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
class SchedulerBackend(str, Enum):
|
|
20
|
+
"""Scheduler backend options for task persistence."""
|
|
21
|
+
|
|
22
|
+
MEMORY = "memory" # In-memory (no persistence, default)
|
|
23
|
+
SQLITE = "sqlite" # SQLite database (requires database component)
|
|
24
|
+
POSTGRES = "postgres" # PostgreSQL (future support)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Core components that are always included in every project
|
|
28
|
+
CORE_COMPONENTS = ["backend", "frontend"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ComponentSpec:
|
|
33
|
+
"""Specification for a single component."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
type: ComponentType
|
|
37
|
+
description: str
|
|
38
|
+
requires: list[str] | None = None # Hard dependencies
|
|
39
|
+
recommends: list[str] | None = None # Soft dependencies
|
|
40
|
+
conflicts: list[str] | None = None # Mutual exclusions
|
|
41
|
+
docker_services: list[str] | None = None
|
|
42
|
+
pyproject_deps: list[str] | None = None
|
|
43
|
+
template_files: list[str] | None = None
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
"""Ensure all list fields are initialized."""
|
|
47
|
+
if self.requires is None:
|
|
48
|
+
self.requires = []
|
|
49
|
+
if self.recommends is None:
|
|
50
|
+
self.recommends = []
|
|
51
|
+
if self.conflicts is None:
|
|
52
|
+
self.conflicts = []
|
|
53
|
+
if self.docker_services is None:
|
|
54
|
+
self.docker_services = []
|
|
55
|
+
if self.pyproject_deps is None:
|
|
56
|
+
self.pyproject_deps = []
|
|
57
|
+
if self.template_files is None:
|
|
58
|
+
self.template_files = []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Component registry - single source of truth
|
|
62
|
+
COMPONENTS: dict[str, ComponentSpec] = {
|
|
63
|
+
"backend": ComponentSpec(
|
|
64
|
+
name="backend",
|
|
65
|
+
type=ComponentType.CORE,
|
|
66
|
+
description="FastAPI backend server",
|
|
67
|
+
pyproject_deps=["fastapi==0.116.1", "uvicorn==0.35.0"],
|
|
68
|
+
template_files=["app/components/backend/"],
|
|
69
|
+
),
|
|
70
|
+
"frontend": ComponentSpec(
|
|
71
|
+
name="frontend",
|
|
72
|
+
type=ComponentType.CORE,
|
|
73
|
+
description="Flet frontend interface",
|
|
74
|
+
pyproject_deps=["flet==0.28.3"],
|
|
75
|
+
template_files=["app/components/frontend/"],
|
|
76
|
+
),
|
|
77
|
+
"redis": ComponentSpec(
|
|
78
|
+
name="redis",
|
|
79
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
80
|
+
description="Redis cache and message broker",
|
|
81
|
+
docker_services=["redis"],
|
|
82
|
+
pyproject_deps=["redis==5.0.8"],
|
|
83
|
+
),
|
|
84
|
+
"worker": ComponentSpec(
|
|
85
|
+
name="worker",
|
|
86
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
87
|
+
description="Background task processing infrastructure with arq",
|
|
88
|
+
requires=["redis"], # Hard dependency
|
|
89
|
+
pyproject_deps=["arq==0.25.0"],
|
|
90
|
+
docker_services=["worker-system", "worker-load-test"],
|
|
91
|
+
template_files=["app/components/worker/"],
|
|
92
|
+
),
|
|
93
|
+
"scheduler": ComponentSpec(
|
|
94
|
+
name="scheduler",
|
|
95
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
96
|
+
description="Scheduled task execution infrastructure",
|
|
97
|
+
pyproject_deps=["apscheduler==3.10.4"],
|
|
98
|
+
docker_services=["scheduler"],
|
|
99
|
+
template_files=["app/components/scheduler.py", "app/entrypoints/scheduler.py"],
|
|
100
|
+
),
|
|
101
|
+
"database": ComponentSpec(
|
|
102
|
+
name="database",
|
|
103
|
+
type=ComponentType.INFRASTRUCTURE,
|
|
104
|
+
description="SQLite database with SQLModel ORM",
|
|
105
|
+
pyproject_deps=["sqlmodel>=0.0.14", "sqlalchemy>=2.0.0", "aiosqlite>=0.19.0"],
|
|
106
|
+
template_files=["app/core/db.py"],
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_component(name: str) -> ComponentSpec:
|
|
112
|
+
"""Get component specification by name."""
|
|
113
|
+
if name not in COMPONENTS:
|
|
114
|
+
raise ValueError(f"Unknown component: {name}")
|
|
115
|
+
return COMPONENTS[name]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_components_by_type(component_type: ComponentType) -> dict[str, ComponentSpec]:
|
|
119
|
+
"""Get all components of a specific type."""
|
|
120
|
+
return {
|
|
121
|
+
name: spec for name, spec in COMPONENTS.items() if spec.type == component_type
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def list_available_components() -> list[str]:
|
|
126
|
+
"""Get list of all available component names."""
|
|
127
|
+
return list(COMPONENTS.keys())
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copier template engine integration.
|
|
3
|
+
|
|
4
|
+
This module provides Copier template generation functionality alongside
|
|
5
|
+
the existing Cookiecutter engine. It's designed to maintain feature parity
|
|
6
|
+
during the migration period.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
from copier import run_copy, run_update
|
|
14
|
+
|
|
15
|
+
from .post_gen_tasks import cleanup_components, run_post_generation_tasks
|
|
16
|
+
from .template_generator import TemplateGenerator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_with_copier(template_gen: TemplateGenerator, output_dir: Path) -> Path:
|
|
20
|
+
"""
|
|
21
|
+
Generate project using Copier template engine.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
template_gen: Template generator with project configuration
|
|
25
|
+
output_dir: Directory to create the project in
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Path to the generated project
|
|
29
|
+
|
|
30
|
+
Note:
|
|
31
|
+
This function uses the Copier template which is currently incomplete
|
|
32
|
+
(missing conditional _exclude patterns). Projects will include all
|
|
33
|
+
components regardless of selection until template is fixed.
|
|
34
|
+
"""
|
|
35
|
+
import subprocess
|
|
36
|
+
|
|
37
|
+
# Get cookiecutter context from template generator
|
|
38
|
+
cookiecutter_context = template_gen.get_template_context()
|
|
39
|
+
|
|
40
|
+
# Convert cookiecutter context to Copier data format
|
|
41
|
+
# Copier uses boolean values instead of "yes"/"no" strings
|
|
42
|
+
copier_data = {
|
|
43
|
+
"project_name": cookiecutter_context["project_name"],
|
|
44
|
+
"project_slug": cookiecutter_context["project_slug"],
|
|
45
|
+
"project_description": cookiecutter_context.get(
|
|
46
|
+
"project_description",
|
|
47
|
+
"A production-ready async Python application built with Aegis Stack",
|
|
48
|
+
),
|
|
49
|
+
"author_name": cookiecutter_context.get("author_name", "Your Name"),
|
|
50
|
+
"author_email": cookiecutter_context.get(
|
|
51
|
+
"author_email", "your.email@example.com"
|
|
52
|
+
),
|
|
53
|
+
"github_username": cookiecutter_context.get("github_username", "your-username"),
|
|
54
|
+
"version": cookiecutter_context.get("version", "0.1.0"),
|
|
55
|
+
"python_version": cookiecutter_context.get("python_version", "3.11"),
|
|
56
|
+
# Convert yes/no strings to booleans
|
|
57
|
+
"include_scheduler": cookiecutter_context["include_scheduler"] == "yes",
|
|
58
|
+
"scheduler_backend": cookiecutter_context["scheduler_backend"],
|
|
59
|
+
"scheduler_with_persistence": cookiecutter_context["scheduler_with_persistence"]
|
|
60
|
+
== "yes",
|
|
61
|
+
"include_worker": cookiecutter_context["include_worker"] == "yes",
|
|
62
|
+
"include_redis": cookiecutter_context["include_redis"] == "yes",
|
|
63
|
+
"include_database": cookiecutter_context["include_database"] == "yes",
|
|
64
|
+
"include_cache": False, # Default to no
|
|
65
|
+
"include_auth": cookiecutter_context.get("include_auth", "no") == "yes",
|
|
66
|
+
"include_ai": cookiecutter_context.get("include_ai", "no") == "yes",
|
|
67
|
+
"ai_providers": cookiecutter_context.get("ai_providers", "openai"),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Get copier template path - point directly at template directory
|
|
71
|
+
# This prevents copying aegis-stack repo files into generated projects
|
|
72
|
+
# The repo root path is set later in .copier-answers.yml for git-aware updates
|
|
73
|
+
template_path = Path(__file__).parent.parent / "templates" / "copier-aegis-project"
|
|
74
|
+
|
|
75
|
+
# Generate project - Copier creates the project_slug directory automatically
|
|
76
|
+
# NOTE: _tasks removed from copier.yml - we run them ourselves below
|
|
77
|
+
run_copy(
|
|
78
|
+
str(
|
|
79
|
+
template_path
|
|
80
|
+
), # Use template directory (not repo root) to avoid copying extra files
|
|
81
|
+
output_dir,
|
|
82
|
+
data=copier_data,
|
|
83
|
+
defaults=True, # Use template defaults, overridden by our explicit data
|
|
84
|
+
unsafe=False, # No tasks in copier.yml anymore - we run them ourselves
|
|
85
|
+
vcs_ref=None, # Don't use git for template versioning - prevents git submodule errors in CI
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Copier creates the project in output_dir/project_slug
|
|
89
|
+
project_path = output_dir / cookiecutter_context["project_slug"]
|
|
90
|
+
|
|
91
|
+
# Clean up unwanted component files based on selection
|
|
92
|
+
# This must happen BEFORE post-generation tasks (which run linting on the remaining files)
|
|
93
|
+
cleanup_components(project_path, copier_data)
|
|
94
|
+
|
|
95
|
+
# Run post-generation tasks with explicit working directory control
|
|
96
|
+
# This ensures consistent behavior with Cookiecutter
|
|
97
|
+
include_auth = copier_data.get("include_auth", False)
|
|
98
|
+
run_post_generation_tasks(project_path, include_auth=include_auth)
|
|
99
|
+
|
|
100
|
+
# Initialize git repository for Copier updates
|
|
101
|
+
# Copier requires a git-tracked project to perform updates
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Configure git user in case CI environment doesn't have it set
|
|
105
|
+
# This is needed for commits to work in CI
|
|
106
|
+
subprocess.run(
|
|
107
|
+
["git", "config", "user.name", "Aegis Stack"],
|
|
108
|
+
cwd=project_path,
|
|
109
|
+
capture_output=True,
|
|
110
|
+
)
|
|
111
|
+
subprocess.run(
|
|
112
|
+
["git", "config", "user.email", "noreply@aegis-stack.dev"],
|
|
113
|
+
cwd=project_path,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
subprocess.run(
|
|
118
|
+
["git", "init"],
|
|
119
|
+
cwd=project_path,
|
|
120
|
+
check=True,
|
|
121
|
+
capture_output=True,
|
|
122
|
+
)
|
|
123
|
+
subprocess.run(
|
|
124
|
+
["git", "add", "."],
|
|
125
|
+
cwd=project_path,
|
|
126
|
+
check=True,
|
|
127
|
+
capture_output=True,
|
|
128
|
+
)
|
|
129
|
+
subprocess.run(
|
|
130
|
+
["git", "commit", "-m", "Initial commit from Aegis Stack"],
|
|
131
|
+
cwd=project_path,
|
|
132
|
+
check=True,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
)
|
|
135
|
+
print("✅ Git repository initialized")
|
|
136
|
+
except subprocess.CalledProcessError as e:
|
|
137
|
+
print(f"⚠️ Failed to initialize git repository: {e}")
|
|
138
|
+
print("💡 Run 'git init && git add . && git commit' manually")
|
|
139
|
+
|
|
140
|
+
# CRITICAL: Update .copier-answers.yml for future updates to work
|
|
141
|
+
# We need to:
|
|
142
|
+
# 1. Store git commit hash (_commit) - tells Copier which template version was used
|
|
143
|
+
# 2. Update template path (_src_path) - point to repo root, not subdirectory
|
|
144
|
+
# (repo root has .git so Copier can detect version changes)
|
|
145
|
+
try:
|
|
146
|
+
# Get current commit hash from aegis-stack repo
|
|
147
|
+
template_root = Path(__file__).parent.parent.parent
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
["git", "rev-parse", "HEAD"],
|
|
150
|
+
cwd=template_root,
|
|
151
|
+
capture_output=True,
|
|
152
|
+
text=True,
|
|
153
|
+
check=True,
|
|
154
|
+
)
|
|
155
|
+
commit_hash = result.stdout.strip()
|
|
156
|
+
|
|
157
|
+
# Update .copier-answers.yml with commit hash AND repo root path
|
|
158
|
+
answers_file = project_path / ".copier-answers.yml"
|
|
159
|
+
if answers_file.exists():
|
|
160
|
+
with open(answers_file) as f:
|
|
161
|
+
answers = yaml.safe_load(f)
|
|
162
|
+
|
|
163
|
+
# Update _commit field (was None, now has actual hash)
|
|
164
|
+
answers["_commit"] = commit_hash
|
|
165
|
+
|
|
166
|
+
# Update _src_path to point to repo root (where .git exists)
|
|
167
|
+
# The copier.yml at repo root has _subdirectory setting to find actual template
|
|
168
|
+
answers["_src_path"] = str(template_root)
|
|
169
|
+
|
|
170
|
+
with open(answers_file, "w") as f:
|
|
171
|
+
yaml.safe_dump(answers, f, default_flow_style=False, sort_keys=False)
|
|
172
|
+
|
|
173
|
+
# Commit the updated .copier-answers.yml
|
|
174
|
+
try:
|
|
175
|
+
subprocess.run(
|
|
176
|
+
["git", "add", ".copier-answers.yml"],
|
|
177
|
+
cwd=project_path,
|
|
178
|
+
check=True,
|
|
179
|
+
capture_output=True,
|
|
180
|
+
)
|
|
181
|
+
subprocess.run(
|
|
182
|
+
[
|
|
183
|
+
"git",
|
|
184
|
+
"commit",
|
|
185
|
+
"-m",
|
|
186
|
+
"Update .copier-answers.yml with template version",
|
|
187
|
+
],
|
|
188
|
+
cwd=project_path,
|
|
189
|
+
check=True,
|
|
190
|
+
capture_output=True,
|
|
191
|
+
)
|
|
192
|
+
except subprocess.CalledProcessError:
|
|
193
|
+
# If commit fails (e.g., no changes), that's OK
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
except Exception:
|
|
197
|
+
# If we can't get commit hash, that's OK - updates won't work but
|
|
198
|
+
# project generation succeeded. This can happen in non-git environments.
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
return project_path
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def is_copier_project(project_path: Path) -> bool:
|
|
205
|
+
"""
|
|
206
|
+
Check if a project was generated with Copier.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
project_path: Path to the project directory
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
True if project has .copier-answers.yml file
|
|
213
|
+
"""
|
|
214
|
+
answers_file = project_path / ".copier-answers.yml"
|
|
215
|
+
return answers_file.exists()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def load_copier_answers(project_path: Path) -> dict[str, Any]:
|
|
219
|
+
"""
|
|
220
|
+
Load existing Copier answers from a project.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
project_path: Path to the project directory
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Dictionary of Copier answers
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
FileNotFoundError: If .copier-answers.yml doesn't exist
|
|
230
|
+
yaml.YAMLError: If answers file is corrupted
|
|
231
|
+
"""
|
|
232
|
+
answers_file = project_path / ".copier-answers.yml"
|
|
233
|
+
|
|
234
|
+
if not answers_file.exists():
|
|
235
|
+
raise FileNotFoundError(
|
|
236
|
+
f"No .copier-answers.yml found in {project_path}. "
|
|
237
|
+
"This doesn't appear to be a Copier-generated project."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
with open(answers_file) as f:
|
|
242
|
+
answers = yaml.safe_load(f)
|
|
243
|
+
if answers is None:
|
|
244
|
+
return {}
|
|
245
|
+
return answers
|
|
246
|
+
except yaml.YAMLError as e:
|
|
247
|
+
raise yaml.YAMLError(f"Failed to parse .copier-answers.yml: {e}") from e
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def update_with_copier(
|
|
251
|
+
project_path: Path,
|
|
252
|
+
additional_data: dict[str, Any] | None = None,
|
|
253
|
+
conflict_mode: str = "rej",
|
|
254
|
+
) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Update an existing Copier-generated project with new data.
|
|
257
|
+
|
|
258
|
+
This function uses Copier's update mechanism to add new components
|
|
259
|
+
or update existing project configuration.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
project_path: Path to the existing project directory
|
|
263
|
+
additional_data: New data to merge (e.g., {"include_scheduler": True})
|
|
264
|
+
conflict_mode: How to handle conflicts - "rej" (separate files) or "inline" (markers)
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
FileNotFoundError: If project doesn't have .copier-answers.yml
|
|
268
|
+
Exception: If Copier update fails
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
# Add scheduler component to existing project
|
|
272
|
+
update_with_copier(
|
|
273
|
+
Path("my-project"),
|
|
274
|
+
{"include_scheduler": True, "scheduler_backend": "memory"}
|
|
275
|
+
)
|
|
276
|
+
"""
|
|
277
|
+
# Validate it's a Copier project
|
|
278
|
+
if not is_copier_project(project_path):
|
|
279
|
+
raise FileNotFoundError(
|
|
280
|
+
f"Project at {project_path} was not generated with Copier.\n"
|
|
281
|
+
f"The 'aegis add' command only works with Copier-generated projects.\n"
|
|
282
|
+
f"To add components, regenerate the project with the new components included."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Load existing answers to validate state
|
|
286
|
+
try:
|
|
287
|
+
load_copier_answers(project_path)
|
|
288
|
+
except yaml.YAMLError as e:
|
|
289
|
+
raise Exception(
|
|
290
|
+
f"Failed to read project configuration: {e}\n"
|
|
291
|
+
f"The .copier-answers.yml file may be corrupted."
|
|
292
|
+
) from e
|
|
293
|
+
|
|
294
|
+
# Prepare update data
|
|
295
|
+
update_data = additional_data or {}
|
|
296
|
+
|
|
297
|
+
# Run Copier update
|
|
298
|
+
# NOTE: We do NOT pass src_path - Copier will read it from .copier-answers.yml
|
|
299
|
+
# This is the key to making updates work!
|
|
300
|
+
try:
|
|
301
|
+
run_update(
|
|
302
|
+
dst_path=str(project_path),
|
|
303
|
+
data=update_data,
|
|
304
|
+
defaults=True, # Use existing answers as defaults
|
|
305
|
+
overwrite=True, # Allow overwriting files
|
|
306
|
+
conflict=conflict_mode, # How to handle conflicts
|
|
307
|
+
unsafe=True, # Allow running tasks (uv sync, make fix)
|
|
308
|
+
vcs_ref="HEAD", # Use latest template (no versioning needed yet)
|
|
309
|
+
)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
raise Exception(
|
|
312
|
+
f"Failed to update project: {e}\n"
|
|
313
|
+
f"This may be due to conflicts with manually modified files.\n"
|
|
314
|
+
f"Check for .rej files in the project directory for details."
|
|
315
|
+
) from e
|