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,360 @@
1
+ """
2
+ Shared rendering utilities for AI chat responses.
3
+
4
+ Provides consistent, beautiful output formatting across streaming
5
+ and non-streaming modes using marko markdown parser with terminal rendering.
6
+ """
7
+
8
+ import re
9
+
10
+ from app.cli.marko_terminal_renderer import TerminalRenderer
11
+ from marko import Markdown
12
+ from marko.ext.gfm import GFM
13
+ from rich.console import Console
14
+
15
+
16
+ class StreamingMarkdownRenderer:
17
+ """
18
+ Line-based streaming markdown renderer using marko.
19
+
20
+ Processes markdown content as it streams in, using marko to parse complete
21
+ blocks and render them with beautiful ANSI styling for terminal output.
22
+ """
23
+
24
+ def __init__(self, console: Console):
25
+ """
26
+ Initialize streaming renderer.
27
+
28
+ Args:
29
+ console: Rich console instance for output management
30
+ """
31
+ self.console = console
32
+ self.buffer = ""
33
+ self.in_code_block = False
34
+ self.code_buffer = []
35
+ self.code_lang = ""
36
+ self.markdown = Markdown(extensions=[GFM], renderer=TerminalRenderer)
37
+
38
+ def add_delta(self, delta: str) -> None:
39
+ """
40
+ Process streaming delta and display formatted content with smart buffering.
41
+
42
+ Uses line-buffering for markdown structures (code blocks, lists, tables)
43
+ and word-streaming for plain conversational text for smooth output.
44
+
45
+ Args:
46
+ delta: New text content to process
47
+ """
48
+ # Add new content to buffer
49
+ self.buffer += delta
50
+
51
+ # Smart buffering based on content type
52
+ if self.in_code_block or self._is_markdown_structure():
53
+ # Use line-buffering for markdown structures (safe, correct formatting)
54
+ self._process_complete_lines()
55
+ else:
56
+ # Use word-streaming for plain text (smooth, responsive)
57
+ self._stream_plain_text()
58
+
59
+ def _is_markdown_structure(self) -> bool:
60
+ """
61
+ Detect if buffer contains markdown structures requiring line-buffering.
62
+
63
+ Returns:
64
+ True if buffer contains markdown patterns, False for plain text
65
+ """
66
+ if not self.buffer.strip():
67
+ return False
68
+
69
+ # Get the current line being built (text after last newline)
70
+ current_line = self.buffer.split("\n")[-1].strip()
71
+
72
+ # Markdown patterns that need careful line-by-line handling
73
+ markdown_indicators = [
74
+ "```", # Code blocks (critical!)
75
+ "#", # Headers
76
+ "- ", # Unordered lists
77
+ "* ", # Unordered lists (alternate)
78
+ "+ ", # Unordered lists (alternate)
79
+ "> ", # Blockquotes
80
+ "|", # Tables
81
+ ]
82
+
83
+ # Check if line starts with markdown
84
+ for indicator in markdown_indicators:
85
+ if current_line.startswith(indicator):
86
+ return True
87
+
88
+ # Check for numbered lists using regex (handles any number)
89
+ # Check for inline formatting that might break across words
90
+ # (bold/italic can be streamed word-by-word safely)
91
+ return bool(re.match(r"^\d+\. ", current_line))
92
+
93
+ def _stream_plain_text(self) -> None:
94
+ """
95
+ Stream plain text word-by-word for smooth, responsive output.
96
+
97
+ Renders complete words immediately while keeping incomplete words
98
+ in buffer. Falls back to line-buffering when newlines are encountered.
99
+ """
100
+ # If we hit a newline, process complete lines normally
101
+ if "\n" in self.buffer:
102
+ self._process_complete_lines()
103
+ return
104
+
105
+ # Word-level streaming for smooth plain text output
106
+ # Split on spaces to identify complete words
107
+ if " " in self.buffer:
108
+ # Split and find word boundaries
109
+ parts = self.buffer.rsplit(" ", 1) # Split from right to keep last word
110
+
111
+ if len(parts) == 2:
112
+ complete_text, incomplete_word = parts
113
+
114
+ # Render complete text with trailing space
115
+ if complete_text:
116
+ # For plain text, just write directly (no markdown parsing needed)
117
+ self.console.file.write(complete_text + " ")
118
+ self.console.file.flush()
119
+
120
+ # Keep incomplete word in buffer
121
+ self.buffer = incomplete_word
122
+
123
+ def _process_complete_lines(self) -> None:
124
+ """Process any complete lines in the buffer."""
125
+ lines = self.buffer.split("\n")
126
+
127
+ # Keep the last (potentially incomplete) line in buffer
128
+ if len(lines) > 1:
129
+ complete_lines = lines[:-1]
130
+ self.buffer = lines[-1]
131
+
132
+ # Process each complete line
133
+ for line in complete_lines:
134
+ self._render_line(line)
135
+
136
+ def _render_line(self, line: str) -> None:
137
+ """
138
+ Render a complete line with markdown formatting using marko.
139
+
140
+ Args:
141
+ line: Complete line to render
142
+ """
143
+ # Handle code blocks (accumulate until closing)
144
+ if self.in_code_block:
145
+ if line.strip() == "```":
146
+ # End of code block - render complete block
147
+ self._render_code_block()
148
+ self.in_code_block = False
149
+ self.code_buffer = []
150
+ self.code_lang = ""
151
+ else:
152
+ # Inside code block, accumulate
153
+ self.code_buffer.append(line)
154
+ return
155
+
156
+ if line.strip().startswith("```"):
157
+ # Start of code block
158
+ self.code_lang = line.strip()[3:].strip() or "text"
159
+ self.in_code_block = True
160
+ self.code_buffer = []
161
+ return
162
+
163
+ # For other content, parse line as markdown and render
164
+ if line.strip():
165
+ # Parse and render single line with marko
166
+ rendered = self.markdown(line)
167
+ # Write to console's file to support both terminal and testing
168
+ self.console.file.write(rendered)
169
+ self.console.file.flush()
170
+ else:
171
+ # Empty line
172
+ self.console.print()
173
+
174
+ def _render_code_block(self) -> None:
175
+ """Render accumulated code block using marko."""
176
+ code_content = "\n".join(self.code_buffer)
177
+
178
+ # Create markdown code block
179
+ markdown_code = f"```{self.code_lang}\n{code_content}\n```"
180
+
181
+ # Parse and render with marko
182
+ rendered = self.markdown(markdown_code)
183
+ # Write to console's file to support both terminal and testing
184
+ self.console.file.write(rendered)
185
+ self.console.file.flush()
186
+
187
+ def finalize(self) -> None:
188
+ """Finalize any remaining content in buffer."""
189
+ if self.buffer.strip():
190
+ # Process any remaining incomplete line
191
+ rendered = self.markdown(self.buffer.strip())
192
+ # Write to console's file to support both terminal and testing
193
+ self.console.file.write(rendered)
194
+ self.console.file.flush()
195
+
196
+ # Handle unclosed code block
197
+ if self.in_code_block and self.code_buffer:
198
+ self._render_code_block()
199
+
200
+
201
+ def render_ai_header(console: Console, inline: bool = True) -> None:
202
+ """
203
+ Render the AI response header.
204
+
205
+ Args:
206
+ console: Rich console instance
207
+ inline: If True, use inline style (🤖:), else use separate line style
208
+
209
+ Examples:
210
+ >>> render_ai_header(console, inline=True) # Outputs: "🤖: "
211
+ >>> render_ai_header(console, inline=False) # Outputs: "🤖 Response:"
212
+ """
213
+ if inline:
214
+ console.print("🤖: ", style="bright_blue", end="")
215
+ else:
216
+ console.print("🤖 ", style="bright_blue", end="")
217
+ console.print("Response:", style="bright_blue bold")
218
+
219
+
220
+ def render_markdown_response(console: Console, content: str) -> None:
221
+ """
222
+ Render markdown content with beautiful terminal styling using marko.
223
+
224
+ Parses complete markdown content and renders with ANSI-styled output
225
+ for beautiful terminal display.
226
+
227
+ Args:
228
+ console: Rich console instance
229
+ content: Markdown content to render
230
+
231
+ Examples:
232
+ >>> render_markdown_response(console, "# Hello World")
233
+ >>> render_markdown_response(console, "```python\\nprint('hi')\\n```")
234
+ """
235
+ if not content or not content.strip():
236
+ console.print("(No response content)", style="dim italic")
237
+ return
238
+
239
+ # Clean up excessive whitespace from AI responses
240
+ cleaned_content = _clean_ai_content(content)
241
+
242
+ # Parse and render with marko (GFM for table support)
243
+ markdown = Markdown(extensions=[GFM], renderer=TerminalRenderer)
244
+ rendered = markdown(cleaned_content)
245
+
246
+ # Write to console's file to support both terminal and testing
247
+ console.file.write(rendered)
248
+ console.file.flush()
249
+
250
+
251
+ def _clean_ai_content(content: str) -> str:
252
+ """
253
+ Clean up excessive whitespace and formatting issues from AI responses.
254
+
255
+ Some AI providers send responses with excessive blank lines or spacing
256
+ that hurts readability. This function normalizes the content.
257
+
258
+ Args:
259
+ content: Raw AI response content
260
+
261
+ Returns:
262
+ Cleaned content with normalized spacing
263
+ """
264
+
265
+ # Split into lines for processing
266
+ lines = content.split("\n")
267
+ cleaned_lines = []
268
+ consecutive_empty = 0
269
+
270
+ for line in lines:
271
+ is_empty = not line.strip()
272
+
273
+ if is_empty:
274
+ consecutive_empty += 1
275
+ # Allow maximum 1 consecutive empty line
276
+ if consecutive_empty <= 1:
277
+ cleaned_lines.append("")
278
+ else:
279
+ consecutive_empty = 0
280
+ # Clean up the line (remove trailing whitespace)
281
+ cleaned_lines.append(line.rstrip())
282
+
283
+ # Remove leading and trailing empty lines
284
+ while cleaned_lines and not cleaned_lines[0].strip():
285
+ cleaned_lines.pop(0)
286
+ while cleaned_lines and not cleaned_lines[-1].strip():
287
+ cleaned_lines.pop()
288
+
289
+ # Join back together
290
+ return "\n".join(cleaned_lines)
291
+
292
+
293
+ def render_conversation_metadata(
294
+ console: Console,
295
+ conversation_id: str,
296
+ message_count: int | None = None,
297
+ response_time: float | None = None,
298
+ ) -> None:
299
+ """
300
+ Render conversation metadata consistently.
301
+
302
+ Args:
303
+ console: Rich console instance
304
+ conversation_id: The conversation identifier
305
+ message_count: Number of messages in conversation
306
+ response_time: Response time in milliseconds
307
+
308
+ Examples:
309
+ >>> render_conversation_metadata(console, "conv-123")
310
+ >>> render_conversation_metadata(
311
+ ... console, "conv-123", message_count=5, response_time=150.5
312
+ ... )
313
+ """
314
+ console.print() # Blank line for spacing
315
+ console.print(f"💬 Conversation: {conversation_id}", style="dim")
316
+ if message_count:
317
+ console.print(f"ℹ️ Messages: {message_count}", style="dim")
318
+ if response_time:
319
+ console.print(f"⏱️ Response time: {response_time:.1f}ms", style="dim")
320
+
321
+
322
+ def render_error_message(
323
+ console: Console, error: str, suggestion: str | None = None
324
+ ) -> None:
325
+ """
326
+ Render an error message consistently.
327
+
328
+ Args:
329
+ console: Rich console instance
330
+ error: The error message to display
331
+ suggestion: Optional suggestion for fixing the error
332
+
333
+ Examples:
334
+ >>> render_error_message(console, "Connection failed")
335
+ >>> render_error_message(console, "API key invalid", "Check your .env file")
336
+ """
337
+ console.print(f"❌ Error: {error}", style="red")
338
+ if suggestion:
339
+ console.print(f"💡 Suggestion: {suggestion}", style="yellow dim")
340
+
341
+
342
+ def render_thinking_spinner(console: Console) -> tuple:
343
+ """
344
+ Create a thinking spinner for AI processing.
345
+
346
+ Returns:
347
+ Tuple of (Spinner, Live) objects to control the spinner
348
+
349
+ Examples:
350
+ >>> spinner, live = render_thinking_spinner(console)
351
+ >>> live.start()
352
+ >>> # ... do work ...
353
+ >>> live.stop()
354
+ """
355
+ from rich.live import Live
356
+ from rich.spinner import Spinner
357
+
358
+ spinner = Spinner("dots", text="🤖 Thinking...", style="bright_blue")
359
+ live = Live(spinner, console=console, refresh_per_second=12)
360
+ return spinner, live
@@ -0,0 +1,253 @@
1
+ """
2
+ Authentication CLI commands.
3
+
4
+ Command-line interface for auth service management tasks.
5
+ """
6
+
7
+ import asyncio
8
+ import secrets
9
+ import string
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
+ import typer
16
+
17
+ from app.core.db import get_async_session
18
+ from app.models.user import UserCreate
19
+ from app.services.auth.user_service import UserService
20
+
21
+ app = typer.Typer(help="Authentication management commands")
22
+
23
+
24
+ def generate_password(length: int = 12) -> str:
25
+ """Generate a secure random password."""
26
+ alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
27
+ return "".join(secrets.choice(alphabet) for _ in range(length))
28
+
29
+
30
+ async def find_next_available_email(
31
+ user_service: UserService, prefix: str = "test", domain: str = "example.com"
32
+ ) -> str:
33
+ """Find the next available email with auto-increment."""
34
+ # Get existing emails with this prefix
35
+ existing_emails = await user_service.find_existing_emails_with_prefix(
36
+ prefix, domain
37
+ )
38
+
39
+ if not existing_emails:
40
+ # No existing emails, start with prefix@domain
41
+ return f"{prefix}@{domain}"
42
+
43
+ # Extract numbers from existing emails
44
+ used_numbers = set()
45
+ for email in existing_emails:
46
+ # Extract the part before @
47
+ local_part = email.split("@")[0]
48
+
49
+ # Check if it matches our pattern (prefix + optional number)
50
+ if local_part == prefix:
51
+ used_numbers.add(0) # Base email without number
52
+ elif local_part.startswith(prefix):
53
+ suffix = local_part[len(prefix):]
54
+ if suffix.isdigit():
55
+ used_numbers.add(int(suffix))
56
+
57
+ # Find the next available number
58
+ counter = 0
59
+ while counter in used_numbers:
60
+ counter += 1
61
+
62
+ # Return the email with the next number
63
+ if counter == 0:
64
+ return f"{prefix}@{domain}"
65
+ else:
66
+ return f"{prefix}{counter}@{domain}"
67
+
68
+
69
+ @app.command()
70
+ def create_test_user(
71
+ email: str | None = typer.Option(
72
+ None,
73
+ help=(
74
+ "User email address (auto-increment: test@example.com, "
75
+ "test1@example.com, etc.)"
76
+ ),
77
+ ),
78
+ password: str | None = typer.Option(
79
+ None, help="User password (generated if not provided)"
80
+ ),
81
+ full_name: str | None = typer.Option(None, help="User full name"),
82
+ prefix: str = typer.Option("test", help="Email prefix for auto-generated emails"),
83
+ domain: str = typer.Option(
84
+ "example.com", help="Email domain for auto-generated emails"
85
+ ),
86
+ ) -> None:
87
+ """Create a test user for development and testing."""
88
+ asyncio.run(_create_test_user(email, password, full_name, prefix, domain))
89
+
90
+
91
+ async def _create_test_user(
92
+ email: str | None,
93
+ password: str | None,
94
+ full_name: str | None,
95
+ prefix: str,
96
+ domain: str,
97
+ ) -> None:
98
+ """Async implementation of create_test_user."""
99
+ # Generate password if not provided
100
+ if password is None:
101
+ password = generate_password()
102
+ generated_password = True
103
+ else:
104
+ generated_password = False
105
+
106
+ try:
107
+ async with get_async_session() as session:
108
+ user_service = UserService(session)
109
+
110
+ # Auto-generate email if not provided
111
+ if email is None:
112
+ email = await find_next_available_email(user_service, prefix, domain)
113
+ typer.echo(f"📧 Auto-generated email: {email} (next in sequence)")
114
+
115
+ # Check if user already exists
116
+ existing_user = await user_service.get_user_by_email(email)
117
+ if existing_user:
118
+ typer.echo(f"❌ User with email '{email}' already exists", err=True)
119
+ raise typer.Exit(1)
120
+
121
+ # Create user data
122
+ user_data = UserCreate(
123
+ email=email,
124
+ password=password,
125
+ full_name=full_name,
126
+ )
127
+
128
+ # Create the user
129
+ user = await user_service.create_user(user_data)
130
+
131
+ # Display success message
132
+ typer.echo("✅ Test user created successfully!")
133
+ typer.echo("=" * 50)
134
+ typer.echo(f"📧 Email: {user.email}")
135
+ typer.echo(f"🔑 Password: {password}")
136
+ if user.full_name:
137
+ typer.echo(f"👤 Name: {user.full_name}")
138
+ typer.echo(f"🆔 User ID: {user.id}")
139
+ typer.echo("=" * 50)
140
+
141
+ if generated_password:
142
+ typer.echo("💡 Password was auto-generated. Save it for testing!")
143
+
144
+ typer.echo("🚀 Ready to test auth endpoints at http://localhost:8000/docs")
145
+
146
+ except Exception as e:
147
+ typer.echo(f"❌ Failed to create test user: {str(e)}", err=True)
148
+ raise typer.Exit(1)
149
+
150
+
151
+ @app.command()
152
+ def create_test_users(
153
+ count: int = typer.Option(5, help="Number of test users to create"),
154
+ prefix: str = typer.Option("test", help="Email prefix for generated users"),
155
+ domain: str = typer.Option("example.com", help="Email domain for generated users"),
156
+ password: str | None = typer.Option(
157
+ None, help="Shared password (generated if not provided)"
158
+ ),
159
+ ) -> None:
160
+ """Create multiple test users for development and testing."""
161
+ asyncio.run(_create_test_users(count, prefix, domain, password))
162
+
163
+
164
+ async def _create_test_users(
165
+ count: int, prefix: str, domain: str, password: str | None
166
+ ) -> None:
167
+ """Async implementation of create_test_users."""
168
+ # Generate password if not provided
169
+ if password is None:
170
+ password = generate_password()
171
+ generated_password = True
172
+ else:
173
+ generated_password = False
174
+
175
+ if count <= 0:
176
+ typer.echo("❌ Count must be greater than 0", err=True)
177
+ raise typer.Exit(1)
178
+
179
+ try:
180
+ async with get_async_session() as session:
181
+ user_service = UserService(session)
182
+ created_users = []
183
+
184
+ typer.echo(f"🚀 Creating {count} test users with prefix '{prefix}'...")
185
+ typer.echo("=" * 60)
186
+
187
+ for i in range(count):
188
+ # Find next available email
189
+ email = await find_next_available_email(user_service, prefix, domain)
190
+
191
+ # Create user data
192
+ user_data = UserCreate(
193
+ email=email,
194
+ password=password,
195
+ full_name=f"Test User {email.split('@')[0].capitalize()}",
196
+ )
197
+
198
+ # Create the user
199
+ user = await user_service.create_user(user_data)
200
+ created_users.append(user)
201
+
202
+ typer.echo(f"✅ Created: {user.email} (ID: {user.id})")
203
+
204
+ # Display summary
205
+ typer.echo("=" * 60)
206
+ typer.echo(f"🎉 Successfully created {len(created_users)} test users!")
207
+ typer.echo(f"🔑 Shared password: {password}")
208
+
209
+ if generated_password:
210
+ typer.echo("💡 Password was auto-generated. Save it for testing!")
211
+
212
+ typer.echo("🚀 Ready to test auth endpoints at http://localhost:8000/docs")
213
+
214
+ except Exception as e:
215
+ typer.echo(f"❌ Failed to create test users: {str(e)}", err=True)
216
+ raise typer.Exit(1)
217
+
218
+
219
+ @app.command()
220
+ def list_users() -> None:
221
+ """List all users in the system."""
222
+ asyncio.run(_list_users())
223
+
224
+
225
+ async def _list_users() -> None:
226
+ """Async implementation of list_users."""
227
+ try:
228
+ async with get_async_session() as session:
229
+ user_service = UserService(session)
230
+ users = await user_service.list_users()
231
+
232
+ if not users:
233
+ typer.echo("No users found.")
234
+ return
235
+
236
+ typer.echo(f"Found {len(users)} user(s):")
237
+ typer.echo("=" * 60)
238
+
239
+ for user in users:
240
+ typer.echo(f"🆔 ID: {user.id}")
241
+ typer.echo(f"📧 Email: {user.email}")
242
+ if user.full_name:
243
+ typer.echo(f"👤 Name: {user.full_name}")
244
+ typer.echo(f"📅 Created: {user.created_at}")
245
+ typer.echo("-" * 40)
246
+
247
+ except Exception as e:
248
+ typer.echo(f"❌ Failed to list users: {str(e)}", err=True)
249
+ raise typer.Exit(1)
250
+
251
+
252
+ if __name__ == "__main__":
253
+ app()