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,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System and orchestration tasks.
|
|
3
|
+
|
|
4
|
+
Contains the load test orchestrator which spawns many tasks to measure queue throughput.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from app.components.worker.constants import LoadTestTypes, TaskNames
|
|
12
|
+
from app.core.config import get_load_test_queue
|
|
13
|
+
from app.core.log import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def load_test_orchestrator(
|
|
17
|
+
ctx: dict[str, Any],
|
|
18
|
+
num_tasks: int = 100,
|
|
19
|
+
task_type: LoadTestTypes = LoadTestTypes.CPU_INTENSIVE,
|
|
20
|
+
batch_size: int = 10,
|
|
21
|
+
delay_ms: int = 0,
|
|
22
|
+
target_queue: str | None = None,
|
|
23
|
+
**kwargs: Any,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Load test orchestrator that spawns many lightweight tasks to measure queue
|
|
27
|
+
throughput.
|
|
28
|
+
|
|
29
|
+
This is the new approach: instead of one task doing heavy work, we spawn
|
|
30
|
+
hundreds of lightweight tasks to actually stress test the queue infrastructure
|
|
31
|
+
and measure meaningful performance metrics like tasks/second.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
num_tasks: Number of tasks to spawn for the load test
|
|
35
|
+
task_type: Type of worker task to spawn (cpu_intensive, io_simulation,
|
|
36
|
+
memory_operations)
|
|
37
|
+
batch_size: How many tasks to send concurrently per batch
|
|
38
|
+
delay_ms: Delay between batches in milliseconds
|
|
39
|
+
target_queue: Which queue to test (defaults to configured load_test queue)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Comprehensive load test results with throughput metrics
|
|
43
|
+
"""
|
|
44
|
+
start_time = datetime.now()
|
|
45
|
+
test_id = ctx.get("job_id", "unknown")
|
|
46
|
+
|
|
47
|
+
# Use configured load test queue if not specified
|
|
48
|
+
if target_queue is None:
|
|
49
|
+
target_queue = get_load_test_queue()
|
|
50
|
+
|
|
51
|
+
logger.info(
|
|
52
|
+
f"🚀 Starting load test orchestrator: {num_tasks} {task_type} tasks "
|
|
53
|
+
f"(batches of {batch_size})"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Initialize tasks_sent before try block to prevent UnboundLocalError
|
|
57
|
+
tasks_sent = 0
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Import here to avoid circular imports
|
|
61
|
+
from app.components.worker.pools import get_queue_pool
|
|
62
|
+
|
|
63
|
+
# Get queue pool for enqueueing
|
|
64
|
+
pool, queue_name = await get_queue_pool(target_queue)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Spawn tasks in batches
|
|
68
|
+
task_ids = []
|
|
69
|
+
|
|
70
|
+
for batch_start in range(0, num_tasks, batch_size):
|
|
71
|
+
batch_end = min(batch_start + batch_size, num_tasks)
|
|
72
|
+
current_batch_size = batch_end - batch_start
|
|
73
|
+
|
|
74
|
+
# Enqueue batch of tasks
|
|
75
|
+
# Map task type to actual function name
|
|
76
|
+
task_func = _get_task_function_name(task_type)
|
|
77
|
+
|
|
78
|
+
batch_jobs = []
|
|
79
|
+
for _ in range(current_batch_size):
|
|
80
|
+
job = await pool.enqueue_job(task_func, _queue_name=queue_name)
|
|
81
|
+
if job is not None:
|
|
82
|
+
batch_jobs.append(job)
|
|
83
|
+
task_ids.append(job.job_id)
|
|
84
|
+
|
|
85
|
+
tasks_sent += current_batch_size
|
|
86
|
+
logger.info(
|
|
87
|
+
f"📤 Sent batch: {current_batch_size} tasks "
|
|
88
|
+
f"(total: {tasks_sent}/{num_tasks})"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Add configurable delay between batches if specified
|
|
92
|
+
if delay_ms > 0 and batch_end < num_tasks:
|
|
93
|
+
await asyncio.sleep(delay_ms / 1000.0)
|
|
94
|
+
|
|
95
|
+
logger.info(f"✅ All {tasks_sent} tasks enqueued to {queue_name}")
|
|
96
|
+
|
|
97
|
+
# Monitor task completion with timeout based on queue configuration
|
|
98
|
+
from app.components.worker.registry import get_queue_metadata
|
|
99
|
+
|
|
100
|
+
queue_metadata = get_queue_metadata(target_queue)
|
|
101
|
+
monitor_timeout = queue_metadata.get("timeout", 300) # Use queue's timeout
|
|
102
|
+
|
|
103
|
+
logger.info(
|
|
104
|
+
f"⏱️ Monitoring task completion (timeout: {monitor_timeout}s)..."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
completion_result = await _monitor_task_completion(
|
|
108
|
+
task_ids=task_ids,
|
|
109
|
+
pool=pool,
|
|
110
|
+
expected_tasks=tasks_sent,
|
|
111
|
+
timeout_seconds=monitor_timeout, # Use configured timeout
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
end_time = datetime.now()
|
|
115
|
+
total_duration = (end_time - start_time).total_seconds()
|
|
116
|
+
|
|
117
|
+
# Combine orchestrator stats with completion monitoring
|
|
118
|
+
result = {
|
|
119
|
+
"test_id": test_id,
|
|
120
|
+
"task_type": task_type.value,
|
|
121
|
+
"tasks_sent": tasks_sent,
|
|
122
|
+
"task_ids": task_ids[:10], # Sample of IDs for debugging
|
|
123
|
+
"batch_size": batch_size,
|
|
124
|
+
"delay_ms": delay_ms,
|
|
125
|
+
"target_queue": target_queue,
|
|
126
|
+
"start_time": start_time.isoformat(),
|
|
127
|
+
"end_time": end_time.isoformat(),
|
|
128
|
+
"total_duration_seconds": round(total_duration, 2),
|
|
129
|
+
**completion_result, # Merge in the monitoring results
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Calculate overall throughput based on completed tasks
|
|
133
|
+
if result.get("tasks_completed", 0) > 0:
|
|
134
|
+
result["overall_throughput_per_second"] = round(
|
|
135
|
+
result["tasks_completed"] / total_duration, 2
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
result["overall_throughput_per_second"] = 0
|
|
139
|
+
|
|
140
|
+
logger.info(
|
|
141
|
+
f"🏁 Load test complete: {result['tasks_completed']}/{tasks_sent} "
|
|
142
|
+
f"tasks in {total_duration:.1f}s"
|
|
143
|
+
)
|
|
144
|
+
logger.info(
|
|
145
|
+
f"📈 Throughput: {result['overall_throughput_per_second']} tasks/sec"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
finally:
|
|
151
|
+
# Always close the pool, even if errors occur
|
|
152
|
+
await pool.aclose()
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Load test orchestrator failed: {e}")
|
|
156
|
+
return {"test_id": test_id, "error": str(e), "tasks_sent": tasks_sent}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _get_task_function_name(task_type: LoadTestTypes) -> str:
|
|
160
|
+
"""Map task type to actual function name."""
|
|
161
|
+
task_map = {
|
|
162
|
+
LoadTestTypes.CPU_INTENSIVE: TaskNames.CPU_INTENSIVE_TASK,
|
|
163
|
+
LoadTestTypes.IO_SIMULATION: TaskNames.IO_SIMULATION_TASK,
|
|
164
|
+
LoadTestTypes.MEMORY_OPERATIONS: TaskNames.MEMORY_OPERATIONS_TASK,
|
|
165
|
+
LoadTestTypes.FAILURE_TESTING: TaskNames.FAILURE_TESTING_TASK,
|
|
166
|
+
}
|
|
167
|
+
return task_map.get(task_type, TaskNames.CPU_INTENSIVE_TASK)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def _monitor_task_completion(
|
|
171
|
+
task_ids: list[str],
|
|
172
|
+
pool: Any,
|
|
173
|
+
expected_tasks: int,
|
|
174
|
+
timeout_seconds: int = 300,
|
|
175
|
+
poll_interval: float = 2.0,
|
|
176
|
+
) -> dict[str, Any]:
|
|
177
|
+
"""
|
|
178
|
+
Monitor task completion by checking job results directly.
|
|
179
|
+
|
|
180
|
+
This avoids Redis queue type errors by tracking job completion
|
|
181
|
+
instead of trying to read queue internals.
|
|
182
|
+
"""
|
|
183
|
+
start_monitor = datetime.now()
|
|
184
|
+
tasks_completed = 0
|
|
185
|
+
tasks_failed = 0
|
|
186
|
+
last_progress_time = start_monitor
|
|
187
|
+
last_completed = 0
|
|
188
|
+
|
|
189
|
+
# Track which task IDs we've seen complete
|
|
190
|
+
completed_ids: set[str] = set()
|
|
191
|
+
failed_ids: set[str] = set()
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
while True:
|
|
195
|
+
# Check each task ID for completion
|
|
196
|
+
for task_id in task_ids:
|
|
197
|
+
if task_id in completed_ids or task_id in failed_ids:
|
|
198
|
+
continue # Already processed
|
|
199
|
+
|
|
200
|
+
# Check if job result exists
|
|
201
|
+
result_key = f"arq:result:{task_id}"
|
|
202
|
+
result_data = await pool.get(result_key)
|
|
203
|
+
|
|
204
|
+
if result_data:
|
|
205
|
+
# Job completed - check if it succeeded or failed
|
|
206
|
+
try:
|
|
207
|
+
# arq stores results as msgpack, but we can check existence
|
|
208
|
+
completed_ids.add(task_id)
|
|
209
|
+
tasks_completed += 1
|
|
210
|
+
except Exception:
|
|
211
|
+
# If we can't parse, assume it completed
|
|
212
|
+
completed_ids.add(task_id)
|
|
213
|
+
tasks_completed += 1
|
|
214
|
+
|
|
215
|
+
tasks_done = tasks_completed + tasks_failed
|
|
216
|
+
|
|
217
|
+
# Calculate throughput
|
|
218
|
+
elapsed = (datetime.now() - start_monitor).total_seconds()
|
|
219
|
+
throughput = tasks_completed / elapsed if elapsed > 0 else 0
|
|
220
|
+
|
|
221
|
+
# Check if we're making progress
|
|
222
|
+
if tasks_completed > last_completed:
|
|
223
|
+
last_progress_time = datetime.now()
|
|
224
|
+
last_completed = tasks_completed
|
|
225
|
+
|
|
226
|
+
# Progress logging (less verbose)
|
|
227
|
+
progress_pct = (
|
|
228
|
+
(tasks_done / expected_tasks * 100) if expected_tasks > 0 else 0
|
|
229
|
+
)
|
|
230
|
+
if (
|
|
231
|
+
tasks_done % 10 == 0 or tasks_done == expected_tasks
|
|
232
|
+
): # Log every 10 tasks or at completion
|
|
233
|
+
logger.info(
|
|
234
|
+
f"📈 Progress: {tasks_done}/{expected_tasks} "
|
|
235
|
+
f"({progress_pct:.0f}% - completed: {tasks_completed}, "
|
|
236
|
+
f"failed: {tasks_failed}) throughput: {throughput:.1f} tasks/sec"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Check completion
|
|
240
|
+
if tasks_done >= expected_tasks:
|
|
241
|
+
logger.info(
|
|
242
|
+
f"✅ All tasks completed: {tasks_completed} success, "
|
|
243
|
+
f"{tasks_failed} failed"
|
|
244
|
+
)
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
# Check timeout
|
|
248
|
+
if elapsed > timeout_seconds:
|
|
249
|
+
logger.warning(f"⏱️ Load test timed out after {timeout_seconds}s")
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
# Check if we're stuck (no progress for 30 seconds)
|
|
253
|
+
stuck_duration = (datetime.now() - last_progress_time).total_seconds()
|
|
254
|
+
if stuck_duration > 30 and tasks_done > 0:
|
|
255
|
+
logger.warning(
|
|
256
|
+
f"⚠️ No progress for {stuck_duration:.0f}s, stopping monitor"
|
|
257
|
+
)
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
await asyncio.sleep(poll_interval)
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.error(f"Task monitoring error: {e}")
|
|
264
|
+
|
|
265
|
+
# Final metrics
|
|
266
|
+
final_elapsed = (datetime.now() - start_monitor).total_seconds()
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
"tasks_completed": tasks_completed,
|
|
270
|
+
"tasks_failed": tasks_failed,
|
|
271
|
+
"monitor_duration_seconds": round(final_elapsed, 2),
|
|
272
|
+
"average_throughput_per_second": round(tasks_completed / final_elapsed, 2)
|
|
273
|
+
if final_elapsed > 0
|
|
274
|
+
else 0,
|
|
275
|
+
"completion_percentage": round((tasks_completed / expected_tasks * 100), 1)
|
|
276
|
+
if expected_tasks > 0
|
|
277
|
+
else 0,
|
|
278
|
+
"failure_rate_percent": round((tasks_failed / expected_tasks * 100), 1)
|
|
279
|
+
if expected_tasks > 0
|
|
280
|
+
else 0,
|
|
281
|
+
}
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# app/core/config.py
|
|
2
|
+
"""
|
|
3
|
+
Application configuration management using Pydantic's BaseSettings.
|
|
4
|
+
|
|
5
|
+
This module centralizes application settings, allowing them to be loaded
|
|
6
|
+
from environment variables for easy configuration in different environments.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Settings(BaseSettings):
|
|
15
|
+
"""
|
|
16
|
+
Defines application settings.
|
|
17
|
+
`model_config` is used to specify that settings should be loaded from a .env file.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Application environment: "dev" or "prod"
|
|
21
|
+
APP_ENV: str = "dev"
|
|
22
|
+
|
|
23
|
+
# Log level for the application
|
|
24
|
+
LOG_LEVEL: str = "INFO"
|
|
25
|
+
|
|
26
|
+
# Port for the web server
|
|
27
|
+
PORT: int = 8000
|
|
28
|
+
|
|
29
|
+
# Development settings
|
|
30
|
+
AUTO_RELOAD: bool = False
|
|
31
|
+
|
|
32
|
+
# Docker settings (used by docker-compose)
|
|
33
|
+
AEGIS_STACK_TAG: str = "aegis-stack:latest"
|
|
34
|
+
AEGIS_STACK_VERSION: str = "dev"
|
|
35
|
+
|
|
36
|
+
# Health monitoring and alerting
|
|
37
|
+
# Health checks are available via API endpoints (/health/)
|
|
38
|
+
# Use external monitoring tools (Prometheus, DataDog, etc.) to poll these endpoints
|
|
39
|
+
HEALTH_CHECK_ENABLED: bool = True
|
|
40
|
+
HEALTH_CHECK_INTERVAL_MINUTES: int = 5 # Recommended interval for monitoring
|
|
41
|
+
|
|
42
|
+
# Health check performance settings
|
|
43
|
+
HEALTH_CHECK_TIMEOUT_SECONDS: float = 2.0
|
|
44
|
+
SYSTEM_METRICS_CACHE_SECONDS: int = 5
|
|
45
|
+
|
|
46
|
+
# Basic alerting configuration
|
|
47
|
+
ALERTING_ENABLED: bool = False
|
|
48
|
+
ALERT_COOLDOWN_MINUTES: int = 60 # Minutes between repeated alerts for same issue
|
|
49
|
+
|
|
50
|
+
# Health check thresholds
|
|
51
|
+
MEMORY_THRESHOLD_PERCENT: float = 90.0
|
|
52
|
+
DISK_THRESHOLD_PERCENT: float = 85.0
|
|
53
|
+
CPU_THRESHOLD_PERCENT: float = 95.0
|
|
54
|
+
|
|
55
|
+
# Flet frontend settings
|
|
56
|
+
FLET_ASSETS_DIR: str = "assets" # Directory for Flet static assets (images, etc.)
|
|
57
|
+
|
|
58
|
+
# Authentication settings
|
|
59
|
+
SECRET_KEY: str = "change-this-secret-key-in-production-use-env-variable"
|
|
60
|
+
JWT_ALGORITHM: str = "HS256"
|
|
61
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
62
|
+
|
|
63
|
+
{% if cookiecutter.include_redis == "yes" %}
|
|
64
|
+
# Redis settings for arq background tasks
|
|
65
|
+
REDIS_URL: str = "redis://redis:6379" # Docker service name by default
|
|
66
|
+
REDIS_URL_LOCAL: str | None = None # Override for local CLI usage
|
|
67
|
+
REDIS_DB: int = 0
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def redis_url_effective(self) -> str:
|
|
71
|
+
"""Get effective Redis URL, preferring local override when not in Docker."""
|
|
72
|
+
# If explicitly overridden for local use
|
|
73
|
+
if self.REDIS_URL_LOCAL and not self.is_docker:
|
|
74
|
+
return self.REDIS_URL_LOCAL
|
|
75
|
+
return self.REDIS_URL
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def is_docker(self) -> bool:
|
|
79
|
+
"""Detect if running inside Docker container."""
|
|
80
|
+
import os
|
|
81
|
+
return (
|
|
82
|
+
os.path.exists("/.dockerenv") or
|
|
83
|
+
bool(os.getenv("DOCKER_CONTAINER"))
|
|
84
|
+
)
|
|
85
|
+
{% endif %}
|
|
86
|
+
|
|
87
|
+
{% if cookiecutter.include_worker == "yes" %}
|
|
88
|
+
# arq worker settings (shared across all workers)
|
|
89
|
+
WORKER_KEEP_RESULT_SECONDS: int = 3600 # Keep job results for 1 hour
|
|
90
|
+
WORKER_MAX_TRIES: int = 3
|
|
91
|
+
|
|
92
|
+
# Redis connection settings for arq workers
|
|
93
|
+
REDIS_CONN_TIMEOUT: int = 5 # Connection timeout in seconds (default: 1)
|
|
94
|
+
REDIS_CONN_RETRIES: int = 5 # Connection retry attempts (default: 5)
|
|
95
|
+
REDIS_CONN_RETRY_DELAY: int = 1 # Delay between retries (default: 1)
|
|
96
|
+
|
|
97
|
+
# Worker health check settings
|
|
98
|
+
WORKER_HEALTH_CHECK_INTERVAL: int = 15 # In seconds (default: 15)
|
|
99
|
+
|
|
100
|
+
# PURE ARQ IMPLEMENTATION - NO CONFIGURATION NEEDED!
|
|
101
|
+
# Worker configuration comes from individual WorkerSettings classes
|
|
102
|
+
# in app/components/worker/queues/ - just import and use as arq intended!
|
|
103
|
+
{% endif %}
|
|
104
|
+
|
|
105
|
+
{% if cookiecutter.include_database == "yes" %}
|
|
106
|
+
# Database settings (SQLite)
|
|
107
|
+
DATABASE_URL: str = "sqlite:///./data/app.db"
|
|
108
|
+
DATABASE_ENGINE_ECHO: bool = False
|
|
109
|
+
DATABASE_CONNECT_ARGS: dict[str, Any] = {"check_same_thread": False}
|
|
110
|
+
{% endif %}
|
|
111
|
+
|
|
112
|
+
{% if cookiecutter.include_ai == "yes" %}
|
|
113
|
+
# AI Service Configuration
|
|
114
|
+
# Primary service settings
|
|
115
|
+
AI_ENABLED: bool = True
|
|
116
|
+
AI_PROVIDER: str = "public" # Default to public provider
|
|
117
|
+
AI_MODEL: str = "auto" # Default model (public provider uses available models)
|
|
118
|
+
AI_TEMPERATURE: float = 0.7
|
|
119
|
+
AI_MAX_TOKENS: int = 1000
|
|
120
|
+
AI_TIMEOUT_SECONDS: float = 30.0
|
|
121
|
+
|
|
122
|
+
# Provider API Keys (optional - many providers offer free tiers)
|
|
123
|
+
OPENAI_API_KEY: str | None = None
|
|
124
|
+
ANTHROPIC_API_KEY: str | None = None
|
|
125
|
+
GOOGLE_API_KEY: str | None = None
|
|
126
|
+
GROQ_API_KEY: str | None = None
|
|
127
|
+
MISTRAL_API_KEY: str | None = None
|
|
128
|
+
COHERE_API_KEY: str | None = None
|
|
129
|
+
|
|
130
|
+
# Conversation settings
|
|
131
|
+
AI_MAX_CONVERSATION_LENGTH: int = 50 # Max messages per conversation
|
|
132
|
+
AI_CONVERSATION_TIMEOUT_HOURS: int = 24 # Auto-cleanup old conversations
|
|
133
|
+
{% endif %}
|
|
134
|
+
|
|
135
|
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
settings = Settings()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
{% if cookiecutter.include_worker == "yes" %}
|
|
142
|
+
# Pure arq queue helper functions - use dynamic discovery
|
|
143
|
+
def get_available_queues() -> list[str]:
|
|
144
|
+
"""Get all available queue names via dynamic discovery."""
|
|
145
|
+
try:
|
|
146
|
+
from app.components.worker.registry import discover_worker_queues
|
|
147
|
+
queues: list[str] = discover_worker_queues()
|
|
148
|
+
return queues
|
|
149
|
+
except ImportError:
|
|
150
|
+
# Worker components not available
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_default_queue() -> str:
|
|
155
|
+
"""Get the default queue name for load testing."""
|
|
156
|
+
# Prefer load_test queue if it exists, otherwise use first available
|
|
157
|
+
available = get_available_queues()
|
|
158
|
+
if "load_test" in available:
|
|
159
|
+
return "load_test"
|
|
160
|
+
return available[0] if available else "system"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_load_test_queue() -> str:
|
|
164
|
+
"""Get the queue name for load testing."""
|
|
165
|
+
available = get_available_queues()
|
|
166
|
+
return "load_test" if "load_test" in available else get_default_queue()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def is_valid_queue(queue_name: str) -> bool:
|
|
170
|
+
"""Check if a queue name is valid."""
|
|
171
|
+
try:
|
|
172
|
+
from app.components.worker.registry import validate_queue_name
|
|
173
|
+
result: bool = validate_queue_name(queue_name)
|
|
174
|
+
return result
|
|
175
|
+
except ImportError:
|
|
176
|
+
# Worker components not available, no queues are valid
|
|
177
|
+
return False
|
|
178
|
+
{% endif %}
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application constants.
|
|
3
|
+
|
|
4
|
+
This module contains truly immutable values that never change across environments.
|
|
5
|
+
For environment-dependent configuration, see app.core.config.
|
|
6
|
+
|
|
7
|
+
Following 12-Factor App principles:
|
|
8
|
+
- Constants = code (version controlled, immutable across deployments)
|
|
9
|
+
- Configuration = environment (varies between dev/staging/production)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APIEndpoints:
|
|
14
|
+
"""API endpoint paths - immutable across all environments."""
|
|
15
|
+
|
|
16
|
+
HEALTH_BASIC = "/health/"
|
|
17
|
+
HEALTH_DETAILED = "/health/detailed"
|
|
18
|
+
HEALTH_DASHBOARD = "/health/dashboard"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Defaults:
|
|
22
|
+
"""Default values for timeouts and limits."""
|
|
23
|
+
|
|
24
|
+
# API timeouts (seconds)
|
|
25
|
+
API_TIMEOUT = 10.0
|
|
26
|
+
HEALTH_CHECK_TIMEOUT = 5.0
|
|
27
|
+
|
|
28
|
+
# Retry configuration
|
|
29
|
+
MAX_RETRIES = 3
|
|
30
|
+
RETRY_BACKOFF = 1.0
|
|
31
|
+
|
|
32
|
+
# Health check intervals (seconds)
|
|
33
|
+
HEALTH_CHECK_INTERVAL = 30
|
|
34
|
+
COMPONENT_CHECK_TIMEOUT = 2.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CLI:
|
|
38
|
+
"""CLI-specific constants."""
|
|
39
|
+
|
|
40
|
+
# Display limits
|
|
41
|
+
MAX_METADATA_DISPLAY_LENGTH = 30
|
|
42
|
+
|
|
43
|
+
# Output formatting
|
|
44
|
+
HEALTH_PERCENTAGE_DECIMALS = 1
|
|
45
|
+
TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HTTP:
|
|
49
|
+
"""HTTP-related constants."""
|
|
50
|
+
|
|
51
|
+
# Status codes we care about
|
|
52
|
+
OK = 200
|
|
53
|
+
SERVICE_UNAVAILABLE = 503
|
|
54
|
+
INTERNAL_SERVER_ERROR = 500
|
|
55
|
+
|
|
56
|
+
# Headers
|
|
57
|
+
CONTENT_TYPE_JSON = "application/json"
|
|
58
|
+
USER_AGENT = "AegisStack-CLI/1.0"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# app/core/db.py
|
|
2
|
+
"""
|
|
3
|
+
Database configuration and session management.
|
|
4
|
+
|
|
5
|
+
This module provides SQLite database connectivity using SQLModel and SQLAlchemy.
|
|
6
|
+
Includes proper session management with transaction handling and foreign key support.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import AsyncGenerator, Generator
|
|
10
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import urlparse
|
|
14
|
+
|
|
15
|
+
from sqlalchemy import create_engine, event
|
|
16
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
17
|
+
from sqlalchemy.orm import sessionmaker
|
|
18
|
+
from sqlmodel import Session, SQLModel
|
|
19
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
20
|
+
|
|
21
|
+
from app.core.config import settings
|
|
22
|
+
from app.core.log import logger
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Extract database file path from URL for backup operations
|
|
26
|
+
def _extract_database_path(database_url: str) -> str:
|
|
27
|
+
"""Extract the file path from a SQLite database URL."""
|
|
28
|
+
parsed = urlparse(database_url)
|
|
29
|
+
if parsed.scheme == "sqlite":
|
|
30
|
+
# Handle both sqlite:/// and sqlite:// formats
|
|
31
|
+
path = parsed.path
|
|
32
|
+
if path.startswith("/") and len(parsed.netloc) == 0:
|
|
33
|
+
# sqlite:///./path/file.db -> ./path/file.db
|
|
34
|
+
return path[1:]
|
|
35
|
+
elif parsed.netloc == "" and not path.startswith("/"):
|
|
36
|
+
# sqlite://./path/file.db -> ./path/file.db
|
|
37
|
+
return path
|
|
38
|
+
else:
|
|
39
|
+
# sqlite:///absolute/path/file.db -> /absolute/path/file.db
|
|
40
|
+
return path
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f"Unsupported database URL scheme: {parsed.scheme}")
|
|
43
|
+
|
|
44
|
+
DATABASE_PATH = _extract_database_path(settings.DATABASE_URL)
|
|
45
|
+
|
|
46
|
+
# Create SQLite engine with proper configuration (sync)
|
|
47
|
+
engine = create_engine(
|
|
48
|
+
settings.DATABASE_URL,
|
|
49
|
+
connect_args=settings.DATABASE_CONNECT_ARGS,
|
|
50
|
+
echo=settings.DATABASE_ENGINE_ECHO,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create async engine for non-blocking operations
|
|
54
|
+
def _get_async_database_url(database_url: str) -> str:
|
|
55
|
+
"""Convert sync database URL to async version."""
|
|
56
|
+
if database_url.startswith("sqlite:///"):
|
|
57
|
+
return database_url.replace("sqlite:///", "sqlite+aiosqlite:///")
|
|
58
|
+
elif database_url.startswith("sqlite://"):
|
|
59
|
+
return database_url.replace("sqlite://", "sqlite+aiosqlite://")
|
|
60
|
+
elif database_url.startswith("postgresql://"):
|
|
61
|
+
return database_url.replace("postgresql://", "postgresql+asyncpg://")
|
|
62
|
+
elif database_url.startswith("mysql://"):
|
|
63
|
+
return database_url.replace("mysql://", "mysql+aiomysql://")
|
|
64
|
+
else:
|
|
65
|
+
# For future database types, return as-is and let SQLAlchemy handle it
|
|
66
|
+
return database_url
|
|
67
|
+
|
|
68
|
+
async_engine = create_async_engine(
|
|
69
|
+
_get_async_database_url(settings.DATABASE_URL),
|
|
70
|
+
echo=settings.DATABASE_ENGINE_ECHO,
|
|
71
|
+
# Only add connect_args for SQLite
|
|
72
|
+
connect_args=(
|
|
73
|
+
settings.DATABASE_CONNECT_ARGS
|
|
74
|
+
if "sqlite" in settings.DATABASE_URL
|
|
75
|
+
else {}
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Enable foreign key constraints for SQLite
|
|
81
|
+
@event.listens_for(engine, "connect")
|
|
82
|
+
def set_sqlite_pragma(dbapi_connection: Any, connection_record: Any) -> None:
|
|
83
|
+
"""Enable foreign key constraints in SQLite."""
|
|
84
|
+
cursor = dbapi_connection.cursor()
|
|
85
|
+
cursor.execute("PRAGMA foreign_keys=ON")
|
|
86
|
+
cursor.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Configure session factory with SQLModel Session (sync)
|
|
90
|
+
SessionLocal = sessionmaker(
|
|
91
|
+
class_=Session, bind=engine, autoflush=False, autocommit=False
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Configure async session factory using SQLModel's AsyncSession
|
|
95
|
+
AsyncSessionLocal = async_sessionmaker(
|
|
96
|
+
async_engine,
|
|
97
|
+
class_=AsyncSession,
|
|
98
|
+
expire_on_commit=False,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@contextmanager
|
|
103
|
+
def db_session(autocommit: bool = True) -> Generator[Session, None, None]:
|
|
104
|
+
"""
|
|
105
|
+
Database session context manager with automatic transaction handling.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
autocommit: Whether to automatically commit the transaction on success
|
|
109
|
+
|
|
110
|
+
Yields:
|
|
111
|
+
Session: Database session instance
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
with db_session() as session:
|
|
115
|
+
# Your database operations here
|
|
116
|
+
result = session.query(MyModel).first()
|
|
117
|
+
"""
|
|
118
|
+
db_session: Session = SessionLocal()
|
|
119
|
+
try:
|
|
120
|
+
yield db_session
|
|
121
|
+
if autocommit:
|
|
122
|
+
db_session.commit()
|
|
123
|
+
except Exception:
|
|
124
|
+
db_session.rollback()
|
|
125
|
+
raise
|
|
126
|
+
finally:
|
|
127
|
+
db_session.close()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@asynccontextmanager
|
|
131
|
+
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
|
132
|
+
"""
|
|
133
|
+
Async database session context manager with automatic transaction handling.
|
|
134
|
+
|
|
135
|
+
Yields:
|
|
136
|
+
AsyncSession: Async database session instance
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
async with get_async_session() as session:
|
|
140
|
+
# Your async database operations here
|
|
141
|
+
result = await session.exec(select(MyModel))
|
|
142
|
+
"""
|
|
143
|
+
async with AsyncSessionLocal() as session:
|
|
144
|
+
try:
|
|
145
|
+
yield session
|
|
146
|
+
await session.commit()
|
|
147
|
+
except Exception:
|
|
148
|
+
await session.rollback()
|
|
149
|
+
raise
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def init_database() -> None:
|
|
153
|
+
"""
|
|
154
|
+
Initialize the database by creating tables and ensuring directory structure.
|
|
155
|
+
|
|
156
|
+
This function:
|
|
157
|
+
1. Creates the database directory if it doesn't exist
|
|
158
|
+
2. Creates all tables defined by SQLModel models
|
|
159
|
+
3. Logs the initialization status
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
# Ensure database directory exists
|
|
163
|
+
db_path = Path(DATABASE_PATH)
|
|
164
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
# Create all tables
|
|
167
|
+
SQLModel.metadata.create_all(engine)
|
|
168
|
+
|
|
169
|
+
if db_path.exists():
|
|
170
|
+
logger.info(f"✅ Database initialized: {DATABASE_PATH}")
|
|
171
|
+
else:
|
|
172
|
+
logger.info(f"✅ Database will be created on first use: {DATABASE_PATH}")
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"❌ Database initialization failed: {e}")
|
|
176
|
+
raise
|