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,1333 @@
1
+ """
2
+ System health monitoring functions.
3
+
4
+ Pure functions for system health checking, monitoring, and status reporting.
5
+ All functions use Pydantic models for type safety and validation.
6
+ """
7
+
8
+ import asyncio
9
+ from collections.abc import Awaitable, Callable
10
+ from datetime import UTC, datetime
11
+ import os
12
+ import sqlite3
13
+ import sys
14
+ from typing import Any, cast
15
+
16
+ import psutil
17
+
18
+ from app.core.config import settings
19
+ from app.core.log import logger
20
+
21
+ from .alerts import send_critical_alert, send_health_alert
22
+ from .models import ComponentStatus, ComponentStatusType, SystemStatus
23
+
24
+ # Global registry for custom health checks
25
+ _health_checks: dict[str, Callable[[], Awaitable[ComponentStatus]]] = {}
26
+
27
+ # Global registry for service health checks
28
+ _service_health_checks: dict[str, Callable[[], Awaitable[ComponentStatus]]] = {}
29
+
30
+
31
+ def format_bytes(size: int) -> str:
32
+ """Format bytes into human-readable string."""
33
+ if size == 0:
34
+ return "0 B"
35
+
36
+ size_float = float(size)
37
+ for unit in ['B', 'KB', 'MB', 'GB']:
38
+ if size_float < 1024.0:
39
+ if unit == 'B':
40
+ return f"{int(size_float)} {unit}"
41
+ else:
42
+ return f"{size_float:.1f} {unit}"
43
+ size_float /= 1024.0
44
+ return f"{size_float:.1f} TB"
45
+
46
+
47
+ def propagate_status(child_statuses: list[ComponentStatusType]) -> ComponentStatusType:
48
+ """
49
+ Determine parent status from child statuses using standard hierarchy.
50
+
51
+ Status priority (highest to lowest):
52
+ 1. UNHEALTHY - Any unhealthy child makes parent unhealthy
53
+ 2. WARNING - Any warning child makes parent warning (if no unhealthy)
54
+ 3. INFO - Any info child makes parent info (if no unhealthy/warning)
55
+ 4. HEALTHY - All children healthy makes parent healthy
56
+
57
+ Args:
58
+ child_statuses: List of ComponentStatusType from child components
59
+
60
+ Returns:
61
+ ComponentStatusType for the parent component
62
+ """
63
+ if not child_statuses:
64
+ return ComponentStatusType.HEALTHY
65
+
66
+ if any(status == ComponentStatusType.UNHEALTHY for status in child_statuses):
67
+ return ComponentStatusType.UNHEALTHY
68
+ elif any(status == ComponentStatusType.WARNING for status in child_statuses):
69
+ return ComponentStatusType.WARNING
70
+ elif any(status == ComponentStatusType.INFO for status in child_statuses):
71
+ return ComponentStatusType.INFO
72
+ elif all(status == ComponentStatusType.HEALTHY for status in child_statuses):
73
+ return ComponentStatusType.HEALTHY
74
+ else:
75
+ return ComponentStatusType.HEALTHY # Default for edge cases
76
+
77
+ # Cache for system metrics to improve performance
78
+ _system_metrics_cache: dict[str, tuple[ComponentStatus, datetime]] = {}
79
+
80
+
81
+ def register_health_check(
82
+ name: str, check_fn: Callable[[], Awaitable[ComponentStatus]]
83
+ ) -> None:
84
+ """
85
+ Register a custom health check function.
86
+
87
+ Args:
88
+ name: Unique name for the health check
89
+ check_fn: Async function that returns ComponentStatus or bool
90
+ """
91
+ _health_checks[name] = check_fn
92
+ logger.info(f"Registered custom health check: {name}")
93
+
94
+
95
+ def register_service_health_check(
96
+ name: str, check_fn: Callable[[], Awaitable[ComponentStatus]]
97
+ ) -> None:
98
+ """
99
+ Register a service health check function.
100
+
101
+ Args:
102
+ name: Unique name for the service health check
103
+ check_fn: Async function that returns ComponentStatus
104
+ """
105
+ _service_health_checks[name] = check_fn
106
+ logger.info(f"Registered service health check: {name}")
107
+
108
+
109
+ async def get_system_status() -> SystemStatus:
110
+ """
111
+ Get comprehensive system status.
112
+
113
+ Returns:
114
+ SystemStatus with all component health information organized as Aegis tree
115
+ """
116
+ start_time = datetime.now(UTC)
117
+
118
+ # Run custom component checks (these are top-level components)
119
+ component_results = {}
120
+ component_tasks = []
121
+ for name, check_fn in _health_checks.items():
122
+ task = asyncio.create_task(_run_health_check(name, check_fn))
123
+ component_tasks.append((name, task))
124
+
125
+ # Collect component results
126
+ for name, task in component_tasks:
127
+ try:
128
+ component_results[name] = await task
129
+ except Exception as e:
130
+ logger.error(f"Component check failed for {name}: {e}")
131
+ component_results[name] = ComponentStatus(
132
+ name=name,
133
+ status=ComponentStatusType.UNHEALTHY,
134
+ message=f"Health check failed: {str(e)}",
135
+ response_time_ms=None,
136
+ )
137
+
138
+ # Run service health checks
139
+ service_results = {}
140
+ service_tasks = []
141
+ for name, check_fn in _service_health_checks.items():
142
+ task = asyncio.create_task(_run_health_check(name, check_fn))
143
+ service_tasks.append((name, task))
144
+
145
+ # Collect service results
146
+ for name, task in service_tasks:
147
+ try:
148
+ service_results[name] = await task
149
+ except Exception as e:
150
+ logger.error(f"Service check failed for {name}: {e}")
151
+ service_results[name] = ComponentStatus(
152
+ name=name,
153
+ status=ComponentStatusType.UNHEALTHY,
154
+ message=f"Service health check failed: {str(e)}",
155
+ response_time_ms=None,
156
+ )
157
+
158
+ # Get system metrics (with caching for performance)
159
+ system_metrics = await _get_cached_system_metrics(start_time)
160
+
161
+ # Group system metrics under backend component if it exists
162
+ if "backend" in component_results:
163
+ # Backend exists - recreate with system metrics as sub-components
164
+ backend_component = component_results["backend"]
165
+
166
+ # Propagate status from system metrics and original backend status
167
+ system_metrics_statuses = [
168
+ getattr(metric, 'status', ComponentStatusType.HEALTHY)
169
+ for metric in system_metrics.values()
170
+ ]
171
+ original_backend_status = getattr(
172
+ backend_component, 'status', ComponentStatusType.HEALTHY
173
+ )
174
+ all_backend_statuses = system_metrics_statuses + [original_backend_status]
175
+
176
+ backend_status = propagate_status(all_backend_statuses)
177
+
178
+ component_results["backend"] = ComponentStatus(
179
+ name=backend_component.name,
180
+ status=backend_status,
181
+ message=backend_component.message,
182
+ response_time_ms=backend_component.response_time_ms,
183
+ metadata=backend_component.metadata,
184
+ sub_components=system_metrics,
185
+ )
186
+ else:
187
+ # Backend doesn't exist - create a virtual backend component to hold
188
+ # system metrics
189
+ backend_healthy = all(metric.healthy for metric in system_metrics.values())
190
+
191
+ # Propagate status from system metrics only
192
+ system_metrics_statuses = [
193
+ getattr(metric, 'status', ComponentStatusType.HEALTHY)
194
+ for metric in system_metrics.values()
195
+ ]
196
+ backend_status = propagate_status(system_metrics_statuses)
197
+
198
+ backend_message = (
199
+ "System container metrics"
200
+ if backend_healthy
201
+ else "System container has issues"
202
+ )
203
+
204
+ component_results["backend"] = ComponentStatus(
205
+ name="backend",
206
+ status=backend_status,
207
+ message=backend_message,
208
+ response_time_ms=None,
209
+ metadata={"type": "system_container", "virtual": True},
210
+ sub_components=system_metrics,
211
+ )
212
+
213
+ # Calculate overall health (including sub-components and services)
214
+ all_statuses = list(component_results.values()) + list(service_results.values())
215
+ for component in component_results.values():
216
+ all_statuses.extend(component.sub_components.values())
217
+ overall_healthy = all(status.healthy for status in all_statuses)
218
+
219
+ # Create Aegis root structure with components underneath
220
+ aegis_healthy = all(status.healthy for status in all_statuses)
221
+
222
+ # Propagate status from all top-level components and services
223
+ component_statuses = [
224
+ getattr(component, 'status', ComponentStatusType.HEALTHY)
225
+ for component in component_results.values()
226
+ ]
227
+ service_statuses = [
228
+ getattr(service, 'status', ComponentStatusType.HEALTHY)
229
+ for service in service_results.values()
230
+ ]
231
+ all_top_level_statuses = component_statuses + service_statuses
232
+ aegis_status = propagate_status(all_top_level_statuses)
233
+
234
+ aegis_message = (
235
+ "Aegis Stack application" if aegis_healthy else "Aegis Stack has issues"
236
+ )
237
+
238
+ # Create aegis sub-components structure with components and services grouped
239
+ aegis_sub_components = {}
240
+
241
+ # Group all components under "components" if any exist
242
+ if component_results:
243
+ components_status = propagate_status(component_statuses)
244
+ components_healthy = all(
245
+ component.healthy for component in component_results.values()
246
+ )
247
+ components_message = (
248
+ f"{len(component_results)} components available"
249
+ if components_healthy
250
+ else "Some components have issues"
251
+ )
252
+
253
+ aegis_sub_components["components"] = ComponentStatus(
254
+ name="components",
255
+ status=components_status,
256
+ message=components_message,
257
+ response_time_ms=None,
258
+ metadata={
259
+ "type": "components_group",
260
+ "total_components": len(component_results),
261
+ "component_names": list(component_results.keys()),
262
+ },
263
+ sub_components=component_results,
264
+ )
265
+
266
+ # Add services as a sub-component if any services are registered
267
+ if service_results:
268
+ # Determine services component status
269
+ services_status = propagate_status(service_statuses)
270
+ services_healthy = all(service.healthy for service in service_results.values())
271
+ services_message = (
272
+ f"{len(service_results)} services available"
273
+ if services_healthy
274
+ else "Some services have issues"
275
+ )
276
+
277
+ aegis_sub_components["services"] = ComponentStatus(
278
+ name="services",
279
+ status=services_status,
280
+ message=services_message,
281
+ response_time_ms=None,
282
+ metadata={
283
+ "type": "services_group",
284
+ "total_services": len(service_results),
285
+ "service_names": list(service_results.keys()),
286
+ },
287
+ sub_components=service_results,
288
+ )
289
+
290
+ root_components = {
291
+ "aegis": ComponentStatus(
292
+ name="aegis",
293
+ status=aegis_status,
294
+ message=aegis_message,
295
+ response_time_ms=None,
296
+ metadata={"type": "application_root", "version": "1.0"},
297
+ sub_components=aegis_sub_components,
298
+ )
299
+ }
300
+
301
+ # Get system information
302
+ system_info = _get_system_info()
303
+
304
+ status = SystemStatus(
305
+ components=root_components,
306
+ overall_healthy=overall_healthy,
307
+ timestamp=start_time,
308
+ system_info=system_info,
309
+ )
310
+
311
+ # Log unhealthy components
312
+ if not overall_healthy:
313
+ logger.warning(
314
+ f"System unhealthy: {status.unhealthy_components}",
315
+ extra={"unhealthy_components": status.unhealthy_components},
316
+ )
317
+
318
+ return status
319
+
320
+
321
+ async def is_system_healthy() -> bool:
322
+ """Quick check if system is overall healthy."""
323
+ status = await get_system_status()
324
+ return status.overall_healthy
325
+
326
+
327
+ async def check_system_status() -> None:
328
+ """
329
+ Scheduled health check function for use in APScheduler jobs.
330
+
331
+ This function gets the system status and logs any issues.
332
+ Can be extended to send alerts to Slack, email, etc.
333
+ """
334
+ logger.info("🩺 Running scheduled system health check")
335
+
336
+ try:
337
+ status = await get_system_status()
338
+
339
+ if status.overall_healthy:
340
+ log_msg = (
341
+ f"✅ System healthy: {len(status.healthy_top_level_components)}/"
342
+ f"{status.total_components} components OK"
343
+ )
344
+ logger.info(log_msg)
345
+ else:
346
+ logger.warning(
347
+ f"⚠️ System issues detected: "
348
+ f"{len(status.unhealthy_components)} unhealthy components",
349
+ extra={
350
+ "unhealthy_components": status.unhealthy_components,
351
+ "health_percentage": status.health_percentage,
352
+ },
353
+ )
354
+
355
+ # Log details for each unhealthy component
356
+ for component_name in status.unhealthy_components:
357
+ component = status.components[component_name]
358
+ logger.error(
359
+ f"❌ {component_name}: {component.message}",
360
+ extra={"component": component.name, "metadata": component.metadata},
361
+ )
362
+
363
+ # Send health alerts
364
+ await send_health_alert(status)
365
+
366
+ except Exception as e:
367
+ logger.error(f"💥 System health check failed: {e}")
368
+ # Send critical alert about monitoring failure
369
+ await send_critical_alert(f"Health monitoring failed: {e}", str(e))
370
+
371
+
372
+ async def _get_cached_system_metrics(
373
+ current_time: datetime,
374
+ ) -> dict[str, ComponentStatus]:
375
+ """Get system metrics with caching for better performance."""
376
+ cache_duration = settings.SYSTEM_METRICS_CACHE_SECONDS
377
+ system_metric_checks = {
378
+ "memory": _check_memory,
379
+ "disk": _check_disk_space,
380
+ "cpu": _check_cpu_usage,
381
+ }
382
+
383
+ system_metrics = {}
384
+ tasks = []
385
+
386
+ for name, check_fn in system_metric_checks.items():
387
+ # Check if we have a valid cached result
388
+ if name in _system_metrics_cache:
389
+ cached_result, cached_time = _system_metrics_cache[name]
390
+ age_seconds = (current_time - cached_time).total_seconds()
391
+
392
+ if age_seconds < cache_duration:
393
+ # Use cached result
394
+ system_metrics[name] = cached_result
395
+ continue
396
+
397
+ # Need to run the check
398
+ task = asyncio.create_task(
399
+ _run_health_check_with_cache(name, check_fn, current_time)
400
+ )
401
+ tasks.append((name, task))
402
+
403
+ # Collect results from non-cached checks
404
+ for name, task in tasks:
405
+ try:
406
+ system_metrics[name] = await task
407
+ except Exception as e:
408
+ logger.error(f"System metric check failed for {name}: {e}")
409
+ system_metrics[name] = ComponentStatus(
410
+ name=name,
411
+ status=ComponentStatusType.UNHEALTHY,
412
+ message=f"Health check failed: {str(e)}",
413
+ response_time_ms=None,
414
+ )
415
+
416
+ return system_metrics
417
+
418
+
419
+ async def _run_health_check_with_cache(
420
+ name: str, check_fn: Callable[[], Awaitable[ComponentStatus]], timestamp: datetime
421
+ ) -> ComponentStatus:
422
+ """Run health check and cache the result."""
423
+ result = await _run_health_check(name, check_fn)
424
+ _system_metrics_cache[name] = (result, timestamp)
425
+ return result
426
+
427
+
428
+ async def _run_health_check(
429
+ name: str, check_fn: Callable[[], Awaitable[ComponentStatus]]
430
+ ) -> ComponentStatus:
431
+ """Run a single health check with timing."""
432
+ start_time = datetime.now(UTC)
433
+ try:
434
+ result = await check_fn()
435
+ end_time = datetime.now(UTC)
436
+ response_time = (end_time - start_time).total_seconds() * 1000
437
+
438
+ if isinstance(result, ComponentStatus):
439
+ result.response_time_ms = response_time
440
+ return result
441
+ else:
442
+ return ComponentStatus(
443
+ name=name,
444
+ status=(
445
+ ComponentStatusType.HEALTHY if bool(result)
446
+ else ComponentStatusType.UNHEALTHY
447
+ ),
448
+ message="OK" if result else "Failed",
449
+ response_time_ms=response_time,
450
+ )
451
+ except Exception as e:
452
+ end_time = datetime.now(UTC)
453
+ response_time = (end_time - start_time).total_seconds() * 1000
454
+ return ComponentStatus(
455
+ name=name,
456
+ status=ComponentStatusType.UNHEALTHY,
457
+ message=f"Error: {str(e)}",
458
+ response_time_ms=response_time,
459
+ )
460
+
461
+
462
+ def _get_system_info() -> dict[str, Any]:
463
+ """Get general system information."""
464
+ try:
465
+ return {
466
+ "python_version": (
467
+ f"{sys.version_info.major}."
468
+ f"{sys.version_info.minor}."
469
+ f"{sys.version_info.micro}"
470
+ ),
471
+ "platform": psutil.WINDOWS if psutil.WINDOWS else "unix",
472
+ "containerized": "docker" if os.path.exists("/.dockerenv") else "false",
473
+ }
474
+ except Exception as e:
475
+ logger.warning(f"Failed to get system info: {e}")
476
+ return {"error": str(e)}
477
+
478
+
479
+ async def _check_memory() -> ComponentStatus:
480
+ """Check system memory usage."""
481
+ try:
482
+ # Run in thread to avoid blocking
483
+ memory = await asyncio.to_thread(psutil.virtual_memory)
484
+ memory_percent = memory.percent
485
+
486
+ # Determine status based on memory usage thresholds
487
+ if memory_percent >= settings.MEMORY_THRESHOLD_PERCENT:
488
+ status = ComponentStatusType.UNHEALTHY
489
+ elif memory_percent >= settings.MEMORY_THRESHOLD_PERCENT * 0.8:
490
+ status = ComponentStatusType.WARNING
491
+ else:
492
+ status = ComponentStatusType.HEALTHY
493
+
494
+ return ComponentStatus(
495
+ name="memory",
496
+ status=status,
497
+ message=f"Memory usage: {memory_percent:.1f}%",
498
+ response_time_ms=None,
499
+ metadata={
500
+ "percent_used": memory_percent,
501
+ "total_gb": round(memory.total / (1024**3), 2),
502
+ "available_gb": round(memory.available / (1024**3), 2),
503
+ "threshold_percent": settings.MEMORY_THRESHOLD_PERCENT,
504
+ },
505
+ )
506
+ except Exception as e:
507
+ return ComponentStatus(
508
+ name="memory",
509
+ status=ComponentStatusType.UNHEALTHY,
510
+ message=f"Failed to check memory: {e}",
511
+ response_time_ms=None,
512
+ )
513
+
514
+
515
+ async def _check_disk_space() -> ComponentStatus:
516
+ """Check disk space usage."""
517
+ try:
518
+ # Run in thread to avoid blocking
519
+ disk = await asyncio.to_thread(psutil.disk_usage, "/")
520
+ disk_percent = (disk.used / disk.total) * 100
521
+
522
+ # Determine status based on disk usage thresholds
523
+ if disk_percent >= settings.DISK_THRESHOLD_PERCENT:
524
+ status = ComponentStatusType.UNHEALTHY
525
+ elif disk_percent >= settings.DISK_THRESHOLD_PERCENT * 0.8:
526
+ status = ComponentStatusType.WARNING
527
+ else:
528
+ status = ComponentStatusType.HEALTHY
529
+
530
+ return ComponentStatus(
531
+ name="disk",
532
+ status=status,
533
+ message=f"Disk usage: {disk_percent:.1f}%",
534
+ response_time_ms=None,
535
+ metadata={
536
+ "percent_used": disk_percent,
537
+ "total_gb": round(disk.total / (1024**3), 2),
538
+ "free_gb": round(disk.free / (1024**3), 2),
539
+ "threshold_percent": settings.DISK_THRESHOLD_PERCENT,
540
+ },
541
+ )
542
+ except Exception as e:
543
+ return ComponentStatus(
544
+ name="disk",
545
+ status=ComponentStatusType.UNHEALTHY,
546
+ message=f"Failed to check disk space: {e}",
547
+ response_time_ms=None,
548
+ )
549
+
550
+
551
+ async def _check_cpu_usage() -> ComponentStatus:
552
+ """Check CPU usage (instant sampling)."""
553
+ try:
554
+ # Get instant CPU usage (non-blocking, immediate reading)
555
+ cpu_percent = await asyncio.to_thread(psutil.cpu_percent, None)
556
+
557
+ # Determine status based on CPU usage thresholds
558
+ if cpu_percent >= settings.CPU_THRESHOLD_PERCENT:
559
+ status = ComponentStatusType.UNHEALTHY
560
+ elif cpu_percent >= settings.CPU_THRESHOLD_PERCENT * 0.8:
561
+ status = ComponentStatusType.WARNING
562
+ else:
563
+ status = ComponentStatusType.HEALTHY
564
+
565
+ return ComponentStatus(
566
+ name="cpu",
567
+ status=status,
568
+ message=f"CPU usage: {cpu_percent:.1f}%",
569
+ response_time_ms=None,
570
+ metadata={
571
+ "percent_used": cpu_percent,
572
+ "cpu_count": psutil.cpu_count(),
573
+ "threshold_percent": settings.CPU_THRESHOLD_PERCENT,
574
+ },
575
+ )
576
+ except Exception as e:
577
+ return ComponentStatus(
578
+ name="cpu",
579
+ status=ComponentStatusType.UNHEALTHY,
580
+ message=f"Failed to check CPU usage: {e}",
581
+ response_time_ms=None,
582
+ )
583
+
584
+
585
+ {% if cookiecutter.include_redis == "yes" %}
586
+ async def check_cache_health() -> ComponentStatus:
587
+ """
588
+ Check cache connectivity and basic functionality.
589
+
590
+ Returns:
591
+ ComponentStatus indicating cache health
592
+ """
593
+ try:
594
+ import redis.asyncio as aioredis
595
+
596
+ # Create Redis connection with timeout
597
+ redis_url = (
598
+ settings.redis_url_effective
599
+ if hasattr(settings, 'redis_url_effective')
600
+ else settings.REDIS_URL
601
+ )
602
+ redis_connection = aioredis.from_url( # type: ignore[no-untyped-call]
603
+ redis_url,
604
+ db=settings.REDIS_DB,
605
+ socket_timeout=settings.HEALTH_CHECK_TIMEOUT_SECONDS,
606
+ socket_connect_timeout=settings.HEALTH_CHECK_TIMEOUT_SECONDS,
607
+ )
608
+ redis_client: aioredis.Redis = cast(aioredis.Redis, redis_connection)
609
+
610
+ start_time = datetime.now(UTC)
611
+
612
+ # Test basic connectivity with ping
613
+ await redis_client.ping()
614
+
615
+ # Test basic set/get functionality
616
+ test_key = "health_check:test"
617
+ test_value = f"test_{start_time.timestamp()}"
618
+ await redis_client.set(test_key, test_value, ex=10) # Expire in 10 seconds
619
+ retrieved_value = await redis_client.get(test_key)
620
+
621
+ # Cleanup test key
622
+ await redis_client.delete(test_key)
623
+ await redis_client.aclose()
624
+
625
+ # Verify test worked
626
+ if retrieved_value.decode() != test_value:
627
+ raise Exception("Redis set/get test failed")
628
+
629
+ # Get comprehensive Redis info for detailed monitoring
630
+ redis_info_connection = aioredis.from_url( # type: ignore[no-untyped-call]
631
+ redis_url, db=settings.REDIS_DB
632
+ )
633
+ redis_info_client: aioredis.Redis = cast(aioredis.Redis, redis_info_connection)
634
+
635
+ # Get multiple INFO sections for comprehensive metrics
636
+ info = await redis_info_client.info()
637
+ stats_info = await redis_info_client.info('stats')
638
+ memory_info = await redis_info_client.info('memory')
639
+ clients_info = await redis_info_client.info('clients')
640
+ keyspace_info = await redis_info_client.info('keyspace')
641
+
642
+ await redis_info_client.aclose()
643
+
644
+ # Calculate derived metrics
645
+ keyspace_hits = stats_info.get('keyspace_hits', 0)
646
+ keyspace_misses = stats_info.get('keyspace_misses', 0)
647
+ total_keyspace_ops = keyspace_hits + keyspace_misses
648
+ hit_rate = (keyspace_hits / max(total_keyspace_ops, 1)) * 100
649
+
650
+ # Extract total keys from all databases
651
+ total_keys = 0
652
+ keys_with_expiry = 0
653
+ for key, value in keyspace_info.items():
654
+ if key.startswith('db'):
655
+ # Redis info('keyspace') returns nested dict format
656
+ if isinstance(value, dict):
657
+ total_keys += value.get('keys', 0)
658
+ keys_with_expiry += value.get('expires', 0)
659
+
660
+ # Memory usage calculations
661
+ used_memory = memory_info.get('used_memory', 0)
662
+ used_memory_peak = memory_info.get('used_memory_peak', 0)
663
+ mem_fragmentation_ratio = memory_info.get('mem_fragmentation_ratio', 1.0)
664
+
665
+ return ComponentStatus(
666
+ name="cache",
667
+ status=ComponentStatusType.HEALTHY,
668
+ message="Redis cache connection and operations successful",
669
+ response_time_ms=None, # Will be set by caller
670
+ metadata={
671
+ "implementation": "redis",
672
+ "version": info.get("redis_version", "unknown"),
673
+ "url": redis_url,
674
+ "db": settings.REDIS_DB,
675
+
676
+ # Connection and client metrics
677
+ "connected_clients": clients_info.get("connected_clients", 0),
678
+ "blocked_clients": clients_info.get("blocked_clients", 0),
679
+ "client_longest_output_list": clients_info.get(
680
+ "client_longest_output_list", 0
681
+ ),
682
+
683
+ # Server uptime
684
+ "uptime_in_seconds": info.get("uptime_in_seconds", 0),
685
+
686
+ # Memory metrics
687
+ "used_memory": used_memory,
688
+ "used_memory_human": memory_info.get("used_memory_human", "unknown"),
689
+ "used_memory_peak": used_memory_peak,
690
+ "used_memory_peak_human": memory_info.get(
691
+ "used_memory_peak_human", "unknown"
692
+ ),
693
+ "mem_fragmentation_ratio": mem_fragmentation_ratio,
694
+ "maxmemory": memory_info.get("maxmemory", 0),
695
+ "maxmemory_human": memory_info.get("maxmemory_human", "0B"),
696
+
697
+ # Performance and cache metrics
698
+ "instantaneous_ops_per_sec": stats_info.get(
699
+ "instantaneous_ops_per_sec", 0
700
+ ),
701
+ "keyspace_hits": keyspace_hits,
702
+ "keyspace_misses": keyspace_misses,
703
+ "hit_rate_percent": hit_rate,
704
+ "evicted_keys": stats_info.get("evicted_keys", 0),
705
+ "expired_keys": stats_info.get("expired_keys", 0),
706
+
707
+ # Keyspace statistics
708
+ "total_keys": total_keys,
709
+ "keys_with_expiry": keys_with_expiry,
710
+
711
+ # Additional useful stats
712
+ "total_commands_processed": stats_info.get(
713
+ "total_commands_processed", 0
714
+ ),
715
+ "total_connections_received": stats_info.get(
716
+ "total_connections_received", 0
717
+ ),
718
+ "rejected_connections": stats_info.get("rejected_connections", 0),
719
+ },
720
+ )
721
+
722
+ except ImportError:
723
+ return ComponentStatus(
724
+ name="cache",
725
+ status=ComponentStatusType.UNHEALTHY,
726
+ message="Cache library not installed",
727
+ response_time_ms=None,
728
+ metadata={
729
+ "implementation": "redis",
730
+ "error": "Redis library not available",
731
+ },
732
+ )
733
+ except Exception as e:
734
+ return ComponentStatus(
735
+ name="cache",
736
+ status=ComponentStatusType.UNHEALTHY,
737
+ message=f"Cache health check failed: {str(e)}",
738
+ response_time_ms=None,
739
+ metadata={
740
+ "implementation": "redis",
741
+ "url": (
742
+ settings.redis_url_effective
743
+ if hasattr(settings, 'redis_url_effective')
744
+ else settings.REDIS_URL
745
+ ),
746
+ "db": settings.REDIS_DB,
747
+ "error": str(e),
748
+ # Provide fallback values for card display
749
+ "connected_clients": 0,
750
+ "used_memory_human": "unknown",
751
+ "uptime_in_seconds": 0,
752
+ "instantaneous_ops_per_sec": 0,
753
+ "hit_rate_percent": 0,
754
+ "total_keys": 0,
755
+ "evicted_keys": 0,
756
+ "expired_keys": 0,
757
+ },
758
+ )
759
+ {% endif %}
760
+
761
+
762
+ {% if cookiecutter.include_database == "yes" %}
763
+ async def check_database_health() -> ComponentStatus:
764
+ """
765
+ Check database connectivity and basic functionality.
766
+
767
+ Returns:
768
+ ComponentStatus indicating database health
769
+ """
770
+ try:
771
+ # Import db_session from generated project
772
+ from app.core.db import db_session
773
+ from pathlib import Path
774
+
775
+ # Check if database file exists for SQLite
776
+ db_url = settings.DATABASE_URL
777
+ if db_url.startswith("sqlite:///"):
778
+ # Extract path from SQLite URL
779
+ db_path = db_url.replace("sqlite:///", "").lstrip("./")
780
+
781
+ # Check if database file exists
782
+ if not Path(db_path).exists():
783
+ return ComponentStatus(
784
+ name="database",
785
+ status=ComponentStatusType.WARNING,
786
+ message="Database not initialized - file does not exist",
787
+ response_time_ms=None,
788
+ metadata={
789
+ "implementation": "sqlite",
790
+ "database_exists": False,
791
+ "expected_path": db_path,
792
+ "url": settings.DATABASE_URL,
793
+ "recommendation": (
794
+ "Run database migrations or create database file"
795
+ ),
796
+ },
797
+ )
798
+
799
+ # Test database connection with simple query and collect enhanced metadata
800
+ enhanced_metadata = {
801
+ "implementation": "sqlite",
802
+ "url": settings.DATABASE_URL,
803
+ "database_exists": True,
804
+ "engine_echo": settings.DATABASE_ENGINE_ECHO,
805
+ }
806
+
807
+ # Collect additional metadata for SQLite databases
808
+ if db_url.startswith("sqlite:///"):
809
+ try:
810
+ # Add SQLite version
811
+ enhanced_metadata["version"] = sqlite3.sqlite_version
812
+
813
+ # Extract and add file size information
814
+ db_path = db_url.replace("sqlite:///", "").lstrip("./")
815
+ if Path(db_path).exists():
816
+ file_size = Path(db_path).stat().st_size
817
+ enhanced_metadata["file_size_bytes"] = file_size
818
+ enhanced_metadata["file_size_human"] = format_bytes(file_size)
819
+
820
+ # Get engine and connection pool information
821
+ from app.core.db import engine
822
+ if hasattr(engine.pool, 'size'):
823
+ enhanced_metadata["connection_pool_size"] = engine.pool.size()
824
+ else:
825
+ # SQLite typically uses NullPool or StaticPool with size 1
826
+ enhanced_metadata["connection_pool_size"] = 1
827
+
828
+ except Exception as e:
829
+ # If any enhanced metadata collection fails, log but don't break
830
+ # health check
831
+ logger.debug(
832
+ "Failed to collect enhanced database metadata", exc_info=True
833
+ )
834
+
835
+ # Test database connection and collect PRAGMA settings
836
+ with db_session(autocommit=False) as session:
837
+ # Execute a simple query to test connectivity
838
+ from sqlalchemy import text
839
+ session.execute(text("SELECT 1"))
840
+
841
+ # Collect table information for SQLite databases
842
+ table_info = []
843
+ if db_url.startswith("sqlite:///"):
844
+ try:
845
+ # Get all table names and row counts
846
+ from sqlalchemy import inspect
847
+ inspector = inspect(session.bind)
848
+ if inspector is None:
849
+ # Skip table introspection if inspector is None
850
+ table_names = []
851
+ else:
852
+ table_names = inspector.get_table_names()
853
+
854
+ for table_name in table_names:
855
+ try:
856
+ # Get row count for each table using quoted identifier
857
+ from sqlalchemy import quoted_name
858
+ quoted_table = quoted_name(table_name, quote=True)
859
+ count_result = session.execute(
860
+ text(f"SELECT COUNT(*) FROM {quoted_table}")
861
+ ).fetchone()
862
+ row_count = count_result[0] if count_result else 0
863
+
864
+ table_info.append({
865
+ "name": table_name,
866
+ "rows": row_count
867
+ })
868
+ except Exception as e:
869
+ # If we can't count rows for a table, still include it
870
+ logger.debug(
871
+ f"Failed to count rows for table {table_name}: {e}"
872
+ )
873
+ table_info.append({
874
+ "name": table_name,
875
+ "rows": 0
876
+ })
877
+
878
+ enhanced_metadata["tables"] = table_info
879
+ enhanced_metadata["table_count"] = len(table_names)
880
+
881
+ except Exception as e:
882
+ logger.debug("Failed to collect table information", exc_info=True)
883
+ enhanced_metadata["tables"] = []
884
+ enhanced_metadata["table_count"] = 0
885
+
886
+ # Collect SQLite PRAGMA settings for additional metadata
887
+ if db_url.startswith("sqlite:///"):
888
+ try:
889
+ pragma_settings = {}
890
+
891
+ # Get foreign keys setting
892
+ result = session.execute(text("PRAGMA foreign_keys")).fetchone()
893
+ if result:
894
+ pragma_settings["foreign_keys"] = bool(result[0])
895
+
896
+ # Get journal mode
897
+ result = session.execute(text("PRAGMA journal_mode")).fetchone()
898
+ if result:
899
+ journal_mode = result[0].lower()
900
+ pragma_settings["journal_mode"] = journal_mode
901
+ enhanced_metadata["wal_enabled"] = journal_mode == "wal"
902
+
903
+ # Add cache size if available
904
+ result = session.execute(text("PRAGMA cache_size")).fetchone()
905
+ if result:
906
+ pragma_settings["cache_size"] = result[0]
907
+
908
+ enhanced_metadata["pragma_settings"] = pragma_settings
909
+
910
+ except Exception as e:
911
+ # PRAGMA queries can fail in some SQLite configurations
912
+ logger.debug(
913
+ "Failed to collect SQLite PRAGMA settings", exc_info=True
914
+ )
915
+
916
+ # No need to commit since we're just testing connectivity
917
+
918
+ return ComponentStatus(
919
+ name="database",
920
+ status=ComponentStatusType.HEALTHY,
921
+ message="Database connection successful",
922
+ response_time_ms=None, # Will be set by caller
923
+ metadata=enhanced_metadata,
924
+ )
925
+
926
+ except ImportError:
927
+ return ComponentStatus(
928
+ name="database",
929
+ status=ComponentStatusType.UNHEALTHY,
930
+ message="Database module not available",
931
+ response_time_ms=None,
932
+ metadata={
933
+ "implementation": "sqlite",
934
+ "error": "Database module not imported or configured",
935
+ },
936
+ )
937
+ except Exception as e:
938
+ # Check if it's a file not found error
939
+ error_str = str(e).lower()
940
+ if "unable to open database file" in error_str or "no such file" in error_str:
941
+ return ComponentStatus(
942
+ name="database",
943
+ status=ComponentStatusType.WARNING,
944
+ message="Database file not accessible",
945
+ response_time_ms=None,
946
+ metadata={
947
+ "implementation": "sqlite",
948
+ "url": settings.DATABASE_URL,
949
+ "error": str(e),
950
+ "recommendation": "Check database file path and permissions",
951
+ },
952
+ )
953
+
954
+ return ComponentStatus(
955
+ name="database",
956
+ status=ComponentStatusType.UNHEALTHY,
957
+ message=f"Database connection failed: {str(e)}",
958
+ response_time_ms=None,
959
+ metadata={
960
+ "implementation": "sqlite",
961
+ "url": settings.DATABASE_URL,
962
+ "error": str(e),
963
+ },
964
+ )
965
+ {% endif %}
966
+
967
+
968
+ {% if cookiecutter.include_worker == "yes" %}
969
+ async def check_worker_health() -> ComponentStatus:
970
+ """
971
+ Check arq worker status using arq's native health checks and queue configuration.
972
+
973
+ Returns:
974
+ ComponentStatus indicating worker infrastructure health with queue
975
+ sub-components
976
+ """
977
+ try:
978
+ import re
979
+
980
+ import redis.asyncio as aioredis
981
+
982
+ # Create Redis connection with auto-detection for local vs Docker
983
+ # Step 1: Call untyped function with explicit ignore
984
+ redis_connection = aioredis.from_url( # type: ignore[no-untyped-call]
985
+ settings.redis_url_effective,
986
+ db=settings.REDIS_DB
987
+ )
988
+ # Step 2: Cast the result to proper type
989
+ redis_client: aioredis.Redis = cast(aioredis.Redis, redis_connection)
990
+
991
+ # Get queue metadata from WorkerSettings classes via dynamic discovery
992
+ from app.components.worker.registry import get_all_queue_metadata
993
+ functional_queues = get_all_queue_metadata()
994
+
995
+ # Check each queue and create sub-components
996
+ queue_sub_components = {}
997
+ total_queued = 0
998
+ total_completed = 0
999
+ total_failed = 0
1000
+ total_retried = 0
1001
+ total_ongoing = 0
1002
+ overall_healthy = True
1003
+ active_workers = 0
1004
+
1005
+ for queue_type, queue_config in functional_queues.items():
1006
+ queue_name = queue_config["queue_name"]
1007
+
1008
+ try:
1009
+ # Get queue length (actual queued jobs) - arq uses sorted sets
1010
+ queue_length_result = redis_client.zcard(queue_name)
1011
+ if hasattr(queue_length_result, '__await__'):
1012
+ queue_length = await queue_length_result
1013
+ else:
1014
+ queue_length = queue_length_result
1015
+ total_queued += queue_length
1016
+
1017
+ # Look for arq health check key for this queue
1018
+ # arq health check key format: {queue_name}:health-check
1019
+ health_check_key = f"{queue_name}:health-check"
1020
+ health_check_data = await redis_client.get(health_check_key)
1021
+
1022
+ # Parse arq health check data if available
1023
+ j_complete = j_failed = j_retried = j_ongoing = 0
1024
+ worker_alive = False
1025
+ last_health_check = None
1026
+
1027
+ if health_check_data:
1028
+ health_string = health_check_data.decode()
1029
+ # Parse format: "Mar-01 17:41:22 j_complete=0 j_failed=0 ..."
1030
+
1031
+ # Extract timestamp (first part before job stats)
1032
+ timestamp_match = re.match(r"^(\w+-\d+ \d+:\d+:\d+)", health_string)
1033
+ if timestamp_match:
1034
+ last_health_check = timestamp_match.group(1)
1035
+
1036
+ # Extract job statistics using regex
1037
+ j_complete_match = re.search(r"j_complete=(\d+)", health_string)
1038
+ j_failed_match = re.search(r"j_failed=(\d+)", health_string)
1039
+ j_retried_match = re.search(r"j_retried=(\d+)", health_string)
1040
+ j_ongoing_match = re.search(r"j_ongoing=(\d+)", health_string)
1041
+
1042
+ if j_complete_match:
1043
+ j_complete = int(j_complete_match.group(1))
1044
+ total_completed += j_complete
1045
+ if j_failed_match:
1046
+ j_failed = int(j_failed_match.group(1))
1047
+ total_failed += j_failed
1048
+ if j_retried_match:
1049
+ j_retried = int(j_retried_match.group(1))
1050
+ total_retried += j_retried
1051
+ if j_ongoing_match:
1052
+ j_ongoing = int(j_ongoing_match.group(1))
1053
+ total_ongoing += j_ongoing
1054
+
1055
+ # Worker is alive if we have health check data
1056
+ # arq health checks expire automatically, having data means recent
1057
+ worker_alive = True
1058
+
1059
+ if worker_alive:
1060
+ active_workers += 1
1061
+
1062
+ # Create queue status message
1063
+ status_parts = []
1064
+ if not worker_alive:
1065
+ status_parts.append("worker offline - no health check data")
1066
+ elif j_ongoing > 0:
1067
+ status_parts.append(f"{j_ongoing} processing")
1068
+ elif queue_length > 0:
1069
+ status_parts.append(f"{queue_length} queued")
1070
+ else:
1071
+ status_parts.append("idle")
1072
+
1073
+ # Add job statistics to status if worker is alive
1074
+ if worker_alive and (j_complete > 0 or j_failed > 0):
1075
+ if j_failed > 0:
1076
+ failure_rate = (j_failed / max(j_complete + j_failed, 1)) * 100
1077
+ status_parts.append(f"{j_failed} failed ({failure_rate:.1f}%)")
1078
+ if j_complete > 0:
1079
+ status_parts.append(f"{j_complete} completed")
1080
+
1081
+ # Check if queue has no functions configured (empty functions list)
1082
+ queue_functions = queue_config.get("functions", [])
1083
+ has_functions = len(queue_functions) > 0
1084
+
1085
+ # Determine queue status based on worker health and failure rate
1086
+ failure_rate = (
1087
+ (j_failed / max(j_complete + j_failed, 1)) * 100
1088
+ if worker_alive
1089
+ else 100
1090
+ )
1091
+
1092
+ if not worker_alive and not has_functions:
1093
+ # Queue configured but no functions - show as INFO
1094
+ queue_status = ComponentStatusType.INFO
1095
+ status_parts = ["configured - no functions defined"]
1096
+ elif not worker_alive:
1097
+ queue_status = ComponentStatusType.UNHEALTHY
1098
+ elif failure_rate > 25: # Unhealthy threshold at 25%
1099
+ queue_status = ComponentStatusType.UNHEALTHY
1100
+ elif failure_rate > 10: # Warning threshold at 10%
1101
+ queue_status = ComponentStatusType.WARNING
1102
+ else:
1103
+ queue_status = ComponentStatusType.HEALTHY
1104
+
1105
+ queue_message = (
1106
+ f"{queue_config['description']}: {', '.join(status_parts)}"
1107
+ )
1108
+
1109
+ # Update overall health based on this queue
1110
+ if queue_status == ComponentStatusType.UNHEALTHY:
1111
+ overall_healthy = False
1112
+
1113
+ queue_metadata = {
1114
+ "queue_type": queue_type,
1115
+ "queue_name": queue_name,
1116
+ "queued_jobs": queue_length,
1117
+ "max_concurrency": queue_config["max_jobs"],
1118
+ "timeout_seconds": queue_config["timeout"],
1119
+ "description": queue_config["description"],
1120
+ "worker_alive": worker_alive,
1121
+ "health_check_key": health_check_key,
1122
+ }
1123
+
1124
+ # Add arq health check statistics if available
1125
+ if worker_alive:
1126
+ queue_metadata.update(
1127
+ {
1128
+ "jobs_completed": j_complete,
1129
+ "jobs_failed": j_failed,
1130
+ "jobs_retried": j_retried,
1131
+ "jobs_ongoing": j_ongoing,
1132
+ "failure_rate_percent": round(failure_rate, 1),
1133
+ "last_health_check": last_health_check,
1134
+ }
1135
+ )
1136
+ else:
1137
+ queue_metadata["offline_reason"] = "Health check key not found"
1138
+
1139
+ queue_sub_components[queue_type] = ComponentStatus(
1140
+ name=queue_type,
1141
+ status=queue_status,
1142
+ message=queue_message,
1143
+ response_time_ms=None,
1144
+ metadata=queue_metadata,
1145
+ sub_components={},
1146
+ )
1147
+
1148
+ except aioredis.ConnectionError as e:
1149
+ logger.error(f"Redis connection failed for {queue_type}: {e}")
1150
+ overall_healthy = False
1151
+
1152
+ # Extract more specific connection error details
1153
+ error_details = str(e).lower()
1154
+ if "connection refused" in error_details:
1155
+ connection_issue = "Redis server not running"
1156
+ elif (
1157
+ "name or service not known" in error_details
1158
+ or "nodename nor servname" in error_details
1159
+ ):
1160
+ connection_issue = "Redis server DNS resolution failed"
1161
+ elif "timeout" in error_details:
1162
+ connection_issue = "Redis server connection timeout"
1163
+ else:
1164
+ connection_issue = "Redis server unreachable"
1165
+
1166
+ queue_sub_components[queue_type] = ComponentStatus(
1167
+ name=queue_type,
1168
+ status=ComponentStatusType.UNHEALTHY,
1169
+ message=f"{connection_issue} - worker offline",
1170
+ response_time_ms=None,
1171
+ metadata={
1172
+ "queue_type": queue_type,
1173
+ "queue_name": queue_name,
1174
+ "error_type": "redis_connection_error",
1175
+ "error": str(e),
1176
+ "connection_issue": connection_issue,
1177
+ "recommendation": (
1178
+ "Check Redis server status and network connectivity"
1179
+ ),
1180
+ },
1181
+ sub_components={},
1182
+ )
1183
+ except aioredis.ResponseError as e:
1184
+ if "WRONGTYPE" in str(e):
1185
+ logger.error(f"Redis data corruption for {queue_type}: {e}")
1186
+ message = f"Redis data corruption detected"
1187
+ recommendation = "Clear Redis cache to fix data type conflicts"
1188
+ error_type = "redis_key_type_error"
1189
+ else:
1190
+ logger.error(f"Redis operation failed for {queue_type}: {e}")
1191
+ message = f"Redis operation failed"
1192
+ recommendation = "Check Redis configuration and permissions"
1193
+ error_type = "redis_response_error"
1194
+
1195
+ overall_healthy = False
1196
+ queue_sub_components[queue_type] = ComponentStatus(
1197
+ name=queue_type,
1198
+ status=ComponentStatusType.UNHEALTHY,
1199
+ message=message,
1200
+ response_time_ms=None,
1201
+ metadata={
1202
+ "queue_type": queue_type,
1203
+ "queue_name": queue_name,
1204
+ "error_type": error_type,
1205
+ "error": str(e),
1206
+ "recommendation": recommendation,
1207
+ },
1208
+ sub_components={},
1209
+ )
1210
+ except Exception as e:
1211
+ logger.error(
1212
+ f"Unexpected error checking {queue_type} queue health: {e}"
1213
+ )
1214
+ overall_healthy = False
1215
+ queue_sub_components[queue_type] = ComponentStatus(
1216
+ name=queue_type,
1217
+ status=ComponentStatusType.UNHEALTHY,
1218
+ message=f"Health check failed: {type(e).__name__}",
1219
+ response_time_ms=None,
1220
+ metadata={
1221
+ "queue_type": queue_type,
1222
+ "queue_name": queue_name,
1223
+ "error_type": "unexpected_error",
1224
+ "error": str(e),
1225
+ "exception_class": type(e).__name__,
1226
+ },
1227
+ sub_components={},
1228
+ )
1229
+
1230
+ await redis_client.aclose()
1231
+
1232
+ # Create main worker status message
1233
+ message_parts = []
1234
+ if active_workers == 0:
1235
+ message_parts.append("No active workers")
1236
+ overall_healthy = False
1237
+ else:
1238
+ message_parts.append(
1239
+ f"{active_workers}/{len(functional_queues)} workers active"
1240
+ )
1241
+
1242
+ if total_queued > 0:
1243
+ message_parts.append(f"{total_queued} queued")
1244
+ if total_ongoing > 0:
1245
+ message_parts.append(f"{total_ongoing} processing")
1246
+ if total_failed > 0:
1247
+ failure_rate = (total_failed / max(total_completed + total_failed, 1)) * 100
1248
+ message_parts.append(f"{total_failed} failed ({failure_rate:.1f}%)")
1249
+
1250
+ main_message = f"arq worker infrastructure: {', '.join(message_parts)}"
1251
+
1252
+ # Create a "queues" intermediate component that contains all queue
1253
+ # sub-components - determine status from child statuses
1254
+ queue_statuses = [queue.status for queue in queue_sub_components.values()]
1255
+ queues_status = propagate_status(queue_statuses)
1256
+
1257
+
1258
+ queues_message = f"{len(functional_queues)} functional queues configured"
1259
+ if active_workers < len(functional_queues):
1260
+ queues_message += f" ({active_workers} active)"
1261
+
1262
+ queues_component = ComponentStatus(
1263
+ name="queues",
1264
+ status=queues_status,
1265
+ message=queues_message,
1266
+ response_time_ms=None,
1267
+ metadata={
1268
+ "configured_queues": len(functional_queues),
1269
+ "active_workers": active_workers,
1270
+ "queue_types": list(functional_queues.keys()),
1271
+ },
1272
+ sub_components=queue_sub_components,
1273
+ )
1274
+
1275
+ # Determine worker status based on overall health and queues status
1276
+ if not overall_healthy:
1277
+ worker_status = ComponentStatusType.UNHEALTHY
1278
+ else:
1279
+ worker_status = propagate_status([queues_status])
1280
+
1281
+ return ComponentStatus(
1282
+ name="worker",
1283
+ status=worker_status,
1284
+ message=main_message,
1285
+ response_time_ms=None,
1286
+ metadata={
1287
+ "total_queued": total_queued,
1288
+ "total_completed": total_completed,
1289
+ "total_failed": total_failed,
1290
+ "total_retried": total_retried,
1291
+ "total_ongoing": total_ongoing,
1292
+ "overall_failure_rate_percent": (
1293
+ round(
1294
+ (total_failed / max(total_completed + total_failed, 1)) * 100, 1
1295
+ )
1296
+ if total_completed + total_failed > 0
1297
+ else 0
1298
+ ),
1299
+ "redis_url": settings.REDIS_URL,
1300
+ "queue_configuration": {
1301
+ queue_type: {
1302
+ "description": config["description"],
1303
+ "max_jobs": config["max_jobs"],
1304
+ "timeout_seconds": config["timeout"],
1305
+ }
1306
+ for queue_type, config in functional_queues.items()
1307
+ },
1308
+ },
1309
+ sub_components={"queues": queues_component},
1310
+ )
1311
+
1312
+ except ImportError:
1313
+ return ComponentStatus(
1314
+ name="worker",
1315
+ status=ComponentStatusType.UNHEALTHY,
1316
+ message="Redis library not available for worker health check",
1317
+ response_time_ms=None,
1318
+ sub_components={},
1319
+ )
1320
+ except Exception as e:
1321
+ logger.error(f"Worker health check failed: {e}")
1322
+ return ComponentStatus(
1323
+ name="worker",
1324
+ status=ComponentStatusType.UNHEALTHY,
1325
+ message=f"Worker health check failed: {str(e)}",
1326
+ response_time_ms=None,
1327
+ metadata={
1328
+ "error": str(e),
1329
+ "redis_url": settings.REDIS_URL,
1330
+ },
1331
+ sub_components={},
1332
+ )
1333
+ {% endif %}