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.
- aegis/__init__.py +5 -0
- aegis/__main__.py +51 -0
- aegis/cli/__init__.py +6 -0
- aegis/cli/callbacks.py +114 -0
- aegis/cli/interactive.py +611 -0
- aegis/cli/utils.py +70 -0
- aegis/cli/validators.py +34 -0
- aegis/commands/__init__.py +6 -0
- aegis/commands/add.py +353 -0
- aegis/commands/add_service.py +332 -0
- aegis/commands/components.py +35 -0
- aegis/commands/init.py +370 -0
- aegis/commands/remove.py +227 -0
- aegis/commands/services.py +52 -0
- aegis/commands/update.py +252 -0
- aegis/commands/version.py +12 -0
- aegis/config/__init__.py +1 -0
- aegis/config/shared_files.py +136 -0
- aegis/core/CLAUDE.md +377 -0
- aegis/core/__init__.py +6 -0
- aegis/core/component_files.py +228 -0
- aegis/core/component_utils.py +220 -0
- aegis/core/components.py +127 -0
- aegis/core/copier_manager.py +315 -0
- aegis/core/copier_updater.py +475 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/manual_updater.py +554 -0
- aegis/core/post_gen_tasks.py +547 -0
- aegis/core/service_resolver.py +261 -0
- aegis/core/services.py +157 -0
- aegis/core/template_generator.py +266 -0
- aegis/core/version_compatibility.py +259 -0
- aegis/templates/CLAUDE.md +591 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +39 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +214 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +130 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +236 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/alembic.ini.j2 +111 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/env.py.j2 +91 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/script.py.mako +25 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/versions/001_initial_auth.py.j2 +51 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai.py.j2 +700 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai_rendering.py +361 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/auth.py.j2 +253 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py.j2 +419 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py.j2 +656 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py.j2 +65 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/marko_terminal_renderer.py +489 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/tasks.py.j2 +328 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/{% if cookiecutter.include_scheduler == /"yes/" %}tasks.py{% endif %}" +340 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/__init__.py +8 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/router.py +329 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/router.py +64 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/deps.py +58 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +163 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +280 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/scheduler.py.j2 +121 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/worker.py.j2 +478 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +144 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +31 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +418 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/database_init.py.j2 +83 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/__init__.py +27 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/table.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/text.py +142 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/__init__.py.j2 +47 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/base_card.py +256 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/database_card.py +420 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/services_card.py +233 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py.j2 +653 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/theme.py +48 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md.j2 +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +97 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +55 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +49 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +44 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +120 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +507 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +33 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +281 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +178 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +58 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py.j2 +176 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +92 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/security.py +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/user.py +44 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/__init__.py +8 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/config.py +130 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/conversation.py +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/health.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/models.py +229 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/providers.py +370 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/service.py +388 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/auth_service.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/health.py +164 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/user_service.py +83 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/middleware_inspector.py.j2 +223 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/models.py.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/route_inspector.py.j2 +155 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +679 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +266 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/__init__.py.j2 +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/models.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/scheduled_task_manager.py.j2 +273 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/task_monitor.py.j2 +189 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/backup.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1333 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +243 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-dark.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-square-backup.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.env.example.j2 +64 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.gitignore +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/README.md.j2 +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/docker-compose.yml.j2 +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/pyproject.toml.j2 +120 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/uv.lock +1673 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +200 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md.j2 +621 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +93 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_auth_endpoints.py.j2 +307 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +262 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_scheduler_endpoints.py.j2 +214 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_worker_endpoints.py.j2 +165 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_ai_rendering.py +427 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_conversation_memory.py +465 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +43 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +195 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/conftest.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_health.py +157 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_models.py +164 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_service.py +198 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_auth_integration.py.j2 +528 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +387 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_conversation_persistence.py +342 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +663 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +619 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +603 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_middleware_inspector.py.j2 +248 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_scheduled_task_manager.py.j2 +292 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +98 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +257 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +49 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.dockerignore +71 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.env.example.jinja +130 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.gitignore +131 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/Dockerfile +53 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/Makefile.jinja +236 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/README.md.jinja +196 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/alembic.ini.jinja +111 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/env.py.jinja +91 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/script.py.mako +25 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/versions/001_initial_auth.py.jinja +51 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/__init__.py.jinja +5 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/__init__.py.jinja +6 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai.py.jinja +700 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai_rendering.py +360 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/auth.py.jinja +253 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/health.py.jinja +419 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/load_test.py.jinja +656 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/main.py.jinja +65 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/marko_terminal_renderer.py +489 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/tasks.py.jinja +328 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/__init__.py +8 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/router.py +329 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/router.py +64 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/deps.py +58 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/health.py.jinja +163 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/models.py.jinja +280 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/routing.py.jinja +32 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/scheduler.py.jinja +121 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/worker.py.jinja +478 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/hooks.py +144 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/main.py +31 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/component_health.py.jinja +418 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/database_init.py.jinja +83 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/__init__.py +5 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/__init__.py +27 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/table.py +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/text.py +142 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/__init__.py.jinja +47 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/base_card.py +256 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/database_card.py +420 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/services_card.py +233 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/main.py.jinja +653 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/theme.py +48 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/main.py.jinja +156 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/CLAUDE.md.jinja +213 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/__init__.py +6 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/constants.py.jinja +30 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/pools.py +97 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/load_test.py +55 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/media.py +49 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/system.py +44 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/registry.py +139 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/__init__.py +120 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/load_tasks.py +507 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/simple_system_tasks.py +33 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/system_tasks.py +281 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/config.py.jinja +178 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/constants.py +58 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/db.py.jinja +176 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/log.py +92 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/security.py +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/scheduler.py.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/webserver.py +39 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/main.py +61 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/user.py +44 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/py.typed +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/__init__.py +8 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/config.py +130 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/conversation.py +213 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/health.py +96 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/models.py +229 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/providers.py.jinja +370 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/service.py +387 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/auth_service.py +40 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/health.py +162 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/user_service.py +82 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/middleware_inspector.py.jinja +223 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/models.py.jinja +70 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/route_inspector.py.jinja +155 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test.py +678 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test_models.py +265 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/__init__.py.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/models.py.jinja +119 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/scheduled_task_manager.py.jinja +273 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/task_monitor.py.jinja +189 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/__init__.py +15 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/models.py +26 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/__init__.py +52 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/alerts.py +94 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/backup.py.jinja +119 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/health.py.jinja +1333 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/models.py +243 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/ui.py +52 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57223!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57224!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57225!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57533!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57534!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57538!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57897!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57898!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57904!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58315!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58316!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58324!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58837!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58838!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58849!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/.env.example.jinja +64 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/README.md.jinja +172 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/docker-compose.yml.jinja +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/mkdocs.yml.jinja +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/pyproject.toml.jinja +120 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docker-compose.yml.jinja +200 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/api.md.jinja +191 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md.jinja +621 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/development.md.jinja +215 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/health.md.jinja +240 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/mkdocs.yml.jinja +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/pyproject.toml.jinja +131 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh +87 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh.jinja +93 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/gen_docs.py +16 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_auth_endpoints.py.jinja +307 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_health_endpoints.py.jinja +262 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_scheduler_endpoints.py.jinja +214 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_worker_endpoints.py.jinja +165 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_ai_rendering.py +427 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_conversation_memory.py +465 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/components/test_scheduler.py +43 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/conftest.py.jinja +195 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/conftest.py +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_health.py +157 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_models.py +164 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_service.py +198 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_auth_integration.py.jinja +528 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_component_integration.py.jinja +387 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_conversation_persistence.py +342 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_health_logic.py.jinja +663 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_models.py +619 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_service.py +603 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_middleware_inspector.py.jinja +248 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_scheduled_task_manager.py.jinja +292 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_system_service.py +98 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_worker_health_registration.py.jinja +257 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/test_core.py +49 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/uv.lock +1673 -0
- aegis_stack-0.2.0rc2.dist-info/METADATA +165 -0
- aegis_stack-0.2.0rc2.dist-info/RECORD +392 -0
- aegis_stack-0.2.0rc2.dist-info/WHEEL +4 -0
- aegis_stack-0.2.0rc2.dist-info/entry_points.txt +3 -0
- 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()
|