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,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for load test data structures.
|
|
3
|
+
|
|
4
|
+
Provides type safety and validation for load test configurations,
|
|
5
|
+
results, and analysis data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from app.components.worker.constants import LoadTestTypes
|
|
13
|
+
from app.core.config import get_load_test_queue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoadTestError(Exception):
|
|
17
|
+
"""Custom exception for load test operations."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LoadTestConfiguration(BaseModel):
|
|
23
|
+
"""Load test configuration with validation and defaults."""
|
|
24
|
+
|
|
25
|
+
num_tasks: int = Field(
|
|
26
|
+
default=100, ge=10, le=10000, description="Number of tasks to spawn"
|
|
27
|
+
)
|
|
28
|
+
task_type: LoadTestTypes = Field(
|
|
29
|
+
default=LoadTestTypes.CPU_INTENSIVE, description="Type of load test to run"
|
|
30
|
+
)
|
|
31
|
+
batch_size: int = Field(default=10, ge=1, le=100, description="Tasks per batch")
|
|
32
|
+
delay_ms: int = Field(
|
|
33
|
+
default=0, ge=0, le=5000, description="Delay between batches (ms)"
|
|
34
|
+
)
|
|
35
|
+
target_queue: str | None = Field(
|
|
36
|
+
default=None, description="Target queue for testing"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@field_validator("target_queue")
|
|
40
|
+
@classmethod
|
|
41
|
+
def set_default_queue(cls, v: str | None) -> str:
|
|
42
|
+
"""Set default queue if not specified."""
|
|
43
|
+
return v if v is not None else get_load_test_queue()
|
|
44
|
+
|
|
45
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
46
|
+
"""Convert configuration to dictionary for task enqueueing."""
|
|
47
|
+
data = super().model_dump(**kwargs)
|
|
48
|
+
# Convert enum to string value for task enqueueing
|
|
49
|
+
data["task_type"] = self.task_type.value
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LoadTestMetrics(BaseModel):
|
|
54
|
+
"""Metrics from load test execution."""
|
|
55
|
+
|
|
56
|
+
tasks_sent: int = Field(..., ge=0, description="Total tasks enqueued")
|
|
57
|
+
tasks_completed: int = Field(..., ge=0, description="Successfully completed tasks")
|
|
58
|
+
tasks_failed: int = Field(0, ge=0, description="Failed tasks")
|
|
59
|
+
total_duration_seconds: float = Field(..., ge=0, description="Total test duration")
|
|
60
|
+
overall_throughput: float = Field(
|
|
61
|
+
0, ge=0, description="Overall throughput (tasks/sec)"
|
|
62
|
+
)
|
|
63
|
+
failure_rate_percent: float = Field(
|
|
64
|
+
0, ge=0, le=100, description="Failure rate percentage"
|
|
65
|
+
)
|
|
66
|
+
completion_percentage: float = Field(
|
|
67
|
+
0, ge=0, le=100, description="Completion percentage"
|
|
68
|
+
)
|
|
69
|
+
average_throughput_per_second: float = Field(
|
|
70
|
+
0, ge=0, description="Average throughput"
|
|
71
|
+
)
|
|
72
|
+
monitor_duration_seconds: float = Field(0, ge=0, description="Monitoring duration")
|
|
73
|
+
|
|
74
|
+
@field_validator("tasks_completed")
|
|
75
|
+
@classmethod
|
|
76
|
+
def completed_not_exceed_sent(cls, v: int, info: ValidationInfo) -> int:
|
|
77
|
+
"""Ensure completed tasks don't exceed sent tasks."""
|
|
78
|
+
if info.data and "tasks_sent" in info.data and v > info.data["tasks_sent"]:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Completed tasks ({v}) cannot exceed sent tasks "
|
|
81
|
+
f"({info.data['tasks_sent']})"
|
|
82
|
+
)
|
|
83
|
+
return v
|
|
84
|
+
|
|
85
|
+
@field_validator("tasks_failed")
|
|
86
|
+
@classmethod
|
|
87
|
+
def failed_not_exceed_sent(cls, v: int, info: ValidationInfo) -> int:
|
|
88
|
+
"""Ensure failed tasks don't exceed sent tasks."""
|
|
89
|
+
if info.data and "tasks_sent" in info.data and v > info.data["tasks_sent"]:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Failed tasks ({v}) cannot exceed sent tasks "
|
|
92
|
+
f"({info.data['tasks_sent']})"
|
|
93
|
+
)
|
|
94
|
+
return v
|
|
95
|
+
|
|
96
|
+
@field_validator("failure_rate_percent")
|
|
97
|
+
@classmethod
|
|
98
|
+
def validate_failure_rate_consistency(cls, v: float, info: ValidationInfo) -> float:
|
|
99
|
+
"""Ensure failure rate percentage matches task counts."""
|
|
100
|
+
if info.data and "tasks_sent" in info.data and "tasks_failed" in info.data:
|
|
101
|
+
tasks_sent = info.data["tasks_sent"]
|
|
102
|
+
tasks_failed = info.data["tasks_failed"]
|
|
103
|
+
if tasks_sent > 0:
|
|
104
|
+
calculated_rate = (tasks_failed / tasks_sent) * 100
|
|
105
|
+
# Allow small floating point differences (within 0.1%)
|
|
106
|
+
if abs(v - calculated_rate) > 0.1:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Failure rate {v}% doesn't match task counts "
|
|
109
|
+
f"({tasks_failed}/{tasks_sent} = {calculated_rate:.1f}%)"
|
|
110
|
+
)
|
|
111
|
+
return v
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PerformanceAnalysis(BaseModel):
|
|
115
|
+
"""Performance analysis results."""
|
|
116
|
+
|
|
117
|
+
throughput_rating: str = Field(
|
|
118
|
+
...,
|
|
119
|
+
pattern=r"^(unknown|poor|fair|good|excellent)$",
|
|
120
|
+
description="Throughput performance rating",
|
|
121
|
+
)
|
|
122
|
+
efficiency_rating: str = Field(
|
|
123
|
+
...,
|
|
124
|
+
pattern=r"^(unknown|poor|fair|good|excellent)$",
|
|
125
|
+
description="Task completion efficiency",
|
|
126
|
+
)
|
|
127
|
+
queue_pressure: str = Field(
|
|
128
|
+
...,
|
|
129
|
+
pattern=r"^(unknown|low|medium|high)$",
|
|
130
|
+
description="Queue saturation level",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ValidationStatus(BaseModel):
|
|
135
|
+
"""Test execution validation status."""
|
|
136
|
+
|
|
137
|
+
test_type_verified: bool = Field(
|
|
138
|
+
default=False, description="Test type executed correctly"
|
|
139
|
+
)
|
|
140
|
+
expected_metrics_present: bool = Field(
|
|
141
|
+
default=False, description="Expected metrics are present"
|
|
142
|
+
)
|
|
143
|
+
performance_signature_match: str = Field(
|
|
144
|
+
default="unknown",
|
|
145
|
+
pattern=r"^(unknown|verified|partial|failed)$",
|
|
146
|
+
description="Performance matches expected patterns",
|
|
147
|
+
)
|
|
148
|
+
issues: list[str] = Field(default_factory=list, description="Validation issues")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestTypeInfo(BaseModel):
|
|
152
|
+
"""Information about a specific test type."""
|
|
153
|
+
|
|
154
|
+
name: str = Field(..., description="Human-readable test name")
|
|
155
|
+
description: str = Field(..., description="Test description")
|
|
156
|
+
expected_metrics: list[str] = Field(..., description="Expected result metrics")
|
|
157
|
+
performance_signature: str = Field(..., description="Expected performance pattern")
|
|
158
|
+
typical_duration_ms: str = Field(..., description="Typical execution time")
|
|
159
|
+
concurrency_impact: str = Field(..., description="Concurrency characteristics")
|
|
160
|
+
validation_keys: list[str] = Field(..., description="Keys for result validation")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class LoadTestAnalysis(BaseModel):
|
|
164
|
+
"""Complete load test analysis."""
|
|
165
|
+
|
|
166
|
+
test_type_info: TestTypeInfo = Field(..., description="Test type information")
|
|
167
|
+
performance_analysis: PerformanceAnalysis = Field(
|
|
168
|
+
..., description="Performance analysis"
|
|
169
|
+
)
|
|
170
|
+
validation_status: ValidationStatus = Field(..., description="Validation results")
|
|
171
|
+
recommendations: list[str] = Field(..., description="Improvement recommendations")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class LoadTestResult(BaseModel):
|
|
175
|
+
"""Complete load test result with analysis."""
|
|
176
|
+
|
|
177
|
+
task: str = Field(default="load_test_orchestrator", description="Task name")
|
|
178
|
+
status: str = Field(
|
|
179
|
+
...,
|
|
180
|
+
pattern=r"^(completed|failed|timed_out)$",
|
|
181
|
+
description="Test execution status",
|
|
182
|
+
)
|
|
183
|
+
test_id: str = Field(..., description="Unique test identifier")
|
|
184
|
+
configuration: LoadTestConfiguration = Field(..., description="Test configuration")
|
|
185
|
+
metrics: LoadTestMetrics = Field(..., description="Execution metrics")
|
|
186
|
+
start_time: str | None = Field(None, description="Test start time")
|
|
187
|
+
end_time: str | None = Field(None, description="Test end time")
|
|
188
|
+
task_ids: list[str] = Field(default_factory=list, description="Individual task IDs")
|
|
189
|
+
error: str | None = Field(None, description="Error message if failed")
|
|
190
|
+
analysis: LoadTestAnalysis | None = Field(None, description="Performance analysis")
|
|
191
|
+
|
|
192
|
+
@model_validator(mode="after")
|
|
193
|
+
def validate_status_consistency(self) -> "LoadTestResult":
|
|
194
|
+
"""Validate status consistency with error field."""
|
|
195
|
+
if self.status == "failed" and not self.error:
|
|
196
|
+
raise ValueError("Failed status requires error message")
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class OrchestratorRawResult(BaseModel):
|
|
201
|
+
"""Raw orchestrator result format for transformation."""
|
|
202
|
+
|
|
203
|
+
test_id: str = Field(..., description="Test identifier")
|
|
204
|
+
task_type: str = Field(..., description="Task type executed")
|
|
205
|
+
tasks_sent: int = Field(..., description="Tasks enqueued")
|
|
206
|
+
tasks_completed: int = Field(0, description="Successfully completed")
|
|
207
|
+
tasks_failed: int = Field(0, description="Failed tasks")
|
|
208
|
+
total_duration_seconds: float = Field(..., description="Total duration")
|
|
209
|
+
overall_throughput_per_second: float = Field(0, description="Overall throughput")
|
|
210
|
+
failure_rate_percent: float = Field(0, description="Failure rate")
|
|
211
|
+
completion_percentage: float = Field(0, description="Completion rate")
|
|
212
|
+
average_throughput_per_second: float = Field(0, description="Average throughput")
|
|
213
|
+
monitor_duration_seconds: float = Field(0, description="Monitor duration")
|
|
214
|
+
batch_size: int = Field(1, description="Batch size used")
|
|
215
|
+
delay_ms: int = Field(0, description="Delay between batches")
|
|
216
|
+
target_queue: str = Field(..., description="Target queue")
|
|
217
|
+
start_time: str | None = Field(None, description="Start time")
|
|
218
|
+
end_time: str | None = Field(None, description="End time")
|
|
219
|
+
task_ids: list[str] = Field(default_factory=list, description="Task IDs")
|
|
220
|
+
|
|
221
|
+
def to_load_test_result(self) -> LoadTestResult:
|
|
222
|
+
"""Transform to standard LoadTestResult format."""
|
|
223
|
+
configuration = LoadTestConfiguration(
|
|
224
|
+
task_type=LoadTestTypes(self.task_type),
|
|
225
|
+
num_tasks=self.tasks_sent,
|
|
226
|
+
batch_size=self.batch_size,
|
|
227
|
+
delay_ms=self.delay_ms,
|
|
228
|
+
target_queue=self.target_queue,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
metrics = LoadTestMetrics(
|
|
232
|
+
tasks_sent=self.tasks_sent,
|
|
233
|
+
tasks_completed=self.tasks_completed,
|
|
234
|
+
tasks_failed=self.tasks_failed,
|
|
235
|
+
total_duration_seconds=self.total_duration_seconds,
|
|
236
|
+
overall_throughput=self.overall_throughput_per_second,
|
|
237
|
+
failure_rate_percent=self.failure_rate_percent,
|
|
238
|
+
completion_percentage=self.completion_percentage,
|
|
239
|
+
average_throughput_per_second=self.average_throughput_per_second,
|
|
240
|
+
monitor_duration_seconds=self.monitor_duration_seconds,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return LoadTestResult(
|
|
244
|
+
status="completed",
|
|
245
|
+
test_id=self.test_id,
|
|
246
|
+
configuration=configuration,
|
|
247
|
+
metrics=metrics,
|
|
248
|
+
start_time=self.start_time,
|
|
249
|
+
end_time=self.end_time,
|
|
250
|
+
task_ids=self.task_ids,
|
|
251
|
+
error=None,
|
|
252
|
+
analysis=None,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class LoadTestErrorModel(BaseModel):
|
|
257
|
+
"""Load test error result with partial information."""
|
|
258
|
+
|
|
259
|
+
task: str = Field(default="load_test_orchestrator", description="Task name")
|
|
260
|
+
status: str = Field(
|
|
261
|
+
..., pattern=r"^(failed|timed_out)$", description="Error status"
|
|
262
|
+
)
|
|
263
|
+
test_id: str = Field(..., description="Unique test identifier")
|
|
264
|
+
error: str = Field(..., description="Error message")
|
|
265
|
+
partial_info: str | None = Field(None, description="Partial completion info")
|
|
266
|
+
tasks_sent: int | None = Field(None, ge=0, description="Tasks that were sent")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{% if cookiecutter.scheduler_backend != "memory" %}
|
|
2
|
+
"""Scheduler service layer for async task management."""
|
|
3
|
+
|
|
4
|
+
from .models import (
|
|
5
|
+
APSchedulerJob,
|
|
6
|
+
ScheduledTask,
|
|
7
|
+
TaskStatistics,
|
|
8
|
+
SchedulerHealthMetadata,
|
|
9
|
+
UpcomingTask,
|
|
10
|
+
)
|
|
11
|
+
from .scheduled_task_manager import ScheduledTaskManager
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ScheduledTaskManager",
|
|
15
|
+
"ScheduledTask",
|
|
16
|
+
"TaskStatistics",
|
|
17
|
+
"APSchedulerJob",
|
|
18
|
+
"SchedulerHealthMetadata",
|
|
19
|
+
"UpcomingTask",
|
|
20
|
+
]
|
|
21
|
+
{% endif %}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Models for scheduled task management - both database and service layer."""
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
{% if cookiecutter.scheduler_backend != "memory" %}
|
|
9
|
+
from sqlalchemy import Column, Float, LargeBinary, String
|
|
10
|
+
from sqlmodel import Field as SQLField, SQLModel
|
|
11
|
+
{% endif %}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
{% if cookiecutter.scheduler_backend != "memory" %}
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# DATABASE MODELS (SQLModel)
|
|
17
|
+
# ============================================================================
|
|
18
|
+
|
|
19
|
+
class APSchedulerJob(SQLModel, table=True):
|
|
20
|
+
"""
|
|
21
|
+
Mirror of APScheduler's job persistence table.
|
|
22
|
+
This model allows us to read APScheduler data using SQLModel's async capabilities.
|
|
23
|
+
|
|
24
|
+
Note: APScheduler creates this table, we only read from it.
|
|
25
|
+
The table structure matches APScheduler 3.x exactly.
|
|
26
|
+
"""
|
|
27
|
+
__tablename__ = "apscheduler_jobs"
|
|
28
|
+
|
|
29
|
+
# Use SA Column types for exact compatibility with APScheduler's table structure
|
|
30
|
+
id: str = SQLField(
|
|
31
|
+
sa_column=Column(String(191), primary_key=True)
|
|
32
|
+
)
|
|
33
|
+
next_run_time: float | None = SQLField(
|
|
34
|
+
sa_column=Column(Float(25), nullable=True, index=True)
|
|
35
|
+
)
|
|
36
|
+
job_state: bytes = SQLField(
|
|
37
|
+
sa_column=Column(LargeBinary, nullable=False)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def get_job_data(self) -> dict[str, Any]:
|
|
41
|
+
"""
|
|
42
|
+
Deserialize the pickled job state.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict: Job data with standard APScheduler keys.
|
|
46
|
+
|
|
47
|
+
Note:
|
|
48
|
+
This is safe because we're reading APScheduler's own data format.
|
|
49
|
+
APScheduler writes pickled job data to this table - we're just reading it.
|
|
50
|
+
"""
|
|
51
|
+
return pickle.loads(self.job_state) # type: ignore[no-any-return]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ============================================================================
|
|
55
|
+
# SERVICE LAYER MODELS (Pydantic)
|
|
56
|
+
# ============================================================================
|
|
57
|
+
|
|
58
|
+
class ScheduledTask(BaseModel):
|
|
59
|
+
"""
|
|
60
|
+
Represents a scheduled task with all relevant information.
|
|
61
|
+
This is the clean data model used by CLI/API consumers.
|
|
62
|
+
"""
|
|
63
|
+
job_id: str = Field(..., description="Unique task identifier")
|
|
64
|
+
name: str = Field(..., description="Human-readable task name")
|
|
65
|
+
function: str = Field(..., description="Function reference (module:function)")
|
|
66
|
+
schedule: str = Field(..., description="Human-readable schedule (e.g., 'Every 5m')")
|
|
67
|
+
trigger_type: Literal["interval", "cron", "date", "unknown"] = Field(
|
|
68
|
+
..., description="Type of scheduling trigger"
|
|
69
|
+
)
|
|
70
|
+
next_run_time: datetime | None = Field(
|
|
71
|
+
None, description="Next scheduled execution time"
|
|
72
|
+
)
|
|
73
|
+
status: Literal["active", "paused"] = Field(
|
|
74
|
+
..., description="Current task status"
|
|
75
|
+
)
|
|
76
|
+
max_instances: int = Field(
|
|
77
|
+
1, description="Maximum concurrent instances allowed"
|
|
78
|
+
)
|
|
79
|
+
coalesce: bool = Field(
|
|
80
|
+
True, description="Whether to coalesce missed runs"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def is_active(self) -> bool:
|
|
85
|
+
"""Check if the task is currently active."""
|
|
86
|
+
return self.status == "active"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TaskStatistics(BaseModel):
|
|
90
|
+
"""Statistics about scheduled tasks in the system."""
|
|
91
|
+
total_tasks: int = Field(0, description="Total number of scheduled tasks")
|
|
92
|
+
active_tasks: int = Field(0, description="Number of active tasks")
|
|
93
|
+
paused_tasks: int = Field(0, description="Number of paused tasks")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
{% endif %}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ============================================================================
|
|
100
|
+
# HEALTH MONITORING MODELS (Available for all scheduler modes)
|
|
101
|
+
# ============================================================================
|
|
102
|
+
|
|
103
|
+
class UpcomingTask(BaseModel):
|
|
104
|
+
"""Information about an upcoming scheduled task."""
|
|
105
|
+
job_id: str = Field(..., description="Task identifier")
|
|
106
|
+
name: str = Field(..., description="Human-readable task name")
|
|
107
|
+
next_run: str = Field(..., description="ISO datetime string of next execution")
|
|
108
|
+
schedule: str = Field(..., description="Human-readable schedule description")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class SchedulerHealthMetadata(BaseModel):
|
|
112
|
+
"""Health metadata for scheduler components."""
|
|
113
|
+
total_tasks: int = Field(0, description="Total number of scheduled tasks")
|
|
114
|
+
active_tasks: int = Field(0, description="Number of active tasks")
|
|
115
|
+
paused_tasks: int = Field(0, description="Number of paused tasks")
|
|
116
|
+
upcoming_tasks: list[UpcomingTask] = Field(
|
|
117
|
+
default_factory=list, description="List of upcoming tasks (max 5)"
|
|
118
|
+
)
|
|
119
|
+
scheduler_state: str = Field("unknown", description="Scheduler state description")
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
{% if cookiecutter.scheduler_backend != "memory" %}
|
|
2
|
+
"""Async scheduled task manager using SQLModel for database abstraction."""
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import inspect
|
|
8
|
+
from sqlmodel import select
|
|
9
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
10
|
+
|
|
11
|
+
from app.core.db import async_engine, get_async_session
|
|
12
|
+
from app.core.log import logger
|
|
13
|
+
|
|
14
|
+
from .models import APSchedulerJob, ScheduledTask, TaskStatistics
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ScheduledTaskManager:
|
|
18
|
+
"""
|
|
19
|
+
Manages scheduled tasks via async database operations.
|
|
20
|
+
|
|
21
|
+
Uses SQLModel for database abstraction, supporting SQLite, PostgreSQL, MySQL.
|
|
22
|
+
Only available when scheduler persistence is enabled - reads from
|
|
23
|
+
apscheduler_jobs table.
|
|
24
|
+
"""
|
|
25
|
+
async def has_persistence(self) -> bool:
|
|
26
|
+
"""Check if apscheduler_jobs table exists."""
|
|
27
|
+
try:
|
|
28
|
+
async with async_engine.begin() as conn:
|
|
29
|
+
# Use inspector to check tables
|
|
30
|
+
tables = await conn.run_sync(
|
|
31
|
+
lambda sync_conn: inspect(sync_conn).get_table_names()
|
|
32
|
+
)
|
|
33
|
+
return "apscheduler_jobs" in tables
|
|
34
|
+
except Exception as e:
|
|
35
|
+
logger.error(f"Error checking persistence: {e}")
|
|
36
|
+
return False
|
|
37
|
+
async def list_tasks(self) -> list[ScheduledTask]:
|
|
38
|
+
"""
|
|
39
|
+
List all scheduled tasks from the database.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
list[ScheduledTask]: List of all scheduled tasks with their details.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
RuntimeError: If persistence is not available.
|
|
46
|
+
"""
|
|
47
|
+
if not await self.has_persistence():
|
|
48
|
+
raise RuntimeError(
|
|
49
|
+
"Scheduled task listing requires persistence. "
|
|
50
|
+
"The apscheduler_jobs table was not found in the database."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async with get_async_session() as session:
|
|
54
|
+
# Query all jobs ordered by next run time (nulls last)
|
|
55
|
+
result = await session.exec(
|
|
56
|
+
select(APSchedulerJob).order_by(
|
|
57
|
+
APSchedulerJob.next_run_time.desc().nulls_last() # type: ignore[union-attr]
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
jobs = result.all()
|
|
61
|
+
|
|
62
|
+
tasks = []
|
|
63
|
+
for job in jobs:
|
|
64
|
+
try:
|
|
65
|
+
job_data = job.get_job_data()
|
|
66
|
+
task = ScheduledTask(
|
|
67
|
+
job_id=job.id,
|
|
68
|
+
name=job_data.get("name", job.id),
|
|
69
|
+
function=job_data.get("func", "unknown"),
|
|
70
|
+
schedule=self._format_trigger(job_data.get("trigger")),
|
|
71
|
+
trigger_type=self._get_trigger_type(job_data.get("trigger")),
|
|
72
|
+
next_run_time=(
|
|
73
|
+
datetime.fromtimestamp(job.next_run_time)
|
|
74
|
+
if job.next_run_time else None
|
|
75
|
+
),
|
|
76
|
+
status="active" if job.next_run_time else "paused",
|
|
77
|
+
max_instances=job_data.get("max_instances", 1),
|
|
78
|
+
coalesce=job_data.get("coalesce", True),
|
|
79
|
+
)
|
|
80
|
+
tasks.append(task)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Error processing job {job.id}: {e}")
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
return tasks
|
|
86
|
+
|
|
87
|
+
async def get_task(self, task_id: str) -> ScheduledTask | None:
|
|
88
|
+
"""
|
|
89
|
+
Get a specific task by ID.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
task_id: The unique identifier of the task.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
ScheduledTask | None: The task if found, None otherwise.
|
|
96
|
+
"""
|
|
97
|
+
async with get_async_session() as session:
|
|
98
|
+
result = await session.exec(
|
|
99
|
+
select(APSchedulerJob).where(APSchedulerJob.id == task_id)
|
|
100
|
+
)
|
|
101
|
+
job = result.first()
|
|
102
|
+
|
|
103
|
+
if not job:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
job_data = job.get_job_data()
|
|
108
|
+
return ScheduledTask(
|
|
109
|
+
job_id=job.id,
|
|
110
|
+
name=job_data.get("name", job.id),
|
|
111
|
+
function=job_data.get("func", "unknown"),
|
|
112
|
+
schedule=self._format_trigger(job_data.get("trigger")),
|
|
113
|
+
trigger_type=self._get_trigger_type(job_data.get("trigger")),
|
|
114
|
+
next_run_time=(
|
|
115
|
+
datetime.fromtimestamp(job.next_run_time)
|
|
116
|
+
if job.next_run_time else None
|
|
117
|
+
),
|
|
118
|
+
status="active" if job.next_run_time else "paused",
|
|
119
|
+
max_instances=job_data.get("max_instances", 1),
|
|
120
|
+
coalesce=job_data.get("coalesce", True),
|
|
121
|
+
)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"Error processing job {task_id}: {e}")
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
async def get_statistics(self) -> TaskStatistics:
|
|
127
|
+
"""
|
|
128
|
+
Get task statistics.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
TaskStatistics: Summary statistics about all scheduled tasks.
|
|
132
|
+
"""
|
|
133
|
+
# TODO: Optimize with SQL aggregation queries instead of fetching all tasks
|
|
134
|
+
tasks = await self.list_tasks()
|
|
135
|
+
active = sum(1 for t in tasks if t.status == "active")
|
|
136
|
+
paused = sum(1 for t in tasks if t.status == "paused")
|
|
137
|
+
|
|
138
|
+
return TaskStatistics(
|
|
139
|
+
total_tasks=len(tasks),
|
|
140
|
+
active_tasks=active,
|
|
141
|
+
paused_tasks=paused,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _format_trigger(self, trigger: Any) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Convert APScheduler trigger to human-readable string.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
trigger: APScheduler trigger object (IntervalTrigger, CronTrigger, etc.)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: Human-readable schedule description.
|
|
153
|
+
"""
|
|
154
|
+
if not trigger:
|
|
155
|
+
return "Unknown"
|
|
156
|
+
|
|
157
|
+
trigger_type = type(trigger).__name__
|
|
158
|
+
|
|
159
|
+
if trigger_type == "IntervalTrigger":
|
|
160
|
+
if hasattr(trigger, "interval"):
|
|
161
|
+
seconds = trigger.interval.total_seconds()
|
|
162
|
+
if seconds < 60:
|
|
163
|
+
return f"Every {int(seconds)}s"
|
|
164
|
+
elif seconds < 3600:
|
|
165
|
+
minutes = int(seconds / 60)
|
|
166
|
+
return f"Every {minutes}m"
|
|
167
|
+
elif seconds < 86400:
|
|
168
|
+
hours = seconds / 3600
|
|
169
|
+
if hours == int(hours):
|
|
170
|
+
return f"Every {int(hours)}h"
|
|
171
|
+
else:
|
|
172
|
+
return f"Every {hours:.1f}h"
|
|
173
|
+
else:
|
|
174
|
+
days = int(seconds / 86400)
|
|
175
|
+
return f"Every {days}d"
|
|
176
|
+
|
|
177
|
+
elif trigger_type == "CronTrigger":
|
|
178
|
+
if hasattr(trigger, "fields"):
|
|
179
|
+
parts = []
|
|
180
|
+
for field in trigger.fields:
|
|
181
|
+
field_str = str(field)
|
|
182
|
+
if field_str != "*":
|
|
183
|
+
parts.append(f"{field.name}={field_str}")
|
|
184
|
+
if parts:
|
|
185
|
+
return "Cron: " + ", ".join(parts)
|
|
186
|
+
return "Cron: * * * * *"
|
|
187
|
+
|
|
188
|
+
elif trigger_type == "DateTrigger":
|
|
189
|
+
if hasattr(trigger, "run_date"):
|
|
190
|
+
return f"Once at {trigger.run_date.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
191
|
+
|
|
192
|
+
return trigger_type.replace("Trigger", "")
|
|
193
|
+
|
|
194
|
+
def _get_trigger_type(self, trigger: Any) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Extract trigger type as string.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
trigger: APScheduler trigger object.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
str: Trigger type name (interval, cron, date, unknown).
|
|
203
|
+
"""
|
|
204
|
+
if not trigger:
|
|
205
|
+
return "unknown"
|
|
206
|
+
|
|
207
|
+
trigger_name = type(trigger).__name__
|
|
208
|
+
if "Interval" in trigger_name:
|
|
209
|
+
return "interval"
|
|
210
|
+
elif "Cron" in trigger_name:
|
|
211
|
+
return "cron"
|
|
212
|
+
elif "Date" in trigger_name:
|
|
213
|
+
return "date"
|
|
214
|
+
return "unknown"
|
|
215
|
+
|
|
216
|
+
# Placeholder methods for CLI functionality
|
|
217
|
+
async def get_job_statistics(self, job_id: str) -> dict[str, Any]:
|
|
218
|
+
"""Get statistics for a specific job."""
|
|
219
|
+
# TODO: Implement job-specific statistics
|
|
220
|
+
task = await self.get_task(job_id)
|
|
221
|
+
if not task:
|
|
222
|
+
return {}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"job_id": job_id,
|
|
226
|
+
"total_executions": 0, # Would need execution history table
|
|
227
|
+
"successful_executions": 0,
|
|
228
|
+
"failed_executions": 0,
|
|
229
|
+
"success_rate_percent": 0.0,
|
|
230
|
+
"avg_duration_ms": 0.0,
|
|
231
|
+
"min_duration_ms": 0.0,
|
|
232
|
+
"max_duration_ms": 0.0,
|
|
233
|
+
"last_execution": "Never",
|
|
234
|
+
"next_run": (
|
|
235
|
+
task.next_run_time.isoformat()
|
|
236
|
+
if task.next_run_time
|
|
237
|
+
else "Not scheduled"
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async def get_overall_statistics(self) -> dict[str, Any]:
|
|
242
|
+
"""Get overall scheduler statistics."""
|
|
243
|
+
stats = await self.get_statistics()
|
|
244
|
+
return {
|
|
245
|
+
"total_jobs": stats.total_tasks,
|
|
246
|
+
"active_jobs": stats.active_tasks,
|
|
247
|
+
"paused_jobs": stats.paused_tasks,
|
|
248
|
+
"total_executions": 0, # Would need execution history table
|
|
249
|
+
"successful_executions": 0,
|
|
250
|
+
"failed_executions": 0,
|
|
251
|
+
"success_rate_percent": 0.0,
|
|
252
|
+
"avg_duration_ms": 0.0,
|
|
253
|
+
"uptime": "Unknown",
|
|
254
|
+
"last_activity": "No recent activity",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async def get_job_history(
|
|
258
|
+
self, limit: int, job_id: str | None = None
|
|
259
|
+
) -> list[dict[str, Any]]:
|
|
260
|
+
"""Get recent job execution history."""
|
|
261
|
+
# TODO: Implement execution history - would need additional table
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
async def trigger_job(
|
|
265
|
+
self, job_id: str, wait: bool = True, timeout: int = 30
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""Trigger manual job execution."""
|
|
268
|
+
# TODO: Implement job triggering - would need scheduler integration
|
|
269
|
+
return {
|
|
270
|
+
"status": "error",
|
|
271
|
+
"error": "Manual job triggering not yet implemented",
|
|
272
|
+
}
|
|
273
|
+
{% endif %}
|