@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.
Files changed (237) hide show
  1. package/dist/index.js +561 -469
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -5
  4. package/templates/README.md +0 -122
  5. package/templates/api/.env.example +0 -65
  6. package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
  7. package/templates/api/Dockerfile +0 -32
  8. package/templates/api/README.md +0 -264
  9. package/templates/api/alembic/env.py +0 -114
  10. package/templates/api/alembic/script.py.mako +0 -28
  11. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
  12. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
  13. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
  14. package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
  16. package/templates/api/alembic.ini +0 -36
  17. package/templates/api/config/generators.yaml +0 -237
  18. package/templates/api/config/storage_config.yaml +0 -26
  19. package/templates/api/docs/ADDING_GENERATORS.md +0 -409
  20. package/templates/api/docs/GENERATORS_API.md +0 -502
  21. package/templates/api/docs/MIGRATIONS.md +0 -472
  22. package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
  23. package/templates/api/docs/storage_providers.md +0 -337
  24. package/templates/api/pyproject.toml +0 -205
  25. package/templates/api/src/boards/__init__.py +0 -10
  26. package/templates/api/src/boards/api/app.py +0 -172
  27. package/templates/api/src/boards/api/auth.py +0 -75
  28. package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
  29. package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
  30. package/templates/api/src/boards/api/endpoints/setup.py +0 -505
  31. package/templates/api/src/boards/api/endpoints/sse.py +0 -129
  32. package/templates/api/src/boards/api/endpoints/storage.py +0 -155
  33. package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
  34. package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
  35. package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
  36. package/templates/api/src/boards/auth/__init__.py +0 -15
  37. package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
  38. package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
  39. package/templates/api/src/boards/auth/adapters/base.py +0 -73
  40. package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
  41. package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
  42. package/templates/api/src/boards/auth/adapters/none.py +0 -102
  43. package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
  44. package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
  45. package/templates/api/src/boards/auth/context.py +0 -35
  46. package/templates/api/src/boards/auth/factory.py +0 -129
  47. package/templates/api/src/boards/auth/middleware.py +0 -221
  48. package/templates/api/src/boards/auth/provisioning.py +0 -129
  49. package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
  50. package/templates/api/src/boards/cli.py +0 -354
  51. package/templates/api/src/boards/config.py +0 -131
  52. package/templates/api/src/boards/database/__init__.py +0 -7
  53. package/templates/api/src/boards/database/cli.py +0 -110
  54. package/templates/api/src/boards/database/connection.py +0 -292
  55. package/templates/api/src/boards/database/models.py +0 -19
  56. package/templates/api/src/boards/database/seed_data.py +0 -182
  57. package/templates/api/src/boards/dbmodels/__init__.py +0 -441
  58. package/templates/api/src/boards/generators/__init__.py +0 -57
  59. package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
  60. package/templates/api/src/boards/generators/artifacts.py +0 -53
  61. package/templates/api/src/boards/generators/base.py +0 -144
  62. package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
  63. package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
  64. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
  65. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
  66. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
  67. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
  68. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
  69. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
  70. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
  71. package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
  72. package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
  73. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
  74. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
  75. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
  76. package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
  77. package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
  78. package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
  79. package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
  80. package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
  81. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
  82. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
  83. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
  84. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
  85. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
  86. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
  87. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
  88. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
  89. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
  90. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
  91. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
  92. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
  93. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
  94. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
  95. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
  96. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
  97. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
  98. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
  99. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
  100. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
  101. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
  102. package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
  103. package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
  104. package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
  105. package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
  106. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
  107. package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
  108. package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
  109. package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
  110. package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
  111. package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
  112. package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
  113. package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
  114. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
  115. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
  116. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
  117. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
  118. package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
  119. package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
  120. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
  121. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
  122. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
  123. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
  124. package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
  125. package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
  126. package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
  127. package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
  128. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
  129. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
  130. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
  131. package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
  132. package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
  133. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
  134. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
  135. package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
  136. package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
  137. package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
  138. package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
  139. package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
  140. package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
  141. package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
  142. package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
  143. package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
  144. package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
  145. package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
  146. package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
  147. package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
  148. package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
  149. package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
  150. package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
  151. package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
  152. package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
  153. package/templates/api/src/boards/generators/loader.py +0 -253
  154. package/templates/api/src/boards/generators/registry.py +0 -114
  155. package/templates/api/src/boards/generators/resolution.py +0 -632
  156. package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
  157. package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
  158. package/templates/api/src/boards/graphql/__init__.py +0 -7
  159. package/templates/api/src/boards/graphql/access_control.py +0 -136
  160. package/templates/api/src/boards/graphql/mutations/root.py +0 -148
  161. package/templates/api/src/boards/graphql/queries/root.py +0 -116
  162. package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
  163. package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
  164. package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
  165. package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
  166. package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
  167. package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
  168. package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
  169. package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
  170. package/templates/api/src/boards/graphql/schema.py +0 -81
  171. package/templates/api/src/boards/graphql/types/board.py +0 -102
  172. package/templates/api/src/boards/graphql/types/generation.py +0 -166
  173. package/templates/api/src/boards/graphql/types/generator.py +0 -17
  174. package/templates/api/src/boards/graphql/types/user.py +0 -47
  175. package/templates/api/src/boards/jobs/repository.py +0 -153
  176. package/templates/api/src/boards/logging.py +0 -195
  177. package/templates/api/src/boards/middleware.py +0 -339
  178. package/templates/api/src/boards/progress/__init__.py +0 -4
  179. package/templates/api/src/boards/progress/models.py +0 -25
  180. package/templates/api/src/boards/progress/publisher.py +0 -64
  181. package/templates/api/src/boards/py.typed +0 -0
  182. package/templates/api/src/boards/redis_pool.py +0 -118
  183. package/templates/api/src/boards/storage/__init__.py +0 -52
  184. package/templates/api/src/boards/storage/base.py +0 -363
  185. package/templates/api/src/boards/storage/config.py +0 -187
  186. package/templates/api/src/boards/storage/factory.py +0 -288
  187. package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
  188. package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
  189. package/templates/api/src/boards/storage/implementations/local.py +0 -201
  190. package/templates/api/src/boards/storage/implementations/s3.py +0 -294
  191. package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
  192. package/templates/api/src/boards/tenant_isolation.py +0 -446
  193. package/templates/api/src/boards/validation.py +0 -262
  194. package/templates/api/src/boards/workers/__init__.py +0 -1
  195. package/templates/api/src/boards/workers/actors.py +0 -274
  196. package/templates/api/src/boards/workers/cli.py +0 -125
  197. package/templates/api/src/boards/workers/context.py +0 -348
  198. package/templates/api/src/boards/workers/middleware.py +0 -58
  199. package/templates/api/src/py.typed +0 -0
  200. package/templates/compose.web.yaml +0 -35
  201. package/templates/compose.yaml +0 -116
  202. package/templates/docker/env.example +0 -23
  203. package/templates/web/.env.example +0 -28
  204. package/templates/web/Dockerfile +0 -51
  205. package/templates/web/components.json +0 -22
  206. package/templates/web/imageLoader.js +0 -18
  207. package/templates/web/next-env.d.ts +0 -5
  208. package/templates/web/next.config.js +0 -36
  209. package/templates/web/package.json +0 -41
  210. package/templates/web/postcss.config.mjs +0 -7
  211. package/templates/web/public/favicon.ico +0 -0
  212. package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
  213. package/templates/web/src/app/globals.css +0 -123
  214. package/templates/web/src/app/layout.tsx +0 -31
  215. package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
  216. package/templates/web/src/app/page.tsx +0 -35
  217. package/templates/web/src/app/providers.tsx +0 -18
  218. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
  219. package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
  220. package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
  221. package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
  222. package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
  223. package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
  224. package/templates/web/src/components/header.tsx +0 -32
  225. package/templates/web/src/components/theme-provider.tsx +0 -10
  226. package/templates/web/src/components/theme-toggle.tsx +0 -75
  227. package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
  228. package/templates/web/src/components/ui/button.tsx +0 -58
  229. package/templates/web/src/components/ui/card.tsx +0 -92
  230. package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
  231. package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
  232. package/templates/web/src/components/ui/toast.tsx +0 -128
  233. package/templates/web/src/components/ui/toaster.tsx +0 -35
  234. package/templates/web/src/components/ui/use-toast.ts +0 -187
  235. package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
  236. package/templates/web/src/lib/utils.ts +0 -6
  237. package/templates/web/tsconfig.json +0 -41
@@ -1,288 +0,0 @@
1
- """Factory for creating storage providers and managers."""
2
-
3
- from pathlib import Path
4
- from typing import Any
5
-
6
- from ..logging import get_logger
7
- from .base import StorageManager, StorageProvider
8
- from .config import StorageConfig, load_storage_config
9
- from .implementations.local import LocalStorageProvider
10
-
11
- logger = get_logger(__name__)
12
-
13
- # Singleton storage configuration
14
- # Loaded once at module import time to avoid re-parsing YAML on every request
15
- _storage_config: StorageConfig | None = None
16
-
17
-
18
- def get_storage_config() -> StorageConfig:
19
- """Get the singleton storage configuration.
20
-
21
- Loads the configuration from settings.storage_config_path on first access.
22
- Subsequent calls return the cached configuration.
23
-
24
- Returns:
25
- StorageConfig instance
26
- """
27
- global _storage_config
28
-
29
- if _storage_config is None:
30
- from ..config import settings
31
-
32
- config_path = Path(settings.storage_config_path) if settings.storage_config_path else None
33
- _storage_config = load_storage_config(config_path)
34
- logger.info(
35
- f"Loaded storage configuration: default_provider={_storage_config.default_provider}, "
36
- f"providers={list(_storage_config.providers.keys())}"
37
- )
38
-
39
- return _storage_config
40
-
41
-
42
- # Optional imports for cloud providers
43
- try:
44
- from .implementations.supabase import SupabaseStorageProvider
45
-
46
- _supabase_available = True
47
- except ImportError:
48
- SupabaseStorageProvider = None
49
- _supabase_available = False
50
- logger.warning(
51
- "Supabase storage not available. "
52
- "Install with: pip install weirdfingers-boards[storage-supabase]"
53
- )
54
-
55
- try:
56
- from .implementations.s3 import S3StorageProvider
57
-
58
- _s3_available = True
59
- except ImportError:
60
- S3StorageProvider = None
61
- _s3_available = False
62
- logger.warning(
63
- "S3 storage not available. Install with: pip install weirdfingers-boards[storage-s3]"
64
- )
65
-
66
- try:
67
- from .implementations.gcs import GCSStorageProvider
68
-
69
- _gcs_available = True
70
- except ImportError:
71
- GCSStorageProvider = None
72
- _gcs_available = False
73
- logger.warning(
74
- "GCS storage not available. Install with: pip install weirdfingers-boards[storage-gcs]"
75
- )
76
-
77
-
78
- def create_storage_provider(provider_type: str, config: dict[str, Any]) -> StorageProvider:
79
- """Create a storage provider instance from configuration.
80
-
81
- Args:
82
- provider_type: Type of provider ('local', 'supabase', 's3')
83
- config: Provider configuration dictionary
84
-
85
- Returns:
86
- StorageProvider instance
87
-
88
- Raises:
89
- ValueError: If provider type is unknown or configuration is invalid
90
- ImportError: If required dependencies are not available
91
- """
92
-
93
- if provider_type == "local":
94
- return _create_local_provider(config)
95
- elif provider_type == "supabase":
96
- if not _supabase_available:
97
- raise ImportError(
98
- "Supabase storage requires additional dependencies. "
99
- "Install with: pip install weirdfingers-boards[storage-supabase]"
100
- )
101
- return _create_supabase_provider(config)
102
- elif provider_type == "s3":
103
- if not _s3_available:
104
- raise ImportError(
105
- "S3 storage requires additional dependencies. "
106
- "Install with: pip install weirdfingers-boards[storage-s3]"
107
- )
108
- return _create_s3_provider(config)
109
- elif provider_type == "gcs":
110
- if not _gcs_available:
111
- raise ImportError(
112
- "GCS storage requires additional dependencies. "
113
- "Install with: pip install weirdfingers-boards[storage-gcs]"
114
- )
115
- return _create_gcs_provider(config)
116
- else:
117
- raise ValueError(f"Unknown storage provider type: {provider_type}")
118
-
119
-
120
- def _create_local_provider(config: dict[str, Any]) -> LocalStorageProvider:
121
- """Create local storage provider."""
122
- base_path = config.get("base_path", "/tmp/boards/storage")
123
- public_url_base = config.get("public_url_base")
124
-
125
- return LocalStorageProvider(base_path=Path(base_path), public_url_base=public_url_base)
126
-
127
-
128
- def _create_supabase_provider(config: dict[str, Any]) -> StorageProvider:
129
- """Create Supabase storage provider."""
130
- if SupabaseStorageProvider is None:
131
- raise ImportError("Supabase storage not available")
132
-
133
- url = config.get("url")
134
- key = config.get("key")
135
- bucket = config.get("bucket", "boards-artifacts")
136
-
137
- if not url:
138
- raise ValueError("Supabase storage requires 'url' in configuration")
139
- if not key:
140
- raise ValueError("Supabase storage requires 'key' in configuration")
141
-
142
- return SupabaseStorageProvider(url=url, key=key, bucket=bucket)
143
-
144
-
145
- def _create_s3_provider(config: dict[str, Any]) -> StorageProvider:
146
- """Create S3 storage provider."""
147
- if S3StorageProvider is None:
148
- raise ImportError("S3 storage not available")
149
-
150
- bucket = config.get("bucket")
151
- if not bucket:
152
- raise ValueError("S3 storage requires 'bucket' in configuration")
153
-
154
- region = config.get("region", "us-east-1")
155
- aws_access_key_id = config.get("aws_access_key_id")
156
- aws_secret_access_key = config.get("aws_secret_access_key")
157
- aws_session_token = config.get("aws_session_token")
158
- endpoint_url = config.get("endpoint_url")
159
- cloudfront_domain = config.get("cloudfront_domain")
160
- upload_config = config.get("upload_config", {})
161
-
162
- return S3StorageProvider(
163
- bucket=bucket,
164
- region=region,
165
- aws_access_key_id=aws_access_key_id,
166
- aws_secret_access_key=aws_secret_access_key,
167
- aws_session_token=aws_session_token,
168
- endpoint_url=endpoint_url,
169
- cloudfront_domain=cloudfront_domain,
170
- upload_config=upload_config,
171
- )
172
-
173
-
174
- def _create_gcs_provider(config: dict[str, Any]) -> StorageProvider:
175
- """Create GCS storage provider."""
176
- if GCSStorageProvider is None:
177
- raise ImportError("GCS storage not available")
178
-
179
- bucket = config.get("bucket")
180
- if not bucket:
181
- raise ValueError("GCS storage requires 'bucket' in configuration")
182
-
183
- project_id = config.get("project_id")
184
- credentials_path = config.get("credentials_path")
185
- credentials_json = config.get("credentials_json")
186
- cdn_domain = config.get("cdn_domain")
187
- upload_config = config.get("upload_config", {})
188
-
189
- return GCSStorageProvider(
190
- bucket=bucket,
191
- project_id=project_id,
192
- credentials_path=credentials_path,
193
- credentials_json=credentials_json,
194
- cdn_domain=cdn_domain,
195
- upload_config=upload_config,
196
- )
197
-
198
-
199
- def _build_storage_manager_from_config(storage_config: StorageConfig) -> StorageManager:
200
- """Build a storage manager from a StorageConfig, registering all providers.
201
-
202
- This is an internal helper that can be used for testing.
203
-
204
- Args:
205
- storage_config: Storage configuration
206
-
207
- Returns:
208
- StorageManager instance with registered providers
209
-
210
- Raises:
211
- RuntimeError: If no storage providers were successfully registered
212
- """
213
- # Create storage manager
214
- manager = StorageManager(storage_config)
215
-
216
- # Register providers
217
- for provider_name, provider_config in storage_config.providers.items():
218
- try:
219
- provider_type = provider_config.get("type", provider_name)
220
- provider_instance = create_storage_provider(
221
- provider_type, provider_config.get("config", {})
222
- )
223
- manager.register_provider(provider_name, provider_instance)
224
-
225
- logger.info(f"Registered storage provider: {provider_name} ({provider_type})")
226
-
227
- except Exception as e:
228
- logger.error(f"Failed to register provider {provider_name}: {e}")
229
- # Continue with other providers rather than failing completely
230
- continue
231
-
232
- # Validate default provider is available
233
- if storage_config.default_provider not in manager.providers:
234
- available = list(manager.providers.keys())
235
- if not available:
236
- raise RuntimeError("No storage providers were successfully registered")
237
-
238
- logger.warning(
239
- f"Default provider '{storage_config.default_provider}' not available. "
240
- f"Using '{available[0]}' instead."
241
- )
242
- manager.default_provider = available[0]
243
-
244
- return manager
245
-
246
-
247
- def create_storage_manager() -> StorageManager:
248
- """Create a configured storage manager using global singleton config.
249
-
250
- The storage configuration is loaded once from settings.storage_config_path
251
- and cached for the lifetime of the process.
252
-
253
- Returns:
254
- StorageManager instance with registered providers
255
- """
256
- storage_config = get_storage_config()
257
- return _build_storage_manager_from_config(storage_config)
258
-
259
-
260
- def create_development_storage() -> StorageManager:
261
- """Create a simple storage manager for development use.
262
-
263
- Uses local filesystem storage with sensible defaults.
264
- This is primarily used for testing and creates a standalone manager
265
- rather than using global settings.
266
- """
267
- config = StorageConfig(
268
- default_provider="local",
269
- providers={
270
- "local": {
271
- "type": "local",
272
- "config": {
273
- "base_path": "/tmp/boards/storage",
274
- "public_url_base": "http://localhost:8088/api/storage",
275
- },
276
- }
277
- },
278
- routing_rules=[{"provider": "local"}],
279
- )
280
-
281
- # Create storage manager directly without using global settings
282
- manager = StorageManager(config)
283
-
284
- # Register the local provider
285
- local_provider = create_storage_provider("local", config.providers["local"]["config"])
286
- manager.register_provider("local", local_provider)
287
-
288
- return manager
@@ -1,27 +0,0 @@
1
- """Storage provider implementations."""
2
-
3
- from .local import LocalStorageProvider
4
-
5
- # Optional cloud providers - imported conditionally to avoid import errors
6
- __all__ = ["LocalStorageProvider"]
7
-
8
- try:
9
- from .supabase import SupabaseStorageProvider
10
-
11
- __all__.append("SupabaseStorageProvider")
12
- except ImportError:
13
- pass
14
-
15
- try:
16
- from .s3 import S3StorageProvider
17
-
18
- __all__.append("S3StorageProvider")
19
- except ImportError:
20
- pass
21
-
22
- try:
23
- from .gcs import GCSStorageProvider
24
-
25
- __all__.append("GCSStorageProvider")
26
- except ImportError:
27
- pass
@@ -1,340 +0,0 @@
1
- """Google Cloud Storage provider with IAM auth and CDN support."""
2
-
3
- import json
4
- import os
5
- from collections.abc import AsyncIterator
6
- from datetime import UTC, datetime, timedelta
7
- from pathlib import Path
8
- from typing import TYPE_CHECKING, Any
9
-
10
- if TYPE_CHECKING:
11
- from google.cloud import storage
12
-
13
- try:
14
- import asyncio
15
-
16
- from google.auth import default
17
- from google.auth.exceptions import DefaultCredentialsError
18
- from google.cloud import storage
19
- from google.cloud.exceptions import GoogleCloudError, NotFound
20
-
21
- _gcs_available = True
22
- except ImportError:
23
- storage = None
24
- NotFound = None
25
- GoogleCloudError = None
26
- default = None
27
- DefaultCredentialsError = None
28
- _gcs_available = False
29
-
30
- from ...logging import get_logger
31
- from ..base import StorageException, StorageProvider
32
-
33
- logger = get_logger(__name__)
34
-
35
-
36
- class GCSStorageProvider(StorageProvider):
37
- """Google Cloud Storage with IAM auth, Cloud CDN, and proper async patterns."""
38
-
39
- def __init__(
40
- self,
41
- bucket: str,
42
- project_id: str | None = None,
43
- credentials_path: str | None = None,
44
- credentials_json: str | None = None,
45
- cdn_domain: str | None = None,
46
- upload_config: dict[str, Any] | None = None,
47
- ):
48
- if not _gcs_available:
49
- raise ImportError(
50
- "google-cloud-storage is required for GCSStorageProvider. "
51
- "Install with: pip install google-cloud-storage"
52
- )
53
-
54
- self.bucket_name = bucket
55
- self.project_id = project_id
56
- self.credentials_path = credentials_path
57
- self.credentials_json = credentials_json
58
- self.cdn_domain = cdn_domain
59
-
60
- # Default upload configuration
61
- self.upload_config = {
62
- "cache_control": "public, max-age=3600",
63
- "predefined_acl": None, # Use bucket's default ACL
64
- **(upload_config or {}),
65
- }
66
-
67
- self._client: Any | None = None
68
- self._bucket: Any | None = None
69
-
70
- # Client will be initialized lazily on first use
71
-
72
- def _get_client(self) -> Any:
73
- """Get or create the GCS client with proper authentication."""
74
- if self._client is None:
75
- if storage is None:
76
- raise ImportError("google-cloud-storage is required for GCSStorageProvider")
77
-
78
- try:
79
- if self.credentials_json:
80
- # Use JSON credentials string
81
- credentials_info = json.loads(self.credentials_json)
82
- from google.oauth2 import service_account
83
-
84
- credentials = service_account.Credentials.from_service_account_info(
85
- credentials_info,
86
- scopes=["https://www.googleapis.com/auth/cloud-platform"],
87
- )
88
- self._client = storage.Client(credentials=credentials, project=self.project_id)
89
- elif self.credentials_path:
90
- # Use service account file
91
- credentials_path = Path(self.credentials_path)
92
- if not credentials_path.exists():
93
- raise FileNotFoundError(
94
- f"Credentials file not found: {self.credentials_path}"
95
- )
96
-
97
- os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = str(credentials_path)
98
- self._client = storage.Client(project=self.project_id)
99
- else:
100
- # Use default credentials (environment variables, gcloud, etc.)
101
- self._client = storage.Client(project=self.project_id)
102
-
103
- # Get bucket reference
104
- self._bucket = self._client.bucket(self.bucket_name)
105
-
106
- except Exception as e:
107
- logger.error(f"Failed to initialize GCS client: {e}")
108
- raise StorageException(f"GCS client initialization failed: {e}") from e
109
-
110
- return self._client
111
-
112
- async def _run_sync(self, func, *args, **kwargs) -> Any:
113
- """Run synchronous GCS operations in thread pool."""
114
- loop = asyncio.get_event_loop()
115
- return await loop.run_in_executor(None, func, *args, **kwargs)
116
-
117
- async def upload(
118
- self,
119
- key: str,
120
- content: bytes | AsyncIterator[bytes],
121
- content_type: str,
122
- metadata: dict[str, Any] | None = None,
123
- ) -> str:
124
- """Upload content to GCS."""
125
- try:
126
- # Get client (initializes on first use)
127
- client = self._get_client()
128
- bucket = client.bucket(self.bucket_name)
129
-
130
- # Create blob object
131
- blob = bucket.blob(key)
132
-
133
- # Set content type
134
- blob.content_type = content_type
135
-
136
- # Set cache control and other configuration
137
- if self.upload_config.get("cache_control"):
138
- blob.cache_control = self.upload_config["cache_control"]
139
-
140
- # Add custom metadata
141
- if metadata:
142
- # GCS metadata keys must be lowercase and can contain only letters,
143
- # numbers, and underscores
144
- gcs_metadata = {}
145
- for k, v in metadata.items():
146
- # Convert key to lowercase and replace invalid characters
147
- clean_key = k.lower().replace("-", "_").replace(" ", "_")
148
- gcs_metadata[clean_key] = str(v)
149
- blob.metadata = gcs_metadata
150
-
151
- # Handle streaming content for large files
152
- if isinstance(content, bytes):
153
- file_content = content
154
- else:
155
- # Collect streaming content into memory for upload
156
- # For very large files, consider using resumable uploads
157
- chunks = []
158
- total_size = 0
159
- async for chunk in content:
160
- chunks.append(chunk)
161
- total_size += len(chunk)
162
- # For files larger than 100MB, we could implement resumable upload
163
- if total_size > 100 * 1024 * 1024:
164
- logger.warning(
165
- f"Large file upload ({total_size} bytes) - "
166
- f"consider implementing resumable upload for key: {key}"
167
- )
168
-
169
- file_content = b"".join(chunks)
170
-
171
- # Upload using thread pool to avoid blocking
172
- await self._run_sync(blob.upload_from_string, file_content, content_type=content_type)
173
-
174
- # Return the CDN URL if configured, otherwise public GCS URL
175
- if self.cdn_domain:
176
- return f"https://{self.cdn_domain}/{key}"
177
- else:
178
- return f"https://storage.googleapis.com/{self.bucket_name}/{key}"
179
-
180
- except Exception as e:
181
- if isinstance(e, StorageException):
182
- raise
183
- logger.error(f"Unexpected error uploading {key} to GCS: {e}")
184
- raise StorageException(f"GCS upload failed: {e}") from e
185
-
186
- async def download(self, key: str) -> bytes:
187
- """Download file content from GCS."""
188
- try:
189
- # Get client (initializes on first use)
190
- client = self._get_client()
191
- bucket = client.bucket(self.bucket_name)
192
-
193
- blob = bucket.blob(key)
194
-
195
- # Download using thread pool to avoid blocking
196
- content = await self._run_sync(blob.download_as_bytes)
197
- return content
198
-
199
- except Exception as e:
200
- if isinstance(e, StorageException):
201
- raise
202
- logger.error(f"Failed to download {key} from GCS: {e}")
203
- raise StorageException(f"GCS download failed: {e}") from e
204
-
205
- async def get_presigned_upload_url(
206
- self,
207
- key: str,
208
- content_type: str,
209
- expires_in: timedelta | None = None,
210
- ) -> dict[str, Any]:
211
- """Generate presigned URL for direct client uploads."""
212
- if expires_in is None:
213
- expires_in = timedelta(hours=1)
214
-
215
- try:
216
- # Get client (initializes on first use)
217
- client = self._get_client()
218
- bucket = client.bucket(self.bucket_name)
219
-
220
- blob = bucket.blob(key)
221
-
222
- # Generate signed URL for PUT operations
223
- url = await self._run_sync(
224
- blob.generate_signed_url,
225
- version="v4",
226
- expiration=expires_in,
227
- method="PUT",
228
- content_type=content_type,
229
- headers={"Content-Type": content_type},
230
- )
231
-
232
- return {
233
- "url": url,
234
- "method": "PUT",
235
- "headers": {"Content-Type": content_type},
236
- "expires_at": (datetime.now(UTC) + expires_in).isoformat(),
237
- }
238
-
239
- except Exception as e:
240
- if isinstance(e, StorageException):
241
- raise
242
- logger.error(f"Failed to create presigned upload URL for {key}: {e}")
243
- raise StorageException(f"GCS presigned URL creation failed: {e}") from e
244
-
245
- async def get_presigned_download_url(
246
- self, key: str, expires_in: timedelta | None = None
247
- ) -> str:
248
- """Generate presigned URL for secure downloads."""
249
- if expires_in is None:
250
- expires_in = timedelta(hours=1)
251
-
252
- try:
253
- # Always use GCS native signed URLs for security
254
- # Get client (initializes on first use)
255
- client = self._get_client()
256
- bucket = client.bucket(self.bucket_name)
257
-
258
- blob = bucket.blob(key)
259
-
260
- # Generate signed URL for GET operations
261
- url = await self._run_sync(
262
- blob.generate_signed_url,
263
- version="v4",
264
- expiration=expires_in,
265
- method="GET",
266
- )
267
-
268
- return url
269
-
270
- except Exception as e:
271
- if isinstance(e, StorageException):
272
- raise
273
- logger.error(f"Failed to create presigned download URL for {key}: {e}")
274
- raise StorageException(f"GCS presigned download URL creation failed: {e}") from e
275
-
276
- async def delete(self, key: str) -> bool:
277
- """Delete file by storage key."""
278
- try:
279
- # Get client (initializes on first use)
280
- client = self._get_client()
281
- bucket = client.bucket(self.bucket_name)
282
-
283
- blob = bucket.blob(key)
284
- await self._run_sync(blob.delete)
285
- return True
286
-
287
- except Exception as e:
288
- logger.error(f"Unexpected error deleting {key} from GCS: {e}")
289
- raise StorageException(f"GCS delete failed: {e}") from e
290
-
291
- async def exists(self, key: str) -> bool:
292
- """Check if file exists."""
293
- try:
294
- # Get client (initializes on first use)
295
- client = self._get_client()
296
- bucket = client.bucket(self.bucket_name)
297
-
298
- blob = bucket.blob(key)
299
- exists = await self._run_sync(blob.exists)
300
- return exists
301
-
302
- except Exception:
303
- return False
304
-
305
- async def get_metadata(self, key: str) -> dict[str, Any]:
306
- """Get file metadata (size, modified date, etc.)."""
307
- try:
308
- # Get client (initializes on first use)
309
- client = self._get_client()
310
- bucket = client.bucket(self.bucket_name)
311
-
312
- blob = bucket.blob(key)
313
-
314
- # Reload blob to get latest metadata
315
- await self._run_sync(blob.reload)
316
-
317
- result = {
318
- "size": blob.size or 0,
319
- "last_modified": blob.updated,
320
- "content_type": blob.content_type,
321
- "etag": blob.etag,
322
- "generation": blob.generation,
323
- "storage_class": blob.storage_class,
324
- "cache_control": blob.cache_control,
325
- "content_encoding": blob.content_encoding,
326
- "content_disposition": blob.content_disposition,
327
- "content_language": blob.content_language,
328
- }
329
-
330
- # Add custom metadata
331
- if blob.metadata:
332
- result["custom_metadata"] = blob.metadata
333
-
334
- return result
335
-
336
- except Exception as e:
337
- if isinstance(e, StorageException):
338
- raise
339
- logger.error(f"Failed to get metadata for {key} from GCS: {e}")
340
- raise StorageException(f"GCS get metadata failed: {e}") from e