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,361 @@
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 marko import Markdown
11
+ from marko.ext.gfm import GFM
12
+ from rich.console import Console
13
+
14
+ from app.cli.marko_terminal_renderer import TerminalRenderer
15
+
16
+
17
+ class StreamingMarkdownRenderer:
18
+ """
19
+ Line-based streaming markdown renderer using marko.
20
+
21
+ Processes markdown content as it streams in, using marko to parse complete
22
+ blocks and render them with beautiful ANSI styling for terminal output.
23
+ """
24
+
25
+ def __init__(self, console: Console):
26
+ """
27
+ Initialize streaming renderer.
28
+
29
+ Args:
30
+ console: Rich console instance for output management
31
+ """
32
+ self.console = console
33
+ self.buffer = ""
34
+ self.in_code_block = False
35
+ self.code_buffer = []
36
+ self.code_lang = ""
37
+ self.markdown = Markdown(extensions=[GFM], renderer=TerminalRenderer)
38
+
39
+ def add_delta(self, delta: str) -> None:
40
+ """
41
+ Process streaming delta and display formatted content with smart buffering.
42
+
43
+ Uses line-buffering for markdown structures (code blocks, lists, tables)
44
+ and word-streaming for plain conversational text for smooth output.
45
+
46
+ Args:
47
+ delta: New text content to process
48
+ """
49
+ # Add new content to buffer
50
+ self.buffer += delta
51
+
52
+ # Smart buffering based on content type
53
+ if self.in_code_block or self._is_markdown_structure():
54
+ # Use line-buffering for markdown structures (safe, correct formatting)
55
+ self._process_complete_lines()
56
+ else:
57
+ # Use word-streaming for plain text (smooth, responsive)
58
+ self._stream_plain_text()
59
+
60
+ def _is_markdown_structure(self) -> bool:
61
+ """
62
+ Detect if buffer contains markdown structures requiring line-buffering.
63
+
64
+ Returns:
65
+ True if buffer contains markdown patterns, False for plain text
66
+ """
67
+ if not self.buffer.strip():
68
+ return False
69
+
70
+ # Get the current line being built (text after last newline)
71
+ current_line = self.buffer.split("\n")[-1].strip()
72
+
73
+ # Markdown patterns that need careful line-by-line handling
74
+ markdown_indicators = [
75
+ "```", # Code blocks (critical!)
76
+ "#", # Headers
77
+ "- ", # Unordered lists
78
+ "* ", # Unordered lists (alternate)
79
+ "+ ", # Unordered lists (alternate)
80
+ "> ", # Blockquotes
81
+ "|", # Tables
82
+ ]
83
+
84
+ # Check if line starts with markdown
85
+ for indicator in markdown_indicators:
86
+ if current_line.startswith(indicator):
87
+ return True
88
+
89
+ # Check for numbered lists using regex (handles any number)
90
+ # Check for inline formatting that might break across words
91
+ # (bold/italic can be streamed word-by-word safely)
92
+ return bool(re.match(r"^\d+\. ", current_line))
93
+
94
+ def _stream_plain_text(self) -> None:
95
+ """
96
+ Stream plain text word-by-word for smooth, responsive output.
97
+
98
+ Renders complete words immediately while keeping incomplete words
99
+ in buffer. Falls back to line-buffering when newlines are encountered.
100
+ """
101
+ # If we hit a newline, process complete lines normally
102
+ if "\n" in self.buffer:
103
+ self._process_complete_lines()
104
+ return
105
+
106
+ # Word-level streaming for smooth plain text output
107
+ # Split on spaces to identify complete words
108
+ if " " in self.buffer:
109
+ # Split and find word boundaries
110
+ parts = self.buffer.rsplit(" ", 1) # Split from right to keep last word
111
+
112
+ if len(parts) == 2:
113
+ complete_text, incomplete_word = parts
114
+
115
+ # Render complete text with trailing space
116
+ if complete_text:
117
+ # For plain text, just write directly (no markdown parsing needed)
118
+ self.console.file.write(complete_text + " ")
119
+ self.console.file.flush()
120
+
121
+ # Keep incomplete word in buffer
122
+ self.buffer = incomplete_word
123
+
124
+ def _process_complete_lines(self) -> None:
125
+ """Process any complete lines in the buffer."""
126
+ lines = self.buffer.split("\n")
127
+
128
+ # Keep the last (potentially incomplete) line in buffer
129
+ if len(lines) > 1:
130
+ complete_lines = lines[:-1]
131
+ self.buffer = lines[-1]
132
+
133
+ # Process each complete line
134
+ for line in complete_lines:
135
+ self._render_line(line)
136
+
137
+ def _render_line(self, line: str) -> None:
138
+ """
139
+ Render a complete line with markdown formatting using marko.
140
+
141
+ Args:
142
+ line: Complete line to render
143
+ """
144
+ # Handle code blocks (accumulate until closing)
145
+ if self.in_code_block:
146
+ if line.strip() == "```":
147
+ # End of code block - render complete block
148
+ self._render_code_block()
149
+ self.in_code_block = False
150
+ self.code_buffer = []
151
+ self.code_lang = ""
152
+ else:
153
+ # Inside code block, accumulate
154
+ self.code_buffer.append(line)
155
+ return
156
+
157
+ if line.strip().startswith("```"):
158
+ # Start of code block
159
+ self.code_lang = line.strip()[3:].strip() or "text"
160
+ self.in_code_block = True
161
+ self.code_buffer = []
162
+ return
163
+
164
+ # For other content, parse line as markdown and render
165
+ if line.strip():
166
+ # Parse and render single line with marko
167
+ rendered = self.markdown(line)
168
+ # Write to console's file to support both terminal and testing
169
+ self.console.file.write(rendered)
170
+ self.console.file.flush()
171
+ else:
172
+ # Empty line
173
+ self.console.print()
174
+
175
+ def _render_code_block(self) -> None:
176
+ """Render accumulated code block using marko."""
177
+ code_content = "\n".join(self.code_buffer)
178
+
179
+ # Create markdown code block
180
+ markdown_code = f"```{self.code_lang}\n{code_content}\n```"
181
+
182
+ # Parse and render with marko
183
+ rendered = self.markdown(markdown_code)
184
+ # Write to console's file to support both terminal and testing
185
+ self.console.file.write(rendered)
186
+ self.console.file.flush()
187
+
188
+ def finalize(self) -> None:
189
+ """Finalize any remaining content in buffer."""
190
+ if self.buffer.strip():
191
+ # Process any remaining incomplete line
192
+ rendered = self.markdown(self.buffer.strip())
193
+ # Write to console's file to support both terminal and testing
194
+ self.console.file.write(rendered)
195
+ self.console.file.flush()
196
+
197
+ # Handle unclosed code block
198
+ if self.in_code_block and self.code_buffer:
199
+ self._render_code_block()
200
+
201
+
202
+ def render_ai_header(console: Console, inline: bool = True) -> None:
203
+ """
204
+ Render the AI response header.
205
+
206
+ Args:
207
+ console: Rich console instance
208
+ inline: If True, use inline style (🤖:), else use separate line style
209
+
210
+ Examples:
211
+ >>> render_ai_header(console, inline=True) # Outputs: "🤖: "
212
+ >>> render_ai_header(console, inline=False) # Outputs: "🤖 Response:"
213
+ """
214
+ if inline:
215
+ console.print("🤖: ", style="bright_blue", end="")
216
+ else:
217
+ console.print("🤖 ", style="bright_blue", end="")
218
+ console.print("Response:", style="bright_blue bold")
219
+
220
+
221
+ def render_markdown_response(console: Console, content: str) -> None:
222
+ """
223
+ Render markdown content with beautiful terminal styling using marko.
224
+
225
+ Parses complete markdown content and renders with ANSI-styled output
226
+ for beautiful terminal display.
227
+
228
+ Args:
229
+ console: Rich console instance
230
+ content: Markdown content to render
231
+
232
+ Examples:
233
+ >>> render_markdown_response(console, "# Hello World")
234
+ >>> render_markdown_response(console, "```python\\nprint('hi')\\n```")
235
+ """
236
+ if not content or not content.strip():
237
+ console.print("(No response content)", style="dim italic")
238
+ return
239
+
240
+ # Clean up excessive whitespace from AI responses
241
+ cleaned_content = _clean_ai_content(content)
242
+
243
+ # Parse and render with marko (GFM for table support)
244
+ markdown = Markdown(extensions=[GFM], renderer=TerminalRenderer)
245
+ rendered = markdown(cleaned_content)
246
+
247
+ # Write to console's file to support both terminal and testing
248
+ console.file.write(rendered)
249
+ console.file.flush()
250
+
251
+
252
+ def _clean_ai_content(content: str) -> str:
253
+ """
254
+ Clean up excessive whitespace and formatting issues from AI responses.
255
+
256
+ Some AI providers send responses with excessive blank lines or spacing
257
+ that hurts readability. This function normalizes the content.
258
+
259
+ Args:
260
+ content: Raw AI response content
261
+
262
+ Returns:
263
+ Cleaned content with normalized spacing
264
+ """
265
+
266
+ # Split into lines for processing
267
+ lines = content.split("\n")
268
+ cleaned_lines = []
269
+ consecutive_empty = 0
270
+
271
+ for line in lines:
272
+ is_empty = not line.strip()
273
+
274
+ if is_empty:
275
+ consecutive_empty += 1
276
+ # Allow maximum 1 consecutive empty line
277
+ if consecutive_empty <= 1:
278
+ cleaned_lines.append("")
279
+ else:
280
+ consecutive_empty = 0
281
+ # Clean up the line (remove trailing whitespace)
282
+ cleaned_lines.append(line.rstrip())
283
+
284
+ # Remove leading and trailing empty lines
285
+ while cleaned_lines and not cleaned_lines[0].strip():
286
+ cleaned_lines.pop(0)
287
+ while cleaned_lines and not cleaned_lines[-1].strip():
288
+ cleaned_lines.pop()
289
+
290
+ # Join back together
291
+ return "\n".join(cleaned_lines)
292
+
293
+
294
+ def render_conversation_metadata(
295
+ console: Console,
296
+ conversation_id: str,
297
+ message_count: int | None = None,
298
+ response_time: float | None = None,
299
+ ) -> None:
300
+ """
301
+ Render conversation metadata consistently.
302
+
303
+ Args:
304
+ console: Rich console instance
305
+ conversation_id: The conversation identifier
306
+ message_count: Number of messages in conversation
307
+ response_time: Response time in milliseconds
308
+
309
+ Examples:
310
+ >>> render_conversation_metadata(console, "conv-123")
311
+ >>> render_conversation_metadata(
312
+ ... console, "conv-123", message_count=5, response_time=150.5
313
+ ... )
314
+ """
315
+ console.print() # Blank line for spacing
316
+ console.print(f"💬 Conversation: {conversation_id}", style="dim")
317
+ if message_count:
318
+ console.print(f"ℹ️ Messages: {message_count}", style="dim")
319
+ if response_time:
320
+ console.print(f"⏱️ Response time: {response_time:.1f}ms", style="dim")
321
+
322
+
323
+ def render_error_message(
324
+ console: Console, error: str, suggestion: str | None = None
325
+ ) -> None:
326
+ """
327
+ Render an error message consistently.
328
+
329
+ Args:
330
+ console: Rich console instance
331
+ error: The error message to display
332
+ suggestion: Optional suggestion for fixing the error
333
+
334
+ Examples:
335
+ >>> render_error_message(console, "Connection failed")
336
+ >>> render_error_message(console, "API key invalid", "Check your .env file")
337
+ """
338
+ console.print(f"❌ Error: {error}", style="red")
339
+ if suggestion:
340
+ console.print(f"💡 Suggestion: {suggestion}", style="yellow dim")
341
+
342
+
343
+ def render_thinking_spinner(console: Console) -> tuple:
344
+ """
345
+ Create a thinking spinner for AI processing.
346
+
347
+ Returns:
348
+ Tuple of (Spinner, Live) objects to control the spinner
349
+
350
+ Examples:
351
+ >>> spinner, live = render_thinking_spinner(console)
352
+ >>> live.start()
353
+ >>> # ... do work ...
354
+ >>> live.stop()
355
+ """
356
+ from rich.live import Live
357
+ from rich.spinner import Spinner
358
+
359
+ spinner = Spinner("dots", text="🤖 Thinking...", style="bright_blue")
360
+ live = Live(spinner, console=console, refresh_per_second=12)
361
+ 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()