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,663 @@
1
+ """
2
+ Tests for core health logic and component status propagation.
3
+
4
+ These tests focus on the pure logic of health checking, warning propagation,
5
+ and component hierarchy without external dependencies like Redis or system metrics.
6
+ """
7
+
8
+ from datetime import UTC, datetime
9
+ from unittest.mock import AsyncMock, MagicMock, patch
10
+ from typing import Any
11
+
12
+ import pytest
13
+
14
+ from app.services.system import ComponentStatus, ComponentStatusType
15
+ from app.services.system.health import (
16
+ get_system_status,
17
+ )
18
+
19
+
20
+
21
+ class TestHealthUtilityFunctions:
22
+ """Test utility functions used in health checks."""
23
+
24
+ def test_format_bytes(self) -> None:
25
+ """Test format_bytes utility function."""
26
+ from app.services.system.health import format_bytes
27
+
28
+ # Test various byte sizes
29
+ assert format_bytes(0) == "0 B"
30
+ assert format_bytes(512) == "512 B"
31
+ assert format_bytes(1024) == "1.0 KB"
32
+ assert format_bytes(1536) == "1.5 KB"
33
+ assert format_bytes(2048) == "2.0 KB"
34
+ assert format_bytes(1048576) == "1.0 MB"
35
+ assert format_bytes(1572864) == "1.5 MB"
36
+ assert format_bytes(1073741824) == "1.0 GB"
37
+ assert format_bytes(1099511627776) == "1.0 TB"
38
+
39
+ # Test edge cases
40
+ assert format_bytes(1) == "1 B"
41
+ assert format_bytes(1023) == "1023 B"
42
+ assert format_bytes(8192) == "8.0 KB"
43
+
44
+
45
+ class TestComponentStatusPropagation:
46
+ """Test warning status propagation through component hierarchies."""
47
+
48
+ def test_component_status_creation_with_warning(self) -> None:
49
+ """Test creating ComponentStatus with warning status."""
50
+ status = ComponentStatus(
51
+ name="test_component",
52
+ status=ComponentStatusType.WARNING,
53
+ message="Has warnings but still healthy",
54
+ response_time_ms=100.0,
55
+ )
56
+
57
+ assert status.name == "test_component"
58
+ assert status.healthy is True
59
+ assert status.status == ComponentStatusType.WARNING
60
+ assert status.message == "Has warnings but still healthy"
61
+
62
+ def test_component_status_defaults_to_healthy(self) -> None:
63
+ """Test that ComponentStatus defaults to HEALTHY status."""
64
+ status = ComponentStatus(
65
+ name="test_component",
66
+ message="All good",
67
+ )
68
+
69
+ assert status.status == ComponentStatusType.HEALTHY
70
+
71
+ def test_unhealthy_component_with_unhealthy_status(self) -> None:
72
+ """Test that unhealthy components get UNHEALTHY status."""
73
+ status = ComponentStatus(
74
+ name="test_component",
75
+ status=ComponentStatusType.UNHEALTHY,
76
+ message="Something is broken",
77
+ )
78
+
79
+ assert status.healthy is False
80
+ assert status.status == ComponentStatusType.UNHEALTHY
81
+
82
+ def test_sub_component_hierarchy(self) -> None:
83
+ """Test component with sub-components for hierarchy testing."""
84
+ # Create sub-components with different statuses
85
+ sub_component_healthy = ComponentStatus(
86
+ name="sub_healthy",
87
+ status=ComponentStatusType.HEALTHY,
88
+ message="Sub-component is healthy",
89
+ )
90
+
91
+ sub_component_warning = ComponentStatus(
92
+ name="sub_warning",
93
+ status=ComponentStatusType.WARNING,
94
+ message="Sub-component has warnings",
95
+ )
96
+
97
+ # Create parent component
98
+ parent_component = ComponentStatus(
99
+ name="parent",
100
+ status=ComponentStatusType.WARNING, # Should propagate from sub-components
101
+ message="Parent has sub-component warnings",
102
+ sub_components={
103
+ "sub_healthy": sub_component_healthy,
104
+ "sub_warning": sub_component_warning,
105
+ }
106
+ )
107
+
108
+ assert parent_component.healthy is True
109
+ assert parent_component.status == ComponentStatusType.WARNING
110
+ assert len(parent_component.sub_components) == 2
111
+ assert (
112
+ parent_component.sub_components["sub_warning"].status
113
+ == ComponentStatusType.WARNING
114
+ )
115
+
116
+
117
+ class TestSystemStatusWarningPropagation:
118
+ """Test warning propagation in real system status scenarios."""
119
+
120
+ @pytest.mark.asyncio
121
+ async def test_system_status_with_mixed_component_health(self) -> None:
122
+ """Test system status calculation with components having different states."""
123
+
124
+ # Mock the health check registry to have controlled components
125
+ mock_healthy_component = AsyncMock(return_value=ComponentStatus(
126
+ name="healthy_service",
127
+ status=ComponentStatusType.HEALTHY,
128
+ message="Service is running well",
129
+ ))
130
+
131
+ mock_warning_component = AsyncMock(return_value=ComponentStatus(
132
+ name="warning_service",
133
+ status=ComponentStatusType.WARNING,
134
+ message="Service has warnings",
135
+ ))
136
+
137
+ mock_unhealthy_component = AsyncMock(return_value=ComponentStatus(
138
+ name="unhealthy_service",
139
+ status=ComponentStatusType.UNHEALTHY,
140
+ message="Service is down",
141
+ ))
142
+
143
+ # Mock the system metrics to avoid actual system calls
144
+ mock_system_metrics = {
145
+ "memory": ComponentStatus(
146
+ name="memory",
147
+ status=ComponentStatusType.HEALTHY,
148
+ message="Memory usage: 50%",
149
+ ),
150
+ "cpu": ComponentStatus(
151
+ name="cpu",
152
+ status=ComponentStatusType.HEALTHY,
153
+ message="CPU usage: 10%",
154
+ ),
155
+ "disk": ComponentStatus(
156
+ name="disk",
157
+ status=ComponentStatusType.HEALTHY,
158
+ message="Disk usage: 30%",
159
+ ),
160
+ }
161
+
162
+ with (
163
+ patch(
164
+ "app.services.system.health._health_checks",
165
+ {
166
+ "healthy_service": mock_healthy_component,
167
+ "warning_service": mock_warning_component,
168
+ "unhealthy_service": mock_unhealthy_component,
169
+ },
170
+ ),
171
+ patch("app.services.system.health._service_health_checks", {}),
172
+ patch(
173
+ "app.services.system.health._get_cached_system_metrics",
174
+ return_value=mock_system_metrics,
175
+ ),
176
+ patch(
177
+ "app.services.system.health._get_system_info",
178
+ return_value={"test": "info"},
179
+ ),
180
+ ):
181
+ system_status = await get_system_status()
182
+
183
+ # System should be unhealthy due to unhealthy_service
184
+ assert system_status.overall_healthy is False
185
+
186
+ # Check that components are present using model's flat list
187
+ assert (
188
+ "aegis.components.healthy_service" in system_status.healthy_components
189
+ )
190
+ assert (
191
+ "aegis.components.warning_service" in system_status.healthy_components
192
+ ) # WARNING is considered healthy
193
+ assert (
194
+ "aegis.components.unhealthy_service"
195
+ in system_status.unhealthy_components
196
+ )
197
+ assert "aegis.components.backend" in system_status.healthy_components
198
+
199
+ # Also verify direct navigation to check statuses
200
+ assert "aegis" in system_status.components
201
+ aegis_component = system_status.components["aegis"]
202
+
203
+ # Navigate to components group
204
+ if "components" in aegis_component.sub_components:
205
+ components_group = aegis_component.sub_components["components"]
206
+ assert "healthy_service" in components_group.sub_components
207
+ assert "warning_service" in components_group.sub_components
208
+ assert "unhealthy_service" in components_group.sub_components
209
+ assert "backend" in components_group.sub_components
210
+
211
+ # Verify component statuses
212
+ assert (
213
+ components_group.sub_components["healthy_service"].status
214
+ == ComponentStatusType.HEALTHY
215
+ )
216
+ assert (
217
+ components_group.sub_components["warning_service"].status
218
+ == ComponentStatusType.WARNING
219
+ )
220
+ assert (
221
+ components_group.sub_components["unhealthy_service"].status
222
+ == ComponentStatusType.UNHEALTHY
223
+ )
224
+
225
+
226
+ @pytest.mark.asyncio
227
+ async def test_system_status_with_only_warnings_stays_healthy(self) -> None:
228
+ """Test that system with only warnings remains overall healthy."""
229
+
230
+ mock_warning_component = AsyncMock(return_value=ComponentStatus(
231
+ name="warning_service",
232
+ status=ComponentStatusType.WARNING,
233
+ message="Service has warnings but functional",
234
+ ))
235
+
236
+ mock_system_metrics = {
237
+ "memory": ComponentStatus(
238
+ name="memory",
239
+ status=ComponentStatusType.HEALTHY,
240
+ message="Memory usage: 50%",
241
+ ),
242
+ }
243
+
244
+ with (
245
+ patch(
246
+ "app.services.system.health._health_checks",
247
+ {
248
+ "warning_service": mock_warning_component,
249
+ },
250
+ ),
251
+ patch("app.services.system.health._service_health_checks", {}),
252
+ patch(
253
+ "app.services.system.health._get_cached_system_metrics",
254
+ return_value=mock_system_metrics,
255
+ ),
256
+ patch(
257
+ "app.services.system.health._get_system_info",
258
+ return_value={"test": "info"},
259
+ ),
260
+ ):
261
+
262
+ system_status = await get_system_status()
263
+
264
+ # System should remain healthy since warnings don't affect overall health
265
+ assert system_status.overall_healthy is True
266
+
267
+ # But aegis component should propagate warning status
268
+ aegis_component = system_status.components["aegis"]
269
+ assert aegis_component.status == ComponentStatusType.WARNING
270
+ assert aegis_component.healthy is True
271
+
272
+
273
+ class TestWorkerHealthLogic:
274
+ """Test the specific worker health check logic and warning propagation."""
275
+
276
+ def test_queue_status_determination_logic(self) -> None:
277
+ """Test the logic for determining queue component status."""
278
+
279
+ # Test case 1: Worker with no functions should be WARNING but healthy
280
+ def check_empty_worker_status(
281
+ queue_type: str,
282
+ has_functions: bool,
283
+ worker_alive: bool,
284
+ failure_rate: float,
285
+ ) -> tuple[bool, ComponentStatusType]:
286
+ """Simulate the queue status logic from worker health check."""
287
+ if not has_functions:
288
+ queue_healthy = True # Empty workers don't affect overall health
289
+ queue_status = ComponentStatusType.WARNING # But show as warning
290
+ else:
291
+ queue_healthy = worker_alive and failure_rate < 25
292
+ queue_status = (
293
+ ComponentStatusType.HEALTHY
294
+ if queue_healthy
295
+ else ComponentStatusType.UNHEALTHY
296
+ )
297
+
298
+ return queue_healthy, queue_status
299
+
300
+ # Empty worker (media/system queues)
301
+ healthy, status = check_empty_worker_status("media", False, False, 100)
302
+ assert healthy is True # Doesn't affect system health
303
+ assert status == ComponentStatusType.WARNING # But shows warning
304
+
305
+ # Active worker with good performance
306
+ healthy, status = check_empty_worker_status("load_test", True, True, 5)
307
+ assert healthy is True
308
+ assert status == ComponentStatusType.HEALTHY
309
+
310
+ # Active worker with high failure rate
311
+ healthy, status = check_empty_worker_status("load_test", True, True, 50)
312
+ assert healthy is False
313
+ assert status == ComponentStatusType.UNHEALTHY
314
+
315
+ # Active worker that's offline
316
+ healthy, status = check_empty_worker_status("load_test", True, False, 0)
317
+ assert healthy is False
318
+ assert status == ComponentStatusType.UNHEALTHY
319
+
320
+ def test_warning_propagation_to_parent_components(self) -> None:
321
+ """Test warning propagation from queue -> queues -> worker."""
322
+
323
+ # Simulate the propagation logic used in worker health check
324
+ def check_warning_propagation(
325
+ sub_components: dict[str, ComponentStatus]
326
+ ) -> ComponentStatusType:
327
+ """Simulate queues component status determination."""
328
+ queues_healthy = all(
329
+ queue.healthy for queue in sub_components.values()
330
+ )
331
+
332
+ has_warnings = any(
333
+ queue.status == ComponentStatusType.WARNING
334
+ for queue in sub_components.values()
335
+ )
336
+
337
+ if has_warnings and queues_healthy:
338
+ return ComponentStatusType.WARNING
339
+ elif queues_healthy:
340
+ return ComponentStatusType.HEALTHY
341
+ else:
342
+ return ComponentStatusType.UNHEALTHY
343
+
344
+ # Test case: Some queues have warnings, all are healthy
345
+ sub_components = {
346
+ "media": ComponentStatus(
347
+ name="media",
348
+ status=ComponentStatusType.WARNING,
349
+ message="No tasks configured",
350
+ ),
351
+ "system": ComponentStatus(
352
+ name="system",
353
+ status=ComponentStatusType.WARNING,
354
+ message="No tasks configured",
355
+ ),
356
+ "load_test": ComponentStatus(
357
+ name="load_test",
358
+ status=ComponentStatusType.HEALTHY,
359
+ message="Active with completed tasks",
360
+ ),
361
+ }
362
+
363
+ queues_status = check_warning_propagation(sub_components)
364
+ assert queues_status == ComponentStatusType.WARNING
365
+
366
+ # Test case: All components healthy
367
+ for component in sub_components.values():
368
+ component.status = ComponentStatusType.HEALTHY
369
+
370
+ queues_status = check_warning_propagation(sub_components)
371
+ assert queues_status == ComponentStatusType.HEALTHY
372
+
373
+ # Test case: One component unhealthy
374
+ sub_components["load_test"].status = ComponentStatusType.UNHEALTHY
375
+
376
+ queues_status = check_warning_propagation(sub_components)
377
+ assert queues_status == ComponentStatusType.UNHEALTHY
378
+
379
+
380
+ class TestComponentMetadata:
381
+ """Test component metadata handling and serialization."""
382
+
383
+ def test_component_status_with_complex_metadata(self) -> None:
384
+ """Test ComponentStatus with complex metadata for different component types."""
385
+
386
+ # Worker component metadata
387
+ worker_metadata = {
388
+ "total_queued": 5,
389
+ "total_completed": 1000,
390
+ "total_failed": 50,
391
+ "overall_failure_rate_percent": 4.8,
392
+ "redis_url": "redis://localhost:6379",
393
+ "queue_configuration": {
394
+ "load_test": {
395
+ "description": "Load testing tasks",
396
+ "max_jobs": 50,
397
+ "timeout_seconds": 300,
398
+ }
399
+ }
400
+ }
401
+
402
+ worker_status = ComponentStatus(
403
+ name="worker",
404
+ status=ComponentStatusType.WARNING,
405
+ message="arq worker infrastructure: 1/3 workers active",
406
+ metadata=worker_metadata,
407
+ )
408
+
409
+ # Verify metadata is preserved
410
+ assert worker_status.metadata["total_completed"] == 1000
411
+ assert worker_status.metadata["overall_failure_rate_percent"] == 4.8
412
+ assert "queue_configuration" in worker_status.metadata
413
+
414
+ # Cache component metadata
415
+ cache_metadata = {
416
+ "implementation": "redis",
417
+ "version": "7.0.0",
418
+ "connected_clients": 2,
419
+ "used_memory_human": "1.5M",
420
+ "uptime_in_seconds": 3600,
421
+ }
422
+
423
+ cache_status = ComponentStatus(
424
+ name="cache",
425
+ status=ComponentStatusType.HEALTHY,
426
+ message="Redis cache connection successful",
427
+ metadata=cache_metadata,
428
+ )
429
+
430
+ assert cache_status.metadata["implementation"] == "redis"
431
+ assert cache_status.metadata["uptime_in_seconds"] == 3600
432
+
433
+ def test_component_status_serialization(self) -> None:
434
+ """Test that ComponentStatus can be properly serialized (for API responses)."""
435
+
436
+ status = ComponentStatus(
437
+ name="test_component",
438
+ status=ComponentStatusType.WARNING,
439
+ message="Component with warning",
440
+ response_time_ms=123.45,
441
+ metadata={"key": "value", "number": 42},
442
+ sub_components={
443
+ "sub1": ComponentStatus(
444
+ name="sub1",
445
+ status=ComponentStatusType.HEALTHY,
446
+ message="Sub-component OK",
447
+ )
448
+ }
449
+ )
450
+
451
+ # Convert to dict (simulates JSON serialization)
452
+ status_dict = status.model_dump()
453
+
454
+ # Verify structure
455
+ assert status_dict["name"] == "test_component"
456
+ assert status_dict["healthy"] is True
457
+ assert status_dict["status"] == "warning"
458
+ assert status_dict["message"] == "Component with warning"
459
+ assert status_dict["response_time_ms"] == 123.45
460
+ assert status_dict["metadata"]["key"] == "value"
461
+ assert "sub1" in status_dict["sub_components"]
462
+ assert status_dict["sub_components"]["sub1"]["status"] == "healthy"
463
+
464
+
465
+ {% if cookiecutter.include_database == "yes" %}
466
+ class TestDatabaseHealthCheck:
467
+ """Test database health check functionality."""
468
+
469
+ @pytest.mark.asyncio
470
+ async def test_database_health_check_success(self, db_session) -> None:
471
+ """Test successful database health check with mocked database."""
472
+ from app.services.system.health import check_database_health
473
+
474
+ # Mock everything to simulate successful database connection with
475
+ # enhanced metadata
476
+ with patch('app.services.system.health.settings') as mock_settings, \
477
+ patch('pathlib.Path.exists', return_value=True), \
478
+ patch('pathlib.Path.stat') as mock_stat, \
479
+ patch('app.core.db.db_session') as mock_db_session, \
480
+ patch('app.core.db.engine') as mock_engine, \
481
+ patch('app.services.system.health.sqlite3') as mock_sqlite3:
482
+
483
+ mock_settings.DATABASE_URL = "sqlite:///./data/test.db"
484
+ mock_settings.DATABASE_ENGINE_ECHO = False
485
+
486
+ # Mock SQLite version
487
+ mock_sqlite3.sqlite_version = "3.43.2"
488
+
489
+ # Mock file size
490
+ mock_stat_result = MagicMock()
491
+ mock_stat_result.st_size = 8192
492
+ mock_stat.return_value = mock_stat_result
493
+
494
+ # Mock engine pool
495
+ mock_engine.pool.size.return_value = 5
496
+
497
+ # Mock successful db_session with PRAGMA queries
498
+ mock_session = MagicMock()
499
+
500
+ # Mock PRAGMA query results
501
+ def mock_execute(query):
502
+ query_str = str(query).lower()
503
+ if "pragma foreign_keys" in query_str:
504
+ result = MagicMock()
505
+ result.fetchone.return_value = [1] # foreign_keys = ON
506
+ return result
507
+ elif "pragma journal_mode" in query_str:
508
+ result = MagicMock()
509
+ result.fetchone.return_value = ["delete"] # journal_mode = delete
510
+ return result
511
+ elif "pragma cache_size" in query_str:
512
+ result = MagicMock()
513
+ result.fetchone.return_value = [2000] # cache_size = 2000
514
+ return result
515
+ else:
516
+ # For "SELECT 1" query
517
+ return None
518
+
519
+ mock_session.execute.side_effect = mock_execute
520
+ mock_db_session.return_value.__enter__ = MagicMock(
521
+ return_value=mock_session
522
+ )
523
+ mock_db_session.return_value.__exit__ = MagicMock(return_value=None)
524
+
525
+ result = await check_database_health()
526
+
527
+ # Test basic health status
528
+ assert result.name == "database"
529
+ assert result.status == ComponentStatusType.HEALTHY
530
+ assert result.message == "Database connection successful"
531
+
532
+ # Test existing metadata fields
533
+ assert result.metadata["implementation"] == "sqlite"
534
+ assert result.metadata["database_exists"] is True
535
+ assert result.metadata["engine_echo"] is False
536
+ assert result.metadata["url"] == "sqlite:///./data/test.db"
537
+
538
+ # Test enhanced metadata fields
539
+ assert result.metadata["version"] == "3.43.2"
540
+ assert result.metadata["file_size_bytes"] == 8192
541
+ assert result.metadata["file_size_human"] == "8.0 KB"
542
+ assert result.metadata["connection_pool_size"] == 5
543
+
544
+ # Test PRAGMA settings
545
+ assert "pragma_settings" in result.metadata
546
+ pragma_settings = result.metadata["pragma_settings"]
547
+ assert pragma_settings["foreign_keys"] is True
548
+ assert pragma_settings["journal_mode"] == "delete"
549
+ assert pragma_settings["cache_size"] == 2000
550
+ assert result.metadata["wal_enabled"] is False
551
+
552
+ @pytest.mark.asyncio
553
+ async def test_database_health_check_import_error(self) -> None:
554
+ """Test database health check when db module not available."""
555
+ from app.services.system.health import check_database_health
556
+
557
+ # Mock ImportError when trying to import db_session from app.core.db
558
+ import builtins
559
+ real_import = builtins.__import__
560
+
561
+ def mock_import(name, *args, **kwargs):
562
+ if name == 'app.core.db':
563
+ raise ImportError("No db module")
564
+ return real_import(name, *args, **kwargs)
565
+
566
+ with patch('builtins.__import__', side_effect=mock_import):
567
+ result = await check_database_health()
568
+
569
+ assert result.name == "database"
570
+ assert result.status == ComponentStatusType.UNHEALTHY
571
+ assert result.message == "Database module not available"
572
+ assert result.metadata["error"] == (
573
+ "Database module not imported or configured"
574
+ )
575
+
576
+ @pytest.mark.asyncio
577
+ async def test_database_health_check_missing_file(self) -> None:
578
+ """Test database health check when database file doesn't exist."""
579
+ from app.services.system.health import check_database_health
580
+
581
+ # Patch settings.DATABASE_URL to point to non-existent file
582
+ with patch('app.services.system.health.settings') as mock_settings:
583
+ mock_settings.DATABASE_URL = "sqlite:///./nonexistent/test.db"
584
+
585
+ result = await check_database_health()
586
+
587
+ assert result.name == "database"
588
+ assert result.status == ComponentStatusType.WARNING
589
+ assert "Database not initialized" in result.message
590
+ assert result.metadata["database_exists"] is False
591
+ assert "nonexistent/test.db" in result.metadata["expected_path"]
592
+
593
+ @pytest.mark.asyncio
594
+ async def test_database_health_check_connection_failure(self) -> None:
595
+ """Test database health check when connection fails due to permissions."""
596
+ from app.services.system.health import check_database_health
597
+
598
+ # Mock settings and Path.exists to simulate file exists but connection fails
599
+ with patch('app.services.system.health.settings') as mock_settings, \
600
+ patch('pathlib.Path.exists', return_value=True), \
601
+ patch('app.core.db.db_session') as mock_db_session:
602
+
603
+ # File exists but connection fails
604
+ mock_settings.DATABASE_URL = "sqlite:///./data/test.db"
605
+
606
+ # Mock db_session to simulate connection error
607
+ mock_session = MagicMock()
608
+ mock_session.execute.side_effect = Exception("unable to open database file")
609
+ mock_db_session.return_value.__enter__ = MagicMock(
610
+ return_value=mock_session
611
+ )
612
+ mock_db_session.return_value.__exit__ = MagicMock(return_value=None)
613
+
614
+ result = await check_database_health()
615
+
616
+ assert result.name == "database"
617
+ assert result.status == ComponentStatusType.WARNING
618
+ assert result.message == "Database file not accessible"
619
+ assert "unable to open database file" in result.metadata["error"]
620
+
621
+ def test_database_status_metadata_structure(self) -> None:
622
+ """Test that database health check includes proper metadata."""
623
+ from app.services.system.models import ComponentStatus, ComponentStatusType
624
+
625
+ # Test successful database component metadata with enhanced fields
626
+ database_metadata = {
627
+ "implementation": "sqlite",
628
+ "url": "sqlite:///:memory:",
629
+ "database_exists": True,
630
+ "engine_echo": False,
631
+ "version": "3.43.2",
632
+ "file_size_bytes": 8192,
633
+ "file_size_human": "8.0 KB",
634
+ "connection_pool_size": 1,
635
+ "pragma_settings": {
636
+ "foreign_keys": True,
637
+ "journal_mode": "delete",
638
+ "cache_size": 2000
639
+ },
640
+ "wal_enabled": False,
641
+ }
642
+
643
+ database_status = ComponentStatus(
644
+ name="database",
645
+ status=ComponentStatusType.HEALTHY,
646
+ message="Database connection successful",
647
+ metadata=database_metadata,
648
+ )
649
+
650
+ # Test existing fields
651
+ assert database_status.metadata["implementation"] == "sqlite"
652
+ assert "sqlite://" in database_status.metadata["url"]
653
+ assert database_status.metadata["database_exists"] is True
654
+ assert database_status.metadata["engine_echo"] is False
655
+
656
+ # Test enhanced metadata fields
657
+ assert database_status.metadata["version"] == "3.43.2"
658
+ assert database_status.metadata["file_size_bytes"] == 8192
659
+ assert database_status.metadata["file_size_human"] == "8.0 KB"
660
+ assert database_status.metadata["connection_pool_size"] == 1
661
+ assert isinstance(database_status.metadata["pragma_settings"], dict)
662
+ assert database_status.metadata["wal_enabled"] is False
663
+ {% endif %}