fastapi-fullstack 0.1.7__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.
- fastapi_fullstack-0.1.7.dist-info/METADATA +739 -0
- fastapi_fullstack-0.1.7.dist-info/RECORD +241 -0
- fastapi_fullstack-0.1.7.dist-info/WHEEL +4 -0
- fastapi_fullstack-0.1.7.dist-info/entry_points.txt +2 -0
- fastapi_fullstack-0.1.7.dist-info/licenses/LICENSE +21 -0
- fastapi_gen/__init__.py +3 -0
- fastapi_gen/cli.py +442 -0
- fastapi_gen/config.py +356 -0
- fastapi_gen/generator.py +207 -0
- fastapi_gen/prompts.py +874 -0
- fastapi_gen/template/VARIABLES.md +276 -0
- fastapi_gen/template/cookiecutter.json +93 -0
- fastapi_gen/template/hooks/post_gen_project.py +355 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +150 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md +55 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +315 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +768 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore +60 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +155 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +30 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +447 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +541 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +98 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +87 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +902 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +395 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +498 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +227 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/items.py +275 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +205 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +168 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +477 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/ws.py +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +221 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +88 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +117 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +266 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +267 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +122 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +271 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +102 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +319 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +96 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +126 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +218 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +244 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +130 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/base.py +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +154 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +838 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/item.py +222 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +318 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +322 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +358 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +57 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +192 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/item.py +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +89 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +850 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/item.py +246 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +432 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +561 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +25 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +106 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +92 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +438 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +180 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_items.py +310 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +253 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +890 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +261 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +183 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +173 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +143 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_pipelines.py +118 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +124 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +363 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +85 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +435 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +241 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md +132 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md +161 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/testing.md +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore +45 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +19 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +648 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +134 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +207 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/package.json +69 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +152 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +24 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +54 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +26 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +108 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +323 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +22 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +234 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +328 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/local-conversation-sidebar.tsx +295 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +167 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +18 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +35 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +203 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +37 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +90 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +78 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/local-chat-store.ts +255 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +27 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +83 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +36 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.setup.ts +56 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
{%- if cookiecutter.enable_webhooks and cookiecutter.use_database %}
|
|
2
|
+
{%- if cookiecutter.use_postgresql %}
|
|
3
|
+
"""Webhook service (PostgreSQL async)."""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import secrets
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import logfire
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
from app.core.exceptions import NotFoundError
|
|
17
|
+
from app.db.models.webhook import Webhook, WebhookDelivery
|
|
18
|
+
from app.repositories import webhook_repo
|
|
19
|
+
from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WebhookService:
|
|
23
|
+
"""Service for webhook management and delivery."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, db: AsyncSession):
|
|
26
|
+
self.db = db
|
|
27
|
+
|
|
28
|
+
async def create_webhook(
|
|
29
|
+
self,
|
|
30
|
+
data: WebhookCreate,
|
|
31
|
+
user_id: UUID | None = None,
|
|
32
|
+
) -> Webhook:
|
|
33
|
+
"""Create a new webhook subscription."""
|
|
34
|
+
# Generate a secure secret for HMAC signing
|
|
35
|
+
secret = secrets.token_urlsafe(32)
|
|
36
|
+
|
|
37
|
+
return await webhook_repo.create(
|
|
38
|
+
self.db,
|
|
39
|
+
name=data.name,
|
|
40
|
+
url=str(data.url),
|
|
41
|
+
secret=secret,
|
|
42
|
+
events=data.events,
|
|
43
|
+
description=data.description,
|
|
44
|
+
user_id=user_id,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def get_webhook(self, webhook_id: UUID) -> Webhook:
|
|
48
|
+
"""Get a webhook by ID."""
|
|
49
|
+
webhook = await webhook_repo.get_by_id(self.db, webhook_id)
|
|
50
|
+
if not webhook:
|
|
51
|
+
raise NotFoundError(message="Webhook not found")
|
|
52
|
+
return webhook
|
|
53
|
+
|
|
54
|
+
async def list_webhooks(
|
|
55
|
+
self,
|
|
56
|
+
user_id: UUID | None = None,
|
|
57
|
+
skip: int = 0,
|
|
58
|
+
limit: int = 50,
|
|
59
|
+
) -> tuple[list[Webhook], int]:
|
|
60
|
+
"""List webhooks, optionally filtered by user."""
|
|
61
|
+
return await webhook_repo.get_list(
|
|
62
|
+
self.db, user_id=user_id, skip=skip, limit=limit
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def update_webhook(
|
|
66
|
+
self,
|
|
67
|
+
webhook_id: UUID,
|
|
68
|
+
data: WebhookUpdate,
|
|
69
|
+
) -> Webhook:
|
|
70
|
+
"""Update a webhook."""
|
|
71
|
+
webhook = await self.get_webhook(webhook_id)
|
|
72
|
+
return await webhook_repo.update(self.db, webhook, data)
|
|
73
|
+
|
|
74
|
+
async def delete_webhook(self, webhook_id: UUID) -> None:
|
|
75
|
+
"""Delete a webhook."""
|
|
76
|
+
webhook = await self.get_webhook(webhook_id)
|
|
77
|
+
await webhook_repo.delete(self.db, webhook)
|
|
78
|
+
|
|
79
|
+
async def regenerate_secret(self, webhook_id: UUID) -> str:
|
|
80
|
+
"""Regenerate the webhook secret."""
|
|
81
|
+
webhook = await self.get_webhook(webhook_id)
|
|
82
|
+
new_secret = secrets.token_urlsafe(32)
|
|
83
|
+
await webhook_repo.update_secret(self.db, webhook, new_secret)
|
|
84
|
+
return new_secret
|
|
85
|
+
|
|
86
|
+
async def test_webhook(self, webhook_id: UUID) -> dict:
|
|
87
|
+
"""Send a test event to the webhook."""
|
|
88
|
+
webhook = await self.get_webhook(webhook_id)
|
|
89
|
+
|
|
90
|
+
test_payload = {
|
|
91
|
+
"event": "webhook.test",
|
|
92
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
93
|
+
"data": {"message": "This is a test webhook delivery"},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
result = await self._deliver(webhook, "webhook.test", test_payload)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
async def dispatch_event(
|
|
100
|
+
self,
|
|
101
|
+
event_type: str,
|
|
102
|
+
data: dict,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Dispatch an event to all subscribed webhooks."""
|
|
105
|
+
webhooks = await webhook_repo.get_by_event(self.db, event_type)
|
|
106
|
+
|
|
107
|
+
payload = {
|
|
108
|
+
"event": event_type,
|
|
109
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
110
|
+
"data": data,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for webhook in webhooks:
|
|
114
|
+
# In production, you'd want to queue this for background processing
|
|
115
|
+
try:
|
|
116
|
+
await self._deliver(webhook, event_type, payload)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logfire.error(
|
|
119
|
+
"Webhook delivery failed",
|
|
120
|
+
webhook_id=str(webhook.id),
|
|
121
|
+
event_type=event_type,
|
|
122
|
+
error=str(e),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def _deliver(
|
|
126
|
+
self,
|
|
127
|
+
webhook: Webhook,
|
|
128
|
+
event_type: str,
|
|
129
|
+
payload: dict,
|
|
130
|
+
) -> dict:
|
|
131
|
+
"""Deliver a payload to a webhook with HMAC signature."""
|
|
132
|
+
payload_json = json.dumps(payload, default=str)
|
|
133
|
+
|
|
134
|
+
# Create HMAC signature
|
|
135
|
+
signature = self._create_signature(webhook.secret, payload_json)
|
|
136
|
+
|
|
137
|
+
headers = {
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
"X-Webhook-Signature": signature,
|
|
140
|
+
"X-Webhook-Event": event_type,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
delivery = WebhookDelivery(
|
|
144
|
+
webhook_id=webhook.id,
|
|
145
|
+
event_type=event_type,
|
|
146
|
+
payload=payload_json,
|
|
147
|
+
)
|
|
148
|
+
self.db.add(delivery)
|
|
149
|
+
await self.db.flush()
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
153
|
+
response = await client.post(
|
|
154
|
+
webhook.url,
|
|
155
|
+
content=payload_json,
|
|
156
|
+
headers=headers,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
delivery.response_status = response.status_code
|
|
160
|
+
delivery.response_body = response.text[:10000] # Limit size
|
|
161
|
+
delivery.success = 200 <= response.status_code < 300
|
|
162
|
+
delivery.delivered_at = datetime.utcnow()
|
|
163
|
+
|
|
164
|
+
logfire.info(
|
|
165
|
+
"Webhook delivered",
|
|
166
|
+
webhook_id=str(webhook.id),
|
|
167
|
+
event_type=event_type,
|
|
168
|
+
status_code=response.status_code,
|
|
169
|
+
success=delivery.success,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
delivery.error_message = str(e)
|
|
174
|
+
delivery.success = False
|
|
175
|
+
|
|
176
|
+
logfire.error(
|
|
177
|
+
"Webhook delivery error",
|
|
178
|
+
webhook_id=str(webhook.id),
|
|
179
|
+
event_type=event_type,
|
|
180
|
+
error=str(e),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
await self.db.flush()
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"success": delivery.success,
|
|
187
|
+
"status_code": delivery.response_status,
|
|
188
|
+
"message": delivery.error_message or "Delivered successfully",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def _create_signature(self, secret: str, payload: str) -> str:
|
|
192
|
+
"""Create HMAC-SHA256 signature for the payload."""
|
|
193
|
+
signature = hmac.new(
|
|
194
|
+
secret.encode("utf-8"),
|
|
195
|
+
payload.encode("utf-8"),
|
|
196
|
+
hashlib.sha256,
|
|
197
|
+
).hexdigest()
|
|
198
|
+
return f"sha256={signature}"
|
|
199
|
+
|
|
200
|
+
async def get_deliveries(
|
|
201
|
+
self,
|
|
202
|
+
webhook_id: UUID,
|
|
203
|
+
*,
|
|
204
|
+
skip: int = 0,
|
|
205
|
+
limit: int = 50,
|
|
206
|
+
) -> tuple[list[WebhookDelivery], int]:
|
|
207
|
+
"""Get delivery history for a webhook."""
|
|
208
|
+
# Verify webhook exists
|
|
209
|
+
await self.get_webhook(webhook_id)
|
|
210
|
+
return await webhook_repo.get_deliveries(self.db, webhook_id, skip=skip, limit=limit)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def verify_signature(secret: str, payload: str, signature: str) -> bool:
|
|
214
|
+
"""Verify a webhook signature."""
|
|
215
|
+
expected = hmac.new(
|
|
216
|
+
secret.encode("utf-8"),
|
|
217
|
+
payload.encode("utf-8"),
|
|
218
|
+
hashlib.sha256,
|
|
219
|
+
).hexdigest()
|
|
220
|
+
expected_signature = f"sha256={expected}"
|
|
221
|
+
return hmac.compare_digest(expected_signature, signature)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
{%- elif cookiecutter.use_sqlite %}
|
|
225
|
+
"""Webhook service (SQLite sync)."""
|
|
226
|
+
|
|
227
|
+
import hashlib
|
|
228
|
+
import hmac
|
|
229
|
+
import json
|
|
230
|
+
import secrets
|
|
231
|
+
from datetime import datetime
|
|
232
|
+
|
|
233
|
+
import httpx
|
|
234
|
+
import logfire
|
|
235
|
+
from sqlalchemy.orm import Session as DBSession
|
|
236
|
+
|
|
237
|
+
from app.core.exceptions import NotFoundError
|
|
238
|
+
from app.db.models.webhook import Webhook, WebhookDelivery
|
|
239
|
+
from app.repositories import webhook_repo
|
|
240
|
+
from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class WebhookService:
|
|
244
|
+
"""Service for webhook management and delivery."""
|
|
245
|
+
|
|
246
|
+
def __init__(self, db: DBSession):
|
|
247
|
+
self.db = db
|
|
248
|
+
|
|
249
|
+
def create_webhook(
|
|
250
|
+
self,
|
|
251
|
+
data: WebhookCreate,
|
|
252
|
+
user_id: str | None = None,
|
|
253
|
+
) -> Webhook:
|
|
254
|
+
"""Create a new webhook subscription."""
|
|
255
|
+
secret = secrets.token_urlsafe(32)
|
|
256
|
+
|
|
257
|
+
return webhook_repo.create(
|
|
258
|
+
self.db,
|
|
259
|
+
name=data.name,
|
|
260
|
+
url=str(data.url),
|
|
261
|
+
secret=secret,
|
|
262
|
+
events=data.events,
|
|
263
|
+
description=data.description,
|
|
264
|
+
user_id=user_id,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def get_webhook(self, webhook_id: str) -> Webhook:
|
|
268
|
+
"""Get a webhook by ID."""
|
|
269
|
+
webhook = webhook_repo.get_by_id(self.db, webhook_id)
|
|
270
|
+
if not webhook:
|
|
271
|
+
raise NotFoundError(message="Webhook not found")
|
|
272
|
+
return webhook
|
|
273
|
+
|
|
274
|
+
def list_webhooks(
|
|
275
|
+
self,
|
|
276
|
+
user_id: str | None = None,
|
|
277
|
+
skip: int = 0,
|
|
278
|
+
limit: int = 50,
|
|
279
|
+
) -> tuple[list[Webhook], int]:
|
|
280
|
+
"""List webhooks, optionally filtered by user."""
|
|
281
|
+
return webhook_repo.get_list(
|
|
282
|
+
self.db, user_id=user_id, skip=skip, limit=limit
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def update_webhook(
|
|
286
|
+
self,
|
|
287
|
+
webhook_id: str,
|
|
288
|
+
data: WebhookUpdate,
|
|
289
|
+
) -> Webhook:
|
|
290
|
+
"""Update a webhook."""
|
|
291
|
+
webhook = self.get_webhook(webhook_id)
|
|
292
|
+
return webhook_repo.update(self.db, webhook, data)
|
|
293
|
+
|
|
294
|
+
def delete_webhook(self, webhook_id: str) -> None:
|
|
295
|
+
"""Delete a webhook."""
|
|
296
|
+
webhook = self.get_webhook(webhook_id)
|
|
297
|
+
webhook_repo.delete(self.db, webhook)
|
|
298
|
+
|
|
299
|
+
def dispatch_event(
|
|
300
|
+
self,
|
|
301
|
+
event_type: str,
|
|
302
|
+
data: dict,
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Dispatch an event to all subscribed webhooks."""
|
|
305
|
+
webhooks = webhook_repo.get_by_event(self.db, event_type)
|
|
306
|
+
|
|
307
|
+
payload = {
|
|
308
|
+
"event": event_type,
|
|
309
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
310
|
+
"data": data,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for webhook in webhooks:
|
|
314
|
+
try:
|
|
315
|
+
self._deliver(webhook, event_type, payload)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logfire.error(
|
|
318
|
+
"Webhook delivery failed",
|
|
319
|
+
webhook_id=str(webhook.id),
|
|
320
|
+
event_type=event_type,
|
|
321
|
+
error=str(e),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def _deliver(
|
|
325
|
+
self,
|
|
326
|
+
webhook: Webhook,
|
|
327
|
+
event_type: str,
|
|
328
|
+
payload: dict,
|
|
329
|
+
) -> dict:
|
|
330
|
+
"""Deliver a payload to a webhook with HMAC signature."""
|
|
331
|
+
payload_json = json.dumps(payload, default=str)
|
|
332
|
+
signature = self._create_signature(webhook.secret, payload_json)
|
|
333
|
+
|
|
334
|
+
headers = {
|
|
335
|
+
"Content-Type": "application/json",
|
|
336
|
+
"X-Webhook-Signature": signature,
|
|
337
|
+
"X-Webhook-Event": event_type,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
delivery = WebhookDelivery(
|
|
341
|
+
webhook_id=webhook.id,
|
|
342
|
+
event_type=event_type,
|
|
343
|
+
payload=payload_json,
|
|
344
|
+
)
|
|
345
|
+
self.db.add(delivery)
|
|
346
|
+
self.db.flush()
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
with httpx.Client(timeout=30.0) as client:
|
|
350
|
+
response = client.post(
|
|
351
|
+
webhook.url,
|
|
352
|
+
content=payload_json,
|
|
353
|
+
headers=headers,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
delivery.response_status = response.status_code
|
|
357
|
+
delivery.response_body = response.text[:10000]
|
|
358
|
+
delivery.success = 200 <= response.status_code < 300
|
|
359
|
+
delivery.delivered_at = datetime.utcnow()
|
|
360
|
+
|
|
361
|
+
except Exception as e:
|
|
362
|
+
delivery.error_message = str(e)
|
|
363
|
+
delivery.success = False
|
|
364
|
+
|
|
365
|
+
self.db.flush()
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
"success": delivery.success,
|
|
369
|
+
"status_code": delivery.response_status,
|
|
370
|
+
"message": delivery.error_message or "Delivered successfully",
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
def _create_signature(self, secret: str, payload: str) -> str:
|
|
374
|
+
"""Create HMAC-SHA256 signature for the payload."""
|
|
375
|
+
signature = hmac.new(
|
|
376
|
+
secret.encode("utf-8"),
|
|
377
|
+
payload.encode("utf-8"),
|
|
378
|
+
hashlib.sha256,
|
|
379
|
+
).hexdigest()
|
|
380
|
+
return f"sha256={signature}"
|
|
381
|
+
|
|
382
|
+
def get_deliveries(
|
|
383
|
+
self,
|
|
384
|
+
webhook_id: str,
|
|
385
|
+
*,
|
|
386
|
+
skip: int = 0,
|
|
387
|
+
limit: int = 50,
|
|
388
|
+
) -> tuple[list[WebhookDelivery], int]:
|
|
389
|
+
"""Get delivery history for a webhook."""
|
|
390
|
+
# Verify webhook exists
|
|
391
|
+
self.get_webhook(webhook_id)
|
|
392
|
+
return webhook_repo.get_deliveries(self.db, webhook_id, skip=skip, limit=limit)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
396
|
+
"""Webhook service (MongoDB)."""
|
|
397
|
+
|
|
398
|
+
import hashlib
|
|
399
|
+
import hmac
|
|
400
|
+
import json
|
|
401
|
+
import secrets
|
|
402
|
+
from datetime import UTC, datetime
|
|
403
|
+
|
|
404
|
+
import httpx
|
|
405
|
+
import logfire
|
|
406
|
+
|
|
407
|
+
from app.core.exceptions import NotFoundError
|
|
408
|
+
from app.db.models.webhook import Webhook, WebhookDelivery
|
|
409
|
+
from app.repositories import webhook_repo
|
|
410
|
+
from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class WebhookService:
|
|
414
|
+
"""Service for webhook management and delivery."""
|
|
415
|
+
|
|
416
|
+
async def create_webhook(
|
|
417
|
+
self,
|
|
418
|
+
data: WebhookCreate,
|
|
419
|
+
user_id: str | None = None,
|
|
420
|
+
) -> Webhook:
|
|
421
|
+
"""Create a new webhook subscription."""
|
|
422
|
+
secret = secrets.token_urlsafe(32)
|
|
423
|
+
|
|
424
|
+
return await webhook_repo.create(
|
|
425
|
+
name=data.name,
|
|
426
|
+
url=str(data.url),
|
|
427
|
+
secret=secret,
|
|
428
|
+
events=data.events,
|
|
429
|
+
description=data.description,
|
|
430
|
+
user_id=user_id,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
async def get_webhook(self, webhook_id: str) -> Webhook:
|
|
434
|
+
"""Get a webhook by ID."""
|
|
435
|
+
webhook = await webhook_repo.get_by_id(webhook_id)
|
|
436
|
+
if not webhook:
|
|
437
|
+
raise NotFoundError(message="Webhook not found")
|
|
438
|
+
return webhook
|
|
439
|
+
|
|
440
|
+
async def list_webhooks(
|
|
441
|
+
self,
|
|
442
|
+
user_id: str | None = None,
|
|
443
|
+
skip: int = 0,
|
|
444
|
+
limit: int = 50,
|
|
445
|
+
) -> tuple[list[Webhook], int]:
|
|
446
|
+
"""List webhooks, optionally filtered by user."""
|
|
447
|
+
return await webhook_repo.get_list(user_id=user_id, skip=skip, limit=limit)
|
|
448
|
+
|
|
449
|
+
async def update_webhook(
|
|
450
|
+
self,
|
|
451
|
+
webhook_id: str,
|
|
452
|
+
data: WebhookUpdate,
|
|
453
|
+
) -> Webhook:
|
|
454
|
+
"""Update a webhook."""
|
|
455
|
+
webhook = await self.get_webhook(webhook_id)
|
|
456
|
+
return await webhook_repo.update(webhook, data)
|
|
457
|
+
|
|
458
|
+
async def delete_webhook(self, webhook_id: str) -> None:
|
|
459
|
+
"""Delete a webhook."""
|
|
460
|
+
webhook = await self.get_webhook(webhook_id)
|
|
461
|
+
await webhook_repo.delete(webhook)
|
|
462
|
+
|
|
463
|
+
async def dispatch_event(
|
|
464
|
+
self,
|
|
465
|
+
event_type: str,
|
|
466
|
+
data: dict,
|
|
467
|
+
) -> None:
|
|
468
|
+
"""Dispatch an event to all subscribed webhooks."""
|
|
469
|
+
webhooks = await webhook_repo.get_by_event(event_type)
|
|
470
|
+
|
|
471
|
+
payload = {
|
|
472
|
+
"event": event_type,
|
|
473
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
474
|
+
"data": data,
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
for webhook in webhooks:
|
|
478
|
+
try:
|
|
479
|
+
await self._deliver(webhook, event_type, payload)
|
|
480
|
+
except Exception as e:
|
|
481
|
+
logfire.error(
|
|
482
|
+
"Webhook delivery failed",
|
|
483
|
+
webhook_id=str(webhook.id),
|
|
484
|
+
event_type=event_type,
|
|
485
|
+
error=str(e),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
async def _deliver(
|
|
489
|
+
self,
|
|
490
|
+
webhook: Webhook,
|
|
491
|
+
event_type: str,
|
|
492
|
+
payload: dict,
|
|
493
|
+
) -> dict:
|
|
494
|
+
"""Deliver a payload to a webhook with HMAC signature."""
|
|
495
|
+
payload_json = json.dumps(payload, default=str)
|
|
496
|
+
signature = self._create_signature(webhook.secret, payload_json)
|
|
497
|
+
|
|
498
|
+
headers = {
|
|
499
|
+
"Content-Type": "application/json",
|
|
500
|
+
"X-Webhook-Signature": signature,
|
|
501
|
+
"X-Webhook-Event": event_type,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
delivery = WebhookDelivery(
|
|
505
|
+
webhook_id=str(webhook.id),
|
|
506
|
+
event_type=event_type,
|
|
507
|
+
payload=payload_json,
|
|
508
|
+
)
|
|
509
|
+
await delivery.insert()
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
513
|
+
response = await client.post(
|
|
514
|
+
webhook.url,
|
|
515
|
+
content=payload_json,
|
|
516
|
+
headers=headers,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
delivery.response_status = response.status_code
|
|
520
|
+
delivery.response_body = response.text[:10000]
|
|
521
|
+
delivery.success = 200 <= response.status_code < 300
|
|
522
|
+
delivery.delivered_at = datetime.now(UTC)
|
|
523
|
+
|
|
524
|
+
except Exception as e:
|
|
525
|
+
delivery.error_message = str(e)
|
|
526
|
+
delivery.success = False
|
|
527
|
+
|
|
528
|
+
await delivery.save()
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
"success": delivery.success,
|
|
532
|
+
"status_code": delivery.response_status,
|
|
533
|
+
"message": delivery.error_message or "Delivered successfully",
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
def _create_signature(self, secret: str, payload: str) -> str:
|
|
537
|
+
"""Create HMAC-SHA256 signature for the payload."""
|
|
538
|
+
signature = hmac.new(
|
|
539
|
+
secret.encode("utf-8"),
|
|
540
|
+
payload.encode("utf-8"),
|
|
541
|
+
hashlib.sha256,
|
|
542
|
+
).hexdigest()
|
|
543
|
+
return f"sha256={signature}"
|
|
544
|
+
|
|
545
|
+
async def get_deliveries(
|
|
546
|
+
self,
|
|
547
|
+
webhook_id: str,
|
|
548
|
+
*,
|
|
549
|
+
skip: int = 0,
|
|
550
|
+
limit: int = 50,
|
|
551
|
+
) -> tuple[list[WebhookDelivery], int]:
|
|
552
|
+
"""Get delivery history for a webhook."""
|
|
553
|
+
# Verify webhook exists
|
|
554
|
+
await self.get_webhook(webhook_id)
|
|
555
|
+
return await webhook_repo.get_deliveries(webhook_id, skip=skip, limit=limit)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
{%- endif %}
|
|
559
|
+
{%- else %}
|
|
560
|
+
"""Webhook service - not configured."""
|
|
561
|
+
{%- endif %}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{%- if cookiecutter.use_celery %}
|
|
2
|
+
"""Celery application configuration."""
|
|
3
|
+
|
|
4
|
+
from celery import Celery
|
|
5
|
+
from celery.schedules import crontab
|
|
6
|
+
|
|
7
|
+
from app.core.config import settings
|
|
8
|
+
{%- if cookiecutter.enable_logfire and cookiecutter.logfire_celery %}
|
|
9
|
+
from app.core.logfire_setup import instrument_celery
|
|
10
|
+
{%- endif %}
|
|
11
|
+
|
|
12
|
+
# Create Celery app
|
|
13
|
+
celery_app = Celery(
|
|
14
|
+
"{{ cookiecutter.project_slug }}",
|
|
15
|
+
broker=settings.CELERY_BROKER_URL,
|
|
16
|
+
backend=settings.CELERY_RESULT_BACKEND,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Celery configuration
|
|
20
|
+
celery_app.conf.update(
|
|
21
|
+
# Task settings
|
|
22
|
+
task_serializer="json",
|
|
23
|
+
accept_content=["json"],
|
|
24
|
+
result_serializer="json",
|
|
25
|
+
timezone="UTC",
|
|
26
|
+
enable_utc=True,
|
|
27
|
+
# Task execution settings
|
|
28
|
+
task_acks_late=True,
|
|
29
|
+
task_reject_on_worker_lost=True,
|
|
30
|
+
# Result settings
|
|
31
|
+
result_expires=3600, # 1 hour
|
|
32
|
+
# Worker settings
|
|
33
|
+
worker_prefetch_multiplier=1,
|
|
34
|
+
worker_concurrency=4,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Autodiscover tasks from app.worker.tasks module
|
|
38
|
+
celery_app.autodiscover_tasks(["app.worker.tasks"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# === Beat Schedule ===
|
|
42
|
+
# Add periodic tasks here
|
|
43
|
+
celery_app.conf.beat_schedule = {
|
|
44
|
+
"example-every-minute": {
|
|
45
|
+
"task": "app.worker.tasks.examples.example_task",
|
|
46
|
+
"schedule": 60.0, # Every 60 seconds
|
|
47
|
+
"args": ("periodic",),
|
|
48
|
+
},
|
|
49
|
+
# Example with crontab (runs at 00:00 every day)
|
|
50
|
+
# "daily-cleanup": {
|
|
51
|
+
# "task": "app.worker.tasks.examples.cleanup_task",
|
|
52
|
+
# "schedule": crontab(hour=0, minute=0),
|
|
53
|
+
# },
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
{%- if cookiecutter.enable_logfire and cookiecutter.logfire_celery %}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Instrument Celery with Logfire
|
|
60
|
+
instrument_celery()
|
|
61
|
+
{%- endif %}
|
|
62
|
+
{%- else %}
|
|
63
|
+
# Celery not enabled for this project
|
|
64
|
+
{%- endif %}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{%- if cookiecutter.use_taskiq %}
|
|
2
|
+
"""Taskiq application configuration."""
|
|
3
|
+
|
|
4
|
+
from taskiq import TaskiqScheduler
|
|
5
|
+
from taskiq_redis import ListQueueBroker, RedisAsyncResultBackend
|
|
6
|
+
|
|
7
|
+
from app.core.config import settings
|
|
8
|
+
|
|
9
|
+
# Create Taskiq broker with Redis
|
|
10
|
+
broker = ListQueueBroker(
|
|
11
|
+
url=settings.TASKIQ_BROKER_URL,
|
|
12
|
+
).with_result_backend(
|
|
13
|
+
RedisAsyncResultBackend(
|
|
14
|
+
redis_url=settings.TASKIQ_RESULT_BACKEND,
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Create scheduler for periodic tasks
|
|
19
|
+
scheduler = TaskiqScheduler(
|
|
20
|
+
broker=broker,
|
|
21
|
+
sources=["app.worker.tasks.schedules"],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Startup/shutdown hooks
|
|
26
|
+
@broker.on_event("startup")
|
|
27
|
+
async def startup() -> None:
|
|
28
|
+
"""Initialize broker on startup."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@broker.on_event("shutdown")
|
|
33
|
+
async def shutdown() -> None:
|
|
34
|
+
"""Cleanup on shutdown."""
|
|
35
|
+
pass
|
|
36
|
+
{%- else %}
|
|
37
|
+
# Taskiq not enabled for this project
|
|
38
|
+
{%- endif %}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{%- if cookiecutter.use_celery or cookiecutter.use_taskiq %}
|
|
2
|
+
"""Background tasks."""
|
|
3
|
+
|
|
4
|
+
{%- if cookiecutter.use_celery %}
|
|
5
|
+
from app.worker.tasks.examples import example_task, long_running_task
|
|
6
|
+
{%- endif %}
|
|
7
|
+
|
|
8
|
+
{%- if cookiecutter.use_taskiq %}
|
|
9
|
+
from app.worker.tasks.taskiq_examples import example_task as taskiq_example_task
|
|
10
|
+
from app.worker.tasks.taskiq_examples import long_running_task as taskiq_long_running_task
|
|
11
|
+
{%- endif %}
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
{%- if cookiecutter.use_celery %}
|
|
15
|
+
"example_task",
|
|
16
|
+
"long_running_task",
|
|
17
|
+
{%- endif %}
|
|
18
|
+
{%- if cookiecutter.use_taskiq %}
|
|
19
|
+
"taskiq_example_task",
|
|
20
|
+
"taskiq_long_running_task",
|
|
21
|
+
{%- endif %}
|
|
22
|
+
]
|
|
23
|
+
{%- else %}
|
|
24
|
+
# Background tasks not enabled
|
|
25
|
+
{%- endif %}
|