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,328 @@
1
+ {%- if cookiecutter.scheduler_backend != "memory" -%}
2
+ """
3
+ Scheduled task CLI commands.
4
+
5
+ Provides command-line interface for managing scheduled jobs,
6
+ viewing job statistics, and manually triggering tasks.
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ from datetime import datetime
12
+ from typing import Any
13
+
14
+ from rich import print as rprint
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.table import Table
18
+ import typer
19
+
20
+ from app.core.log import logger
21
+ from app.services.scheduler import ScheduledTaskManager
22
+ from app.services.scheduler.models import ScheduledTask
23
+
24
+ app = typer.Typer(
25
+ name="tasks",
26
+ help="Scheduled task management commands",
27
+ no_args_is_help=True,
28
+ )
29
+
30
+ console = Console()
31
+
32
+
33
+ @app.command("list")
34
+ def list_jobs() -> None:
35
+ """
36
+ List all scheduled jobs with their current status.
37
+
38
+ Shows job details including next run time, status, and configuration.
39
+ """
40
+
41
+ rprint("📋 [bold blue]Listing Scheduled Jobs[/bold blue]")
42
+
43
+ try:
44
+ tasks = asyncio.run(_get_scheduled_tasks())
45
+
46
+ if not tasks:
47
+ rprint("ℹ️ [yellow]No scheduled jobs found[/yellow]")
48
+ return
49
+
50
+ table = Table(
51
+ title="🕒 Scheduled Jobs",
52
+ show_header=True,
53
+ header_style="bold magenta",
54
+ )
55
+ table.add_column("Job ID", style="cyan", no_wrap=True)
56
+ table.add_column("Name", style="green")
57
+ table.add_column("Status", style="bold")
58
+ table.add_column("Next Run", style="yellow")
59
+ table.add_column("Trigger", style="dim")
60
+
61
+ for task in tasks:
62
+ status_color = "green" if task.is_active else "red"
63
+ next_run = (
64
+ task.next_run_time.strftime("%Y-%m-%d %H:%M:%S")
65
+ if task.next_run_time
66
+ else "Not scheduled"
67
+ )
68
+
69
+ table.add_row(
70
+ task.job_id,
71
+ task.name or task.job_id,
72
+ (
73
+ f"[{status_color}]{'Active' if task.is_active else 'Paused'}"
74
+ f"[/{status_color}]"
75
+ ),
76
+ next_run,
77
+ task.trigger_type or "Unknown"
78
+ )
79
+
80
+ console.print(table)
81
+ rprint(f"\n📊 [cyan]Total jobs:[/cyan] {len(tasks)}")
82
+
83
+ except Exception as e:
84
+ rprint(f"❌ [red]Failed to list jobs:[/red] {e}")
85
+ raise typer.Exit(1)
86
+
87
+
88
+ @app.command("stats")
89
+ def job_statistics(
90
+ job_id: str | None = typer.Argument(
91
+ None, help="Specific job ID to show stats for"
92
+ ),
93
+ json_output: bool = typer.Option(
94
+ False, "--json", help="Output stats in JSON format"
95
+ ),
96
+ ) -> None:
97
+ """
98
+ Show job execution statistics and performance metrics.
99
+
100
+ Without job_id: Shows overall scheduler statistics
101
+ With job_id: Shows detailed statistics for specific job
102
+ """
103
+
104
+ try:
105
+ if job_id:
106
+ rprint(f"📊 [bold blue]Job Statistics for: {job_id}[/bold blue]")
107
+ stats = asyncio.run(_get_job_statistics(job_id))
108
+ else:
109
+ rprint("📊 [bold blue]Overall Scheduler Statistics[/bold blue]")
110
+ stats = asyncio.run(_get_overall_statistics())
111
+
112
+ if json_output:
113
+ print(json.dumps(stats, indent=2, default=str))
114
+ return
115
+
116
+ _display_statistics(stats, job_id)
117
+
118
+ except Exception as e:
119
+ rprint(f"❌ [red]Failed to get statistics:[/red] {e}")
120
+ raise typer.Exit(1)
121
+
122
+
123
+ @app.command("history")
124
+ def job_history(
125
+ limit: int = typer.Option(
126
+ 20, "--limit", "-l", help="Number of recent executions to show", min=1, max=100
127
+ ),
128
+ job_id: str | None = typer.Option(
129
+ None, "--job-id", "-j", help="Filter by specific job ID"
130
+ ),
131
+ json_output: bool = typer.Option(
132
+ False, "--json", help="Output history in JSON format"
133
+ ),
134
+ ) -> None:
135
+ """
136
+ Show recent job execution history.
137
+
138
+ Displays execution times, durations, and results for recent job runs.
139
+ """
140
+
141
+ try:
142
+ if job_id:
143
+ rprint(
144
+ f"📚 [bold blue]Execution History for: {job_id}[/bold blue] "
145
+ f"(last {limit})"
146
+ )
147
+ else:
148
+ rprint(
149
+ f"📚 [bold blue]Recent Job Execution History[/bold blue] "
150
+ f"(last {limit})"
151
+ )
152
+
153
+ history = asyncio.run(_get_job_history(limit, job_id))
154
+
155
+ if json_output:
156
+ print(json.dumps(history, indent=2, default=str))
157
+ return
158
+
159
+ if not history:
160
+ rprint("ℹ️ [yellow]No execution history found[/yellow]")
161
+ return
162
+
163
+ table = Table(
164
+ title="🕰️ Job Execution History",
165
+ show_header=True,
166
+ header_style="bold magenta",
167
+ )
168
+ table.add_column("Timestamp", style="cyan")
169
+ table.add_column("Job ID", style="green")
170
+ table.add_column("Duration", style="yellow")
171
+ table.add_column("Status", style="bold")
172
+ table.add_column("Details", style="dim")
173
+
174
+ for execution in history:
175
+ status_color = "green" if execution.get("status") == "success" else "red"
176
+ duration = f"{execution.get('duration_ms', 0):.1f}ms"
177
+
178
+ status_value = execution.get('status', 'Unknown')
179
+ status_display = f"[{status_color}]{status_value}[/{status_color}]"
180
+ details = execution.get("details", "")[:50]
181
+ if len(execution.get("details", "")) > 50:
182
+ details += "..."
183
+
184
+ table.add_row(
185
+ execution.get("timestamp", "Unknown"),
186
+ execution.get("job_id", "Unknown"),
187
+ duration,
188
+ status_display,
189
+ details,
190
+ )
191
+
192
+ console.print(table)
193
+
194
+ except Exception as e:
195
+ rprint(f"❌ [red]Failed to get job history:[/red] {e}")
196
+ raise typer.Exit(1)
197
+
198
+
199
+ @app.command("trigger")
200
+ def trigger_job(
201
+ job_id: str = typer.Argument(..., help="Job ID to trigger manually"),
202
+ wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for job completion"),
203
+ timeout: int = typer.Option(
204
+ 30, "--timeout", help="Timeout for waiting (seconds)", min=1, max=300
205
+ ),
206
+ ) -> None:
207
+ """
208
+ Manually trigger a scheduled job.
209
+
210
+ Executes the specified job immediately, bypassing the normal schedule.
211
+ Useful for testing job functionality or running jobs on-demand.
212
+ """
213
+
214
+ rprint(f"🚀 [bold blue]Triggering job:[/bold blue] {job_id}")
215
+
216
+ try:
217
+ # First verify the job exists
218
+ tasks = asyncio.run(_get_scheduled_tasks())
219
+ job_exists = any(task.job_id == job_id for task in tasks)
220
+
221
+ if not job_exists:
222
+ rprint(f"❌ [red]Job not found:[/red] {job_id}")
223
+ rprint("💡 [yellow]Use 'tasks list' to see available jobs[/yellow]")
224
+ raise typer.Exit(1)
225
+
226
+ # Trigger the job
227
+ result = asyncio.run(_trigger_job_execution(job_id, wait, timeout))
228
+
229
+ if result.get("status") == "success":
230
+ rprint(f"✅ [green]Job triggered successfully![/green]")
231
+ if wait and result.get("execution_details"):
232
+ details = result["execution_details"]
233
+ duration_ms = details.get('duration_ms', 0)
234
+ rprint(f"⏱️ [cyan]Duration:[/cyan] {duration_ms:.1f}ms")
235
+ rprint(f"📋 [cyan]Result:[/cyan] {details.get('result', 'No result')}")
236
+ else:
237
+ error_msg = result.get('error', 'Unknown error')
238
+ rprint(f"❌ [red]Job execution failed:[/red] {error_msg}")
239
+
240
+ except Exception as e:
241
+ rprint(f"❌ [red]Failed to trigger job:[/red] {e}")
242
+ raise typer.Exit(1)
243
+
244
+
245
+ async def _get_scheduled_tasks() -> list[ScheduledTask]:
246
+ """Get list of scheduled tasks from the scheduler."""
247
+ manager = ScheduledTaskManager()
248
+ return await manager.list_tasks()
249
+
250
+
251
+ async def _get_job_statistics(job_id: str) -> dict[str, Any]:
252
+ """Get statistics for a specific job."""
253
+ manager = ScheduledTaskManager()
254
+ return await manager.get_job_statistics(job_id)
255
+
256
+
257
+ async def _get_overall_statistics() -> dict[str, Any]:
258
+ """Get overall scheduler statistics."""
259
+ manager = ScheduledTaskManager()
260
+ return await manager.get_overall_statistics()
261
+
262
+
263
+ async def _get_job_history(limit: int, job_id: str | None) -> list[dict[str, Any]]:
264
+ """Get recent job execution history."""
265
+ manager = ScheduledTaskManager()
266
+ return await manager.get_job_history(limit, job_id)
267
+
268
+
269
+ async def _trigger_job_execution(
270
+ job_id: str, wait: bool, timeout: int
271
+ ) -> dict[str, Any]:
272
+ """Trigger manual job execution."""
273
+ manager = ScheduledTaskManager()
274
+ return await manager.trigger_job(job_id, wait, timeout)
275
+
276
+
277
+ def _display_statistics(stats: dict[str, Any], job_id: str | None) -> None:
278
+ """Display formatted statistics."""
279
+
280
+ if job_id:
281
+ # Job-specific statistics
282
+ info_text = f"""
283
+ [bold cyan]Job Performance Metrics[/bold cyan]
284
+
285
+ 🏃 [yellow]Executions:[/yellow] {stats.get('total_executions', 0)}
286
+ ✅ [yellow]Successful:[/yellow] {stats.get('successful_executions', 0)}
287
+ ❌ [yellow]Failed:[/yellow] {stats.get('failed_executions', 0)}
288
+ 📊 [yellow]Success Rate:[/yellow] {stats.get('success_rate_percent', 0):.1f}%
289
+
290
+ ⏱️ [yellow]Avg Duration:[/yellow] {stats.get('avg_duration_ms', 0):.1f}ms
291
+ 🚀 [yellow]Min Duration:[/yellow] {stats.get('min_duration_ms', 0):.1f}ms
292
+ 🐌 [yellow]Max Duration:[/yellow] {stats.get('max_duration_ms', 0):.1f}ms
293
+
294
+ 🕐 [yellow]Last Execution:[/yellow] {stats.get('last_execution', 'Never')}
295
+ 🕑 [yellow]Next Scheduled:[/yellow] {stats.get('next_run', 'Not scheduled')}
296
+ """.strip()
297
+
298
+ title = f"📊 Statistics for {job_id}"
299
+ console.print(Panel(info_text, title=title, border_style="blue"))
300
+
301
+ else:
302
+ # Overall scheduler statistics
303
+ info_text = f"""
304
+ [bold cyan]Scheduler Overview[/bold cyan]
305
+
306
+ 📋 [yellow]Total Jobs:[/yellow] {stats.get('total_jobs', 0)}
307
+ 🟢 [yellow]Active Jobs:[/yellow] {stats.get('active_jobs', 0)}
308
+ 🔴 [yellow]Paused Jobs:[/yellow] {stats.get('paused_jobs', 0)}
309
+
310
+ 🏃 [yellow]Total Executions:[/yellow] {stats.get('total_executions', 0)}
311
+ ✅ [yellow]Successful:[/yellow] {stats.get('successful_executions', 0)}
312
+ ❌ [yellow]Failed:[/yellow] {stats.get('failed_executions', 0)}
313
+
314
+ 📊 [yellow]Overall Success Rate:[/yellow] {stats.get('success_rate_percent', 0):.1f}%
315
+ ⏱️ [yellow]Avg Execution Time:[/yellow] {stats.get('avg_duration_ms', 0):.1f}ms
316
+
317
+ 🕐 [yellow]Scheduler Uptime:[/yellow] {stats.get('uptime', 'Unknown')}
318
+ 🔄 [yellow]Last Activity:[/yellow] {stats.get('last_activity', 'No recent activity')}
319
+ """.strip()
320
+
321
+ console.print(
322
+ Panel(info_text, title="📊 Scheduler Statistics", border_style="green")
323
+ )
324
+
325
+
326
+ if __name__ == "__main__":
327
+ app()
328
+ {%- endif -%}
@@ -0,0 +1,340 @@
1
+ """
2
+ Scheduled task CLI commands.
3
+
4
+ Provides command-line interface for managing scheduled jobs,
5
+ viewing job statistics, and manually triggering tasks.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ from typing import Any
11
+
12
+ from rich import print as rprint
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+ import typer
17
+
18
+ from app.services.scheduler import ScheduledTaskManager
19
+
20
+ app = typer.Typer(
21
+ name="tasks",
22
+ help="Scheduled task management commands",
23
+ no_args_is_help=True,
24
+ )
25
+
26
+ console = Console()
27
+
28
+
29
+ @app.command("list")
30
+ def list_jobs() -> None:
31
+ """
32
+ List all scheduled jobs with their current status.
33
+
34
+ Shows job details including next run time, status, and configuration.
35
+ """
36
+
37
+ rprint("📋 [bold blue]Listing Scheduled Jobs[/bold blue]")
38
+
39
+ try:
40
+ tasks = asyncio.run(_get_scheduled_tasks())
41
+
42
+ if not tasks:
43
+ rprint("ℹ️ [yellow]No scheduled jobs found[/yellow]")
44
+ return
45
+
46
+ table = Table(
47
+ title="🕒 Scheduled Jobs",
48
+ show_header=True,
49
+ header_style="bold magenta",
50
+ )
51
+ table.add_column("Job ID", style="cyan", no_wrap=True)
52
+ table.add_column("Name", style="green")
53
+ table.add_column("Status", style="bold")
54
+ table.add_column("Next Run", style="yellow")
55
+ table.add_column("Trigger", style="dim")
56
+
57
+ for task in tasks:
58
+ status_color = "green" if task.is_active else "red"
59
+ next_run = (
60
+ task.next_run_time.strftime("%Y-%m-%d %H:%M:%S")
61
+ if task.next_run_time
62
+ else "Not scheduled"
63
+ )
64
+
65
+ table.add_row(
66
+ task.job_id,
67
+ task.name or task.job_id,
68
+ (
69
+ f"[{status_color}]{'Active' if task.is_active else 'Paused'}"
70
+ f"[/{status_color}]"
71
+ ),
72
+ next_run,
73
+ task.trigger_type or "Unknown"
74
+ )
75
+
76
+ console.print(table)
77
+ rprint(f"\n📊 [cyan]Total jobs:[/cyan] {len(tasks)}")
78
+
79
+ except Exception as e:
80
+ rprint(f"❌ [red]Failed to list jobs:[/red] {e}")
81
+ raise typer.Exit(1)
82
+
83
+
84
+ @app.command("stats")
85
+ def job_statistics(
86
+ job_id: str | None = typer.Argument(None, help="Specific job ID to show stats for"),
87
+ json_output: bool = typer.Option(
88
+ False, "--json", help="Output stats in JSON format"
89
+ ),
90
+ ) -> None:
91
+ """
92
+ Show job execution statistics and performance metrics.
93
+
94
+ Without job_id: Shows overall scheduler statistics
95
+ With job_id: Shows detailed statistics for specific job
96
+ """
97
+
98
+ try:
99
+ if job_id:
100
+ rprint(f"📊 [bold blue]Job Statistics for: {job_id}[/bold blue]")
101
+ stats = asyncio.run(_get_job_statistics(job_id))
102
+ else:
103
+ rprint("📊 [bold blue]Overall Scheduler Statistics[/bold blue]")
104
+ stats = asyncio.run(_get_overall_statistics())
105
+
106
+ if json_output:
107
+ print(json.dumps(stats, indent=2, default=str))
108
+ return
109
+
110
+ _display_statistics(stats, job_id)
111
+
112
+ except Exception as e:
113
+ rprint(f"❌ [red]Failed to get statistics:[/red] {e}")
114
+ raise typer.Exit(1)
115
+
116
+
117
+ @app.command("history")
118
+ def job_history(
119
+ limit: int = typer.Option(
120
+ 20,
121
+ "--limit",
122
+ "-l",
123
+ help="Number of recent executions to show",
124
+ min=1,
125
+ max=100,
126
+ ),
127
+ job_id: str | None = typer.Option(
128
+ None, "--job-id", "-j", help="Filter by specific job ID"
129
+ ),
130
+ json_output: bool = typer.Option(
131
+ False, "--json", help="Output history in JSON format"
132
+ ),
133
+ ) -> None:
134
+ """
135
+ Show recent job execution history.
136
+
137
+ Displays execution times, durations, and results for recent job runs.
138
+ """
139
+
140
+ try:
141
+ if job_id:
142
+ rprint(
143
+ f"📚 [bold blue]Execution History for: {job_id}[/bold blue] "
144
+ f"(last {limit})"
145
+ )
146
+ else:
147
+ rprint(
148
+ f"📚 [bold blue]Recent Job Execution History[/bold blue] "
149
+ f"(last {limit})"
150
+ )
151
+
152
+ history = asyncio.run(_get_job_history(limit, job_id))
153
+
154
+ if json_output:
155
+ print(json.dumps(history, indent=2, default=str))
156
+ return
157
+
158
+ if not history:
159
+ rprint("ℹ️ [yellow]No execution history found[/yellow]")
160
+ return
161
+
162
+ table = Table(
163
+ title="🕰️ Job Execution History",
164
+ show_header=True,
165
+ header_style="bold magenta",
166
+ )
167
+ table.add_column("Timestamp", style="cyan")
168
+ table.add_column("Job ID", style="green")
169
+ table.add_column("Duration", style="yellow")
170
+ table.add_column("Status", style="bold")
171
+ table.add_column("Details", style="dim")
172
+
173
+ for execution in history:
174
+ status_color = "green" if execution.get("status") == "success" else "red"
175
+ duration = f"{execution.get('duration_ms', 0):.1f}ms"
176
+
177
+ status_value = execution.get('status', 'Unknown')
178
+ status_display = f"[{status_color}]{status_value}[/{status_color}]"
179
+ details = execution.get("details", "")[:50]
180
+ if len(execution.get("details", "")) > 50:
181
+ details += "..."
182
+
183
+ table.add_row(
184
+ execution.get("timestamp", "Unknown"),
185
+ execution.get("job_id", "Unknown"),
186
+ duration,
187
+ status_display,
188
+ details,
189
+ )
190
+
191
+ console.print(table)
192
+
193
+ except Exception as e:
194
+ rprint(f"❌ [red]Failed to get job history:[/red] {e}")
195
+ raise typer.Exit(1)
196
+
197
+
198
+ @app.command("trigger")
199
+ def trigger_job(
200
+ job_id: str = typer.Argument(..., help="Job ID to trigger manually"),
201
+ wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for job completion"),
202
+ timeout: int = typer.Option(
203
+ 30, "--timeout", help="Timeout for waiting (seconds)", min=1, max=300
204
+ ),
205
+ ) -> None:
206
+ """
207
+ Manually trigger a scheduled job.
208
+
209
+ Executes the specified job immediately, bypassing the normal schedule.
210
+ Useful for testing job functionality or running jobs on-demand.
211
+ """
212
+
213
+ rprint(f"🚀 [bold blue]Triggering job:[/bold blue] {job_id}")
214
+
215
+ try:
216
+ # First verify the job exists
217
+ tasks = asyncio.run(_get_scheduled_tasks())
218
+ job_exists = any(task.job_id == job_id for task in tasks)
219
+
220
+ if not job_exists:
221
+ rprint(f"❌ [red]Job not found:[/red] {job_id}")
222
+ rprint("💡 [yellow]Use 'tasks list' to see available jobs[/yellow]")
223
+ raise typer.Exit(1)
224
+
225
+ # Trigger the job
226
+ result = asyncio.run(_trigger_job_execution(job_id, wait, timeout))
227
+
228
+ if result.get("status") == "success":
229
+ rprint("✅ [green]Job triggered successfully![/green]")
230
+ if wait and result.get("execution_details"):
231
+ details = result["execution_details"]
232
+ rprint(
233
+ f"⏱️ [cyan]Duration:[/cyan] {details.get('duration_ms', 0):.1f}ms"
234
+ )
235
+ rprint(f"📋 [cyan]Result:[/cyan] {details.get('result', 'No result')}")
236
+ else:
237
+ rprint(
238
+ f"❌ [red]Job execution failed:[/red] "
239
+ f"{result.get('error', 'Unknown error')}"
240
+ )
241
+
242
+ except Exception as e:
243
+ rprint(f"❌ [red]Failed to trigger job:[/red] {e}")
244
+ raise typer.Exit(1)
245
+
246
+
247
+ async def _get_scheduled_tasks() -> list[Any]:
248
+ """Get list of scheduled tasks from the scheduler."""
249
+ manager = ScheduledTaskManager()
250
+ return await manager.list_tasks()
251
+
252
+
253
+ async def _get_job_statistics(job_id: str) -> dict[str, Any]:
254
+ """Get statistics for a specific job."""
255
+ manager = ScheduledTaskManager()
256
+ return await manager.get_job_statistics(job_id)
257
+
258
+
259
+ async def _get_overall_statistics() -> dict[str, Any]:
260
+ """Get overall scheduler statistics."""
261
+ manager = ScheduledTaskManager()
262
+ return await manager.get_overall_statistics()
263
+
264
+
265
+ async def _get_job_history(
266
+ limit: int, job_id: str | None
267
+ ) -> list[dict[str, Any]]:
268
+ """Get recent job execution history."""
269
+ manager = ScheduledTaskManager()
270
+ return await manager.get_job_history(limit, job_id)
271
+
272
+
273
+ async def _trigger_job_execution(
274
+ job_id: str, wait: bool, timeout: int
275
+ ) -> dict[str, Any]:
276
+ """Trigger manual job execution."""
277
+ manager = ScheduledTaskManager()
278
+ return await manager.trigger_job(job_id, wait, timeout)
279
+
280
+
281
+ def _display_statistics(stats: dict[str, Any], job_id: str | None) -> None:
282
+ """Display formatted statistics."""
283
+
284
+ if job_id:
285
+ # Job-specific statistics
286
+ info_text = f"""
287
+ [bold cyan]Job Performance Metrics[/bold cyan]
288
+
289
+ 🏃 [yellow]Executions:[/yellow] {stats.get('total_executions', 0)}
290
+ ✅ [yellow]Successful:[/yellow] {stats.get('successful_executions', 0)}
291
+ ❌ [yellow]Failed:[/yellow] {stats.get('failed_executions', 0)}
292
+ 📊 [yellow]Success Rate:[/yellow] {stats.get('success_rate_percent', 0):.1f}%
293
+
294
+ ⏱️ [yellow]Avg Duration:[/yellow] {stats.get('avg_duration_ms', 0):.1f}ms
295
+ 🚀 [yellow]Min Duration:[/yellow] {stats.get('min_duration_ms', 0):.1f}ms
296
+ 🐌 [yellow]Max Duration:[/yellow] {stats.get('max_duration_ms', 0):.1f}ms
297
+
298
+ 🕐 [yellow]Last Execution:[/yellow] {stats.get('last_execution', 'Never')}
299
+ 🕑 [yellow]Next Scheduled:[/yellow] {stats.get('next_run', 'Not scheduled')}
300
+ """.strip()
301
+
302
+ console.print(
303
+ Panel(
304
+ info_text,
305
+ title=f"📊 Statistics for {job_id}",
306
+ border_style="blue",
307
+ )
308
+ )
309
+
310
+ else:
311
+ # Overall scheduler statistics
312
+ info_text = f"""
313
+ [bold cyan]Scheduler Overview[/bold cyan]
314
+
315
+ 📋 [yellow]Total Jobs:[/yellow] {stats.get('total_jobs', 0)}
316
+ 🟢 [yellow]Active Jobs:[/yellow] {stats.get('active_jobs', 0)}
317
+ 🔴 [yellow]Paused Jobs:[/yellow] {stats.get('paused_jobs', 0)}
318
+
319
+ 🏃 [yellow]Total Executions:[/yellow] {stats.get('total_executions', 0)}
320
+ ✅ [yellow]Successful:[/yellow] {stats.get('successful_executions', 0)}
321
+ ❌ [yellow]Failed:[/yellow] {stats.get('failed_executions', 0)}
322
+
323
+ 📊 [yellow]Overall Success Rate:[/yellow] {stats.get('success_rate_percent', 0):.1f}%
324
+ ⏱️ [yellow]Avg Execution Time:[/yellow] {stats.get('avg_duration_ms', 0):.1f}ms
325
+
326
+ 🕐 [yellow]Scheduler Uptime:[/yellow] {stats.get('uptime', 'Unknown')}
327
+ 🔄 [yellow]Last Activity:[/yellow] {stats.get('last_activity', 'No recent activity')}
328
+ """.strip()
329
+
330
+ console.print(
331
+ Panel(
332
+ info_text,
333
+ title="📊 Scheduler Statistics",
334
+ border_style="green",
335
+ )
336
+ )
337
+
338
+
339
+ if __name__ == "__main__":
340
+ app()
@@ -0,0 +1,8 @@
1
+ """
2
+ AI service API routes.
3
+
4
+ REST API endpoints for AI chat functionality.
5
+ """
6
+
7
+ # API routes will be exported here in ticket #159
8
+ __all__ = []