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,248 @@
1
+ """Tests for FastAPI middleware introspection service."""
2
+
3
+ import pytest
4
+ from unittest.mock import Mock, MagicMock
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+
8
+ from app.services.backend.middleware_inspector import (
9
+ FastAPIMiddlewareInspector,
10
+ get_fastapi_middleware_metadata,
11
+ )
12
+ from app.services.backend.models import MiddlewareInfo, MiddlewareMetadata
13
+
14
+
15
+ class TestFastAPIMiddlewareInspector:
16
+ """Test cases for FastAPI middleware introspection."""
17
+
18
+ def test_configured_app_middleware_detection(self, app: FastAPI):
19
+ """Test middleware detection with configured app (includes CORS by default)."""
20
+ inspector = FastAPIMiddlewareInspector(app)
21
+
22
+ metadata = inspector.get_middleware_metadata()
23
+
24
+ assert isinstance(metadata, MiddlewareMetadata)
25
+ # The configured app should have at least CORS middleware
26
+ assert metadata.total_middleware >= 1
27
+ assert metadata.security_count >= 1
28
+ assert "CORSMiddleware" in metadata.security_middleware
29
+ assert metadata.error is None
30
+ assert metadata.fallback is False
31
+
32
+ def test_empty_middleware_stack(self):
33
+ """Test with FastAPI app that has no middleware."""
34
+ app = FastAPI()
35
+ inspector = FastAPIMiddlewareInspector(app)
36
+
37
+ metadata = inspector.get_middleware_metadata()
38
+
39
+ assert isinstance(metadata, MiddlewareMetadata)
40
+ assert metadata.total_middleware == 0
41
+ assert metadata.security_count == 0
42
+ assert metadata.middleware_stack == []
43
+ assert metadata.security_middleware == []
44
+ assert metadata.error is None
45
+ assert metadata.fallback is False
46
+
47
+ def test_cors_middleware_detection(self):
48
+ """Test detection and configuration extraction for CORS middleware."""
49
+ app = FastAPI()
50
+ app.add_middleware(
51
+ CORSMiddleware,
52
+ allow_origins=["https://example.com"],
53
+ allow_credentials=True,
54
+ allow_methods=["GET", "POST"],
55
+ allow_headers=["X-Test-Header"],
56
+ )
57
+
58
+ inspector = FastAPIMiddlewareInspector(app)
59
+ metadata = inspector.get_middleware_metadata()
60
+
61
+ assert metadata.total_middleware >= 1
62
+ assert metadata.security_count >= 1
63
+ assert "CORSMiddleware" in metadata.security_middleware
64
+
65
+ # Find the CORS middleware in the stack
66
+ cors_middleware = None
67
+ for mw in metadata.middleware_stack:
68
+ if mw.type == "CORSMiddleware":
69
+ cors_middleware = mw
70
+ break
71
+
72
+ assert cors_middleware is not None
73
+ assert cors_middleware.is_security is True
74
+ assert cors_middleware.config.get("allow_origins") == ["https://example.com"]
75
+ assert cors_middleware.config.get("allow_credentials") is True
76
+
77
+ def test_security_middleware_identification(self):
78
+ """Test identification of security-related middleware."""
79
+ inspector = FastAPIMiddlewareInspector(FastAPI())
80
+
81
+ # Test security keyword detection
82
+ assert inspector._is_security_middleware(
83
+ "CORSMiddleware", "fastapi.middleware.cors"
84
+ )
85
+ assert inspector._is_security_middleware(
86
+ "AuthMiddleware", "app.middleware.auth"
87
+ )
88
+ assert inspector._is_security_middleware("JWTMiddleware", "app.middleware.jwt")
89
+ assert inspector._is_security_middleware(
90
+ "RateLimitMiddleware", "app.middleware.rate"
91
+ )
92
+ assert inspector._is_security_middleware(
93
+ "SecurityHeadersMiddleware", "app.middleware.security"
94
+ )
95
+
96
+ # Test non-security middleware
97
+ assert not inspector._is_security_middleware(
98
+ "GZipMiddleware", "fastapi.middleware.gzip"
99
+ )
100
+ assert not inspector._is_security_middleware(
101
+ "LoggingMiddleware", "app.middleware.logging"
102
+ )
103
+
104
+ def test_middleware_order_detection(self):
105
+ """Test that middleware order is correctly detected."""
106
+ app = FastAPI()
107
+
108
+ # Add middleware in specific order
109
+ # Note: FastAPI adds in reverse order of execution
110
+ app.add_middleware(CORSMiddleware)
111
+
112
+ # Mock a custom middleware for testing
113
+ class CustomMiddleware:
114
+ def __init__(self, app):
115
+ self.app = app
116
+
117
+ async def __call__(self, scope, receive, send):
118
+ return await self.app(scope, receive, send)
119
+
120
+ # We can't easily test the exact ordering without a more complex setup,
121
+ # but we can test that order numbers are assigned
122
+ inspector = FastAPIMiddlewareInspector(app)
123
+ metadata = inspector.get_middleware_metadata()
124
+
125
+ # Verify that middleware have order numbers starting from 0
126
+ for idx, middleware in enumerate(metadata.middleware_stack):
127
+ assert middleware.order == idx
128
+
129
+ def test_middleware_config_extraction(self):
130
+ """Test extraction of middleware-specific configuration."""
131
+ inspector = FastAPIMiddlewareInspector(FastAPI())
132
+
133
+ # Mock CORS middleware
134
+ cors_mock = Mock()
135
+ cors_mock.allow_origins = ["http://localhost:3000"]
136
+ cors_mock.allow_methods = ["*"]
137
+ cors_mock.allow_headers = ["*"]
138
+ cors_mock.allow_credentials = True
139
+
140
+ config = inspector._extract_middleware_config(cors_mock)
141
+
142
+ assert config["allow_origins"] == ["http://localhost:3000"]
143
+ assert config["allow_methods"] == ["*"]
144
+ assert config["allow_headers"] == ["*"]
145
+ assert config["allow_credentials"] is True
146
+
147
+ def test_middleware_introspection_error_handling(self):
148
+ """Test error handling in middleware introspection."""
149
+ # Mock an app that raises an exception during introspection
150
+ app_mock = Mock()
151
+ app_mock.app = app_mock # Circular reference to break the loop
152
+
153
+ # Force an exception during middleware traversal
154
+ def side_effect(*args, **kwargs):
155
+ raise ValueError("Test error")
156
+
157
+ app_mock.__getattribute__ = side_effect
158
+
159
+ inspector = FastAPIMiddlewareInspector(app_mock)
160
+ metadata = inspector.get_middleware_metadata()
161
+
162
+ # Should return fallback metadata
163
+ assert metadata.fallback is True
164
+ assert metadata.error is not None
165
+ assert "Mock" in metadata.error # Error is actually about Mock object iteration
166
+ assert metadata.total_middleware == 0
167
+
168
+ def test_extract_middleware_info_error_handling(self):
169
+ """Test error handling in individual middleware info extraction."""
170
+ inspector = FastAPIMiddlewareInspector(FastAPI())
171
+
172
+ # Mock middleware that raises an exception by overriding __module__ access
173
+ class BadMiddleware:
174
+ __name__ = "BadMiddleware"
175
+
176
+ @property
177
+ def __module__(self) -> str:
178
+ raise ZeroDivisionError("Test exception")
179
+
180
+ bad_middleware = BadMiddleware()
181
+
182
+ result = inspector._extract_middleware_info(bad_middleware, 0)
183
+
184
+ # Should return None on error
185
+ assert result is None
186
+
187
+ def test_get_fastapi_middleware_metadata_convenience_function(self):
188
+ """Test the convenience function for getting middleware metadata."""
189
+ app = FastAPI()
190
+ app.add_middleware(CORSMiddleware)
191
+
192
+ metadata = get_fastapi_middleware_metadata(app)
193
+
194
+ assert isinstance(metadata, MiddlewareMetadata)
195
+ assert metadata.total_middleware >= 1
196
+
197
+ def test_middleware_metadata_model_dump(self):
198
+ """Test that middleware metadata can be dumped for ComponentStatus."""
199
+ app = FastAPI()
200
+ app.add_middleware(CORSMiddleware)
201
+
202
+ metadata = get_fastapi_middleware_metadata(app)
203
+ dumped = metadata.model_dump_for_metadata()
204
+
205
+ assert isinstance(dumped, dict)
206
+ assert "middleware_stack" in dumped
207
+ assert "total_middleware" in dumped
208
+ assert "security_count" in dumped
209
+ assert "security_middleware" in dumped
210
+
211
+ def test_middleware_info_model_creation(self):
212
+ """Test MiddlewareInfo model creation and validation."""
213
+ middleware_info = MiddlewareInfo(
214
+ type="CORSMiddleware",
215
+ module="fastapi.middleware.cors",
216
+ order=0,
217
+ config={"allow_origins": ["*"]},
218
+ is_security=True,
219
+ )
220
+
221
+ assert middleware_info.type == "CORSMiddleware"
222
+ assert middleware_info.module == "fastapi.middleware.cors"
223
+ assert middleware_info.order == 0
224
+ assert middleware_info.config == {"allow_origins": ["*"]}
225
+ assert middleware_info.is_security is True
226
+
227
+ def test_middleware_metadata_model_creation(self):
228
+ """Test MiddlewareMetadata model creation and validation."""
229
+ middleware_info = MiddlewareInfo(
230
+ type="CORSMiddleware",
231
+ module="fastapi.middleware.cors",
232
+ order=0,
233
+ is_security=True,
234
+ )
235
+
236
+ metadata = MiddlewareMetadata(
237
+ middleware_stack=[middleware_info],
238
+ total_middleware=1,
239
+ security_middleware=["CORSMiddleware"],
240
+ security_count=1,
241
+ )
242
+
243
+ assert len(metadata.middleware_stack) == 1
244
+ assert metadata.total_middleware == 1
245
+ assert metadata.security_middleware == ["CORSMiddleware"]
246
+ assert metadata.security_count == 1
247
+ assert metadata.error is None
248
+ assert metadata.fallback is False
@@ -0,0 +1,292 @@
1
+ {%- if cookiecutter.scheduler_backend != "memory" %}
2
+ """
3
+ Tests for ScheduledTaskManager service.
4
+
5
+ Tests the service layer for scheduled task management, including database
6
+ operations and task data transformations.
7
+ """
8
+
9
+ import pickle
10
+ from datetime import datetime
11
+ from unittest.mock import AsyncMock, MagicMock, patch
12
+ from typing import Any
13
+
14
+ import pytest
15
+
16
+ from app.services.scheduler.scheduled_task_manager import ScheduledTaskManager
17
+ from app.services.scheduler.models import APSchedulerJob, ScheduledTask, TaskStatistics
18
+
19
+
20
+ class MockTrigger:
21
+ """Simple mock trigger that can be pickled for testing."""
22
+
23
+ def __init__(self):
24
+ self.__class__.__name__ = "IntervalTrigger"
25
+
26
+
27
+ class TestScheduledTaskManager:
28
+ """Test the ScheduledTaskManager service layer."""
29
+
30
+ @pytest.fixture
31
+ def manager(self) -> ScheduledTaskManager:
32
+ """Create a ScheduledTaskManager instance for testing."""
33
+ return ScheduledTaskManager()
34
+
35
+ @pytest.fixture
36
+ def mock_job_data(self) -> dict[str, Any]:
37
+ """Mock job data as stored by APScheduler."""
38
+ return {
39
+ "name": "Test Job",
40
+ "func": "test.module.function",
41
+ "trigger": MockTrigger(),
42
+ "max_instances": 1,
43
+ "coalesce": True,
44
+ }
45
+
46
+ @pytest.fixture
47
+ def mock_apscheduler_job(self, mock_job_data: dict[str, Any]) -> APSchedulerJob:
48
+ """Create a mock APSchedulerJob for testing."""
49
+ # Create job state as APScheduler would pickle it
50
+ job_state = pickle.dumps(mock_job_data)
51
+
52
+ return APSchedulerJob(
53
+ id="test_job_id",
54
+ next_run_time=datetime.now().timestamp(),
55
+ job_state=job_state
56
+ )
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_has_persistence_table_exists(
60
+ self, manager: ScheduledTaskManager
61
+ ) -> None:
62
+ """Test has_persistence returns True when apscheduler_jobs table exists."""
63
+ with patch(
64
+ "app.services.scheduler.scheduled_task_manager.async_engine"
65
+ ) as mock_engine:
66
+ mock_conn = AsyncMock()
67
+ mock_conn.run_sync.return_value = ["apscheduler_jobs", "other_table"]
68
+
69
+ mock_engine.begin.return_value.__aenter__.return_value = mock_conn
70
+
71
+ result = await manager.has_persistence()
72
+ assert result is True
73
+
74
+ @pytest.mark.asyncio
75
+ async def test_has_persistence_table_missing(
76
+ self, manager: ScheduledTaskManager
77
+ ) -> None:
78
+ """Test has_persistence returns False when apscheduler_jobs table missing."""
79
+ with patch(
80
+ "app.services.scheduler.scheduled_task_manager.async_engine"
81
+ ) as mock_engine:
82
+ mock_conn = AsyncMock()
83
+ mock_conn.run_sync.return_value = ["other_table"]
84
+
85
+ mock_engine.begin.return_value.__aenter__.return_value = mock_conn
86
+
87
+ result = await manager.has_persistence()
88
+ assert result is False
89
+
90
+ @pytest.mark.asyncio
91
+ async def test_has_persistence_database_error(
92
+ self, manager: ScheduledTaskManager
93
+ ) -> None:
94
+ """Test has_persistence handles database errors gracefully."""
95
+ with patch(
96
+ "app.services.scheduler.scheduled_task_manager.async_engine"
97
+ ) as mock_engine:
98
+ mock_engine.begin.side_effect = Exception("Database error")
99
+
100
+ result = await manager.has_persistence()
101
+ assert result is False
102
+
103
+ @pytest.mark.asyncio
104
+ async def test_list_tasks_no_persistence(
105
+ self, manager: ScheduledTaskManager
106
+ ) -> None:
107
+ """Test list_tasks raises RuntimeError when persistence not available."""
108
+ with patch.object(manager, "has_persistence", return_value=False):
109
+ with pytest.raises(RuntimeError, match="persistence"):
110
+ await manager.list_tasks()
111
+
112
+ @pytest.mark.asyncio
113
+ async def test_list_tasks_with_jobs(
114
+ self, manager: ScheduledTaskManager, mock_apscheduler_job: APSchedulerJob
115
+ ) -> None:
116
+ """Test list_tasks returns properly formatted tasks."""
117
+ with patch.object(manager, "has_persistence", return_value=True), \
118
+ patch(
119
+ "app.services.scheduler.scheduled_task_manager.get_async_session"
120
+ ) as mock_session:
121
+
122
+ # Mock session and query result
123
+ mock_session_instance = AsyncMock()
124
+ mock_session.return_value.__aenter__.return_value = mock_session_instance
125
+
126
+ mock_result = MagicMock()
127
+ mock_result.all.return_value = [mock_apscheduler_job]
128
+ mock_session_instance.exec.return_value = mock_result
129
+
130
+ tasks = await manager.list_tasks()
131
+
132
+ assert len(tasks) == 1
133
+ task = tasks[0]
134
+ assert isinstance(task, ScheduledTask)
135
+ assert task.job_id == "test_job_id"
136
+ assert task.name == "Test Job"
137
+ assert task.status == "active" # Has next_run_time
138
+
139
+ @pytest.mark.asyncio
140
+ async def test_list_tasks_empty_database(
141
+ self, manager: ScheduledTaskManager
142
+ ) -> None:
143
+ """Test list_tasks returns empty list when no jobs in database."""
144
+ with patch.object(manager, "has_persistence", return_value=True), \
145
+ patch(
146
+ "app.services.scheduler.scheduled_task_manager.get_async_session"
147
+ ) as mock_session:
148
+
149
+ mock_session_instance = AsyncMock()
150
+ mock_session.return_value.__aenter__.return_value = mock_session_instance
151
+
152
+ mock_result = MagicMock()
153
+ mock_result.all.return_value = []
154
+ mock_session_instance.exec.return_value = mock_result
155
+
156
+ tasks = await manager.list_tasks()
157
+ assert len(tasks) == 0
158
+
159
+ @pytest.mark.asyncio
160
+ async def test_get_task_found(
161
+ self, manager: ScheduledTaskManager, mock_apscheduler_job: APSchedulerJob
162
+ ) -> None:
163
+ """Test get_task returns task when found."""
164
+ with patch(
165
+ "app.services.scheduler.scheduled_task_manager.get_async_session"
166
+ ) as mock_session:
167
+ mock_session_instance = AsyncMock()
168
+ mock_session.return_value.__aenter__.return_value = mock_session_instance
169
+
170
+ mock_result = MagicMock()
171
+ mock_result.first.return_value = mock_apscheduler_job
172
+ mock_session_instance.exec.return_value = mock_result
173
+
174
+ task = await manager.get_task("test_job_id")
175
+
176
+ assert task is not None
177
+ assert isinstance(task, ScheduledTask)
178
+ assert task.job_id == "test_job_id"
179
+ assert task.name == "Test Job"
180
+
181
+ @pytest.mark.asyncio
182
+ async def test_get_task_not_found(
183
+ self, manager: ScheduledTaskManager
184
+ ) -> None:
185
+ """Test get_task returns None when task not found."""
186
+ with patch(
187
+ "app.services.scheduler.scheduled_task_manager.get_async_session"
188
+ ) as mock_session:
189
+ mock_session_instance = AsyncMock()
190
+ mock_session.return_value.__aenter__.return_value = mock_session_instance
191
+
192
+ mock_result = MagicMock()
193
+ mock_result.first.return_value = None
194
+ mock_session_instance.exec.return_value = mock_result
195
+
196
+ task = await manager.get_task("non_existent_job")
197
+ assert task is None
198
+
199
+ @pytest.mark.asyncio
200
+ async def test_get_statistics(self, manager: ScheduledTaskManager) -> None:
201
+ """Test get_statistics returns proper TaskStatistics."""
202
+ # Mock list_tasks to return test data
203
+ mock_tasks = [
204
+ ScheduledTask(
205
+ job_id="job1", name="Job 1", function="test.func1", schedule="Every 1m",
206
+ trigger_type="interval", status="active", max_instances=1, coalesce=True
207
+ ),
208
+ ScheduledTask(
209
+ job_id="job2", name="Job 2", function="test.func2", schedule="Every 5m",
210
+ trigger_type="interval", status="paused", max_instances=1, coalesce=True
211
+ ),
212
+ ScheduledTask(
213
+ job_id="job3", name="Job 3", function="test.func3", schedule="Daily",
214
+ trigger_type="cron", status="active", max_instances=1, coalesce=True
215
+ ),
216
+ ]
217
+
218
+ with patch.object(manager, "list_tasks", return_value=mock_tasks):
219
+ stats = await manager.get_statistics()
220
+
221
+ assert isinstance(stats, TaskStatistics)
222
+ assert stats.total_tasks == 3
223
+ assert stats.active_tasks == 2
224
+ assert stats.paused_tasks == 1
225
+
226
+ def test_format_trigger_interval(self, manager: ScheduledTaskManager) -> None:
227
+ """Test _format_trigger handles interval triggers correctly."""
228
+ # Mock IntervalTrigger
229
+ mock_trigger = MagicMock()
230
+ mock_trigger.__class__.__name__ = "IntervalTrigger"
231
+
232
+ # Test seconds
233
+ mock_trigger.interval.total_seconds.return_value = 30
234
+ result = manager._format_trigger(mock_trigger)
235
+ assert result == "Every 30s"
236
+
237
+ # Test minutes
238
+ mock_trigger.interval.total_seconds.return_value = 300 # 5 minutes
239
+ result = manager._format_trigger(mock_trigger)
240
+ assert result == "Every 5m"
241
+
242
+ # Test hours
243
+ mock_trigger.interval.total_seconds.return_value = 7200 # 2 hours
244
+ result = manager._format_trigger(mock_trigger)
245
+ assert result == "Every 2h"
246
+
247
+ def test_format_trigger_cron(self, manager: ScheduledTaskManager) -> None:
248
+ """Test _format_trigger handles cron triggers correctly."""
249
+ mock_trigger = MagicMock()
250
+ mock_trigger.__class__.__name__ = "CronTrigger"
251
+
252
+ # Mock fields
253
+ mock_field = MagicMock()
254
+ mock_field.name = "hour"
255
+ mock_field.__str__ = MagicMock(return_value="2")
256
+ mock_trigger.fields = [mock_field]
257
+
258
+ result = manager._format_trigger(mock_trigger)
259
+ assert result == "Cron: hour=2"
260
+
261
+ def test_format_trigger_unknown(self, manager: ScheduledTaskManager) -> None:
262
+ """Test _format_trigger handles unknown triggers."""
263
+ result = manager._format_trigger(None)
264
+ assert result == "Unknown"
265
+
266
+ mock_trigger = MagicMock()
267
+ mock_trigger.__class__.__name__ = "UnknownTrigger"
268
+ result = manager._format_trigger(mock_trigger)
269
+ assert result == "Unknown"
270
+
271
+ def test_get_trigger_type(self, manager: ScheduledTaskManager) -> None:
272
+ """Test _get_trigger_type extracts trigger types correctly."""
273
+ # Test interval
274
+ mock_trigger = MagicMock()
275
+ mock_trigger.__class__.__name__ = "IntervalTrigger"
276
+ result = manager._get_trigger_type(mock_trigger)
277
+ assert result == "interval"
278
+
279
+ # Test cron
280
+ mock_trigger.__class__.__name__ = "CronTrigger"
281
+ result = manager._get_trigger_type(mock_trigger)
282
+ assert result == "cron"
283
+
284
+ # Test date
285
+ mock_trigger.__class__.__name__ = "DateTrigger"
286
+ result = manager._get_trigger_type(mock_trigger)
287
+ assert result == "date"
288
+
289
+ # Test unknown
290
+ result = manager._get_trigger_type(None)
291
+ assert result == "unknown"
292
+ {%- endif %}
@@ -0,0 +1,98 @@
1
+ """Test system monitoring functions."""
2
+
3
+ import pytest
4
+ from app.services.system import (
5
+ ComponentStatus,
6
+ get_system_status,
7
+ is_system_healthy,
8
+ register_health_check,
9
+ )
10
+
11
+
12
+ class TestSystemService:
13
+ """Test the system monitoring functions."""
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_component_status_creation(self) -> None:
17
+ """Test component status Pydantic model."""
18
+ status = ComponentStatus(
19
+ name="test_component",
20
+ message="All good",
21
+ response_time_ms=100.0,
22
+ metadata={"version": "1.0"},
23
+ )
24
+
25
+ assert status.name == "test_component"
26
+ assert status.healthy is True
27
+ assert status.message == "All good"
28
+ assert status.response_time_ms == 100.0
29
+ assert status.metadata == {"version": "1.0"}
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_system_status_properties(self) -> None:
33
+ """Test system status Pydantic model properties."""
34
+ status = await get_system_status()
35
+
36
+ assert isinstance(status.overall_healthy, bool)
37
+ assert len(status.components) >= 1
38
+ assert isinstance(status.healthy_components, list)
39
+ assert isinstance(status.unhealthy_components, list)
40
+ assert isinstance(status.health_percentage, float)
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_system_health_checks(self) -> None:
44
+ """Test basic health checks functionality."""
45
+ status = await get_system_status()
46
+
47
+ # Test that we get valid system information
48
+ assert hasattr(status, "components")
49
+ assert hasattr(status, "overall_healthy")
50
+ assert hasattr(status, "timestamp")
51
+ assert hasattr(status, "system_info")
52
+
53
+ # Verify components exist (at least core system checks)
54
+ assert len(status.components) > 0
55
+
56
+ # Check that each component has required fields
57
+ for component_name, component_status in status.components.items():
58
+ assert isinstance(component_name, str)
59
+ assert isinstance(component_status.healthy, bool)
60
+ assert isinstance(component_status.message, str)
61
+ assert isinstance(component_status.name, str)
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_is_system_healthy(self) -> None:
65
+ """Test quick health check function."""
66
+ healthy = await is_system_healthy()
67
+ assert isinstance(healthy, bool)
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_custom_health_check_registration(self) -> None:
71
+ """Test custom health check registration."""
72
+
73
+ async def custom_check() -> ComponentStatus:
74
+ return ComponentStatus(
75
+ name="custom_test",
76
+ message="Custom check passed",
77
+ response_time_ms=None,
78
+ )
79
+
80
+ # Register custom check
81
+ register_health_check("custom_test", custom_check)
82
+
83
+ try:
84
+ # Get status and verify custom check is included under aegis component
85
+ status = await get_system_status()
86
+ assert "aegis" in status.components
87
+ aegis_component = status.components["aegis"]
88
+ assert "components" in aegis_component.sub_components
89
+ components_group = aegis_component.sub_components["components"]
90
+ assert "custom_test" in components_group.sub_components
91
+ assert components_group.sub_components["custom_test"].name == "custom_test"
92
+ assert components_group.sub_components["custom_test"].healthy is True
93
+ finally:
94
+ # Clean up the custom health check registration
95
+ from app.services.system.health import _health_checks
96
+
97
+ if "custom_test" in _health_checks:
98
+ del _health_checks["custom_test"]