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.
Files changed (392) hide show
  1. aegis/__init__.py +5 -0
  2. aegis/__main__.py +51 -0
  3. aegis/cli/__init__.py +6 -0
  4. aegis/cli/callbacks.py +114 -0
  5. aegis/cli/interactive.py +611 -0
  6. aegis/cli/utils.py +70 -0
  7. aegis/cli/validators.py +34 -0
  8. aegis/commands/__init__.py +6 -0
  9. aegis/commands/add.py +353 -0
  10. aegis/commands/add_service.py +332 -0
  11. aegis/commands/components.py +35 -0
  12. aegis/commands/init.py +370 -0
  13. aegis/commands/remove.py +227 -0
  14. aegis/commands/services.py +52 -0
  15. aegis/commands/update.py +252 -0
  16. aegis/commands/version.py +12 -0
  17. aegis/config/__init__.py +1 -0
  18. aegis/config/shared_files.py +136 -0
  19. aegis/core/CLAUDE.md +377 -0
  20. aegis/core/__init__.py +6 -0
  21. aegis/core/component_files.py +228 -0
  22. aegis/core/component_utils.py +220 -0
  23. aegis/core/components.py +127 -0
  24. aegis/core/copier_manager.py +315 -0
  25. aegis/core/copier_updater.py +475 -0
  26. aegis/core/dependency_resolver.py +119 -0
  27. aegis/core/manual_updater.py +554 -0
  28. aegis/core/post_gen_tasks.py +547 -0
  29. aegis/core/service_resolver.py +261 -0
  30. aegis/core/services.py +157 -0
  31. aegis/core/template_generator.py +266 -0
  32. aegis/core/version_compatibility.py +259 -0
  33. aegis/templates/CLAUDE.md +591 -0
  34. aegis/templates/cookiecutter-aegis-project/cookiecutter.json +39 -0
  35. aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +214 -0
  36. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
  37. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +130 -0
  38. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +131 -0
  39. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  40. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +236 -0
  41. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
  42. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/alembic.ini.j2 +111 -0
  43. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/env.py.j2 +91 -0
  44. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/script.py.mako +25 -0
  45. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/versions/001_initial_auth.py.j2 +51 -0
  46. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
  47. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
  48. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai.py.j2 +700 -0
  49. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai_rendering.py +361 -0
  50. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/auth.py.j2 +253 -0
  51. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py.j2 +419 -0
  52. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py.j2 +656 -0
  53. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py.j2 +65 -0
  54. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/marko_terminal_renderer.py +489 -0
  55. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/tasks.py.j2 +328 -0
  56. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/{% if cookiecutter.include_scheduler == /"yes/" %}tasks.py{% endif %}" +340 -0
  57. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
  58. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
  59. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/__init__.py +8 -0
  60. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/router.py +329 -0
  61. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/__init__.py +1 -0
  62. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/router.py +64 -0
  63. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/deps.py +58 -0
  64. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +163 -0
  65. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +280 -0
  66. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +32 -0
  67. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/scheduler.py.j2 +121 -0
  68. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/worker.py.j2 +478 -0
  69. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +144 -0
  70. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +31 -0
  71. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
  72. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
  73. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
  74. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
  75. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
  76. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +418 -0
  77. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/database_init.py.j2 +83 -0
  78. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +5 -0
  79. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/__init__.py +27 -0
  80. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/table.py +78 -0
  81. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/text.py +142 -0
  82. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/__init__.py.j2 +47 -0
  83. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
  84. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
  85. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/base_card.py +256 -0
  86. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
  87. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
  88. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/database_card.py +420 -0
  89. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
  90. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
  91. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
  92. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
  93. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/services_card.py +233 -0
  94. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
  95. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py.j2 +653 -0
  96. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/theme.py +48 -0
  97. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
  98. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py.j2 +156 -0
  99. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md.j2 +213 -0
  100. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
  101. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
  102. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +97 -0
  103. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
  104. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +55 -0
  105. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +49 -0
  106. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +44 -0
  107. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
  108. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +120 -0
  109. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +507 -0
  110. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +33 -0
  111. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +281 -0
  112. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +178 -0
  113. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +58 -0
  114. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py.j2 +176 -0
  115. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +92 -0
  116. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/security.py +62 -0
  117. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
  118. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
  119. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
  120. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
  121. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +62 -0
  122. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/__init__.py +1 -0
  123. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/user.py +44 -0
  124. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
  125. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
  126. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/__init__.py +8 -0
  127. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/config.py +130 -0
  128. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/conversation.py +213 -0
  129. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/health.py +96 -0
  130. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/models.py +229 -0
  131. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/providers.py +370 -0
  132. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/service.py +388 -0
  133. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/__init__.py +1 -0
  134. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/auth_service.py +41 -0
  135. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/health.py +164 -0
  136. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/user_service.py +83 -0
  137. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/middleware_inspector.py.j2 +223 -0
  138. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/models.py.j2 +70 -0
  139. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/route_inspector.py.j2 +155 -0
  140. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +679 -0
  141. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +266 -0
  142. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/__init__.py.j2 +21 -0
  143. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/models.py.j2 +119 -0
  144. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/scheduled_task_manager.py.j2 +273 -0
  145. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/task_monitor.py.j2 +189 -0
  146. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
  147. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
  148. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
  149. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
  150. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/backup.py.j2 +119 -0
  151. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1333 -0
  152. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +243 -0
  153. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
  154. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-dark.png +0 -0
  155. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-square-backup.png +0 -0
  156. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto.png +0 -0
  157. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.dockerignore +71 -0
  158. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.env.example.j2 +64 -0
  159. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.gitignore +131 -0
  160. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Dockerfile +53 -0
  161. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Makefile +211 -0
  162. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/README.md.j2 +172 -0
  163. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/docker-compose.yml.j2 +78 -0
  164. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/mkdocs.yml.j2 +62 -0
  165. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/pyproject.toml.j2 +120 -0
  166. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/uv.lock +1673 -0
  167. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +200 -0
  168. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
  169. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +0 -0
  170. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md.j2 +621 -0
  171. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
  172. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
  173. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
  174. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
  175. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
  176. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +131 -0
  177. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
  178. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +93 -0
  179. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
  180. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
  181. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_auth_endpoints.py.j2 +307 -0
  182. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +262 -0
  183. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_scheduler_endpoints.py.j2 +214 -0
  184. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_worker_endpoints.py.j2 +165 -0
  185. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_ai_rendering.py +427 -0
  186. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_conversation_memory.py +465 -0
  187. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +43 -0
  188. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +195 -0
  189. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
  190. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/__init__.py +1 -0
  191. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/conftest.py +78 -0
  192. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_health.py +157 -0
  193. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_models.py +164 -0
  194. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_service.py +198 -0
  195. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_auth_integration.py.j2 +528 -0
  196. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +387 -0
  197. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_conversation_persistence.py +342 -0
  198. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +663 -0
  199. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +619 -0
  200. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +603 -0
  201. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_middleware_inspector.py.j2 +248 -0
  202. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_scheduled_task_manager.py.j2 +292 -0
  203. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +98 -0
  204. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +257 -0
  205. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +49 -0
  206. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
  207. aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja +21 -0
  208. aegis/templates/copier-aegis-project/{{ project_slug }}/.dockerignore +71 -0
  209. aegis/templates/copier-aegis-project/{{ project_slug }}/.env.example.jinja +130 -0
  210. aegis/templates/copier-aegis-project/{{ project_slug }}/.gitignore +131 -0
  211. aegis/templates/copier-aegis-project/{{ project_slug }}/Dockerfile +53 -0
  212. aegis/templates/copier-aegis-project/{{ project_slug }}/Makefile.jinja +236 -0
  213. aegis/templates/copier-aegis-project/{{ project_slug }}/README.md.jinja +196 -0
  214. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/alembic.ini.jinja +111 -0
  215. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/env.py.jinja +91 -0
  216. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/script.py.mako +25 -0
  217. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/versions/001_initial_auth.py.jinja +51 -0
  218. aegis/templates/copier-aegis-project/{{ project_slug }}/app/__init__.py.jinja +5 -0
  219. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/__init__.py.jinja +6 -0
  220. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai.py.jinja +700 -0
  221. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai_rendering.py +360 -0
  222. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/auth.py.jinja +253 -0
  223. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/health.py.jinja +419 -0
  224. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/load_test.py.jinja +656 -0
  225. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/main.py.jinja +65 -0
  226. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/marko_terminal_renderer.py +489 -0
  227. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/tasks.py.jinja +328 -0
  228. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/__init__.py +0 -0
  229. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/__init__.py +0 -0
  230. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/__init__.py +8 -0
  231. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/router.py +329 -0
  232. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/__init__.py +1 -0
  233. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/router.py +64 -0
  234. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/deps.py +58 -0
  235. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/health.py.jinja +163 -0
  236. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/models.py.jinja +280 -0
  237. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/routing.py.jinja +32 -0
  238. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/scheduler.py.jinja +121 -0
  239. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/worker.py.jinja +478 -0
  240. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/hooks.py +144 -0
  241. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/main.py +31 -0
  242. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/__init__.py +1 -0
  243. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/cors.py +20 -0
  244. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/__init__.py +1 -0
  245. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/cleanup.py +14 -0
  246. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/__init__.py +1 -0
  247. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/component_health.py.jinja +418 -0
  248. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/database_init.py.jinja +83 -0
  249. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/__init__.py +5 -0
  250. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/__init__.py +27 -0
  251. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/table.py +78 -0
  252. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/text.py +142 -0
  253. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/__init__.py.jinja +47 -0
  254. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
  255. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
  256. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/base_card.py +256 -0
  257. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
  258. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
  259. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/database_card.py +420 -0
  260. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
  261. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
  262. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
  263. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
  264. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/services_card.py +233 -0
  265. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
  266. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/main.py.jinja +653 -0
  267. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/theme.py +48 -0
  268. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/__init__.py +1 -0
  269. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/main.py.jinja +156 -0
  270. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/CLAUDE.md.jinja +213 -0
  271. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/__init__.py +6 -0
  272. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/constants.py.jinja +30 -0
  273. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/pools.py +97 -0
  274. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/__init__.py +1 -0
  275. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/load_test.py +55 -0
  276. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/media.py +49 -0
  277. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/system.py +44 -0
  278. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/registry.py +139 -0
  279. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/__init__.py +120 -0
  280. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/load_tasks.py +507 -0
  281. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/simple_system_tasks.py +33 -0
  282. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/system_tasks.py +281 -0
  283. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/config.py.jinja +178 -0
  284. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/constants.py +58 -0
  285. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/db.py.jinja +176 -0
  286. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/log.py +92 -0
  287. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/security.py +62 -0
  288. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/__init__.py +1 -0
  289. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/scheduler.py.jinja +21 -0
  290. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/webserver.py +39 -0
  291. aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/__init__.py +0 -0
  292. aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/main.py +61 -0
  293. aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/__init__.py +1 -0
  294. aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/user.py +44 -0
  295. aegis/templates/copier-aegis-project/{{ project_slug }}/app/py.typed +0 -0
  296. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/__init__.py +1 -0
  297. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/__init__.py +8 -0
  298. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/config.py +130 -0
  299. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/conversation.py +213 -0
  300. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/health.py +96 -0
  301. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/models.py +229 -0
  302. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/providers.py.jinja +370 -0
  303. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/service.py +387 -0
  304. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/__init__.py +1 -0
  305. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/auth_service.py +40 -0
  306. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/health.py +162 -0
  307. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/user_service.py +82 -0
  308. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/middleware_inspector.py.jinja +223 -0
  309. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/models.py.jinja +70 -0
  310. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/route_inspector.py.jinja +155 -0
  311. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test.py +678 -0
  312. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test_models.py +265 -0
  313. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/__init__.py.jinja +21 -0
  314. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/models.py.jinja +119 -0
  315. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/scheduled_task_manager.py.jinja +273 -0
  316. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/task_monitor.py.jinja +189 -0
  317. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/__init__.py +15 -0
  318. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/models.py +26 -0
  319. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/__init__.py +52 -0
  320. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/alerts.py +94 -0
  321. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/backup.py.jinja +119 -0
  322. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/health.py.jinja +1333 -0
  323. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/models.py +243 -0
  324. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/ui.py +52 -0
  325. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57223!aegis-manifesto.png +0 -0
  326. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57224!aegis-manifesto-dark.png +0 -0
  327. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57225!aegis-manifesto-square-backup.png +0 -0
  328. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57533!aegis-manifesto.png +0 -0
  329. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57534!aegis-manifesto-dark.png +0 -0
  330. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57538!aegis-manifesto-square-backup.png +0 -0
  331. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57897!aegis-manifesto.png +0 -0
  332. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57898!aegis-manifesto-dark.png +0 -0
  333. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57904!aegis-manifesto-square-backup.png +0 -0
  334. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58315!aegis-manifesto.png +0 -0
  335. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58316!aegis-manifesto-dark.png +0 -0
  336. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58324!aegis-manifesto-square-backup.png +0 -0
  337. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58837!aegis-manifesto.png +0 -0
  338. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58838!aegis-manifesto-dark.png +0 -0
  339. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58849!aegis-manifesto-square-backup.png +0 -0
  340. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-dark.png +0 -0
  341. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-square-backup.png +0 -0
  342. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto.png +0 -0
  343. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/.env.example.jinja +64 -0
  344. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/README.md.jinja +172 -0
  345. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/docker-compose.yml.jinja +78 -0
  346. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/mkdocs.yml.jinja +62 -0
  347. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/pyproject.toml.jinja +120 -0
  348. aegis/templates/copier-aegis-project/{{ project_slug }}/docker-compose.yml.jinja +200 -0
  349. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/api.md.jinja +191 -0
  350. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md +0 -0
  351. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md.jinja +621 -0
  352. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/development.md.jinja +215 -0
  353. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/health.md.jinja +240 -0
  354. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/javascripts/mermaid-config.js +62 -0
  355. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/stylesheets/mermaid.css +95 -0
  356. aegis/templates/copier-aegis-project/{{ project_slug }}/mkdocs.yml.jinja +62 -0
  357. aegis/templates/copier-aegis-project/{{ project_slug }}/pyproject.toml.jinja +131 -0
  358. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh +87 -0
  359. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh.jinja +93 -0
  360. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/gen_docs.py +16 -0
  361. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/__init__.py +1 -0
  362. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_auth_endpoints.py.jinja +307 -0
  363. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_health_endpoints.py.jinja +262 -0
  364. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_scheduler_endpoints.py.jinja +214 -0
  365. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_worker_endpoints.py.jinja +165 -0
  366. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_ai_rendering.py +427 -0
  367. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_conversation_memory.py +465 -0
  368. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/components/test_scheduler.py +43 -0
  369. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/conftest.py.jinja +195 -0
  370. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/__init__.py +1 -0
  371. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/__init__.py +1 -0
  372. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/conftest.py +78 -0
  373. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_health.py +157 -0
  374. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_models.py +164 -0
  375. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_service.py +198 -0
  376. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_auth_integration.py.jinja +528 -0
  377. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_component_integration.py.jinja +387 -0
  378. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_conversation_persistence.py +342 -0
  379. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_health_logic.py.jinja +663 -0
  380. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_models.py +619 -0
  381. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_service.py +603 -0
  382. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_middleware_inspector.py.jinja +248 -0
  383. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_scheduled_task_manager.py.jinja +292 -0
  384. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_system_service.py +98 -0
  385. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_worker_health_registration.py.jinja +257 -0
  386. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/test_core.py +49 -0
  387. aegis/templates/copier-aegis-project/{{ project_slug }}/uv.lock +1673 -0
  388. aegis_stack-0.2.0rc2.dist-info/METADATA +165 -0
  389. aegis_stack-0.2.0rc2.dist-info/RECORD +392 -0
  390. aegis_stack-0.2.0rc2.dist-info/WHEEL +4 -0
  391. aegis_stack-0.2.0rc2.dist-info/entry_points.txt +3 -0
  392. 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
+ }
@@ -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 include_redis %}
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 include_worker %}
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 include_database %}
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 include_ai %}
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 include_worker %}
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 %}
@@ -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