py-storebridge 1.0.0__tar.gz

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 (47) hide show
  1. py_storebridge-1.0.0/LICENSE +21 -0
  2. py_storebridge-1.0.0/PKG-INFO +562 -0
  3. py_storebridge-1.0.0/README.md +498 -0
  4. py_storebridge-1.0.0/pyproject.toml +108 -0
  5. py_storebridge-1.0.0/setup.cfg +4 -0
  6. py_storebridge-1.0.0/src/py_storebridge/__init__.py +126 -0
  7. py_storebridge-1.0.0/src/py_storebridge/client.py +222 -0
  8. py_storebridge-1.0.0/src/py_storebridge/config.py +296 -0
  9. py_storebridge-1.0.0/src/py_storebridge/exceptions.py +209 -0
  10. py_storebridge-1.0.0/src/py_storebridge/factory.py +58 -0
  11. py_storebridge-1.0.0/src/py_storebridge/hooks.py +50 -0
  12. py_storebridge-1.0.0/src/py_storebridge/io.py +79 -0
  13. py_storebridge-1.0.0/src/py_storebridge/keys.py +59 -0
  14. py_storebridge-1.0.0/src/py_storebridge/logging.py +52 -0
  15. py_storebridge-1.0.0/src/py_storebridge/middleware.py +45 -0
  16. py_storebridge-1.0.0/src/py_storebridge/mime.py +49 -0
  17. py_storebridge-1.0.0/src/py_storebridge/providers/__init__.py +9 -0
  18. py_storebridge-1.0.0/src/py_storebridge/providers/_deps.py +15 -0
  19. py_storebridge-1.0.0/src/py_storebridge/providers/appwrite.py +235 -0
  20. py_storebridge-1.0.0/src/py_storebridge/providers/azure.py +277 -0
  21. py_storebridge-1.0.0/src/py_storebridge/providers/b2.py +43 -0
  22. py_storebridge-1.0.0/src/py_storebridge/providers/base.py +402 -0
  23. py_storebridge-1.0.0/src/py_storebridge/providers/firebase.py +48 -0
  24. py_storebridge-1.0.0/src/py_storebridge/providers/gcs.py +269 -0
  25. py_storebridge-1.0.0/src/py_storebridge/providers/minio.py +46 -0
  26. py_storebridge-1.0.0/src/py_storebridge/providers/r2.py +42 -0
  27. py_storebridge-1.0.0/src/py_storebridge/providers/s3.py +39 -0
  28. py_storebridge-1.0.0/src/py_storebridge/providers/s3compat.py +406 -0
  29. py_storebridge-1.0.0/src/py_storebridge/providers/spaces.py +42 -0
  30. py_storebridge-1.0.0/src/py_storebridge/providers/supabase.py +281 -0
  31. py_storebridge-1.0.0/src/py_storebridge/py.typed +0 -0
  32. py_storebridge-1.0.0/src/py_storebridge/retry.py +76 -0
  33. py_storebridge-1.0.0/src/py_storebridge/schemas.py +222 -0
  34. py_storebridge-1.0.0/src/py_storebridge/transfer.py +207 -0
  35. py_storebridge-1.0.0/src/py_storebridge.egg-info/PKG-INFO +562 -0
  36. py_storebridge-1.0.0/src/py_storebridge.egg-info/SOURCES.txt +45 -0
  37. py_storebridge-1.0.0/src/py_storebridge.egg-info/dependency_links.txt +1 -0
  38. py_storebridge-1.0.0/src/py_storebridge.egg-info/requires.txt +47 -0
  39. py_storebridge-1.0.0/src/py_storebridge.egg-info/top_level.txt +1 -0
  40. py_storebridge-1.0.0/tests/test_client.py +105 -0
  41. py_storebridge-1.0.0/tests/test_config.py +91 -0
  42. py_storebridge-1.0.0/tests/test_exceptions.py +27 -0
  43. py_storebridge-1.0.0/tests/test_factory.py +31 -0
  44. py_storebridge-1.0.0/tests/test_hooks_middleware.py +74 -0
  45. py_storebridge-1.0.0/tests/test_keys_mime.py +52 -0
  46. py_storebridge-1.0.0/tests/test_retry.py +42 -0
  47. py_storebridge-1.0.0/tests/test_transfer.py +154 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asif Khan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,562 @@
1
+ Metadata-Version: 2.4
2
+ Name: py_storebridge
3
+ Version: 1.0.0
4
+ Summary: A unified, production-ready cloud storage SDK for Python. Switch providers with one config line.
5
+ Author-email: Asif Khan <ak2311581@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ak9347128658/py_storebridge
8
+ Project-URL: Documentation, https://github.com/ak9347128658/py_storebridge#readme
9
+ Project-URL: Repository, https://github.com/ak9347128658/py_storebridge
10
+ Project-URL: Issues, https://github.com/ak9347128658/py_storebridge/issues
11
+ Project-URL: Changelog, https://github.com/ak9347128658/py_storebridge/blob/main/CHANGELOG.md
12
+ Keywords: storage,s3,gcs,azure,cloudflare,r2,minio,supabase,firebase,appwrite,backblaze,digitalocean,spaces,cloud,upload,pydantic
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Internet :: File Transfer Protocol (FTP)
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: pydantic>=2.5
30
+ Provides-Extra: s3
31
+ Requires-Dist: boto3>=1.34.0; extra == "s3"
32
+ Provides-Extra: r2
33
+ Requires-Dist: boto3>=1.34.0; extra == "r2"
34
+ Provides-Extra: gcs
35
+ Requires-Dist: google-cloud-storage>=2.14.0; extra == "gcs"
36
+ Provides-Extra: azure
37
+ Requires-Dist: azure-storage-blob>=12.19.0; extra == "azure"
38
+ Provides-Extra: spaces
39
+ Requires-Dist: boto3>=1.34.0; extra == "spaces"
40
+ Provides-Extra: b2
41
+ Requires-Dist: boto3>=1.34.0; extra == "b2"
42
+ Provides-Extra: minio
43
+ Requires-Dist: boto3>=1.34.0; extra == "minio"
44
+ Provides-Extra: supabase
45
+ Requires-Dist: httpx>=0.27.0; extra == "supabase"
46
+ Provides-Extra: firebase
47
+ Requires-Dist: google-cloud-storage>=2.14.0; extra == "firebase"
48
+ Requires-Dist: google-auth>=2.23.0; extra == "firebase"
49
+ Provides-Extra: appwrite
50
+ Requires-Dist: httpx>=0.27.0; extra == "appwrite"
51
+ Provides-Extra: all
52
+ Requires-Dist: boto3>=1.34.0; extra == "all"
53
+ Requires-Dist: google-cloud-storage>=2.14.0; extra == "all"
54
+ Requires-Dist: google-auth>=2.23.0; extra == "all"
55
+ Requires-Dist: azure-storage-blob>=12.19.0; extra == "all"
56
+ Requires-Dist: httpx>=0.27.0; extra == "all"
57
+ Provides-Extra: dev
58
+ Requires-Dist: pytest>=8.0; extra == "dev"
59
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
60
+ Provides-Extra: build
61
+ Requires-Dist: build>=1.2; extra == "build"
62
+ Requires-Dist: twine>=5.0; extra == "build"
63
+ Dynamic: license-file
64
+
65
+ # py_storebridge
66
+
67
+ **A unified, production-ready cloud storage SDK for Python.**
68
+
69
+ Switch storage providers by changing a single config line — no code changes required.
70
+
71
+ Python port of [storebridge](https://www.npmjs.com/package/storebridge) (Node.js / TypeScript), with Pydantic validation and **cross-provider transfer** of files, folders, and entire buckets.
72
+
73
+ ---
74
+
75
+ ## Features
76
+
77
+ - **10 storage providers** — one consistent API
78
+ - **Pydantic v2** — every config object and public operation is validated
79
+ - **Retry system** — exponential backoff with full jitter
80
+ - **Middleware** — composable upload pipeline (metadata injection, key prefixes, …)
81
+ - **Hooks** — lifecycle events (before/after upload, delete, copy, move, transfer)
82
+ - **Multipart upload** — high-level helper + low-level lifecycle API
83
+ - **Signed / presigned URLs** — GET, PUT, DELETE
84
+ - **Streaming** — upload from paths / file objects, download as streams or bytes
85
+ - **MIME auto-detection** — from file extension
86
+ - **CDN / custom domain** support
87
+ - **Lazy provider loading** — only the SDK you use is imported
88
+ - **Cross-provider transfer** — move existing files, folders, or a whole bucket from one provider to another
89
+
90
+ ---
91
+
92
+ ## Provider support
93
+
94
+ | Provider | Extra | Install |
95
+ | --- | --- | --- |
96
+ | AWS S3 | `s3` | `pip install py_storebridge[s3]` |
97
+ | Cloudflare R2 | `r2` | `pip install py_storebridge[r2]` |
98
+ | Google Cloud Storage | `gcs` | `pip install py_storebridge[gcs]` |
99
+ | Azure Blob Storage | `azure` | `pip install py_storebridge[azure]` |
100
+ | DigitalOcean Spaces | `spaces` | `pip install py_storebridge[spaces]` |
101
+ | Backblaze B2 | `b2` | `pip install py_storebridge[b2]` |
102
+ | MinIO | `minio` | `pip install py_storebridge[minio]` |
103
+ | Supabase Storage | `supabase` | `pip install py_storebridge[supabase]` |
104
+ | Firebase Storage | `firebase` | `pip install py_storebridge[firebase]` |
105
+ | Appwrite Storage | `appwrite` | `pip install py_storebridge[appwrite]` |
106
+
107
+ Provider SDKs are optional extras. The core package depends only on **Pydantic**.
108
+
109
+ ```bash
110
+ pip install py_storebridge
111
+ pip install py_storebridge[s3]
112
+ pip install py_storebridge[all]
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Feature matrix
118
+
119
+ | Feature | S3 | R2 | GCS | Azure | Spaces | B2 | MinIO | Supabase | Firebase | Appwrite |
120
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
121
+ | Upload | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
122
+ | Delete | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
123
+ | Exists | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
124
+ | Metadata | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
125
+ | List | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
126
+ | Signed URLs | yes | yes | yes | yes | yes | yes | yes | yes | yes | limited |
127
+ | Copy / move | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
128
+ | Stream upload | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
129
+ | Multipart | native | native | fallback | native blocks | native | native | native | fallback | fallback | fallback |
130
+ | ACL / visibility | yes | limited | yes | limited | yes | limited | limited | yes | yes | yes |
131
+ | CDN / custom domain | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
132
+ | **Transfer to another provider** | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
133
+
134
+ ---
135
+
136
+ ## Quick start
137
+
138
+ ```python
139
+ from py_storebridge import create_storage
140
+
141
+ storage = create_storage({
142
+ "provider": "s3",
143
+ "credentials": {
144
+ "access_key_id": "AKI…",
145
+ "secret_access_key": "…",
146
+ "region": "us-east-1",
147
+ "bucket": "my-bucket",
148
+ },
149
+ "visibility": "private",
150
+ })
151
+
152
+ result = storage.upload(key="users/profile.png", file=image_bytes, content_type="image/png")
153
+ print(result.url)
154
+
155
+ url = storage.get_signed_url("users/profile.png", expires_in=3600)
156
+ exists = storage.exists("users/profile.png")
157
+ storage.delete(key="users/profile.png")
158
+ ```
159
+
160
+ CamelCase credential keys from the Node.js SDK (`accessKeyId`, `secretAccessKey`, …) are accepted as well.
161
+
162
+ ---
163
+
164
+ ## Configuration
165
+
166
+ All providers share the same top-level shape. Credentials differ per provider. Pydantic validates the payload before any SDK is loaded.
167
+
168
+ ```python
169
+ from py_storebridge import create_storage
170
+
171
+ storage = create_storage(
172
+ provider="s3",
173
+ credentials={
174
+ "access_key_id": "…",
175
+ "secret_access_key": "…",
176
+ "region": "us-east-1",
177
+ "bucket": "my-bucket",
178
+ },
179
+ visibility="private", # default visibility
180
+ retry={"max_retries": 3, "base_delay": 200, "max_delay": 10000},
181
+ log_level="warn", # debug | info | warn | error | silent
182
+ )
183
+ ```
184
+
185
+ Retry delays are milliseconds, matching the Node.js package.
186
+
187
+ ---
188
+
189
+ ## API
190
+
191
+ ### upload
192
+
193
+ ```python
194
+ result = storage.upload(
195
+ key="images/photo.jpg",
196
+ file=buffer, # bytes | str | pathlib.Path | BinaryIO
197
+ content_type="image/jpeg", # optional — detected from the key
198
+ visibility="public",
199
+ metadata={"author": "alice"},
200
+ cache_control="max-age=31536000",
201
+ on_progress=lambda uploaded, total: print(uploaded, total),
202
+ )
203
+ # result.key, result.url, result.etag, result.size, result.provider
204
+
205
+ storage.upload_file("local/photo.jpg", key="images/photo.jpg")
206
+ ```
207
+
208
+ ### delete / exists / metadata / list
209
+
210
+ ```python
211
+ storage.delete(key="images/photo.jpg")
212
+ storage.exists("images/photo.jpg")
213
+ meta = storage.get_metadata("images/photo.jpg")
214
+
215
+ page = storage.list(prefix="images/", limit=50, cursor=None, delimiter="/")
216
+ for entry in storage.iter_files(prefix="images/"):
217
+ print(entry.key, entry.size)
218
+
219
+ storage.delete_prefix("tmp/")
220
+ ```
221
+
222
+ ### URLs
223
+
224
+ ```python
225
+ storage.get_url("public/logo.png")
226
+ storage.get_signed_url("private/report.pdf", expires_in=900)
227
+ storage.get_signed_url("uploads/raw.csv", method="PUT", expires_in=300, content_type="text/csv")
228
+ ```
229
+
230
+ ### copy / move / rename
231
+
232
+ These operate **inside one provider**.
233
+
234
+ ```python
235
+ storage.copy(source_key="original.png", destination_key="copies/backup.png")
236
+ storage.move(source_key="temp/upload.png", destination_key="final/upload.png")
237
+ storage.rename("old-name.png", "new-name.png")
238
+ ```
239
+
240
+ ### streams and buffers
241
+
242
+ ```python
243
+ stream = storage.get_stream("videos/intro.mp4")
244
+ data = storage.get_buffer("config/settings.json")
245
+ storage.download_file("config/settings.json", "settings.json")
246
+ ```
247
+
248
+ ### multipart
249
+
250
+ ```python
251
+ result = storage.upload_multipart({"key": "large/dataset.parquet", "file": payload}, chunk_size=10 * 1024 * 1024)
252
+
253
+ session = storage.create_multipart_upload("huge.bin", "application/octet-stream")
254
+ parts = [
255
+ storage.upload_part(upload_id=session.upload_id, key=session.key, part_number=i + 1, body=chunk)
256
+ for i, chunk in enumerate(chunks)
257
+ ]
258
+ storage.complete_multipart_upload(session, parts)
259
+ # storage.abort_multipart_upload(session)
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Transfer between providers
265
+
266
+ This is the capability the Node.js `storebridge` package does not have. `transfer()` copies (or moves) **existing** objects from one provider to another: a single file, a folder prefix, an explicit key list, or an entire bucket.
267
+
268
+ ```python
269
+ from py_storebridge import create_storage, transfer
270
+
271
+ s3 = create_storage({"provider": "s3", "credentials": {…}})
272
+ gcs = create_storage({"provider": "gcs", "credentials": {…}})
273
+
274
+ # One file
275
+ transfer(s3, gcs, source_key="users/profile.png", destination_key="migrated/profile.png")
276
+
277
+ # A folder (every key under the prefix)
278
+ transfer(
279
+ s3,
280
+ gcs,
281
+ prefix="images/",
282
+ destination_prefix="backup/images/",
283
+ concurrency=8,
284
+ preserve_metadata=True,
285
+ preserve_visibility=True,
286
+ )
287
+
288
+ # Entire bucket
289
+ result = s3.transfer_to(gcs)
290
+
291
+ # Move (copy then delete from the source)
292
+ transfer(s3, gcs, prefix="uploads/", delete_source=True)
293
+
294
+ # Dry run
295
+ plan = transfer(s3, gcs, prefix="images/", dry_run=True)
296
+ print(plan.planned, plan.items)
297
+
298
+ # Skip objects that already exist on the destination
299
+ transfer(s3, gcs, prefix="images/", skip_existing=True, continue_on_error=True)
300
+ ```
301
+
302
+ `TransferResult` includes `transferred`, `skipped`, `failed`, `bytes_transferred`, and per-item status. Set `continue_on_error=False` to stop on the first failure (`TransferError` then carries the partial result).
303
+
304
+ ---
305
+
306
+ ## Middleware
307
+
308
+ Middleware wraps every `upload()` call (onion model).
309
+
310
+ ```python
311
+ def inject_metadata(ctx, nxt):
312
+ ctx.options.metadata = {
313
+ **(ctx.options.metadata or {}),
314
+ "uploaded_by": "user-123",
315
+ }
316
+ return nxt()
317
+
318
+ def force_prefix(ctx, nxt):
319
+ if not ctx.options.key.startswith("uploads/"):
320
+ ctx.options.key = f"uploads/{ctx.options.key}"
321
+ return nxt()
322
+
323
+ storage.use(inject_metadata)
324
+ storage.use(force_prefix)
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Hooks
330
+
331
+ ```python
332
+ storage.on("after:upload", lambda result: print(result.key, result.provider))
333
+ storage.on("error", lambda payload: log(payload["operation"], payload["error"]))
334
+ storage.on("after:transfer", lambda result: print(result.transferred, result.failed))
335
+
336
+ storage.off("after:upload", handler)
337
+ ```
338
+
339
+ Events: `before:upload`, `after:upload`, `before:delete`, `after:delete`, `before:copy`, `after:copy`, `before:move`, `after:move`, `before:list`, `after:list`, `before:getSignedUrl`, `after:getSignedUrl`, `before:transfer`, `after:transfer`, `error`.
340
+
341
+ ---
342
+
343
+ ## Errors
344
+
345
+ All errors extend `StoreBridgeError` (`StorixError` is an alias).
346
+
347
+ ```python
348
+ from py_storebridge import (
349
+ StoreBridgeError,
350
+ FileNotFoundError,
351
+ PermissionError,
352
+ MaxRetriesExceededError,
353
+ ValidationError,
354
+ MissingDependencyError,
355
+ TransferError,
356
+ )
357
+
358
+ try:
359
+ storage.get_metadata("missing.png")
360
+ except FileNotFoundError as err:
361
+ print(err.key, err.status_code, err.provider)
362
+ except MaxRetriesExceededError as err:
363
+ print(err.attempts)
364
+ except StoreBridgeError as err:
365
+ print(err.code)
366
+ ```
367
+
368
+ Install the matching extra if you see `MissingDependencyError`:
369
+
370
+ ```text
371
+ pip install py_storebridge[s3]
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Provider examples
377
+
378
+ ### AWS S3
379
+
380
+ ```python
381
+ storage = create_storage({
382
+ "provider": "s3",
383
+ "credentials": {
384
+ "access_key_id": "…",
385
+ "secret_access_key": "…",
386
+ "region": "us-east-1",
387
+ "bucket": "my-bucket",
388
+ "cdn_url": "https://cdn.example.com",
389
+ "custom_domain": "https://files.example.com",
390
+ },
391
+ "visibility": "public",
392
+ })
393
+ ```
394
+
395
+ ### Cloudflare R2
396
+
397
+ ```python
398
+ storage = create_storage({
399
+ "provider": "r2",
400
+ "credentials": {
401
+ "account_id": "…",
402
+ "access_key_id": "…",
403
+ "secret_access_key": "…",
404
+ "bucket": "my-bucket",
405
+ "cdn_url": "https://pub-xxx.r2.dev",
406
+ },
407
+ })
408
+ ```
409
+
410
+ ### Google Cloud Storage
411
+
412
+ ```python
413
+ storage = create_storage({
414
+ "provider": "gcs",
415
+ "credentials": {
416
+ "project_id": "my-gcp-project",
417
+ "bucket": "my-bucket",
418
+ "credentials": {
419
+ "client_email": "…",
420
+ "private_key": "-----BEGIN PRIVATE KEY-----\\n…\\n-----END PRIVATE KEY-----\\n",
421
+ },
422
+ },
423
+ })
424
+ ```
425
+
426
+ ### Azure Blob Storage
427
+
428
+ ```python
429
+ storage = create_storage({
430
+ "provider": "azure",
431
+ "credentials": {
432
+ "account_name": "…",
433
+ "account_key": "…",
434
+ "container_name": "uploads",
435
+ },
436
+ })
437
+ ```
438
+
439
+ ### DigitalOcean Spaces
440
+
441
+ ```python
442
+ storage = create_storage({
443
+ "provider": "spaces",
444
+ "credentials": {
445
+ "access_key_id": "…",
446
+ "secret_access_key": "…",
447
+ "region": "nyc3",
448
+ "bucket": "my-space",
449
+ "cdn_url": "https://my-space.nyc3.cdn.digitaloceanspaces.com",
450
+ },
451
+ })
452
+ ```
453
+
454
+ ### Backblaze B2
455
+
456
+ ```python
457
+ storage = create_storage({
458
+ "provider": "b2",
459
+ "credentials": {
460
+ "application_key_id": "…",
461
+ "application_key": "…",
462
+ "bucket": "my-bucket",
463
+ "region": "us-west-004",
464
+ },
465
+ })
466
+ ```
467
+
468
+ ### MinIO
469
+
470
+ ```python
471
+ storage = create_storage({
472
+ "provider": "minio",
473
+ "credentials": {
474
+ "end_point": "minio.example.com",
475
+ "port": 9000,
476
+ "use_ssl": True,
477
+ "access_key": "…",
478
+ "secret_key": "…",
479
+ "bucket": "my-bucket",
480
+ },
481
+ })
482
+ ```
483
+
484
+ ### Supabase Storage
485
+
486
+ ```python
487
+ storage = create_storage({
488
+ "provider": "supabase",
489
+ "credentials": {
490
+ "url": "https://xyz.supabase.co",
491
+ "service_role_key": "…",
492
+ "bucket": "avatars",
493
+ },
494
+ "visibility": "public",
495
+ })
496
+ ```
497
+
498
+ ### Firebase Storage
499
+
500
+ ```python
501
+ storage = create_storage({
502
+ "provider": "firebase",
503
+ "credentials": {
504
+ "project_id": "my-project",
505
+ "bucket": "my-project.appspot.com",
506
+ "service_account": {…}, # full service-account JSON
507
+ },
508
+ })
509
+ ```
510
+
511
+ ### Appwrite Storage
512
+
513
+ Appwrite file IDs cannot contain `/`. py_storebridge stores a stable SHA-256 of the key as the file ID and keeps the original key as the file name, so `users/profile.png` still works.
514
+
515
+ ```python
516
+ storage = create_storage({
517
+ "provider": "appwrite",
518
+ "credentials": {
519
+ "endpoint": "https://cloud.appwrite.io/v1",
520
+ "project_id": "…",
521
+ "api_key": "…",
522
+ "bucket_id": "…",
523
+ },
524
+ })
525
+ ```
526
+
527
+ ---
528
+
529
+ ## Custom provider
530
+
531
+ ```python
532
+ from py_storebridge import BaseProvider, create_storage_from_provider
533
+
534
+ class MyProvider(BaseProvider):
535
+ provider_name = "my-provider"
536
+
537
+ def _upload(self, options):
538
+ ...
539
+
540
+ storage = create_storage_from_provider(MyProvider())
541
+ ```
542
+
543
+ ---
544
+
545
+ ## Development
546
+
547
+ ```bash
548
+ python -m venv .venv
549
+ .venv\Scripts\activate # Windows
550
+ # source .venv/bin/activate # macOS / Linux
551
+
552
+ pip install -e ".[dev]"
553
+ pytest
554
+ ```
555
+
556
+ Publishing to PyPI is documented in [docs/HOW_TO_DEPLOY.md](docs/HOW_TO_DEPLOY.md).
557
+
558
+ ---
559
+
560
+ ## License
561
+
562
+ MIT © [Asif Khan](https://github.com/ak9347128658)