svc-infra 0.1.595__py3-none-any.whl → 1.1.0__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 (274) hide show
  1. svc_infra/__init__.py +58 -2
  2. svc_infra/apf_payments/models.py +68 -38
  3. svc_infra/apf_payments/provider/__init__.py +2 -2
  4. svc_infra/apf_payments/provider/aiydan.py +39 -23
  5. svc_infra/apf_payments/provider/base.py +8 -3
  6. svc_infra/apf_payments/provider/registry.py +3 -5
  7. svc_infra/apf_payments/provider/stripe.py +74 -52
  8. svc_infra/apf_payments/schemas.py +84 -83
  9. svc_infra/apf_payments/service.py +27 -16
  10. svc_infra/apf_payments/settings.py +12 -11
  11. svc_infra/api/__init__.py +61 -0
  12. svc_infra/api/fastapi/__init__.py +34 -0
  13. svc_infra/api/fastapi/admin/__init__.py +3 -0
  14. svc_infra/api/fastapi/admin/add.py +240 -0
  15. svc_infra/api/fastapi/apf_payments/router.py +94 -73
  16. svc_infra/api/fastapi/apf_payments/setup.py +10 -9
  17. svc_infra/api/fastapi/auth/__init__.py +65 -0
  18. svc_infra/api/fastapi/auth/_cookies.py +1 -3
  19. svc_infra/api/fastapi/auth/add.py +14 -15
  20. svc_infra/api/fastapi/auth/gaurd.py +32 -20
  21. svc_infra/api/fastapi/auth/mfa/models.py +3 -4
  22. svc_infra/api/fastapi/auth/mfa/pre_auth.py +13 -9
  23. svc_infra/api/fastapi/auth/mfa/router.py +9 -8
  24. svc_infra/api/fastapi/auth/mfa/security.py +4 -7
  25. svc_infra/api/fastapi/auth/mfa/utils.py +5 -3
  26. svc_infra/api/fastapi/auth/policy.py +0 -1
  27. svc_infra/api/fastapi/auth/providers.py +3 -3
  28. svc_infra/api/fastapi/auth/routers/apikey_router.py +19 -21
  29. svc_infra/api/fastapi/auth/routers/oauth_router.py +98 -52
  30. svc_infra/api/fastapi/auth/routers/session_router.py +6 -5
  31. svc_infra/api/fastapi/auth/security.py +25 -15
  32. svc_infra/api/fastapi/auth/sender.py +5 -0
  33. svc_infra/api/fastapi/auth/settings.py +18 -19
  34. svc_infra/api/fastapi/auth/state.py +5 -4
  35. svc_infra/api/fastapi/auth/ws_security.py +275 -0
  36. svc_infra/api/fastapi/billing/router.py +71 -0
  37. svc_infra/api/fastapi/billing/setup.py +19 -0
  38. svc_infra/api/fastapi/cache/add.py +9 -5
  39. svc_infra/api/fastapi/db/__init__.py +5 -1
  40. svc_infra/api/fastapi/db/http.py +10 -9
  41. svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
  42. svc_infra/api/fastapi/db/nosql/mongo/add.py +35 -30
  43. svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +39 -21
  44. svc_infra/api/fastapi/db/sql/__init__.py +5 -1
  45. svc_infra/api/fastapi/db/sql/add.py +62 -25
  46. svc_infra/api/fastapi/db/sql/crud_router.py +205 -30
  47. svc_infra/api/fastapi/db/sql/session.py +19 -2
  48. svc_infra/api/fastapi/db/sql/users.py +18 -9
  49. svc_infra/api/fastapi/dependencies/ratelimit.py +76 -14
  50. svc_infra/api/fastapi/docs/add.py +163 -0
  51. svc_infra/api/fastapi/docs/landing.py +6 -6
  52. svc_infra/api/fastapi/docs/scoped.py +75 -36
  53. svc_infra/api/fastapi/dual/__init__.py +12 -2
  54. svc_infra/api/fastapi/dual/dualize.py +2 -2
  55. svc_infra/api/fastapi/dual/protected.py +123 -10
  56. svc_infra/api/fastapi/dual/public.py +25 -0
  57. svc_infra/api/fastapi/dual/router.py +18 -8
  58. svc_infra/api/fastapi/dx.py +33 -2
  59. svc_infra/api/fastapi/ease.py +59 -7
  60. svc_infra/api/fastapi/http/concurrency.py +2 -1
  61. svc_infra/api/fastapi/http/conditional.py +2 -2
  62. svc_infra/api/fastapi/middleware/debug.py +4 -1
  63. svc_infra/api/fastapi/middleware/errors/exceptions.py +2 -5
  64. svc_infra/api/fastapi/middleware/errors/handlers.py +50 -10
  65. svc_infra/api/fastapi/middleware/graceful_shutdown.py +95 -0
  66. svc_infra/api/fastapi/middleware/idempotency.py +190 -68
  67. svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
  68. svc_infra/api/fastapi/middleware/optimistic_lock.py +39 -0
  69. svc_infra/api/fastapi/middleware/ratelimit.py +125 -28
  70. svc_infra/api/fastapi/middleware/ratelimit_store.py +45 -13
  71. svc_infra/api/fastapi/middleware/request_id.py +24 -10
  72. svc_infra/api/fastapi/middleware/request_size_limit.py +3 -3
  73. svc_infra/api/fastapi/middleware/timeout.py +176 -0
  74. svc_infra/api/fastapi/object_router.py +1060 -0
  75. svc_infra/api/fastapi/openapi/apply.py +4 -3
  76. svc_infra/api/fastapi/openapi/conventions.py +13 -6
  77. svc_infra/api/fastapi/openapi/mutators.py +144 -17
  78. svc_infra/api/fastapi/openapi/pipeline.py +2 -2
  79. svc_infra/api/fastapi/openapi/responses.py +4 -6
  80. svc_infra/api/fastapi/openapi/security.py +1 -1
  81. svc_infra/api/fastapi/ops/add.py +73 -0
  82. svc_infra/api/fastapi/pagination.py +47 -32
  83. svc_infra/api/fastapi/routers/__init__.py +16 -10
  84. svc_infra/api/fastapi/routers/ping.py +1 -0
  85. svc_infra/api/fastapi/setup.py +167 -54
  86. svc_infra/api/fastapi/tenancy/add.py +20 -0
  87. svc_infra/api/fastapi/tenancy/context.py +113 -0
  88. svc_infra/api/fastapi/versioned.py +102 -0
  89. svc_infra/app/README.md +5 -5
  90. svc_infra/app/__init__.py +3 -1
  91. svc_infra/app/env.py +70 -4
  92. svc_infra/app/logging/add.py +10 -2
  93. svc_infra/app/logging/filter.py +1 -1
  94. svc_infra/app/logging/formats.py +13 -5
  95. svc_infra/app/root.py +3 -3
  96. svc_infra/billing/__init__.py +40 -0
  97. svc_infra/billing/async_service.py +167 -0
  98. svc_infra/billing/jobs.py +231 -0
  99. svc_infra/billing/models.py +146 -0
  100. svc_infra/billing/quotas.py +101 -0
  101. svc_infra/billing/schemas.py +34 -0
  102. svc_infra/bundled_docs/README.md +5 -0
  103. svc_infra/bundled_docs/__init__.py +1 -0
  104. svc_infra/bundled_docs/getting-started.md +6 -0
  105. svc_infra/cache/__init__.py +21 -5
  106. svc_infra/cache/add.py +167 -0
  107. svc_infra/cache/backend.py +9 -7
  108. svc_infra/cache/decorators.py +75 -20
  109. svc_infra/cache/demo.py +2 -2
  110. svc_infra/cache/keys.py +26 -6
  111. svc_infra/cache/recache.py +26 -27
  112. svc_infra/cache/resources.py +6 -5
  113. svc_infra/cache/tags.py +19 -44
  114. svc_infra/cache/ttl.py +2 -3
  115. svc_infra/cache/utils.py +4 -3
  116. svc_infra/cli/__init__.py +44 -8
  117. svc_infra/cli/__main__.py +4 -0
  118. svc_infra/cli/cmds/__init__.py +39 -2
  119. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +18 -14
  120. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +9 -10
  121. svc_infra/cli/cmds/db/ops_cmds.py +267 -0
  122. svc_infra/cli/cmds/db/sql/alembic_cmds.py +97 -29
  123. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
  124. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +13 -13
  125. svc_infra/cli/cmds/docs/docs_cmds.py +139 -0
  126. svc_infra/cli/cmds/dx/__init__.py +12 -0
  127. svc_infra/cli/cmds/dx/dx_cmds.py +110 -0
  128. svc_infra/cli/cmds/health/__init__.py +179 -0
  129. svc_infra/cli/cmds/health/health_cmds.py +8 -0
  130. svc_infra/cli/cmds/help.py +4 -0
  131. svc_infra/cli/cmds/jobs/__init__.py +1 -0
  132. svc_infra/cli/cmds/jobs/jobs_cmds.py +42 -0
  133. svc_infra/cli/cmds/obs/obs_cmds.py +31 -13
  134. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  135. svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
  136. svc_infra/cli/foundation/runner.py +4 -5
  137. svc_infra/cli/foundation/typer_bootstrap.py +1 -2
  138. svc_infra/data/__init__.py +83 -0
  139. svc_infra/data/add.py +61 -0
  140. svc_infra/data/backup.py +56 -0
  141. svc_infra/data/erasure.py +46 -0
  142. svc_infra/data/fixtures.py +42 -0
  143. svc_infra/data/retention.py +56 -0
  144. svc_infra/db/__init__.py +15 -0
  145. svc_infra/db/crud_schema.py +14 -13
  146. svc_infra/db/inbox.py +67 -0
  147. svc_infra/db/nosql/__init__.py +2 -0
  148. svc_infra/db/nosql/constants.py +1 -1
  149. svc_infra/db/nosql/core.py +19 -5
  150. svc_infra/db/nosql/indexes.py +12 -9
  151. svc_infra/db/nosql/management.py +4 -4
  152. svc_infra/db/nosql/mongo/README.md +13 -13
  153. svc_infra/db/nosql/mongo/client.py +21 -4
  154. svc_infra/db/nosql/mongo/settings.py +1 -1
  155. svc_infra/db/nosql/repository.py +46 -27
  156. svc_infra/db/nosql/resource.py +28 -16
  157. svc_infra/db/nosql/scaffold.py +14 -12
  158. svc_infra/db/nosql/service.py +2 -1
  159. svc_infra/db/nosql/service_with_hooks.py +4 -3
  160. svc_infra/db/nosql/utils.py +4 -4
  161. svc_infra/db/ops.py +380 -0
  162. svc_infra/db/outbox.py +105 -0
  163. svc_infra/db/sql/apikey.py +34 -15
  164. svc_infra/db/sql/authref.py +8 -6
  165. svc_infra/db/sql/constants.py +5 -1
  166. svc_infra/db/sql/core.py +13 -13
  167. svc_infra/db/sql/management.py +5 -6
  168. svc_infra/db/sql/repository.py +92 -26
  169. svc_infra/db/sql/resource.py +18 -12
  170. svc_infra/db/sql/scaffold.py +11 -11
  171. svc_infra/db/sql/service.py +2 -1
  172. svc_infra/db/sql/service_with_hooks.py +4 -3
  173. svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
  174. svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
  175. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
  176. svc_infra/db/sql/tenant.py +80 -0
  177. svc_infra/db/sql/uniq.py +8 -7
  178. svc_infra/db/sql/uniq_hooks.py +12 -11
  179. svc_infra/db/sql/utils.py +105 -47
  180. svc_infra/db/sql/versioning.py +14 -0
  181. svc_infra/db/utils.py +3 -3
  182. svc_infra/deploy/__init__.py +531 -0
  183. svc_infra/documents/__init__.py +100 -0
  184. svc_infra/documents/add.py +263 -0
  185. svc_infra/documents/ease.py +233 -0
  186. svc_infra/documents/models.py +114 -0
  187. svc_infra/documents/storage.py +262 -0
  188. svc_infra/dx/__init__.py +58 -0
  189. svc_infra/dx/add.py +63 -0
  190. svc_infra/dx/changelog.py +74 -0
  191. svc_infra/dx/checks.py +68 -0
  192. svc_infra/exceptions.py +141 -0
  193. svc_infra/health/__init__.py +863 -0
  194. svc_infra/http/__init__.py +13 -0
  195. svc_infra/http/client.py +101 -0
  196. svc_infra/jobs/__init__.py +79 -0
  197. svc_infra/jobs/builtins/outbox_processor.py +38 -0
  198. svc_infra/jobs/builtins/webhook_delivery.py +93 -0
  199. svc_infra/jobs/easy.py +33 -0
  200. svc_infra/jobs/loader.py +49 -0
  201. svc_infra/jobs/queue.py +106 -0
  202. svc_infra/jobs/redis_queue.py +242 -0
  203. svc_infra/jobs/runner.py +75 -0
  204. svc_infra/jobs/scheduler.py +53 -0
  205. svc_infra/jobs/worker.py +40 -0
  206. svc_infra/loaders/__init__.py +186 -0
  207. svc_infra/loaders/base.py +143 -0
  208. svc_infra/loaders/github.py +309 -0
  209. svc_infra/loaders/models.py +147 -0
  210. svc_infra/loaders/url.py +229 -0
  211. svc_infra/logging/__init__.py +375 -0
  212. svc_infra/mcp/__init__.py +82 -0
  213. svc_infra/mcp/svc_infra_mcp.py +91 -33
  214. svc_infra/obs/README.md +2 -0
  215. svc_infra/obs/add.py +68 -11
  216. svc_infra/obs/cloud_dash.py +2 -1
  217. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  218. svc_infra/obs/metrics/__init__.py +6 -7
  219. svc_infra/obs/metrics/asgi.py +8 -7
  220. svc_infra/obs/metrics/base.py +13 -13
  221. svc_infra/obs/metrics/http.py +3 -3
  222. svc_infra/obs/metrics/sqlalchemy.py +14 -13
  223. svc_infra/obs/metrics.py +9 -8
  224. svc_infra/resilience/__init__.py +44 -0
  225. svc_infra/resilience/circuit_breaker.py +328 -0
  226. svc_infra/resilience/retry.py +289 -0
  227. svc_infra/security/__init__.py +167 -0
  228. svc_infra/security/add.py +213 -0
  229. svc_infra/security/audit.py +97 -18
  230. svc_infra/security/audit_service.py +10 -9
  231. svc_infra/security/headers.py +15 -2
  232. svc_infra/security/hibp.py +14 -7
  233. svc_infra/security/jwt_rotation.py +78 -29
  234. svc_infra/security/lockout.py +23 -16
  235. svc_infra/security/models.py +77 -44
  236. svc_infra/security/oauth_models.py +73 -0
  237. svc_infra/security/org_invites.py +12 -12
  238. svc_infra/security/passwords.py +3 -3
  239. svc_infra/security/permissions.py +31 -7
  240. svc_infra/security/session.py +7 -8
  241. svc_infra/security/signed_cookies.py +26 -6
  242. svc_infra/storage/__init__.py +93 -0
  243. svc_infra/storage/add.py +250 -0
  244. svc_infra/storage/backends/__init__.py +11 -0
  245. svc_infra/storage/backends/local.py +331 -0
  246. svc_infra/storage/backends/memory.py +213 -0
  247. svc_infra/storage/backends/s3.py +334 -0
  248. svc_infra/storage/base.py +239 -0
  249. svc_infra/storage/easy.py +181 -0
  250. svc_infra/storage/settings.py +193 -0
  251. svc_infra/testing/__init__.py +682 -0
  252. svc_infra/utils.py +170 -5
  253. svc_infra/webhooks/__init__.py +69 -0
  254. svc_infra/webhooks/add.py +327 -0
  255. svc_infra/webhooks/encryption.py +115 -0
  256. svc_infra/webhooks/fastapi.py +37 -0
  257. svc_infra/webhooks/router.py +55 -0
  258. svc_infra/webhooks/service.py +69 -0
  259. svc_infra/webhooks/signing.py +34 -0
  260. svc_infra/websocket/__init__.py +79 -0
  261. svc_infra/websocket/add.py +139 -0
  262. svc_infra/websocket/client.py +283 -0
  263. svc_infra/websocket/config.py +57 -0
  264. svc_infra/websocket/easy.py +76 -0
  265. svc_infra/websocket/exceptions.py +61 -0
  266. svc_infra/websocket/manager.py +343 -0
  267. svc_infra/websocket/models.py +49 -0
  268. svc_infra-1.1.0.dist-info/LICENSE +21 -0
  269. svc_infra-1.1.0.dist-info/METADATA +362 -0
  270. svc_infra-1.1.0.dist-info/RECORD +364 -0
  271. svc_infra-0.1.595.dist-info/METADATA +0 -80
  272. svc_infra-0.1.595.dist-info/RECORD +0 -253
  273. {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
  274. {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,181 @@
1
+ """
2
+ Easy storage backend builder with auto-detection.
3
+
4
+ Simplifies storage backend initialization with sensible defaults.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+
10
+ from .backends import LocalBackend, MemoryBackend, S3Backend
11
+ from .base import StorageBackend
12
+ from .settings import StorageSettings
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def easy_storage(
18
+ backend: str | None = None,
19
+ **kwargs,
20
+ ) -> StorageBackend:
21
+ """
22
+ Create a storage backend with auto-detection or explicit selection.
23
+
24
+ This is the recommended way to initialize storage in most applications.
25
+ It handles environment-based configuration and provides sensible defaults.
26
+
27
+ Args:
28
+ backend: Explicit backend type ("local", "s3", "gcs", "cloudinary", "memory")
29
+ If None, auto-detects from environment variables
30
+ **kwargs: Backend-specific configuration overrides
31
+
32
+ Returns:
33
+ Initialized storage backend
34
+
35
+ Auto-Detection Order:
36
+ 1. Explicit backend parameter
37
+ 2. STORAGE_BACKEND environment variable
38
+ 3. Railway volume (RAILWAY_VOLUME_MOUNT_PATH) → LocalBackend
39
+ 4. S3 credentials (AWS_ACCESS_KEY_ID or STORAGE_S3_BUCKET) → S3Backend
40
+ 5. GCS credentials (GOOGLE_APPLICATION_CREDENTIALS) → GCSBackend
41
+ 6. Cloudinary credentials (CLOUDINARY_URL) → CloudinaryBackend
42
+ 7. Default: MemoryBackend (with warning)
43
+
44
+ Examples:
45
+ >>> # Auto-detect backend from environment
46
+ >>> storage = easy_storage()
47
+ >>>
48
+ >>> # Explicit local backend
49
+ >>> storage = easy_storage(
50
+ ... backend="local",
51
+ ... base_path="/data/uploads"
52
+ ... )
53
+ >>>
54
+ >>> # Explicit S3 backend
55
+ >>> storage = easy_storage(
56
+ ... backend="s3",
57
+ ... bucket="my-uploads",
58
+ ... region="us-west-2"
59
+ ... )
60
+ >>>
61
+ >>> # DigitalOcean Spaces
62
+ >>> storage = easy_storage(
63
+ ... backend="s3",
64
+ ... bucket="my-uploads",
65
+ ... region="nyc3",
66
+ ... endpoint="https://nyc3.digitaloceanspaces.com"
67
+ ... )
68
+
69
+ Environment Variables:
70
+ See StorageSettings for full list of environment variables.
71
+
72
+ Raises:
73
+ ValueError: If backend type is unsupported or configuration is invalid
74
+ ImportError: If required backend dependencies are not installed
75
+
76
+ Note:
77
+ For production deployments, it's recommended to set STORAGE_BACKEND
78
+ explicitly to avoid unexpected auto-detection behavior.
79
+ """
80
+ # Load settings
81
+ settings = StorageSettings()
82
+
83
+ # Determine backend type
84
+ backend_type = backend or settings.detect_backend()
85
+
86
+ logger.info(f"Initializing {backend_type} storage backend")
87
+
88
+ # Create backend instance
89
+ if backend_type == "memory":
90
+ # Memory backend
91
+ if backend_type == settings.detect_backend() and not backend:
92
+ logger.warning(
93
+ "Using MemoryBackend (in-memory storage). "
94
+ "Data will be lost on restart. "
95
+ "Set STORAGE_BACKEND environment variable for production."
96
+ )
97
+
98
+ max_size = kwargs.get("max_size", 100_000_000)
99
+ return MemoryBackend(max_size=max_size)
100
+
101
+ elif backend_type == "local":
102
+ # Local filesystem backend
103
+ base_path = kwargs.get("base_path") or settings.storage_base_path
104
+
105
+ # Check for Railway volume
106
+ railway_volume = os.getenv("RAILWAY_VOLUME_MOUNT_PATH")
107
+ if railway_volume and not kwargs.get("base_path"):
108
+ base_path = railway_volume
109
+ logger.info(f"Detected Railway volume at {base_path}")
110
+
111
+ base_url = kwargs.get("base_url") or settings.storage_base_url
112
+ signing_secret = kwargs.get("signing_secret") or settings.storage_signing_secret
113
+
114
+ return LocalBackend(
115
+ base_path=base_path,
116
+ base_url=base_url,
117
+ signing_secret=signing_secret,
118
+ )
119
+
120
+ elif backend_type == "s3":
121
+ # S3-compatible backend
122
+ bucket = kwargs.get("bucket") or settings.storage_s3_bucket
123
+ if not bucket:
124
+ raise ValueError(
125
+ "S3 bucket is required. "
126
+ "Set STORAGE_S3_BUCKET environment variable or pass bucket parameter."
127
+ )
128
+
129
+ region = kwargs.get("region") or settings.storage_s3_region
130
+ endpoint = kwargs.get("endpoint") or settings.storage_s3_endpoint
131
+
132
+ # Get credentials with fallback
133
+ access_key = kwargs.get("access_key")
134
+ secret_key = kwargs.get("secret_key")
135
+
136
+ if not access_key or not secret_key:
137
+ access_key_from_settings, secret_key_from_settings = settings.get_s3_credentials()
138
+ access_key = access_key or access_key_from_settings
139
+ secret_key = secret_key or secret_key_from_settings
140
+
141
+ # Log provider detection
142
+ if endpoint:
143
+ if "digitalocean" in endpoint:
144
+ logger.info("Detected DigitalOcean Spaces")
145
+ elif "wasabi" in endpoint:
146
+ logger.info("Detected Wasabi")
147
+ elif "backblaze" in endpoint:
148
+ logger.info("Detected Backblaze B2")
149
+ else:
150
+ logger.info(f"Using custom S3 endpoint: {endpoint}")
151
+ else:
152
+ logger.info("Using AWS S3")
153
+
154
+ return S3Backend(
155
+ bucket=bucket,
156
+ region=region,
157
+ endpoint=endpoint,
158
+ access_key=access_key,
159
+ secret_key=secret_key,
160
+ )
161
+
162
+ elif backend_type == "gcs":
163
+ # Google Cloud Storage backend
164
+ raise NotImplementedError(
165
+ "GCS backend not yet implemented. Use 'local' or 's3' backend for now."
166
+ )
167
+
168
+ elif backend_type == "cloudinary":
169
+ # Cloudinary backend
170
+ raise NotImplementedError(
171
+ "Cloudinary backend not yet implemented. Use 'local' or 's3' backend for now."
172
+ )
173
+
174
+ else:
175
+ raise ValueError(
176
+ f"Unsupported storage backend: {backend_type}. "
177
+ f"Supported: local, s3, memory (gcs, cloudinary coming soon)"
178
+ )
179
+
180
+
181
+ __all__ = ["easy_storage"]
@@ -0,0 +1,193 @@
1
+ """
2
+ Storage configuration and settings.
3
+
4
+ Handles environment-based configuration and auto-detection of storage backends.
5
+ """
6
+
7
+ import os
8
+ from typing import Literal
9
+
10
+ from pydantic import Field
11
+ from pydantic_settings import BaseSettings
12
+
13
+
14
+ class StorageSettings(BaseSettings):
15
+ """
16
+ Storage system configuration.
17
+
18
+ Supports multiple backends with auto-detection from environment variables.
19
+
20
+ Environment Variables:
21
+ STORAGE_BACKEND: Explicit backend selection ("local", "s3", "gcs", "cloudinary", "memory")
22
+
23
+ Local Backend:
24
+ STORAGE_BASE_PATH: Base directory for file storage (default: /data/uploads)
25
+ STORAGE_BASE_URL: Base URL for file serving (default: http://localhost:8000/files)
26
+ STORAGE_SIGNING_SECRET: Secret key for URL signing (auto-generated if not set)
27
+
28
+ S3 Backend:
29
+ STORAGE_S3_BUCKET: S3 bucket name (required for S3)
30
+ STORAGE_S3_REGION: AWS region (default: us-east-1)
31
+ STORAGE_S3_ENDPOINT: Custom endpoint for S3-compatible services (optional)
32
+ STORAGE_S3_ACCESS_KEY: AWS access key (optional, uses AWS_ACCESS_KEY_ID if not set)
33
+ STORAGE_S3_SECRET_KEY: AWS secret key (optional, uses AWS_SECRET_ACCESS_KEY if not set)
34
+
35
+ GCS Backend:
36
+ STORAGE_GCS_BUCKET: GCS bucket name (required for GCS)
37
+ STORAGE_GCS_PROJECT: GCP project ID (optional)
38
+ STORAGE_GCS_CREDENTIALS_PATH: Path to service account JSON (optional)
39
+
40
+ Cloudinary Backend:
41
+ STORAGE_CLOUDINARY_CLOUD_NAME: Cloudinary cloud name (required)
42
+ STORAGE_CLOUDINARY_API_KEY: Cloudinary API key (required)
43
+ STORAGE_CLOUDINARY_API_SECRET: Cloudinary API secret (required)
44
+
45
+ Example:
46
+ >>> # Auto-detect backend from environment
47
+ >>> settings = StorageSettings()
48
+ >>> backend = settings.detect_backend()
49
+ >>>
50
+ >>> # Explicit backend selection
51
+ >>> settings = StorageSettings(storage_backend="s3")
52
+ """
53
+
54
+ # Backend selection
55
+ storage_backend: Literal["local", "s3", "gcs", "cloudinary", "memory"] | None = Field(
56
+ default=None,
57
+ description="Storage backend type (auto-detected if not set)",
58
+ )
59
+
60
+ # Local backend settings
61
+ storage_base_path: str = Field(
62
+ default="/data/uploads",
63
+ description="Base directory for local file storage",
64
+ )
65
+ storage_base_url: str = Field(
66
+ default="http://localhost:8000/files",
67
+ description="Base URL for serving files",
68
+ )
69
+ storage_signing_secret: str | None = Field(
70
+ default=None,
71
+ description="Secret key for URL signing (auto-generated if not set)",
72
+ )
73
+
74
+ # S3 backend settings
75
+ storage_s3_bucket: str | None = Field(
76
+ default=None,
77
+ description="S3 bucket name",
78
+ )
79
+ storage_s3_region: str = Field(
80
+ default="us-east-1",
81
+ description="AWS region",
82
+ )
83
+ storage_s3_endpoint: str | None = Field(
84
+ default=None,
85
+ description="Custom S3 endpoint (for DigitalOcean Spaces, Wasabi, etc.)",
86
+ )
87
+ storage_s3_access_key: str | None = Field(
88
+ default=None,
89
+ description="S3 access key (falls back to AWS_ACCESS_KEY_ID)",
90
+ )
91
+ storage_s3_secret_key: str | None = Field(
92
+ default=None,
93
+ description="S3 secret key (falls back to AWS_SECRET_ACCESS_KEY)",
94
+ )
95
+
96
+ # GCS backend settings
97
+ storage_gcs_bucket: str | None = Field(
98
+ default=None,
99
+ description="Google Cloud Storage bucket name",
100
+ )
101
+ storage_gcs_project: str | None = Field(
102
+ default=None,
103
+ description="GCP project ID",
104
+ )
105
+ storage_gcs_credentials_path: str | None = Field(
106
+ default=None,
107
+ description="Path to GCP service account JSON",
108
+ )
109
+
110
+ # Cloudinary backend settings
111
+ storage_cloudinary_cloud_name: str | None = Field(
112
+ default=None,
113
+ description="Cloudinary cloud name",
114
+ )
115
+ storage_cloudinary_api_key: str | None = Field(
116
+ default=None,
117
+ description="Cloudinary API key",
118
+ )
119
+ storage_cloudinary_api_secret: str | None = Field(
120
+ default=None,
121
+ description="Cloudinary API secret",
122
+ )
123
+
124
+ model_config = {
125
+ "env_file": ".env",
126
+ "case_sensitive": False,
127
+ "extra": "ignore", # Ignore unknown environment variables
128
+ }
129
+
130
+ def detect_backend(self) -> str:
131
+ """
132
+ Auto-detect storage backend from environment.
133
+
134
+ Detection order:
135
+ 1. Explicit STORAGE_BACKEND setting
136
+ 2. Railway volume (RAILWAY_VOLUME_MOUNT_PATH) → local
137
+ 3. S3 credentials (AWS_ACCESS_KEY_ID or STORAGE_S3_BUCKET) → s3
138
+ 4. GCS credentials (GOOGLE_APPLICATION_CREDENTIALS or STORAGE_GCS_BUCKET) → gcs
139
+ 5. Cloudinary credentials (CLOUDINARY_URL or STORAGE_CLOUDINARY_CLOUD_NAME) → cloudinary
140
+ 6. Default → memory (with warning)
141
+
142
+ Returns:
143
+ Backend type string
144
+
145
+ Example:
146
+ >>> settings = StorageSettings()
147
+ >>> backend_type = settings.detect_backend()
148
+ >>> print(f"Using {backend_type} backend")
149
+ """
150
+ # Explicit setting takes precedence
151
+ if self.storage_backend:
152
+ return self.storage_backend
153
+
154
+ # Check for Railway volume
155
+ railway_volume = os.getenv("RAILWAY_VOLUME_MOUNT_PATH")
156
+ if railway_volume:
157
+ return "local"
158
+
159
+ # Check for S3
160
+ has_s3_key = os.getenv("AWS_ACCESS_KEY_ID") or self.storage_s3_access_key
161
+ if has_s3_key or self.storage_s3_bucket:
162
+ return "s3"
163
+
164
+ # Check for GCS
165
+ has_gcs_creds = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
166
+ if has_gcs_creds or self.storage_gcs_bucket:
167
+ return "gcs"
168
+
169
+ # Check for Cloudinary
170
+ has_cloudinary = os.getenv("CLOUDINARY_URL")
171
+ if has_cloudinary or self.storage_cloudinary_cloud_name:
172
+ return "cloudinary"
173
+
174
+ # Default to memory (for development/testing)
175
+ return "memory"
176
+
177
+ def get_s3_credentials(self) -> tuple[str | None, str | None]:
178
+ """
179
+ Get S3 credentials with fallback to AWS environment variables.
180
+
181
+ Returns:
182
+ Tuple of (access_key, secret_key)
183
+
184
+ Example:
185
+ >>> settings = StorageSettings()
186
+ >>> access_key, secret_key = settings.get_s3_credentials()
187
+ """
188
+ access_key = self.storage_s3_access_key or os.getenv("AWS_ACCESS_KEY_ID")
189
+ secret_key = self.storage_s3_secret_key or os.getenv("AWS_SECRET_ACCESS_KEY")
190
+ return access_key, secret_key
191
+
192
+
193
+ __all__ = ["StorageSettings"]