svc-infra 0.1.600__py3-none-any.whl → 0.1.664__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.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

Files changed (140) hide show
  1. svc_infra/api/fastapi/admin/__init__.py +3 -0
  2. svc_infra/api/fastapi/admin/add.py +231 -0
  3. svc_infra/api/fastapi/apf_payments/setup.py +0 -2
  4. svc_infra/api/fastapi/auth/add.py +0 -4
  5. svc_infra/api/fastapi/auth/routers/oauth_router.py +19 -4
  6. svc_infra/api/fastapi/billing/router.py +64 -0
  7. svc_infra/api/fastapi/billing/setup.py +19 -0
  8. svc_infra/api/fastapi/cache/add.py +9 -5
  9. svc_infra/api/fastapi/db/nosql/mongo/add.py +33 -27
  10. svc_infra/api/fastapi/db/sql/add.py +40 -18
  11. svc_infra/api/fastapi/db/sql/crud_router.py +176 -14
  12. svc_infra/api/fastapi/db/sql/session.py +16 -0
  13. svc_infra/api/fastapi/dependencies/ratelimit.py +57 -7
  14. svc_infra/api/fastapi/docs/add.py +160 -0
  15. svc_infra/api/fastapi/docs/landing.py +1 -1
  16. svc_infra/api/fastapi/docs/scoped.py +41 -6
  17. svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
  18. svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
  19. svc_infra/api/fastapi/middleware/ratelimit.py +59 -1
  20. svc_infra/api/fastapi/middleware/ratelimit_store.py +12 -6
  21. svc_infra/api/fastapi/middleware/timeout.py +148 -0
  22. svc_infra/api/fastapi/openapi/mutators.py +114 -0
  23. svc_infra/api/fastapi/ops/add.py +73 -0
  24. svc_infra/api/fastapi/pagination.py +3 -1
  25. svc_infra/api/fastapi/routers/ping.py +1 -0
  26. svc_infra/api/fastapi/setup.py +21 -13
  27. svc_infra/api/fastapi/tenancy/add.py +19 -0
  28. svc_infra/api/fastapi/tenancy/context.py +112 -0
  29. svc_infra/api/fastapi/versioned.py +101 -0
  30. svc_infra/app/README.md +5 -5
  31. svc_infra/billing/__init__.py +23 -0
  32. svc_infra/billing/async_service.py +147 -0
  33. svc_infra/billing/jobs.py +230 -0
  34. svc_infra/billing/models.py +131 -0
  35. svc_infra/billing/quotas.py +101 -0
  36. svc_infra/billing/schemas.py +33 -0
  37. svc_infra/billing/service.py +115 -0
  38. svc_infra/bundled_docs/README.md +5 -0
  39. svc_infra/bundled_docs/__init__.py +1 -0
  40. svc_infra/bundled_docs/getting-started.md +6 -0
  41. svc_infra/cache/__init__.py +4 -0
  42. svc_infra/cache/add.py +158 -0
  43. svc_infra/cache/backend.py +5 -2
  44. svc_infra/cache/decorators.py +19 -1
  45. svc_infra/cache/keys.py +24 -4
  46. svc_infra/cli/__init__.py +28 -8
  47. svc_infra/cli/cmds/__init__.py +8 -0
  48. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
  49. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
  50. svc_infra/cli/cmds/db/sql/alembic_cmds.py +80 -11
  51. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
  52. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
  53. svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
  54. svc_infra/cli/cmds/dx/__init__.py +12 -0
  55. svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
  56. svc_infra/cli/cmds/help.py +4 -0
  57. svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
  58. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  59. svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
  60. svc_infra/data/add.py +61 -0
  61. svc_infra/data/backup.py +53 -0
  62. svc_infra/data/erasure.py +45 -0
  63. svc_infra/data/fixtures.py +40 -0
  64. svc_infra/data/retention.py +55 -0
  65. svc_infra/db/nosql/mongo/README.md +13 -13
  66. svc_infra/db/sql/repository.py +51 -11
  67. svc_infra/db/sql/resource.py +5 -0
  68. svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
  69. svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
  70. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
  71. svc_infra/db/sql/tenant.py +79 -0
  72. svc_infra/db/sql/utils.py +18 -4
  73. svc_infra/docs/acceptance-matrix.md +88 -0
  74. svc_infra/docs/acceptance.md +44 -0
  75. svc_infra/docs/admin.md +425 -0
  76. svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
  77. svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
  78. svc_infra/docs/adr/0004-tenancy-model.md +42 -0
  79. svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
  80. svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
  81. svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
  82. svc_infra/docs/adr/0008-billing-primitives.md +143 -0
  83. svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
  84. svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
  85. svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
  86. svc_infra/docs/adr/0012-generic-file-storage.md +498 -0
  87. svc_infra/docs/api.md +186 -0
  88. svc_infra/docs/auth.md +11 -0
  89. svc_infra/docs/billing.md +190 -0
  90. svc_infra/docs/cache.md +76 -0
  91. svc_infra/docs/cli.md +74 -0
  92. svc_infra/docs/contributing.md +34 -0
  93. svc_infra/docs/data-lifecycle.md +52 -0
  94. svc_infra/docs/database.md +14 -0
  95. svc_infra/docs/docs-and-sdks.md +62 -0
  96. svc_infra/docs/environment.md +114 -0
  97. svc_infra/docs/getting-started.md +63 -0
  98. svc_infra/docs/idempotency.md +111 -0
  99. svc_infra/docs/jobs.md +67 -0
  100. svc_infra/docs/observability.md +16 -0
  101. svc_infra/docs/ops.md +37 -0
  102. svc_infra/docs/rate-limiting.md +125 -0
  103. svc_infra/docs/repo-review.md +48 -0
  104. svc_infra/docs/security.md +176 -0
  105. svc_infra/docs/storage.md +982 -0
  106. svc_infra/docs/tenancy.md +35 -0
  107. svc_infra/docs/timeouts-and-resource-limits.md +147 -0
  108. svc_infra/docs/versioned-integrations.md +146 -0
  109. svc_infra/docs/webhooks.md +112 -0
  110. svc_infra/dx/add.py +63 -0
  111. svc_infra/dx/changelog.py +74 -0
  112. svc_infra/dx/checks.py +67 -0
  113. svc_infra/http/__init__.py +13 -0
  114. svc_infra/http/client.py +72 -0
  115. svc_infra/jobs/builtins/webhook_delivery.py +14 -2
  116. svc_infra/jobs/queue.py +9 -1
  117. svc_infra/jobs/runner.py +75 -0
  118. svc_infra/jobs/worker.py +17 -1
  119. svc_infra/mcp/svc_infra_mcp.py +85 -28
  120. svc_infra/obs/add.py +54 -7
  121. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  122. svc_infra/security/headers.py +15 -2
  123. svc_infra/security/hibp.py +6 -2
  124. svc_infra/security/models.py +27 -7
  125. svc_infra/security/oauth_models.py +59 -0
  126. svc_infra/security/permissions.py +1 -0
  127. svc_infra/storage/__init__.py +93 -0
  128. svc_infra/storage/add.py +250 -0
  129. svc_infra/storage/backends/__init__.py +11 -0
  130. svc_infra/storage/backends/local.py +331 -0
  131. svc_infra/storage/backends/memory.py +214 -0
  132. svc_infra/storage/backends/s3.py +329 -0
  133. svc_infra/storage/base.py +239 -0
  134. svc_infra/storage/easy.py +182 -0
  135. svc_infra/storage/settings.py +192 -0
  136. svc_infra/webhooks/service.py +10 -2
  137. {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/METADATA +45 -14
  138. {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/RECORD +140 -52
  139. {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/WHEEL +0 -0
  140. {svc_infra-0.1.600.dist-info → svc_infra-0.1.664.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,59 @@
1
+ """
2
+ OAuth provider account models (opt-in).
3
+
4
+ These models are only registered when a project explicitly enables OAuth.
5
+ Import this module only when enable_oauth=True is passed to add_auth_users.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import uuid
11
+ from datetime import datetime, timezone
12
+ from typing import TYPE_CHECKING, Optional
13
+
14
+ from sqlalchemy import JSON, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, text
15
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
16
+
17
+ from svc_infra.db.sql.base import ModelBase
18
+ from svc_infra.db.sql.types import GUID
19
+
20
+ if TYPE_CHECKING:
21
+ from svc_infra.security.models import User
22
+
23
+
24
+ class ProviderAccount(ModelBase):
25
+ """OAuth provider account linking (Google, GitHub, etc.)."""
26
+
27
+ __tablename__ = "provider_accounts"
28
+
29
+ id: Mapped[uuid.UUID] = mapped_column(GUID(), primary_key=True, default=uuid.uuid4)
30
+ user_id: Mapped[uuid.UUID] = mapped_column(
31
+ GUID(), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False
32
+ )
33
+ provider: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
34
+ provider_account_id: Mapped[str] = mapped_column(String(255), nullable=False)
35
+ access_token: Mapped[Optional[str]] = mapped_column(Text)
36
+ refresh_token: Mapped[Optional[str]] = mapped_column(Text)
37
+ expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
38
+ raw_claims: Mapped[Optional[dict]] = mapped_column(JSON)
39
+
40
+ # Bidirectional relationship to User model
41
+ user: Mapped["User"] = relationship(back_populates="provider_accounts")
42
+
43
+ created_at = mapped_column(
44
+ DateTime(timezone=True), server_default=text("CURRENT_TIMESTAMP"), nullable=False
45
+ )
46
+ updated_at = mapped_column(
47
+ DateTime(timezone=True),
48
+ server_default=text("CURRENT_TIMESTAMP"),
49
+ onupdate=lambda: datetime.now(timezone.utc),
50
+ nullable=False,
51
+ )
52
+
53
+ __table_args__ = (
54
+ UniqueConstraint("provider", "provider_account_id", name="uq_provider_account"),
55
+ Index("ix_provider_accounts_user_provider", "user_id", "provider"),
56
+ )
57
+
58
+
59
+ __all__ = ["ProviderAccount"]
@@ -16,6 +16,7 @@ PERMISSION_REGISTRY: Dict[str, Set[str]] = {
16
16
  "billing.write",
17
17
  "security.session.revoke",
18
18
  "security.session.list",
19
+ "admin.impersonate",
19
20
  },
20
21
  "support": {"user.read", "billing.read"},
21
22
  "auditor": {"user.read", "billing.read", "audit.read"},
@@ -0,0 +1,93 @@
1
+ """
2
+ Generic file storage system for svc-infra.
3
+
4
+ Provides backend-agnostic file storage with support for multiple providers:
5
+ - Local filesystem (Railway volumes, Render, development)
6
+ - S3-compatible (AWS S3, DigitalOcean Spaces, Wasabi, Backblaze B2, Minio)
7
+ - Google Cloud Storage (coming soon)
8
+ - Cloudinary (coming soon)
9
+ - In-memory (testing)
10
+
11
+ Quick Start:
12
+ >>> from svc_infra.storage import add_storage, easy_storage
13
+ >>> from fastapi import FastAPI
14
+ >>>
15
+ >>> app = FastAPI()
16
+ >>>
17
+ >>> # Auto-detect backend from environment
18
+ >>> storage = add_storage(app)
19
+ >>>
20
+ >>> # Or explicit backend
21
+ >>> backend = easy_storage(backend="s3", bucket="my-uploads")
22
+ >>> storage = add_storage(app, backend)
23
+
24
+ Usage in Routes:
25
+ >>> from svc_infra.storage import get_storage, StorageBackend
26
+ >>> from fastapi import Depends, UploadFile
27
+ >>>
28
+ >>> @router.post("/upload")
29
+ >>> async def upload_file(
30
+ ... file: UploadFile,
31
+ ... storage: StorageBackend = Depends(get_storage),
32
+ ... ):
33
+ ... content = await file.read()
34
+ ... url = await storage.put(
35
+ ... key=f"uploads/{file.filename}",
36
+ ... data=content,
37
+ ... content_type=file.content_type or "application/octet-stream",
38
+ ... metadata={"user_id": "user_123"}
39
+ ... )
40
+ ... return {"url": url}
41
+
42
+ Environment Variables:
43
+ STORAGE_BACKEND: Backend type (local, s3, gcs, cloudinary, memory)
44
+
45
+ Local:
46
+ STORAGE_BASE_PATH: Directory for files (default: /data/uploads)
47
+ STORAGE_BASE_URL: URL for file serving (default: http://localhost:8000/files)
48
+
49
+ S3:
50
+ STORAGE_S3_BUCKET: Bucket name (required)
51
+ STORAGE_S3_REGION: AWS region (default: us-east-1)
52
+ STORAGE_S3_ENDPOINT: Custom endpoint for S3-compatible services
53
+ STORAGE_S3_ACCESS_KEY: Access key (falls back to AWS_ACCESS_KEY_ID)
54
+ STORAGE_S3_SECRET_KEY: Secret key (falls back to AWS_SECRET_ACCESS_KEY)
55
+
56
+ See Also:
57
+ - ADR-0012: Generic File Storage System design
58
+ - docs/storage.md: Comprehensive storage guide
59
+ """
60
+
61
+ from .add import add_storage, get_storage, health_check_storage
62
+ from .backends import LocalBackend, MemoryBackend, S3Backend
63
+ from .base import (
64
+ FileNotFoundError,
65
+ InvalidKeyError,
66
+ PermissionDeniedError,
67
+ QuotaExceededError,
68
+ StorageBackend,
69
+ StorageError,
70
+ )
71
+ from .easy import easy_storage
72
+ from .settings import StorageSettings
73
+
74
+ __all__ = [
75
+ # Main API
76
+ "add_storage",
77
+ "easy_storage",
78
+ "get_storage",
79
+ "health_check_storage",
80
+ # Base types
81
+ "StorageBackend",
82
+ "StorageSettings",
83
+ # Backends
84
+ "LocalBackend",
85
+ "MemoryBackend",
86
+ "S3Backend",
87
+ # Exceptions
88
+ "StorageError",
89
+ "FileNotFoundError",
90
+ "PermissionDeniedError",
91
+ "QuotaExceededError",
92
+ "InvalidKeyError",
93
+ ]
@@ -0,0 +1,250 @@
1
+ """
2
+ FastAPI integration for storage system.
3
+
4
+ Provides helpers to integrate storage backends with FastAPI applications.
5
+ """
6
+
7
+ import logging
8
+ from contextlib import asynccontextmanager
9
+ from typing import Optional
10
+
11
+ from fastapi import FastAPI, HTTPException, Query, Request
12
+ from fastapi.responses import StreamingResponse
13
+
14
+ from .base import FileNotFoundError, PermissionDeniedError, StorageBackend, StorageError
15
+ from .easy import easy_storage
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def add_storage(
21
+ app: FastAPI,
22
+ backend: Optional[StorageBackend] = None,
23
+ serve_files: bool = False,
24
+ file_route_prefix: str = "/files",
25
+ ) -> StorageBackend:
26
+ """
27
+ Add storage backend to FastAPI application.
28
+
29
+ This function:
30
+ - Stores backend in app.state.storage
31
+ - Registers startup/shutdown hooks
32
+ - Optionally mounts file serving route
33
+ - Adds health check integration
34
+
35
+ Args:
36
+ app: FastAPI application instance
37
+ backend: Storage backend instance (auto-detected if None)
38
+ serve_files: If True, mount route to serve files (LocalBackend only)
39
+ file_route_prefix: URL prefix for file serving (default: "/files")
40
+
41
+ Returns:
42
+ Storage backend instance
43
+
44
+ Example:
45
+ >>> from fastapi import FastAPI
46
+ >>> from svc_infra.storage import add_storage, easy_storage
47
+ >>>
48
+ >>> app = FastAPI()
49
+ >>>
50
+ >>> # Auto-detect backend
51
+ >>> storage = add_storage(app)
52
+ >>>
53
+ >>> # Explicit backend
54
+ >>> backend = easy_storage(backend="s3", bucket="my-uploads")
55
+ >>> storage = add_storage(app, backend)
56
+ >>>
57
+ >>> # With file serving (LocalBackend only)
58
+ >>> backend = easy_storage(backend="local")
59
+ >>> storage = add_storage(app, backend, serve_files=True)
60
+
61
+ Note:
62
+ File serving is only supported for LocalBackend. For S3/GCS,
63
+ use presigned URLs instead.
64
+ """
65
+ # Auto-detect backend if not provided
66
+ if backend is None:
67
+ backend = easy_storage()
68
+
69
+ # Store in app state
70
+ app.state.storage = backend
71
+
72
+ # Get existing lifespan or create new one
73
+ existing_lifespan = getattr(app.router, "lifespan_context", None)
74
+
75
+ @asynccontextmanager
76
+ async def storage_lifespan(app: FastAPI):
77
+ # Startup
78
+ logger.info(f"Storage backend initialized: {backend.__class__.__name__}")
79
+
80
+ # Test connection for S3 backend
81
+ if hasattr(backend, "bucket"):
82
+ try:
83
+ # Try to list keys (limit 1 to minimize cost)
84
+ await backend.list_keys(limit=1)
85
+ logger.info(f"Successfully connected to storage: {backend.bucket}")
86
+ except Exception as e:
87
+ logger.error(f"Failed to connect to storage: {e}")
88
+ # Don't fail startup, let health check catch it
89
+
90
+ # Call existing lifespan if present
91
+ if existing_lifespan is not None:
92
+ async with existing_lifespan(app):
93
+ yield
94
+ else:
95
+ yield
96
+
97
+ # Shutdown
98
+ logger.info("Storage backend shutdown")
99
+
100
+ # Replace lifespan
101
+ app.router.lifespan_context = storage_lifespan
102
+
103
+ # Mount file serving route if requested (LocalBackend only)
104
+ if serve_files:
105
+ from .backends.local import LocalBackend
106
+
107
+ if not isinstance(backend, LocalBackend):
108
+ logger.warning(
109
+ f"File serving only supported for LocalBackend, "
110
+ f"got {backend.__class__.__name__}. Skipping route mount."
111
+ )
112
+ else:
113
+ # Create file serving route
114
+ @app.get(f"{file_route_prefix}/{{key:path}}")
115
+ async def serve_file(
116
+ key: str,
117
+ expires: str = Query(..., description="Expiration timestamp"),
118
+ signature: str = Query(..., description="HMAC signature"),
119
+ download: bool = Query(False, description="Force download"),
120
+ ):
121
+ """
122
+ Serve files from local storage with signature validation.
123
+
124
+ Requires valid signature generated by LocalBackend.get_url().
125
+ """
126
+ # Verify signature
127
+ if not backend.verify_url(key, expires, signature, download):
128
+ raise HTTPException(
129
+ status_code=403,
130
+ detail="Invalid or expired signature",
131
+ )
132
+
133
+ # Get file
134
+ try:
135
+ data = await backend.get(key)
136
+ metadata = await backend.get_metadata(key)
137
+
138
+ # Determine content disposition
139
+ if download:
140
+ filename = key.split("/")[-1]
141
+ content_disposition = f'attachment; filename="{filename}"'
142
+ else:
143
+ content_disposition = "inline"
144
+
145
+ # Return file
146
+ return StreamingResponse(
147
+ iter([data]),
148
+ media_type=metadata.get("content_type", "application/octet-stream"),
149
+ headers={
150
+ "Content-Disposition": content_disposition,
151
+ "Content-Length": str(len(data)),
152
+ },
153
+ )
154
+
155
+ except FileNotFoundError:
156
+ raise HTTPException(status_code=404, detail="File not found")
157
+ except PermissionDeniedError:
158
+ raise HTTPException(status_code=403, detail="Permission denied")
159
+ except StorageError as e:
160
+ logger.error(f"Storage error serving file {key}: {e}")
161
+ raise HTTPException(status_code=500, detail="Storage error")
162
+
163
+ logger.info(f"File serving enabled at {file_route_prefix}")
164
+
165
+ return backend
166
+
167
+
168
+ def get_storage(request: Request) -> StorageBackend:
169
+ """
170
+ FastAPI dependency to inject storage backend.
171
+
172
+ Use this in route handlers to access the storage backend.
173
+
174
+ Example:
175
+ >>> from fastapi import APIRouter, Depends, UploadFile
176
+ >>> from svc_infra.storage import get_storage, StorageBackend
177
+ >>>
178
+ >>> router = APIRouter()
179
+ >>>
180
+ >>> @router.post("/upload")
181
+ >>> async def upload_file(
182
+ ... file: UploadFile,
183
+ ... storage: StorageBackend = Depends(get_storage),
184
+ ... ):
185
+ ... content = await file.read()
186
+ ... url = await storage.put(
187
+ ... key=f"uploads/{file.filename}",
188
+ ... data=content,
189
+ ... content_type=file.content_type or "application/octet-stream"
190
+ ... )
191
+ ... return {"url": url}
192
+
193
+ Raises:
194
+ RuntimeError: If storage not initialized with add_storage()
195
+ """
196
+ if not hasattr(request.app.state, "storage"):
197
+ raise RuntimeError(
198
+ "Storage not initialized. " "Call add_storage(app) during application setup."
199
+ )
200
+
201
+ return request.app.state.storage
202
+
203
+
204
+ async def health_check_storage(request: Request) -> dict:
205
+ """
206
+ Health check for storage backend.
207
+
208
+ Returns storage status and basic statistics.
209
+
210
+ Example:
211
+ >>> from fastapi import FastAPI
212
+ >>> from svc_infra.storage import add_storage, health_check_storage
213
+ >>>
214
+ >>> app = FastAPI()
215
+ >>> add_storage(app)
216
+ >>>
217
+ >>> @app.get("/_health/storage")
218
+ >>> async def storage_health(request: Request):
219
+ ... return await health_check_storage(request)
220
+
221
+ Returns:
222
+ Dict with status and backend information
223
+ """
224
+ try:
225
+ storage = get_storage(request)
226
+
227
+ # Get backend type
228
+ backend_type = storage.__class__.__name__.replace("Backend", "").lower()
229
+
230
+ # Try a simple operation
231
+ await storage.list_keys(limit=1)
232
+
233
+ return {
234
+ "status": "healthy",
235
+ "backend": backend_type,
236
+ }
237
+
238
+ except Exception as e:
239
+ logger.error(f"Storage health check failed: {e}")
240
+ return {
241
+ "status": "unhealthy",
242
+ "error": str(e),
243
+ }
244
+
245
+
246
+ __all__ = [
247
+ "add_storage",
248
+ "get_storage",
249
+ "health_check_storage",
250
+ ]
@@ -0,0 +1,11 @@
1
+ """Storage backend implementations."""
2
+
3
+ from .local import LocalBackend
4
+ from .memory import MemoryBackend
5
+ from .s3 import S3Backend
6
+
7
+ __all__ = [
8
+ "LocalBackend",
9
+ "MemoryBackend",
10
+ "S3Backend",
11
+ ]