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,478 @@
|
|
|
1
|
+
{%- if cookiecutter.include_worker == "yes" %}
|
|
2
|
+
"""Worker task API endpoints."""
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
|
|
9
|
+
from app.components.backend.api.models import (
|
|
10
|
+
LoadTestRequest,
|
|
11
|
+
TaskListResponse,
|
|
12
|
+
TaskRequest,
|
|
13
|
+
TaskResponse,
|
|
14
|
+
TaskResultResponse,
|
|
15
|
+
TaskStatusResponse,
|
|
16
|
+
)
|
|
17
|
+
from app.components.worker.constants import LoadTestTypes
|
|
18
|
+
from app.components.worker.pools import get_queue_pool
|
|
19
|
+
from app.components.worker.tasks import get_task_by_name, list_available_tasks
|
|
20
|
+
from app.core.config import get_default_queue
|
|
21
|
+
from app.core.log import logger
|
|
22
|
+
|
|
23
|
+
router = APIRouter(prefix="/tasks", tags=["worker"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("/", response_model=TaskListResponse)
|
|
27
|
+
async def list_tasks() -> TaskListResponse:
|
|
28
|
+
"""Get list of all available background tasks."""
|
|
29
|
+
available_tasks = list_available_tasks()
|
|
30
|
+
|
|
31
|
+
from app.components.worker.tasks import get_queue_for_task
|
|
32
|
+
from app.core.config import get_available_queues
|
|
33
|
+
|
|
34
|
+
queues: dict[str, list[str]] = {
|
|
35
|
+
queue_type: [] for queue_type in get_available_queues()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for task in available_tasks:
|
|
39
|
+
queue_type = get_queue_for_task(task)
|
|
40
|
+
queues[queue_type].append(task)
|
|
41
|
+
|
|
42
|
+
return TaskListResponse(
|
|
43
|
+
available_tasks=available_tasks,
|
|
44
|
+
total_count=len(available_tasks),
|
|
45
|
+
queues=queues,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.post("/enqueue", response_model=TaskResponse)
|
|
50
|
+
async def enqueue_task(task_request: TaskRequest) -> TaskResponse:
|
|
51
|
+
"""Enqueue a background task for processing."""
|
|
52
|
+
logger.info(
|
|
53
|
+
f"Enqueueing task: {task_request.task_name} (queue: {task_request.queue_type})"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
task_func = get_task_by_name(task_request.task_name)
|
|
57
|
+
if not task_func:
|
|
58
|
+
available_tasks = list_available_tasks()
|
|
59
|
+
raise HTTPException(
|
|
60
|
+
status_code=400,
|
|
61
|
+
detail={
|
|
62
|
+
"error": "invalid_task_name",
|
|
63
|
+
"message": f"Task '{task_request.task_name}' not found",
|
|
64
|
+
"available_tasks": available_tasks,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
from app.core.config import is_valid_queue, get_available_queues
|
|
69
|
+
|
|
70
|
+
if not is_valid_queue(task_request.queue_type):
|
|
71
|
+
available_queues = get_available_queues()
|
|
72
|
+
raise HTTPException(
|
|
73
|
+
status_code=400,
|
|
74
|
+
detail={
|
|
75
|
+
"error": "invalid_queue_type",
|
|
76
|
+
"message": f"Queue type must be one of: {available_queues}",
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
pool, queue_name = await get_queue_pool(task_request.queue_type)
|
|
82
|
+
|
|
83
|
+
job = await pool.enqueue_job(
|
|
84
|
+
task_request.task_name,
|
|
85
|
+
*task_request.args,
|
|
86
|
+
_queue_name=queue_name,
|
|
87
|
+
_defer_by=task_request.delay_seconds,
|
|
88
|
+
**task_request.task_kwargs,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
queued_at = datetime.now()
|
|
92
|
+
estimated_start = None
|
|
93
|
+
if task_request.delay_seconds:
|
|
94
|
+
estimated_start = queued_at + timedelta(seconds=task_request.delay_seconds)
|
|
95
|
+
|
|
96
|
+
await pool.aclose()
|
|
97
|
+
|
|
98
|
+
if job is None:
|
|
99
|
+
raise HTTPException(status_code=500, detail="Failed to enqueue task")
|
|
100
|
+
|
|
101
|
+
logger.info(f"✅ Task enqueued: {job.job_id} ({task_request.task_name})")
|
|
102
|
+
|
|
103
|
+
return TaskResponse(
|
|
104
|
+
task_id=job.job_id,
|
|
105
|
+
task_name=task_request.task_name,
|
|
106
|
+
queue_type=task_request.queue_type,
|
|
107
|
+
queued_at=queued_at,
|
|
108
|
+
estimated_start=estimated_start,
|
|
109
|
+
message=(
|
|
110
|
+
f"Task '{task_request.task_name}' enqueued to "
|
|
111
|
+
f"{task_request.queue_type} queue"
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error(f"Failed to enqueue task {task_request.task_name}: {e}")
|
|
117
|
+
raise HTTPException(
|
|
118
|
+
status_code=500,
|
|
119
|
+
detail={
|
|
120
|
+
"error": "enqueue_failed",
|
|
121
|
+
"message": f"Failed to enqueue task: {str(e)}",
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@router.get("/status/{task_id}", response_model=TaskStatusResponse)
|
|
127
|
+
async def get_task_status(task_id: str) -> TaskStatusResponse:
|
|
128
|
+
"""Get the status of a background task."""
|
|
129
|
+
try:
|
|
130
|
+
pool, _ = await get_queue_pool(get_default_queue())
|
|
131
|
+
|
|
132
|
+
job_key = f"arq:job:{task_id}"
|
|
133
|
+
result_key = f"arq:result:{task_id}"
|
|
134
|
+
|
|
135
|
+
job_exists = await pool.exists(job_key)
|
|
136
|
+
result_exists = await pool.exists(result_key)
|
|
137
|
+
|
|
138
|
+
if not job_exists and not result_exists:
|
|
139
|
+
await pool.aclose()
|
|
140
|
+
raise HTTPException(
|
|
141
|
+
status_code=404,
|
|
142
|
+
detail={
|
|
143
|
+
"error": "task_not_found",
|
|
144
|
+
"message": f"Task {task_id} not found",
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if result_exists:
|
|
149
|
+
result_data = await pool.get(result_key)
|
|
150
|
+
if result_data:
|
|
151
|
+
try:
|
|
152
|
+
import pickle
|
|
153
|
+
|
|
154
|
+
result = pickle.loads(result_data)
|
|
155
|
+
if isinstance(result, dict) and result.get("error"):
|
|
156
|
+
status = "failed"
|
|
157
|
+
error = result.get("error")
|
|
158
|
+
else:
|
|
159
|
+
status = "complete"
|
|
160
|
+
error = None
|
|
161
|
+
except Exception:
|
|
162
|
+
status = "complete"
|
|
163
|
+
error = None
|
|
164
|
+
else:
|
|
165
|
+
status = "complete"
|
|
166
|
+
error = None
|
|
167
|
+
elif job_exists:
|
|
168
|
+
status = "queued"
|
|
169
|
+
error = None
|
|
170
|
+
else:
|
|
171
|
+
status = "unknown"
|
|
172
|
+
error = None
|
|
173
|
+
|
|
174
|
+
await pool.aclose()
|
|
175
|
+
|
|
176
|
+
return TaskStatusResponse(
|
|
177
|
+
task_id=task_id,
|
|
178
|
+
status=status,
|
|
179
|
+
result_available=result_exists,
|
|
180
|
+
error=error,
|
|
181
|
+
enqueue_time=None,
|
|
182
|
+
start_time=None,
|
|
183
|
+
finish_time=None
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error(f"Failed to get task status for {task_id}: {e}")
|
|
188
|
+
raise HTTPException(
|
|
189
|
+
status_code=500, detail={"error": "status_check_failed", "message": str(e)}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@router.get("/result/{task_id}", response_model=TaskResultResponse)
|
|
194
|
+
async def get_task_result(task_id: str) -> TaskResultResponse:
|
|
195
|
+
"""Get the result of a completed background task."""
|
|
196
|
+
try:
|
|
197
|
+
pool, _ = await get_queue_pool(get_default_queue())
|
|
198
|
+
|
|
199
|
+
result_key = f"arq:result:{task_id}"
|
|
200
|
+
result_exists = await pool.exists(result_key)
|
|
201
|
+
|
|
202
|
+
if not result_exists:
|
|
203
|
+
job_key = f"arq:job:{task_id}"
|
|
204
|
+
job_exists = await pool.exists(job_key)
|
|
205
|
+
await pool.aclose()
|
|
206
|
+
|
|
207
|
+
if not job_exists:
|
|
208
|
+
raise HTTPException(
|
|
209
|
+
status_code=404,
|
|
210
|
+
detail={
|
|
211
|
+
"error": "task_not_found",
|
|
212
|
+
"message": f"Task {task_id} not found",
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
raise HTTPException(
|
|
217
|
+
status_code=400,
|
|
218
|
+
detail={
|
|
219
|
+
"error": "task_not_completed",
|
|
220
|
+
"message": f"Task {task_id} has not completed yet",
|
|
221
|
+
"current_status": "queued or in_progress",
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
result_data = await pool.get(result_key)
|
|
226
|
+
await pool.aclose()
|
|
227
|
+
|
|
228
|
+
if not result_data:
|
|
229
|
+
raise HTTPException(
|
|
230
|
+
status_code=500,
|
|
231
|
+
detail={
|
|
232
|
+
"error": "result_data_missing",
|
|
233
|
+
"message": "Result data is missing",
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
import pickle
|
|
239
|
+
|
|
240
|
+
result = pickle.loads(result_data)
|
|
241
|
+
|
|
242
|
+
if isinstance(result, Exception):
|
|
243
|
+
result_data = {
|
|
244
|
+
"error_type": type(result).__name__,
|
|
245
|
+
"error_message": str(result),
|
|
246
|
+
"task_failed": True,
|
|
247
|
+
}
|
|
248
|
+
task_status = "failed"
|
|
249
|
+
else:
|
|
250
|
+
try:
|
|
251
|
+
import json
|
|
252
|
+
|
|
253
|
+
json.dumps(result)
|
|
254
|
+
result_data = result
|
|
255
|
+
task_status = "completed"
|
|
256
|
+
except (TypeError, ValueError):
|
|
257
|
+
result_data = {
|
|
258
|
+
"result_type": type(result).__name__,
|
|
259
|
+
"result_str": str(result),
|
|
260
|
+
"note": "Result was not JSON-serializable, converted to string",
|
|
261
|
+
}
|
|
262
|
+
task_status = "completed"
|
|
263
|
+
|
|
264
|
+
return TaskResultResponse(
|
|
265
|
+
task_id=task_id,
|
|
266
|
+
status=task_status,
|
|
267
|
+
result=result_data,
|
|
268
|
+
enqueue_time=None,
|
|
269
|
+
start_time=None,
|
|
270
|
+
finish_time=None
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
raise HTTPException(
|
|
275
|
+
status_code=500,
|
|
276
|
+
detail={
|
|
277
|
+
"error": "result_deserialization_failed",
|
|
278
|
+
"message": f"Failed to deserialize result: {str(e)}",
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
except HTTPException:
|
|
283
|
+
raise
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.error(f"Failed to get task result for {task_id}: {e}")
|
|
286
|
+
raise HTTPException(
|
|
287
|
+
status_code=500, detail={"error": "result_fetch_failed", "message": str(e)}
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@router.post("/load-test", response_model=TaskResponse)
|
|
292
|
+
async def start_load_test(load_test_config: LoadTestRequest) -> TaskResponse:
|
|
293
|
+
"""Start a comprehensive load test that measures queue throughput."""
|
|
294
|
+
from app.components.worker.constants import TaskNames
|
|
295
|
+
from app.services.load_test import LoadTestConfiguration, LoadTestService
|
|
296
|
+
|
|
297
|
+
logger.info(
|
|
298
|
+
f"🚀 Starting load test: {load_test_config.num_tasks} "
|
|
299
|
+
f"{load_test_config.task_type} tasks"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
valid_types = [
|
|
303
|
+
LoadTestTypes.CPU_INTENSIVE,
|
|
304
|
+
LoadTestTypes.IO_SIMULATION,
|
|
305
|
+
LoadTestTypes.MEMORY_OPERATIONS,
|
|
306
|
+
LoadTestTypes.FAILURE_TESTING,
|
|
307
|
+
]
|
|
308
|
+
if load_test_config.task_type not in valid_types:
|
|
309
|
+
raise HTTPException(
|
|
310
|
+
status_code=400,
|
|
311
|
+
detail={
|
|
312
|
+
"error": "invalid_task_type",
|
|
313
|
+
"message": f"Invalid task type: {load_test_config.task_type}",
|
|
314
|
+
"valid_types": valid_types,
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
config = LoadTestConfiguration(
|
|
319
|
+
num_tasks=load_test_config.num_tasks,
|
|
320
|
+
task_type=load_test_config.task_type,
|
|
321
|
+
batch_size=load_test_config.batch_size,
|
|
322
|
+
delay_ms=load_test_config.delay_ms,
|
|
323
|
+
target_queue=load_test_config.target_queue,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
task_id = await LoadTestService.enqueue_load_test(config)
|
|
328
|
+
|
|
329
|
+
return TaskResponse(
|
|
330
|
+
task_id=task_id,
|
|
331
|
+
task_name=TaskNames.LOAD_TEST_ORCHESTRATOR,
|
|
332
|
+
queue_type=load_test_config.target_queue,
|
|
333
|
+
queued_at=datetime.now(),
|
|
334
|
+
estimated_start=None,
|
|
335
|
+
message=(
|
|
336
|
+
f"Load test '{load_test_config.task_type}' enqueued: "
|
|
337
|
+
f"{load_test_config.num_tasks} tasks to "
|
|
338
|
+
f"{load_test_config.target_queue} queue"
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error(f"Failed to enqueue load test: {e}")
|
|
344
|
+
raise HTTPException(
|
|
345
|
+
status_code=500,
|
|
346
|
+
detail={
|
|
347
|
+
"error": "load_test_failed",
|
|
348
|
+
"message": f"Failed to start load test: {str(e)}",
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@router.post("/examples/load-test-small", response_model=TaskResponse)
|
|
354
|
+
async def enqueue_small_load_test() -> TaskResponse:
|
|
355
|
+
"""Example: Small load test with 50 CPU tasks."""
|
|
356
|
+
load_test_config = LoadTestRequest(
|
|
357
|
+
num_tasks=50,
|
|
358
|
+
task_type=LoadTestTypes.CPU_INTENSIVE,
|
|
359
|
+
batch_size=10,
|
|
360
|
+
delay_ms=0,
|
|
361
|
+
target_queue=get_default_queue(),
|
|
362
|
+
)
|
|
363
|
+
return await start_load_test(load_test_config)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@router.post("/examples/load-test-medium", response_model=TaskResponse)
|
|
367
|
+
async def enqueue_medium_load_test() -> TaskResponse:
|
|
368
|
+
"""Example: Medium load test with 200 I/O tasks."""
|
|
369
|
+
load_test_config = LoadTestRequest(
|
|
370
|
+
num_tasks=200,
|
|
371
|
+
task_type=LoadTestTypes.IO_SIMULATION,
|
|
372
|
+
batch_size=20,
|
|
373
|
+
delay_ms=50,
|
|
374
|
+
target_queue=get_default_queue(),
|
|
375
|
+
)
|
|
376
|
+
return await start_load_test(load_test_config)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@router.post("/examples/load-test-large", response_model=TaskResponse)
|
|
380
|
+
async def enqueue_large_load_test() -> TaskResponse:
|
|
381
|
+
"""Example: Large load test with 1000 memory tasks."""
|
|
382
|
+
load_test_config = LoadTestRequest(
|
|
383
|
+
num_tasks=1000,
|
|
384
|
+
task_type=LoadTestTypes.MEMORY_OPERATIONS,
|
|
385
|
+
batch_size=50,
|
|
386
|
+
delay_ms=0,
|
|
387
|
+
target_queue=get_default_queue(),
|
|
388
|
+
)
|
|
389
|
+
return await start_load_test(load_test_config)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@router.get("/load-test-result/{task_id}")
|
|
393
|
+
async def get_load_test_result(
|
|
394
|
+
task_id: str, target_queue: str | None = None
|
|
395
|
+
) -> dict[str, Any]:
|
|
396
|
+
"""Get enhanced load test results with analysis and verification."""
|
|
397
|
+
from app.services.load_test import LoadTestService
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
result = await LoadTestService.get_load_test_result(task_id, target_queue)
|
|
401
|
+
|
|
402
|
+
if not result:
|
|
403
|
+
raise HTTPException(
|
|
404
|
+
status_code=404,
|
|
405
|
+
detail={
|
|
406
|
+
"error": "load_test_not_found",
|
|
407
|
+
"message": f"No load test results found for task {task_id}",
|
|
408
|
+
"task_id": task_id,
|
|
409
|
+
"target_queue": target_queue,
|
|
410
|
+
},
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return result
|
|
414
|
+
|
|
415
|
+
except HTTPException:
|
|
416
|
+
raise
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logger.error(f"Failed to get load test result for {task_id}: {e}")
|
|
419
|
+
raise HTTPException(
|
|
420
|
+
status_code=500,
|
|
421
|
+
detail={
|
|
422
|
+
"error": "result_retrieval_failed",
|
|
423
|
+
"message": f"Failed to retrieve load test results: {str(e)}",
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@router.get("/load-test-types")
|
|
429
|
+
async def get_load_test_types() -> dict[str, Any]:
|
|
430
|
+
"""Get information about available load test types."""
|
|
431
|
+
from app.components.worker.constants import LoadTestTypes
|
|
432
|
+
from app.services.load_test import LoadTestService
|
|
433
|
+
|
|
434
|
+
test_types = {}
|
|
435
|
+
|
|
436
|
+
all_types = [
|
|
437
|
+
LoadTestTypes.CPU_INTENSIVE,
|
|
438
|
+
LoadTestTypes.IO_SIMULATION,
|
|
439
|
+
LoadTestTypes.MEMORY_OPERATIONS,
|
|
440
|
+
LoadTestTypes.FAILURE_TESTING,
|
|
441
|
+
]
|
|
442
|
+
for test_type in all_types:
|
|
443
|
+
test_types[test_type] = LoadTestService.get_test_type_info(test_type)
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
"available_test_types": test_types,
|
|
447
|
+
"usage_examples": {
|
|
448
|
+
"quick_cpu_test": {
|
|
449
|
+
"description": "Quick CPU test with 50 tasks",
|
|
450
|
+
"parameters": {
|
|
451
|
+
"num_tasks": 50,
|
|
452
|
+
"task_type": "cpu_intensive",
|
|
453
|
+
"batch_size": 10,
|
|
454
|
+
"target_queue": "load_test",
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
"io_stress_test": {
|
|
458
|
+
"description": "I/O stress test with concurrent operations",
|
|
459
|
+
"parameters": {
|
|
460
|
+
"num_tasks": 200,
|
|
461
|
+
"task_type": "io_simulation",
|
|
462
|
+
"batch_size": 20,
|
|
463
|
+
"delay_ms": 50,
|
|
464
|
+
"target_queue": "load_test",
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
"memory_load_test": {
|
|
468
|
+
"description": "Memory allocation test with GC pressure",
|
|
469
|
+
"parameters": {
|
|
470
|
+
"num_tasks": 500,
|
|
471
|
+
"task_type": "memory_operations",
|
|
472
|
+
"batch_size": 25,
|
|
473
|
+
"target_queue": "media",
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
{%- endif %}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# app/components/backend/hooks.py
|
|
2
|
+
"""
|
|
3
|
+
Backend-specific hook management system for drop-in extensibility.
|
|
4
|
+
|
|
5
|
+
This system automatically discovers and registers:
|
|
6
|
+
- Middleware from app/components/backend/middleware/
|
|
7
|
+
- Startup hooks from app/components/backend/startup/
|
|
8
|
+
- Shutdown hooks from app/components/backend/shutdown/
|
|
9
|
+
|
|
10
|
+
Just drop files in the appropriate folders - no central registration required.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import importlib
|
|
14
|
+
import inspect
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from app.core.log import logger
|
|
20
|
+
from fastapi import FastAPI
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BackendHooks:
|
|
24
|
+
"""Backend-specific hook management system."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self.startup_hooks: list[Callable[[], Any]] = []
|
|
28
|
+
self.shutdown_hooks: list[Callable[[], Any]] = []
|
|
29
|
+
|
|
30
|
+
def discover_and_register_middleware(self, app: FastAPI) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Auto-discover and register middleware. This must be called
|
|
33
|
+
before the application starts.
|
|
34
|
+
"""
|
|
35
|
+
middleware_dir = Path(__file__).parent / "middleware"
|
|
36
|
+
if not middleware_dir.exists():
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
for middleware_file in middleware_dir.glob("*.py"):
|
|
40
|
+
if middleware_file.name.startswith("_"):
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
module_name = f"app.components.backend.middleware.{middleware_file.stem}"
|
|
44
|
+
try:
|
|
45
|
+
module = importlib.import_module(module_name)
|
|
46
|
+
if hasattr(module, "register_middleware"):
|
|
47
|
+
logger.info(f"Registering middleware from {module_name}")
|
|
48
|
+
# Middleware registration is synchronous
|
|
49
|
+
module.register_middleware(app)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(f"Failed to load middleware {module_name}: {e}")
|
|
52
|
+
|
|
53
|
+
async def discover_lifespan_hooks(self) -> None:
|
|
54
|
+
"""Discover startup and shutdown hooks for the lifespan event."""
|
|
55
|
+
await self._discover_startup_hooks()
|
|
56
|
+
await self._discover_shutdown_hooks()
|
|
57
|
+
|
|
58
|
+
async def _discover_startup_hooks(self) -> None:
|
|
59
|
+
"""Auto-discover startup hooks from app/components/backend/startup/."""
|
|
60
|
+
startup_dir = Path(__file__).parent / "startup"
|
|
61
|
+
if not startup_dir.exists():
|
|
62
|
+
logger.info("No backend startup directory found")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
for startup_file in startup_dir.glob("*.py"):
|
|
66
|
+
if startup_file.name.startswith("_"):
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
module_name = f"app.components.backend.startup.{startup_file.stem}"
|
|
70
|
+
try:
|
|
71
|
+
module = importlib.import_module(module_name)
|
|
72
|
+
|
|
73
|
+
# Look for startup_hook function
|
|
74
|
+
if hasattr(module, "startup_hook"):
|
|
75
|
+
# Prevent duplicate registration
|
|
76
|
+
if module.startup_hook not in self.startup_hooks:
|
|
77
|
+
logger.info(f"Registered startup hook from {module_name}")
|
|
78
|
+
self.startup_hooks.append(module.startup_hook)
|
|
79
|
+
else:
|
|
80
|
+
logger.debug(
|
|
81
|
+
f"Startup hook from {module_name} already registered"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Failed to load startup hook {module_name}: {e}")
|
|
86
|
+
|
|
87
|
+
async def _discover_shutdown_hooks(self) -> None:
|
|
88
|
+
"""Auto-discover shutdown hooks from app/components/backend/shutdown/."""
|
|
89
|
+
shutdown_dir = Path(__file__).parent / "shutdown"
|
|
90
|
+
if not shutdown_dir.exists():
|
|
91
|
+
logger.info("No backend shutdown directory found")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
for shutdown_file in shutdown_dir.glob("*.py"):
|
|
95
|
+
if shutdown_file.name.startswith("_"):
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
module_name = f"app.components.backend.shutdown.{shutdown_file.stem}"
|
|
99
|
+
try:
|
|
100
|
+
module = importlib.import_module(module_name)
|
|
101
|
+
|
|
102
|
+
# Look for shutdown_hook function
|
|
103
|
+
if hasattr(module, "shutdown_hook"):
|
|
104
|
+
# Prevent duplicate registration
|
|
105
|
+
if module.shutdown_hook not in self.shutdown_hooks:
|
|
106
|
+
logger.info(f"Registered shutdown hook from {module_name}")
|
|
107
|
+
self.shutdown_hooks.append(module.shutdown_hook)
|
|
108
|
+
else:
|
|
109
|
+
logger.debug(
|
|
110
|
+
f"Shutdown hook from {module_name} already registered"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Failed to load shutdown hook {module_name}: {e}")
|
|
115
|
+
|
|
116
|
+
async def execute_startup_hooks(self) -> None:
|
|
117
|
+
"""Execute all discovered startup hooks."""
|
|
118
|
+
logger.info(f"Executing {len(self.startup_hooks)} backend startup hooks")
|
|
119
|
+
for hook in self.startup_hooks:
|
|
120
|
+
try:
|
|
121
|
+
if inspect.iscoroutinefunction(hook):
|
|
122
|
+
await hook()
|
|
123
|
+
else:
|
|
124
|
+
hook()
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(f"Startup hook failed: {e}")
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
async def execute_shutdown_hooks(self) -> None:
|
|
130
|
+
"""Execute all discovered shutdown hooks in reverse order."""
|
|
131
|
+
logger.info(f"Executing {len(self.shutdown_hooks)} backend shutdown hooks")
|
|
132
|
+
for hook in reversed(self.shutdown_hooks):
|
|
133
|
+
try:
|
|
134
|
+
if inspect.iscoroutinefunction(hook):
|
|
135
|
+
await hook()
|
|
136
|
+
else:
|
|
137
|
+
hook()
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error(f"Shutdown hook failed: {e}")
|
|
140
|
+
# Continue with other shutdown hooks even if one fails
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Global backend hooks instance
|
|
144
|
+
backend_hooks = BackendHooks()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from app.components.backend.api.routing import include_routers
|
|
2
|
+
from app.components.backend.hooks import backend_hooks
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
|
|
5
|
+
# Store the configured FastAPI app instance for introspection
|
|
6
|
+
_configured_app: FastAPI | None = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_backend_app(app: FastAPI) -> FastAPI:
|
|
10
|
+
"""Configure FastAPI app with all backend concerns"""
|
|
11
|
+
global _configured_app
|
|
12
|
+
|
|
13
|
+
# Store the app instance for later introspection
|
|
14
|
+
_configured_app = app
|
|
15
|
+
|
|
16
|
+
# Auto-discover and register middleware
|
|
17
|
+
backend_hooks.discover_and_register_middleware(app)
|
|
18
|
+
|
|
19
|
+
# Include all routes
|
|
20
|
+
include_routers(app)
|
|
21
|
+
|
|
22
|
+
return app
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_configured_app() -> FastAPI | None:
|
|
26
|
+
"""
|
|
27
|
+
Get the configured backend FastAPI app instance.
|
|
28
|
+
Returns:
|
|
29
|
+
The configured FastAPI app instance, or None if not yet configured.
|
|
30
|
+
"""
|
|
31
|
+
return _configured_app
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Backend middleware
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# app/components/backend/middleware/cors.py
|
|
2
|
+
"""
|
|
3
|
+
Auto-discovered CORS middleware for development.
|
|
4
|
+
|
|
5
|
+
This middleware is automatically registered with FastAPI when the backend starts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_middleware(app: FastAPI) -> None:
|
|
13
|
+
"""Auto-discovered middleware registration."""
|
|
14
|
+
app.add_middleware(
|
|
15
|
+
CORSMiddleware,
|
|
16
|
+
allow_origins=["http://localhost:3000", "http://localhost:8080"],
|
|
17
|
+
allow_credentials=True,
|
|
18
|
+
allow_methods=["*"],
|
|
19
|
+
allow_headers=["*"],
|
|
20
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Backend shutdown hooks
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# app/components/backend/shutdown/cleanup.py
|
|
2
|
+
"""
|
|
3
|
+
Auto-discovered cleanup shutdown hook.
|
|
4
|
+
|
|
5
|
+
This hook performs cleanup when the backend shuts down.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from app.core.log import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def shutdown_hook() -> None:
|
|
12
|
+
"""Auto-discovered shutdown hook for cleanup."""
|
|
13
|
+
logger.info("🧹 Running backend cleanup...")
|
|
14
|
+
logger.info("✅ Backend shutdown cleanup complete")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Backend startup hooks
|