@weirdfingers/baseboards 0.9.5 → 0.9.7
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.
- package/dist/index.js +561 -469
- package/dist/index.js.map +1 -1
- package/package.json +2 -5
- package/templates/README.md +0 -122
- package/templates/api/.env.example +0 -65
- package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
- package/templates/api/Dockerfile +0 -32
- package/templates/api/README.md +0 -264
- package/templates/api/alembic/env.py +0 -114
- package/templates/api/alembic/script.py.mako +0 -28
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
- package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
- package/templates/api/alembic.ini +0 -36
- package/templates/api/config/generators.yaml +0 -237
- package/templates/api/config/storage_config.yaml +0 -26
- package/templates/api/docs/ADDING_GENERATORS.md +0 -409
- package/templates/api/docs/GENERATORS_API.md +0 -502
- package/templates/api/docs/MIGRATIONS.md +0 -472
- package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
- package/templates/api/docs/storage_providers.md +0 -337
- package/templates/api/pyproject.toml +0 -205
- package/templates/api/src/boards/__init__.py +0 -10
- package/templates/api/src/boards/api/app.py +0 -172
- package/templates/api/src/boards/api/auth.py +0 -75
- package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
- package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
- package/templates/api/src/boards/api/endpoints/setup.py +0 -505
- package/templates/api/src/boards/api/endpoints/sse.py +0 -129
- package/templates/api/src/boards/api/endpoints/storage.py +0 -155
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
- package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
- package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
- package/templates/api/src/boards/auth/__init__.py +0 -15
- package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
- package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
- package/templates/api/src/boards/auth/adapters/base.py +0 -73
- package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
- package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
- package/templates/api/src/boards/auth/adapters/none.py +0 -102
- package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
- package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
- package/templates/api/src/boards/auth/context.py +0 -35
- package/templates/api/src/boards/auth/factory.py +0 -129
- package/templates/api/src/boards/auth/middleware.py +0 -221
- package/templates/api/src/boards/auth/provisioning.py +0 -129
- package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
- package/templates/api/src/boards/cli.py +0 -354
- package/templates/api/src/boards/config.py +0 -131
- package/templates/api/src/boards/database/__init__.py +0 -7
- package/templates/api/src/boards/database/cli.py +0 -110
- package/templates/api/src/boards/database/connection.py +0 -292
- package/templates/api/src/boards/database/models.py +0 -19
- package/templates/api/src/boards/database/seed_data.py +0 -182
- package/templates/api/src/boards/dbmodels/__init__.py +0 -441
- package/templates/api/src/boards/generators/__init__.py +0 -57
- package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
- package/templates/api/src/boards/generators/artifacts.py +0 -53
- package/templates/api/src/boards/generators/base.py +0 -144
- package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
- package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
- package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
- package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
- package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
- package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
- package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
- package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
- package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
- package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
- package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
- package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
- package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
- package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
- package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
- package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
- package/templates/api/src/boards/generators/loader.py +0 -253
- package/templates/api/src/boards/generators/registry.py +0 -114
- package/templates/api/src/boards/generators/resolution.py +0 -632
- package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
- package/templates/api/src/boards/graphql/__init__.py +0 -7
- package/templates/api/src/boards/graphql/access_control.py +0 -136
- package/templates/api/src/boards/graphql/mutations/root.py +0 -148
- package/templates/api/src/boards/graphql/queries/root.py +0 -116
- package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
- package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
- package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
- package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
- package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
- package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
- package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
- package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
- package/templates/api/src/boards/graphql/schema.py +0 -81
- package/templates/api/src/boards/graphql/types/board.py +0 -102
- package/templates/api/src/boards/graphql/types/generation.py +0 -166
- package/templates/api/src/boards/graphql/types/generator.py +0 -17
- package/templates/api/src/boards/graphql/types/user.py +0 -47
- package/templates/api/src/boards/jobs/repository.py +0 -153
- package/templates/api/src/boards/logging.py +0 -195
- package/templates/api/src/boards/middleware.py +0 -339
- package/templates/api/src/boards/progress/__init__.py +0 -4
- package/templates/api/src/boards/progress/models.py +0 -25
- package/templates/api/src/boards/progress/publisher.py +0 -64
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +0 -118
- package/templates/api/src/boards/storage/__init__.py +0 -52
- package/templates/api/src/boards/storage/base.py +0 -363
- package/templates/api/src/boards/storage/config.py +0 -187
- package/templates/api/src/boards/storage/factory.py +0 -288
- package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
- package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
- package/templates/api/src/boards/storage/implementations/local.py +0 -201
- package/templates/api/src/boards/storage/implementations/s3.py +0 -294
- package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
- package/templates/api/src/boards/tenant_isolation.py +0 -446
- package/templates/api/src/boards/validation.py +0 -262
- package/templates/api/src/boards/workers/__init__.py +0 -1
- package/templates/api/src/boards/workers/actors.py +0 -274
- package/templates/api/src/boards/workers/cli.py +0 -125
- package/templates/api/src/boards/workers/context.py +0 -348
- package/templates/api/src/boards/workers/middleware.py +0 -58
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.web.yaml +0 -35
- package/templates/compose.yaml +0 -116
- package/templates/docker/env.example +0 -23
- package/templates/web/.env.example +0 -28
- package/templates/web/Dockerfile +0 -51
- package/templates/web/components.json +0 -22
- package/templates/web/imageLoader.js +0 -18
- package/templates/web/next-env.d.ts +0 -5
- package/templates/web/next.config.js +0 -36
- package/templates/web/package.json +0 -41
- package/templates/web/postcss.config.mjs +0 -7
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
- package/templates/web/src/app/globals.css +0 -123
- package/templates/web/src/app/layout.tsx +0 -31
- package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
- package/templates/web/src/app/page.tsx +0 -35
- package/templates/web/src/app/providers.tsx +0 -18
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
- package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
- package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
- package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
- package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
- package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
- package/templates/web/src/components/header.tsx +0 -32
- package/templates/web/src/components/theme-provider.tsx +0 -10
- package/templates/web/src/components/theme-toggle.tsx +0 -75
- package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
- package/templates/web/src/components/ui/button.tsx +0 -58
- package/templates/web/src/components/ui/card.tsx +0 -92
- package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
- package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
- package/templates/web/src/components/ui/toast.tsx +0 -128
- package/templates/web/src/components/ui/toaster.tsx +0 -35
- package/templates/web/src/components/ui/use-toast.ts +0 -187
- package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
- package/templates/web/src/lib/utils.ts +0 -6
- package/templates/web/tsconfig.json +0 -41
|
@@ -1,505 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Setup endpoints for initial tenant configuration.
|
|
3
|
-
|
|
4
|
-
These endpoints help with one-time setup for single-tenant deployments
|
|
5
|
-
or initial configuration of multi-tenant environments.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from datetime import UTC
|
|
11
|
-
from typing import Any
|
|
12
|
-
from uuid import UUID
|
|
13
|
-
|
|
14
|
-
from fastapi import APIRouter, HTTPException, Path
|
|
15
|
-
from pydantic import BaseModel, Field
|
|
16
|
-
from sqlalchemy import delete, select, update
|
|
17
|
-
|
|
18
|
-
from ...config import settings
|
|
19
|
-
from ...database.connection import get_async_session
|
|
20
|
-
from ...database.seed_data import ensure_tenant, seed_tenant_with_data
|
|
21
|
-
from ...dbmodels import Tenants
|
|
22
|
-
from ...logging import get_logger
|
|
23
|
-
|
|
24
|
-
logger = get_logger(__name__)
|
|
25
|
-
|
|
26
|
-
router = APIRouter()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class TenantSetupRequest(BaseModel):
|
|
30
|
-
"""Request model for tenant setup."""
|
|
31
|
-
|
|
32
|
-
name: str = Field(..., min_length=1, max_length=255, description="Display name for the tenant")
|
|
33
|
-
slug: str = Field(
|
|
34
|
-
...,
|
|
35
|
-
min_length=1,
|
|
36
|
-
max_length=255,
|
|
37
|
-
pattern=r"^[a-z0-9-]+$",
|
|
38
|
-
description="URL-safe slug for the tenant (lowercase, numbers, hyphens only)",
|
|
39
|
-
)
|
|
40
|
-
settings: dict[str, Any] = Field(
|
|
41
|
-
default_factory=dict, description="Optional tenant-specific settings"
|
|
42
|
-
)
|
|
43
|
-
include_sample_data: bool = Field(
|
|
44
|
-
default=False, description="Whether to include sample boards and data"
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class TenantUpdateRequest(BaseModel):
|
|
49
|
-
"""Request model for tenant updates."""
|
|
50
|
-
|
|
51
|
-
name: str | None = Field(
|
|
52
|
-
default=None,
|
|
53
|
-
min_length=1,
|
|
54
|
-
max_length=255,
|
|
55
|
-
description="Display name for the tenant",
|
|
56
|
-
)
|
|
57
|
-
slug: str | None = Field(
|
|
58
|
-
None,
|
|
59
|
-
min_length=1,
|
|
60
|
-
max_length=255,
|
|
61
|
-
pattern=r"^[a-z0-9-]+$",
|
|
62
|
-
description="URL-safe slug for the tenant (lowercase, numbers, hyphens only)",
|
|
63
|
-
)
|
|
64
|
-
settings: dict[str, Any] | None = Field(None, description="Tenant-specific settings")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class TenantResponse(BaseModel):
|
|
68
|
-
"""Response model for tenant operations."""
|
|
69
|
-
|
|
70
|
-
tenant_id: str = Field(..., description="UUID of the tenant")
|
|
71
|
-
name: str = Field(..., description="Display name of the tenant")
|
|
72
|
-
slug: str = Field(..., description="Slug of the tenant")
|
|
73
|
-
settings: dict[str, Any] = Field(..., description="Tenant-specific settings")
|
|
74
|
-
created_at: str = Field(..., description="Creation timestamp")
|
|
75
|
-
updated_at: str = Field(..., description="Last update timestamp")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class TenantSetupResponse(BaseModel):
|
|
79
|
-
"""Response model for tenant setup."""
|
|
80
|
-
|
|
81
|
-
tenant_id: str = Field(..., description="UUID of the created tenant")
|
|
82
|
-
name: str = Field(..., description="Display name of the tenant")
|
|
83
|
-
slug: str = Field(..., description="Slug of the tenant")
|
|
84
|
-
message: str = Field(..., description="Success message")
|
|
85
|
-
existing: bool = Field(..., description="Whether tenant already existed")
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class TenantListResponse(BaseModel):
|
|
89
|
-
"""Response model for tenant list."""
|
|
90
|
-
|
|
91
|
-
tenants: list[TenantResponse] = Field(..., description="List of tenants")
|
|
92
|
-
total_count: int = Field(..., description="Total number of tenants")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@router.post("/tenant", response_model=TenantSetupResponse)
|
|
96
|
-
async def setup_tenant(request: TenantSetupRequest) -> TenantSetupResponse:
|
|
97
|
-
"""
|
|
98
|
-
Create or configure a tenant for initial setup.
|
|
99
|
-
|
|
100
|
-
This endpoint is useful for:
|
|
101
|
-
- Single-tenant initial setup
|
|
102
|
-
- Creating new tenants in multi-tenant mode
|
|
103
|
-
- Demo/development tenant creation
|
|
104
|
-
|
|
105
|
-
In single-tenant mode, this is typically called once during deployment.
|
|
106
|
-
In multi-tenant mode, this can be used for admin tenant creation.
|
|
107
|
-
"""
|
|
108
|
-
logger.info(
|
|
109
|
-
"Setting up tenant",
|
|
110
|
-
name=request.name,
|
|
111
|
-
slug=request.slug,
|
|
112
|
-
include_sample_data=request.include_sample_data,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
async with get_async_session() as db:
|
|
117
|
-
# Check if tenant already exists
|
|
118
|
-
existing_tenant_id = await ensure_tenant(
|
|
119
|
-
db,
|
|
120
|
-
name=request.name,
|
|
121
|
-
slug=request.slug,
|
|
122
|
-
settings_dict=request.settings,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
# For now, we'll consider it "existing" if the ensure_tenant call
|
|
126
|
-
# found an existing tenant. We could enhance this by checking
|
|
127
|
-
# if the tenant was created or updated.
|
|
128
|
-
try:
|
|
129
|
-
# Try to create with sample data if requested
|
|
130
|
-
if request.include_sample_data:
|
|
131
|
-
tenant_id = await seed_tenant_with_data(
|
|
132
|
-
db,
|
|
133
|
-
tenant_name=request.name,
|
|
134
|
-
tenant_slug=request.slug,
|
|
135
|
-
tenant_settings=request.settings,
|
|
136
|
-
include_sample_data=True,
|
|
137
|
-
)
|
|
138
|
-
existing = False # seed_tenant_with_data creates new tenant
|
|
139
|
-
else:
|
|
140
|
-
tenant_id = existing_tenant_id
|
|
141
|
-
existing = True # assume it existed (could be enhanced)
|
|
142
|
-
|
|
143
|
-
except Exception as e:
|
|
144
|
-
# If seeding fails, we still have the basic tenant
|
|
145
|
-
logger.warning(
|
|
146
|
-
"Sample data creation failed, but tenant was created",
|
|
147
|
-
error=str(e),
|
|
148
|
-
tenant_id=str(existing_tenant_id),
|
|
149
|
-
)
|
|
150
|
-
tenant_id = existing_tenant_id
|
|
151
|
-
existing = True
|
|
152
|
-
|
|
153
|
-
logger.info(
|
|
154
|
-
"Tenant setup completed",
|
|
155
|
-
tenant_id=str(tenant_id),
|
|
156
|
-
name=request.name,
|
|
157
|
-
slug=request.slug,
|
|
158
|
-
existing=existing,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
return TenantSetupResponse(
|
|
162
|
-
tenant_id=str(tenant_id),
|
|
163
|
-
name=request.name,
|
|
164
|
-
slug=request.slug,
|
|
165
|
-
message=f"Tenant {'configured' if existing else 'created'} successfully",
|
|
166
|
-
existing=existing,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
except Exception as e:
|
|
170
|
-
logger.error("Tenant setup failed", error=str(e))
|
|
171
|
-
raise HTTPException(status_code=500, detail=f"Failed to setup tenant: {str(e)}") from e
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
@router.get("/tenant/{tenant_id}", response_model=TenantResponse)
|
|
175
|
-
async def get_tenant(
|
|
176
|
-
tenant_id: UUID = Path(..., description="UUID of the tenant to retrieve"),
|
|
177
|
-
) -> TenantResponse:
|
|
178
|
-
"""
|
|
179
|
-
Get a specific tenant by ID.
|
|
180
|
-
|
|
181
|
-
This endpoint retrieves detailed information about a tenant including
|
|
182
|
-
its settings, creation date, and other metadata.
|
|
183
|
-
"""
|
|
184
|
-
logger.info("Retrieving tenant", tenant_id=str(tenant_id))
|
|
185
|
-
|
|
186
|
-
try:
|
|
187
|
-
async with get_async_session() as db:
|
|
188
|
-
# Query for the tenant
|
|
189
|
-
stmt = select(Tenants).where(Tenants.id == tenant_id)
|
|
190
|
-
result = await db.execute(stmt)
|
|
191
|
-
tenant = result.scalar_one_or_none()
|
|
192
|
-
|
|
193
|
-
if not tenant:
|
|
194
|
-
raise HTTPException(status_code=404, detail=f"Tenant with ID {tenant_id} not found")
|
|
195
|
-
|
|
196
|
-
logger.info(
|
|
197
|
-
"Tenant retrieved successfully",
|
|
198
|
-
tenant_id=str(tenant_id),
|
|
199
|
-
slug=tenant.slug,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
return TenantResponse(
|
|
203
|
-
tenant_id=str(tenant.id),
|
|
204
|
-
name=tenant.name,
|
|
205
|
-
slug=tenant.slug,
|
|
206
|
-
settings=tenant.settings or {},
|
|
207
|
-
created_at=tenant.created_at.isoformat() if tenant.created_at else "",
|
|
208
|
-
updated_at=tenant.updated_at.isoformat() if tenant.updated_at else "",
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
except HTTPException:
|
|
212
|
-
# Re-raise HTTP exceptions as-is
|
|
213
|
-
raise
|
|
214
|
-
except Exception as e:
|
|
215
|
-
logger.error("Failed to retrieve tenant", tenant_id=str(tenant_id), error=str(e))
|
|
216
|
-
raise HTTPException(status_code=500, detail=f"Failed to retrieve tenant: {str(e)}") from e
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
@router.get("/tenants", response_model=TenantListResponse)
|
|
220
|
-
async def list_tenants() -> TenantListResponse:
|
|
221
|
-
"""
|
|
222
|
-
List all tenants in the system.
|
|
223
|
-
|
|
224
|
-
This endpoint is useful for:
|
|
225
|
-
- Multi-tenant administration
|
|
226
|
-
- Tenant discovery and management
|
|
227
|
-
- System overview and monitoring
|
|
228
|
-
"""
|
|
229
|
-
logger.info("Listing all tenants")
|
|
230
|
-
|
|
231
|
-
try:
|
|
232
|
-
async with get_async_session() as db:
|
|
233
|
-
# Query for all tenants, ordered by creation date
|
|
234
|
-
stmt = select(Tenants).order_by(Tenants.created_at.desc())
|
|
235
|
-
result = await db.execute(stmt)
|
|
236
|
-
tenants = result.scalars().all()
|
|
237
|
-
|
|
238
|
-
tenant_responses = [
|
|
239
|
-
TenantResponse(
|
|
240
|
-
tenant_id=str(tenant.id),
|
|
241
|
-
name=tenant.name,
|
|
242
|
-
slug=tenant.slug,
|
|
243
|
-
settings=tenant.settings or {},
|
|
244
|
-
created_at=(tenant.created_at.isoformat() if tenant.created_at else ""),
|
|
245
|
-
updated_at=(tenant.updated_at.isoformat() if tenant.updated_at else ""),
|
|
246
|
-
)
|
|
247
|
-
for tenant in tenants
|
|
248
|
-
]
|
|
249
|
-
|
|
250
|
-
logger.info(
|
|
251
|
-
"Tenants listed successfully",
|
|
252
|
-
total_count=len(tenant_responses),
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
return TenantListResponse(
|
|
256
|
-
tenants=tenant_responses,
|
|
257
|
-
total_count=len(tenant_responses),
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
except Exception as e:
|
|
261
|
-
logger.error("Failed to list tenants", error=str(e))
|
|
262
|
-
raise HTTPException(status_code=500, detail=f"Failed to list tenants: {str(e)}") from e
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
@router.put("/tenant/{tenant_id}", response_model=TenantResponse)
|
|
266
|
-
async def update_tenant(
|
|
267
|
-
request: TenantUpdateRequest,
|
|
268
|
-
tenant_id: UUID = Path(..., description="UUID of the tenant to update"),
|
|
269
|
-
) -> TenantResponse:
|
|
270
|
-
"""
|
|
271
|
-
Update a specific tenant.
|
|
272
|
-
|
|
273
|
-
This endpoint allows updating tenant information including:
|
|
274
|
-
- Display name
|
|
275
|
-
- Slug (URL identifier)
|
|
276
|
-
- Settings (JSON metadata)
|
|
277
|
-
|
|
278
|
-
Only provided fields will be updated; others remain unchanged.
|
|
279
|
-
"""
|
|
280
|
-
logger.info(
|
|
281
|
-
"Updating tenant",
|
|
282
|
-
tenant_id=str(tenant_id),
|
|
283
|
-
name=request.name,
|
|
284
|
-
slug=request.slug,
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
try:
|
|
288
|
-
async with get_async_session() as db:
|
|
289
|
-
# First, check if tenant exists
|
|
290
|
-
stmt = select(Tenants).where(Tenants.id == tenant_id)
|
|
291
|
-
result = await db.execute(stmt)
|
|
292
|
-
existing_tenant = result.scalar_one_or_none()
|
|
293
|
-
|
|
294
|
-
if not existing_tenant:
|
|
295
|
-
raise HTTPException(status_code=404, detail=f"Tenant with ID {tenant_id} not found")
|
|
296
|
-
|
|
297
|
-
# Check for slug conflicts if slug is being updated
|
|
298
|
-
if request.slug and request.slug != existing_tenant.slug:
|
|
299
|
-
slug_check = select(Tenants).where(
|
|
300
|
-
(Tenants.slug == request.slug) & (Tenants.id != tenant_id)
|
|
301
|
-
)
|
|
302
|
-
result = await db.execute(slug_check)
|
|
303
|
-
if result.scalar_one_or_none():
|
|
304
|
-
raise HTTPException(
|
|
305
|
-
status_code=409,
|
|
306
|
-
detail=f"A tenant with slug '{request.slug}' already exists",
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
# Build update data - only include provided fields
|
|
310
|
-
update_data = {}
|
|
311
|
-
if request.name is not None:
|
|
312
|
-
update_data["name"] = request.name
|
|
313
|
-
if request.slug is not None:
|
|
314
|
-
update_data["slug"] = request.slug
|
|
315
|
-
if request.settings is not None:
|
|
316
|
-
update_data["settings"] = request.settings
|
|
317
|
-
|
|
318
|
-
# Add updated_at timestamp
|
|
319
|
-
from datetime import datetime
|
|
320
|
-
|
|
321
|
-
update_data["updated_at"] = datetime.now(UTC)
|
|
322
|
-
|
|
323
|
-
if update_data:
|
|
324
|
-
# Perform the update
|
|
325
|
-
stmt = update(Tenants).where(Tenants.id == tenant_id).values(**update_data)
|
|
326
|
-
await db.execute(stmt)
|
|
327
|
-
await db.commit()
|
|
328
|
-
|
|
329
|
-
# Fetch the updated tenant
|
|
330
|
-
stmt = select(Tenants).where(Tenants.id == tenant_id)
|
|
331
|
-
result = await db.execute(stmt)
|
|
332
|
-
updated_tenant = result.scalar_one()
|
|
333
|
-
|
|
334
|
-
logger.info(
|
|
335
|
-
"Tenant updated successfully",
|
|
336
|
-
tenant_id=str(tenant_id),
|
|
337
|
-
name=updated_tenant.name,
|
|
338
|
-
slug=updated_tenant.slug,
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
return TenantResponse(
|
|
342
|
-
tenant_id=str(updated_tenant.id),
|
|
343
|
-
name=updated_tenant.name,
|
|
344
|
-
slug=updated_tenant.slug,
|
|
345
|
-
settings=updated_tenant.settings or {},
|
|
346
|
-
created_at=(
|
|
347
|
-
updated_tenant.created_at.isoformat() if updated_tenant.created_at else ""
|
|
348
|
-
),
|
|
349
|
-
updated_at=(
|
|
350
|
-
updated_tenant.updated_at.isoformat() if updated_tenant.updated_at else ""
|
|
351
|
-
),
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
except HTTPException:
|
|
355
|
-
# Re-raise HTTP exceptions as-is
|
|
356
|
-
raise
|
|
357
|
-
except Exception as e:
|
|
358
|
-
logger.error("Failed to update tenant", tenant_id=str(tenant_id), error=str(e))
|
|
359
|
-
raise HTTPException(status_code=500, detail=f"Failed to update tenant: {str(e)}") from e
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
@router.delete("/tenant/{tenant_id}")
|
|
363
|
-
async def delete_tenant(
|
|
364
|
-
tenant_id: UUID = Path(..., description="UUID of the tenant to delete"),
|
|
365
|
-
) -> dict[str, Any]:
|
|
366
|
-
"""
|
|
367
|
-
Delete a specific tenant.
|
|
368
|
-
|
|
369
|
-
**WARNING**: This operation is destructive and will cascade delete all related data:
|
|
370
|
-
- All users in this tenant
|
|
371
|
-
- All boards and their content
|
|
372
|
-
- All generations and media
|
|
373
|
-
- All provider configurations
|
|
374
|
-
- All credit transactions
|
|
375
|
-
|
|
376
|
-
This operation cannot be undone. Use with extreme caution.
|
|
377
|
-
"""
|
|
378
|
-
logger.warning(
|
|
379
|
-
"Attempting to delete tenant - this is a destructive operation",
|
|
380
|
-
tenant_id=str(tenant_id),
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
try:
|
|
384
|
-
async with get_async_session() as db:
|
|
385
|
-
# First, check if tenant exists and get its info for logging
|
|
386
|
-
stmt = select(Tenants).where(Tenants.id == tenant_id)
|
|
387
|
-
result = await db.execute(stmt)
|
|
388
|
-
existing_tenant = result.scalar_one_or_none()
|
|
389
|
-
|
|
390
|
-
if not existing_tenant:
|
|
391
|
-
raise HTTPException(status_code=404, detail=f"Tenant with ID {tenant_id} not found")
|
|
392
|
-
|
|
393
|
-
# Prevent deletion of default tenant in single-tenant mode
|
|
394
|
-
if (
|
|
395
|
-
not settings.multi_tenant_mode
|
|
396
|
-
and existing_tenant.slug == settings.default_tenant_slug
|
|
397
|
-
):
|
|
398
|
-
raise HTTPException(
|
|
399
|
-
status_code=400,
|
|
400
|
-
detail="Cannot delete the default tenant in single-tenant mode. "
|
|
401
|
-
"Enable multi-tenant mode or use a different default tenant first.",
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
tenant_name = existing_tenant.name
|
|
405
|
-
tenant_slug = existing_tenant.slug
|
|
406
|
-
|
|
407
|
-
# Perform the deletion (CASCADE will handle related records)
|
|
408
|
-
stmt = delete(Tenants).where(Tenants.id == tenant_id)
|
|
409
|
-
result = await db.execute(stmt)
|
|
410
|
-
await db.commit()
|
|
411
|
-
|
|
412
|
-
if result.rowcount == 0:
|
|
413
|
-
# This shouldn't happen since we checked existence above
|
|
414
|
-
raise HTTPException(status_code=404, detail=f"Tenant with ID {tenant_id} not found")
|
|
415
|
-
|
|
416
|
-
logger.warning(
|
|
417
|
-
"Tenant deleted successfully - all related data has been removed",
|
|
418
|
-
tenant_id=str(tenant_id),
|
|
419
|
-
name=tenant_name,
|
|
420
|
-
slug=tenant_slug,
|
|
421
|
-
deleted_records=result.rowcount,
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
"message": f"Tenant '{tenant_name}' ({tenant_slug}) deleted successfully",
|
|
426
|
-
"tenant_id": str(tenant_id),
|
|
427
|
-
"warning": "All related data (users, boards, generations, etc.) has been permanently deleted", # noqa: E501
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
except HTTPException:
|
|
431
|
-
# Re-raise HTTP exceptions as-is
|
|
432
|
-
raise
|
|
433
|
-
except Exception as e:
|
|
434
|
-
logger.error("Failed to delete tenant", tenant_id=str(tenant_id), error=str(e))
|
|
435
|
-
raise HTTPException(status_code=500, detail=f"Failed to delete tenant: {str(e)}") from e
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
@router.get("/status")
|
|
439
|
-
async def setup_status() -> dict[str, Any]:
|
|
440
|
-
"""
|
|
441
|
-
Get the current setup status of the application.
|
|
442
|
-
|
|
443
|
-
This endpoint provides information about:
|
|
444
|
-
- Whether a default tenant exists
|
|
445
|
-
- Current configuration mode
|
|
446
|
-
- Setup recommendations
|
|
447
|
-
"""
|
|
448
|
-
try:
|
|
449
|
-
async with get_async_session() as db:
|
|
450
|
-
# Check if default tenant exists
|
|
451
|
-
try:
|
|
452
|
-
default_tenant_id = await ensure_tenant(db, slug=settings.default_tenant_slug)
|
|
453
|
-
has_default_tenant = True
|
|
454
|
-
default_tenant_uuid = str(default_tenant_id)
|
|
455
|
-
except Exception:
|
|
456
|
-
has_default_tenant = False
|
|
457
|
-
default_tenant_uuid = None
|
|
458
|
-
|
|
459
|
-
setup_needed = not has_default_tenant and not settings.multi_tenant_mode
|
|
460
|
-
|
|
461
|
-
return {
|
|
462
|
-
"setup_needed": setup_needed,
|
|
463
|
-
"has_default_tenant": has_default_tenant,
|
|
464
|
-
"default_tenant_id": default_tenant_uuid,
|
|
465
|
-
"default_tenant_slug": settings.default_tenant_slug,
|
|
466
|
-
"multi_tenant_mode": settings.multi_tenant_mode,
|
|
467
|
-
"auth_provider": settings.auth_provider,
|
|
468
|
-
"recommendations": _get_setup_recommendations(
|
|
469
|
-
setup_needed, has_default_tenant, settings.multi_tenant_mode
|
|
470
|
-
),
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
except Exception as e:
|
|
474
|
-
logger.error("Failed to get setup status", error=str(e))
|
|
475
|
-
raise HTTPException(status_code=500, detail=f"Failed to get setup status: {str(e)}") from e
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def _get_setup_recommendations(
|
|
479
|
-
setup_needed: bool, has_default_tenant: bool, multi_tenant_mode: bool
|
|
480
|
-
) -> list[str]:
|
|
481
|
-
"""Get setup recommendations based on current state."""
|
|
482
|
-
recommendations = []
|
|
483
|
-
|
|
484
|
-
if setup_needed:
|
|
485
|
-
recommendations.append(
|
|
486
|
-
f"Create a default tenant using POST /api/setup/tenant with slug '{settings.default_tenant_slug}'" # noqa: E501
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
if not has_default_tenant and not multi_tenant_mode:
|
|
490
|
-
recommendations.append(
|
|
491
|
-
"Run database migrations to create the default tenant: 'alembic upgrade head'"
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
if settings.auth_provider == "none" and not multi_tenant_mode:
|
|
495
|
-
recommendations.append(
|
|
496
|
-
"Consider configuring a proper authentication provider for production use"
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
if not recommendations:
|
|
500
|
-
if multi_tenant_mode:
|
|
501
|
-
recommendations.append("System is ready for multi-tenant operation")
|
|
502
|
-
else:
|
|
503
|
-
recommendations.append("Single-tenant setup is complete and ready to use")
|
|
504
|
-
|
|
505
|
-
return recommendations
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Server-Sent Events (SSE) endpoints for real-time generation progress.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
|
|
9
|
-
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
10
|
-
from fastapi.responses import StreamingResponse
|
|
11
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
-
|
|
13
|
-
from ...config import Settings
|
|
14
|
-
from ...database.connection import get_db_session
|
|
15
|
-
from ...jobs import repository as jobs_repo
|
|
16
|
-
from ...logging import get_logger
|
|
17
|
-
from ...redis_pool import get_redis_client
|
|
18
|
-
from ..auth import AuthenticatedUser, get_current_user
|
|
19
|
-
|
|
20
|
-
logger = get_logger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
router = APIRouter()
|
|
24
|
-
_settings = Settings()
|
|
25
|
-
# Use the shared Redis connection pool
|
|
26
|
-
_redis = get_redis_client()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@router.get("/generations/{generation_id}/progress")
|
|
30
|
-
async def generation_progress_stream(
|
|
31
|
-
generation_id: str,
|
|
32
|
-
request: Request,
|
|
33
|
-
db: AsyncSession = Depends(get_db_session),
|
|
34
|
-
current_user: AuthenticatedUser = Depends(get_current_user),
|
|
35
|
-
):
|
|
36
|
-
"""Server-sent events for job progress, backed by Redis pub/sub.
|
|
37
|
-
|
|
38
|
-
Requires authentication. Users can only monitor progress for their own generations
|
|
39
|
-
or generations within their tenant (depending on access control policy).
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
logger.info(
|
|
43
|
-
"SSE: generation progress stream requested",
|
|
44
|
-
generation_id=generation_id,
|
|
45
|
-
url=request.url,
|
|
46
|
-
)
|
|
47
|
-
# Verify user has access to this generation
|
|
48
|
-
try:
|
|
49
|
-
generation = await jobs_repo.get_generation(db, generation_id)
|
|
50
|
-
|
|
51
|
-
# Check if user owns this generation or belongs to the same tenant
|
|
52
|
-
if generation.user_id != current_user.user_id:
|
|
53
|
-
# Allow tenant-level access (users in same tenant can see each other's jobs)
|
|
54
|
-
# You may want to make this more restrictive based on your requirements
|
|
55
|
-
if generation.tenant_id != current_user.tenant_id:
|
|
56
|
-
logger.warning(
|
|
57
|
-
"User attempted to access generation belonging to different user",
|
|
58
|
-
user_id=str(current_user.user_id),
|
|
59
|
-
generation_id=generation_id,
|
|
60
|
-
owner_user_id=str(generation.user_id),
|
|
61
|
-
)
|
|
62
|
-
raise HTTPException(
|
|
63
|
-
status_code=403,
|
|
64
|
-
detail="You don't have permission to access this generation",
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
logger.info(
|
|
68
|
-
"User connected to progress stream",
|
|
69
|
-
user_id=str(current_user.user_id),
|
|
70
|
-
generation_id=generation_id,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
except HTTPException:
|
|
74
|
-
raise
|
|
75
|
-
except Exception as e:
|
|
76
|
-
logger.error(
|
|
77
|
-
"Failed to verify access to generation",
|
|
78
|
-
generation_id=generation_id,
|
|
79
|
-
error=str(e),
|
|
80
|
-
)
|
|
81
|
-
raise HTTPException(status_code=404, detail="Generation not found") from e
|
|
82
|
-
|
|
83
|
-
channel = f"job:{generation_id}:progress"
|
|
84
|
-
|
|
85
|
-
async def event_stream():
|
|
86
|
-
pubsub = _redis.pubsub()
|
|
87
|
-
await pubsub.subscribe(channel)
|
|
88
|
-
logger.info(
|
|
89
|
-
"SSE: Subscribed to Redis channel",
|
|
90
|
-
generation_id=generation_id,
|
|
91
|
-
channel=channel,
|
|
92
|
-
)
|
|
93
|
-
try:
|
|
94
|
-
while True:
|
|
95
|
-
if await request.is_disconnected():
|
|
96
|
-
logger.info(
|
|
97
|
-
"Client disconnected from progress stream",
|
|
98
|
-
generation_id=generation_id,
|
|
99
|
-
)
|
|
100
|
-
break
|
|
101
|
-
msg = await pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0)
|
|
102
|
-
if msg:
|
|
103
|
-
logger.info(
|
|
104
|
-
"SSE: pubsub message received",
|
|
105
|
-
message=msg,
|
|
106
|
-
msg_type=msg.get("type"),
|
|
107
|
-
)
|
|
108
|
-
if msg and msg.get("type") == "message":
|
|
109
|
-
data = msg["data"]
|
|
110
|
-
logger.info(
|
|
111
|
-
"SSE: sending progress data to client",
|
|
112
|
-
generation_id=generation_id,
|
|
113
|
-
data_preview=(data[:100] if isinstance(data, str) else str(data)[:100]),
|
|
114
|
-
)
|
|
115
|
-
yield f"data: {data}\n\n"
|
|
116
|
-
else:
|
|
117
|
-
# Send keep-alive every 15 seconds to prevent timeout
|
|
118
|
-
await asyncio.sleep(15)
|
|
119
|
-
logger.debug("SSE: sending keep-alive", generation_id=generation_id)
|
|
120
|
-
yield ": keep-alive\n\n"
|
|
121
|
-
finally:
|
|
122
|
-
logger.info(
|
|
123
|
-
"SSE: Cleaning up stream",
|
|
124
|
-
generation_id=generation_id,
|
|
125
|
-
)
|
|
126
|
-
await pubsub.unsubscribe(channel)
|
|
127
|
-
await pubsub.close()
|
|
128
|
-
|
|
129
|
-
return StreamingResponse(event_stream(), media_type="text/event-stream")
|