supython 0.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.
Files changed (200) hide show
  1. supython/__init__.py +24 -0
  2. supython/admin/__init__.py +3 -0
  3. supython/admin/api/__init__.py +24 -0
  4. supython/admin/api/auth.py +118 -0
  5. supython/admin/api/auth_templates.py +67 -0
  6. supython/admin/api/auth_users.py +225 -0
  7. supython/admin/api/db.py +174 -0
  8. supython/admin/api/functions.py +92 -0
  9. supython/admin/api/jobs.py +192 -0
  10. supython/admin/api/ops.py +224 -0
  11. supython/admin/api/realtime.py +281 -0
  12. supython/admin/api/service_auth.py +49 -0
  13. supython/admin/api/service_auth_templates.py +83 -0
  14. supython/admin/api/service_auth_users.py +346 -0
  15. supython/admin/api/service_db.py +214 -0
  16. supython/admin/api/service_functions.py +287 -0
  17. supython/admin/api/service_jobs.py +282 -0
  18. supython/admin/api/service_ops.py +213 -0
  19. supython/admin/api/service_realtime.py +30 -0
  20. supython/admin/api/service_storage.py +220 -0
  21. supython/admin/api/storage.py +117 -0
  22. supython/admin/api/system.py +37 -0
  23. supython/admin/audit.py +29 -0
  24. supython/admin/deps.py +22 -0
  25. supython/admin/errors.py +16 -0
  26. supython/admin/schemas.py +310 -0
  27. supython/admin/session.py +52 -0
  28. supython/admin/spa.py +38 -0
  29. supython/admin/static/assets/Alert-dluGVkos.js +49 -0
  30. supython/admin/static/assets/Audit-Njung3HI.js +2 -0
  31. supython/admin/static/assets/Backups-DzPlFgrm.js +2 -0
  32. supython/admin/static/assets/Buckets-ByacGkU1.js +2 -0
  33. supython/admin/static/assets/Channels-BoIuTtam.js +353 -0
  34. supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +2 -0
  35. supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +2 -0
  36. supython/admin/static/assets/Crons-B67vc39F.js +2 -0
  37. supython/admin/static/assets/DashboardView-CUTFVL6k.js +2 -0
  38. supython/admin/static/assets/DataTable-COAAWEft.js +747 -0
  39. supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +75 -0
  40. supython/admin/static/assets/DrawerContent-TpYTFgF1.js +139 -0
  41. supython/admin/static/assets/Empty-cr2r7e2u.js +25 -0
  42. supython/admin/static/assets/EmptyState-DeDck-OL.js +2 -0
  43. supython/admin/static/assets/Grid-hFkp9F4P.js +2 -0
  44. supython/admin/static/assets/Input-DppYTq9C.js +259 -0
  45. supython/admin/static/assets/Invoke-DW3Nveeh.js +2 -0
  46. supython/admin/static/assets/JsonField-DibyJgun.js +2 -0
  47. supython/admin/static/assets/LoginView-BjLyE3Ds.css +1 -0
  48. supython/admin/static/assets/LoginView-CoOjECT_.js +111 -0
  49. supython/admin/static/assets/Logs-D9WYrnIT.js +2 -0
  50. supython/admin/static/assets/Logs-DS1XPa0h.css +1 -0
  51. supython/admin/static/assets/Migrations-DOSC2ddQ.js +2 -0
  52. supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +2 -0
  53. supython/admin/static/assets/Queue-CywZs6vI.js +2 -0
  54. supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +2 -0
  55. supython/admin/static/assets/RlsEditor-BSlH9vSc.js +2 -0
  56. supython/admin/static/assets/Routes-BiLXE49D.js +2 -0
  57. supython/admin/static/assets/Routes-C-ianIGD.css +1 -0
  58. supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +1 -0
  59. supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +2 -0
  60. supython/admin/static/assets/Select-DIzZyRZb.js +434 -0
  61. supython/admin/static/assets/Space-n5-XcguU.js +400 -0
  62. supython/admin/static/assets/SqlEditor-b8pTsILY.js +3 -0
  63. supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +104 -0
  64. supython/admin/static/assets/TableData-CQIagLKn.js +2 -0
  65. supython/admin/static/assets/Tag-D1fOKpTH.js +72 -0
  66. supython/admin/static/assets/Templates-BS-ugkdq.js +2 -0
  67. supython/admin/static/assets/Thing-CEAniuMg.js +107 -0
  68. supython/admin/static/assets/Users-wzwajhlh.js +2 -0
  69. supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +1 -0
  70. supython/admin/static/assets/dist-VXIJLCYq.js +13 -0
  71. supython/admin/static/assets/format-length-CGCY1rMh.js +2 -0
  72. supython/admin/static/assets/get-Ca6unauB.js +2 -0
  73. supython/admin/static/assets/index-CeE6v959.js +951 -0
  74. supython/admin/static/assets/pinia-COXwfrOX.js +2 -0
  75. supython/admin/static/assets/resources-Bt6thQCD.js +44 -0
  76. supython/admin/static/assets/use-locale-mtgM0a3a.js +2 -0
  77. supython/admin/static/assets/use-merged-state-BvhkaHNX.js +2 -0
  78. supython/admin/static/assets/useConfirm-tMjvBFXR.js +2 -0
  79. supython/admin/static/assets/useResource-C_rJCY8C.js +2 -0
  80. supython/admin/static/assets/useTable-CnZc5zhi.js +363 -0
  81. supython/admin/static/assets/useTable-Dg0XlRlq.css +1 -0
  82. supython/admin/static/assets/useToast-DsZKx0IX.js +2 -0
  83. supython/admin/static/assets/utils-sbXoq7Ir.js +2 -0
  84. supython/admin/static/favicon.svg +1 -0
  85. supython/admin/static/icons.svg +24 -0
  86. supython/admin/static/index.html +24 -0
  87. supython/app.py +162 -0
  88. supython/auth/__init__.py +3 -0
  89. supython/auth/_email_job.py +11 -0
  90. supython/auth/providers/__init__.py +34 -0
  91. supython/auth/providers/github.py +22 -0
  92. supython/auth/providers/google.py +19 -0
  93. supython/auth/providers/oauth.py +56 -0
  94. supython/auth/providers/registry.py +16 -0
  95. supython/auth/ratelimit.py +39 -0
  96. supython/auth/router.py +282 -0
  97. supython/auth/schemas.py +79 -0
  98. supython/auth/service.py +587 -0
  99. supython/backups/__init__.py +24 -0
  100. supython/backups/_backup_job.py +170 -0
  101. supython/backups/schemas.py +18 -0
  102. supython/backups/service.py +217 -0
  103. supython/body_size.py +184 -0
  104. supython/cli.py +1663 -0
  105. supython/client/__init__.py +67 -0
  106. supython/client/_auth.py +249 -0
  107. supython/client/_client.py +145 -0
  108. supython/client/_config.py +92 -0
  109. supython/client/_functions.py +69 -0
  110. supython/client/_storage.py +255 -0
  111. supython/client/py.typed +0 -0
  112. supython/db.py +151 -0
  113. supython/db_admin.py +8 -0
  114. supython/extensions.py +36 -0
  115. supython/functions/__init__.py +19 -0
  116. supython/functions/context.py +262 -0
  117. supython/functions/loader.py +307 -0
  118. supython/functions/router.py +228 -0
  119. supython/functions/schemas.py +50 -0
  120. supython/gen/__init__.py +5 -0
  121. supython/gen/_introspect.py +137 -0
  122. supython/gen/types_py.py +270 -0
  123. supython/gen/types_ts.py +365 -0
  124. supython/health.py +229 -0
  125. supython/hooks.py +117 -0
  126. supython/jobs/__init__.py +31 -0
  127. supython/jobs/backends.py +97 -0
  128. supython/jobs/context.py +58 -0
  129. supython/jobs/cron.py +152 -0
  130. supython/jobs/cron_inproc.py +119 -0
  131. supython/jobs/decorators.py +76 -0
  132. supython/jobs/registry.py +79 -0
  133. supython/jobs/router.py +136 -0
  134. supython/jobs/schemas.py +92 -0
  135. supython/jobs/service.py +311 -0
  136. supython/jobs/worker.py +219 -0
  137. supython/jwks.py +257 -0
  138. supython/keyset.py +279 -0
  139. supython/logging_config.py +291 -0
  140. supython/mail.py +33 -0
  141. supython/mailer.py +65 -0
  142. supython/migrate.py +81 -0
  143. supython/migrations/0001_extensions_and_roles.sql +46 -0
  144. supython/migrations/0002_auth_schema.sql +66 -0
  145. supython/migrations/0003_demo_todos.sql +42 -0
  146. supython/migrations/0004_auth_v0_2.sql +47 -0
  147. supython/migrations/0005_storage_schema.sql +117 -0
  148. supython/migrations/0006_realtime_schema.sql +206 -0
  149. supython/migrations/0007_jobs_schema.sql +254 -0
  150. supython/migrations/0008_jobs_last_error.sql +56 -0
  151. supython/migrations/0009_auth_rate_limits.sql +33 -0
  152. supython/migrations/0010_worker_heartbeat.sql +14 -0
  153. supython/migrations/0011_admin_schema.sql +45 -0
  154. supython/migrations/0012_auth_banned_until.sql +10 -0
  155. supython/migrations/0013_email_templates.sql +19 -0
  156. supython/migrations/0014_realtime_payload_warning.sql +96 -0
  157. supython/migrations/0015_backups_schema.sql +14 -0
  158. supython/passwords.py +15 -0
  159. supython/realtime/__init__.py +6 -0
  160. supython/realtime/broker.py +814 -0
  161. supython/realtime/protocol.py +234 -0
  162. supython/realtime/router.py +184 -0
  163. supython/realtime/schemas.py +207 -0
  164. supython/realtime/service.py +261 -0
  165. supython/realtime/topics.py +175 -0
  166. supython/realtime/websocket.py +586 -0
  167. supython/scaffold/__init__.py +5 -0
  168. supython/scaffold/init_project.py +144 -0
  169. supython/scaffold/templates/Caddyfile.tmpl +4 -0
  170. supython/scaffold/templates/README.md.tmpl +22 -0
  171. supython/scaffold/templates/apps_hooks.py.tmpl +11 -0
  172. supython/scaffold/templates/apps_jobs.py.tmpl +8 -0
  173. supython/scaffold/templates/asgi.py.tmpl +14 -0
  174. supython/scaffold/templates/docker-compose.prod.yml.tmpl +84 -0
  175. supython/scaffold/templates/docker-compose.yml.tmpl +45 -0
  176. supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +9 -0
  177. supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +3 -0
  178. supython/scaffold/templates/env.example.tmpl +168 -0
  179. supython/scaffold/templates/functions_README.md.tmpl +21 -0
  180. supython/scaffold/templates/gitignore.tmpl +14 -0
  181. supython/scaffold/templates/manage.py.tmpl +11 -0
  182. supython/scaffold/templates/migrations/.gitkeep +0 -0
  183. supython/scaffold/templates/package_init.py.tmpl +1 -0
  184. supython/scaffold/templates/settings.py.tmpl +31 -0
  185. supython/secretset.py +347 -0
  186. supython/security_headers.py +78 -0
  187. supython/settings.py +244 -0
  188. supython/settings_module.py +117 -0
  189. supython/storage/__init__.py +5 -0
  190. supython/storage/backends.py +392 -0
  191. supython/storage/router.py +341 -0
  192. supython/storage/schemas.py +50 -0
  193. supython/storage/service.py +445 -0
  194. supython/storage/signing.py +119 -0
  195. supython/tokens.py +85 -0
  196. supython-0.1.0.dist-info/METADATA +756 -0
  197. supython-0.1.0.dist-info/RECORD +200 -0
  198. supython-0.1.0.dist-info/WHEEL +4 -0
  199. supython-0.1.0.dist-info/entry_points.txt +2 -0
  200. supython-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,341 @@
1
+ """HTTP surface for storage. Thin: parse, delegate, translate, stream."""
2
+
3
+ from collections.abc import AsyncIterator
4
+ from typing import Annotated, Any
5
+
6
+ import jwt
7
+ from fastapi import (
8
+ APIRouter,
9
+ Depends,
10
+ File,
11
+ Header,
12
+ HTTPException,
13
+ Query,
14
+ UploadFile,
15
+ status,
16
+ )
17
+ from fastapi.responses import StreamingResponse
18
+
19
+ from .. import db, tokens
20
+ from . import service
21
+ from .backends import ObjectStream, get_backend
22
+ from .schemas import (
23
+ BucketResponse,
24
+ CreateBucketRequest,
25
+ ObjectResponse,
26
+ SignedUrlRequest,
27
+ SignedUrlResponse,
28
+ )
29
+
30
+ router = APIRouter(prefix="/storage/v1", tags=["storage"])
31
+
32
+
33
+ _UPLOAD_CHUNK = 64 * 1024
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Helpers
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ def _storage_error(exc: service.StorageError) -> HTTPException:
42
+ return HTTPException(
43
+ status_code=exc.status,
44
+ detail={"code": exc.code, "message": exc.message},
45
+ )
46
+
47
+
48
+ async def _current_claims(
49
+ authorization: Annotated[str | None, Header()] = None,
50
+ ) -> dict[str, Any]:
51
+ """Return the decoded JWT claims so we can pump them into ``as_role``."""
52
+ if not authorization or not authorization.lower().startswith("bearer "):
53
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing bearer token")
54
+ token = authorization.split(" ", 1)[1]
55
+ try:
56
+ return tokens.decode_access_token(token)
57
+ except jwt.PyJWTError as exc:
58
+ raise HTTPException(
59
+ status.HTTP_401_UNAUTHORIZED, f"Invalid token: {exc}"
60
+ ) from exc
61
+
62
+
63
+ def _claims_role(claims: dict[str, Any]) -> str:
64
+ role = claims.get("role")
65
+ if not isinstance(role, str) or not role:
66
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Token missing role claim")
67
+ return role
68
+
69
+
70
+ def _parse_range(header: str | None, total_unknown: bool = True) -> tuple[int, int | None] | None:
71
+ """Parse an HTTP ``Range: bytes=START-END`` header. Returns None if absent.
72
+
73
+ Only single-range, byte-unit requests are supported. Multi-range requests
74
+ are intentionally rejected — they materially complicate streaming and are
75
+ rare in practice.
76
+ """
77
+ if not header:
78
+ return None
79
+ if not header.lower().startswith("bytes="):
80
+ raise HTTPException(
81
+ status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
82
+ "Only byte ranges are supported",
83
+ )
84
+ spec = header.split("=", 1)[1].strip()
85
+ if "," in spec:
86
+ raise HTTPException(
87
+ status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
88
+ "Multi-range requests are not supported",
89
+ )
90
+ if "-" not in spec:
91
+ raise HTTPException(
92
+ status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
93
+ "Malformed range",
94
+ )
95
+ start_s, end_s = spec.split("-", 1)
96
+ try:
97
+ start = int(start_s) if start_s else 0
98
+ end: int | None = int(end_s) if end_s else None
99
+ except ValueError as exc:
100
+ raise HTTPException(
101
+ status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
102
+ "Malformed range",
103
+ ) from exc
104
+ return start, end
105
+
106
+
107
+ def _stream_response(stream: ObjectStream, *, filename: str | None = None) -> StreamingResponse:
108
+ headers: dict[str, str] = {
109
+ "content-length": str(stream.content_length),
110
+ "accept-ranges": "bytes",
111
+ }
112
+ if stream.etag:
113
+ headers["etag"] = stream.etag
114
+ if stream.content_range:
115
+ headers["content-range"] = stream.content_range
116
+ if filename:
117
+ headers["content-disposition"] = f'inline; filename="{filename}"'
118
+ media_type = stream.content_type or "application/octet-stream"
119
+ return StreamingResponse(
120
+ stream.iterator,
121
+ status_code=stream.status_code,
122
+ headers=headers,
123
+ media_type=media_type,
124
+ )
125
+
126
+
127
+ async def _file_iterator(file: UploadFile) -> AsyncIterator[bytes]:
128
+ while True:
129
+ chunk = await file.read(_UPLOAD_CHUNK)
130
+ if not chunk:
131
+ break
132
+ yield chunk
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Bucket endpoints
137
+ # ---------------------------------------------------------------------------
138
+
139
+
140
+ @router.post("/bucket", response_model=BucketResponse, status_code=201)
141
+ async def create_bucket(
142
+ payload: CreateBucketRequest,
143
+ claims: Annotated[dict[str, Any], Depends(_current_claims)],
144
+ ) -> BucketResponse:
145
+ try:
146
+ async with db.as_role(_claims_role(claims), claims) as conn:
147
+ return await service.create_bucket(
148
+ conn,
149
+ name=payload.name,
150
+ public=payload.public,
151
+ file_size_limit=payload.file_size_limit,
152
+ allowed_mime_types=payload.allowed_mime_types,
153
+ )
154
+ except service.StorageError as exc:
155
+ raise _storage_error(exc) from exc
156
+
157
+
158
+ @router.get("/bucket", response_model=list[BucketResponse])
159
+ async def list_buckets(
160
+ claims: Annotated[dict[str, Any], Depends(_current_claims)],
161
+ ) -> list[BucketResponse]:
162
+ async with db.as_role(_claims_role(claims), claims) as conn:
163
+ return await service.list_buckets(conn)
164
+
165
+
166
+ @router.get("/bucket/{name}", response_model=BucketResponse)
167
+ async def get_bucket(
168
+ name: str,
169
+ claims: Annotated[dict[str, Any], Depends(_current_claims)],
170
+ ) -> BucketResponse:
171
+ try:
172
+ async with db.as_role(_claims_role(claims), claims) as conn:
173
+ return await service.get_bucket(conn, name)
174
+ except service.StorageError as exc:
175
+ raise _storage_error(exc) from exc
176
+
177
+
178
+ @router.delete("/bucket/{name}", status_code=204)
179
+ async def delete_bucket(
180
+ name: str,
181
+ claims: Annotated[dict[str, Any], Depends(_current_claims)],
182
+ ) -> None:
183
+ backend = get_backend()
184
+ try:
185
+ async with db.as_role(_claims_role(claims), claims) as conn:
186
+ await service.delete_bucket(conn, backend, name)
187
+ except service.StorageError as exc:
188
+ raise _storage_error(exc) from exc
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Signed URLs (registered before generic /{bucket}/{path} to avoid shadowing)
193
+ # ---------------------------------------------------------------------------
194
+
195
+
196
+ @router.post(
197
+ "/object/sign/{bucket}/{path:path}",
198
+ response_model=SignedUrlResponse,
199
+ )
200
+ async def sign_object(
201
+ bucket: str,
202
+ path: str,
203
+ claims: Annotated[dict[str, Any], Depends(_current_claims)],
204
+ payload: SignedUrlRequest | None = None,
205
+ ) -> SignedUrlResponse:
206
+ expires_in = payload.expires_in if payload else None
207
+ try:
208
+ async with db.as_role(_claims_role(claims), claims) as conn:
209
+ return await service.issue_signed_url(
210
+ conn,
211
+ bucket_name=bucket,
212
+ path=path,
213
+ expires_in=expires_in,
214
+ )
215
+ except service.StorageError as exc:
216
+ raise _storage_error(exc) from exc
217
+
218
+
219
+ @router.get("/object/signed/{bucket}/{path:path}")
220
+ async def fetch_signed_object(
221
+ bucket: str,
222
+ path: str,
223
+ token: Annotated[str, Query(description="HMAC token from /object/sign/...")],
224
+ range_header: Annotated[str | None, Header(alias="range")] = None,
225
+ ) -> StreamingResponse:
226
+ byte_range = _parse_range(range_header)
227
+ backend = get_backend()
228
+ try:
229
+ async with db.acquire() as conn:
230
+ obj, stream = await service.verify_signed_download(
231
+ conn,
232
+ backend,
233
+ bucket_name=bucket,
234
+ path=path,
235
+ token=token,
236
+ byte_range=byte_range,
237
+ )
238
+ except service.StorageError as exc:
239
+ raise _storage_error(exc) from exc
240
+ return _stream_response(stream, filename=obj.name.rsplit("/", 1)[-1])
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Public buckets (registered before generic /{bucket}/{path} to avoid shadowing)
245
+ # ---------------------------------------------------------------------------
246
+
247
+
248
+ @router.get("/object/public/{bucket}/{path:path}")
249
+ async def fetch_public_object(
250
+ bucket: str,
251
+ path: str,
252
+ range_header: Annotated[str | None, Header(alias="range")] = None,
253
+ ) -> StreamingResponse:
254
+ byte_range = _parse_range(range_header)
255
+ backend = get_backend()
256
+ try:
257
+ async with db.as_role("anon", {"role": "anon"}) as conn:
258
+ obj, stream = await service.download_public_object(
259
+ conn,
260
+ backend,
261
+ bucket_name=bucket,
262
+ path=path,
263
+ byte_range=byte_range,
264
+ )
265
+ except service.StorageError as exc:
266
+ raise _storage_error(exc) from exc
267
+ return _stream_response(stream, filename=obj.name.rsplit("/", 1)[-1])
268
+
269
+
270
+ # ---------------------------------------------------------------------------
271
+ # Object endpoints (generic wildcard routes registered last)
272
+ # ---------------------------------------------------------------------------
273
+
274
+
275
+ @router.post(
276
+ "/object/{bucket}/{path:path}",
277
+ response_model=ObjectResponse,
278
+ status_code=201,
279
+ )
280
+ async def upload_object(
281
+ bucket: str,
282
+ path: str,
283
+ claims: Annotated[dict[str, Any], Depends(_current_claims)],
284
+ file: Annotated[UploadFile, File(description="The file to upload")],
285
+ ) -> ObjectResponse:
286
+ backend = get_backend()
287
+ content_type = file.content_type
288
+ try:
289
+ async with db.as_role(_claims_role(claims), claims) as conn:
290
+ return await service.upload_object(
291
+ conn,
292
+ backend,
293
+ bucket_name=bucket,
294
+ path=path,
295
+ data=_file_iterator(file),
296
+ content_type=content_type,
297
+ )
298
+ except service.StorageError as exc:
299
+ raise _storage_error(exc) from exc
300
+
301
+
302
+ @router.get("/object/{bucket}/{path:path}")
303
+ async def download_object(
304
+ bucket: str,
305
+ path: str,
306
+ claims: Annotated[dict[str, Any], Depends(_current_claims)],
307
+ range_header: Annotated[str | None, Header(alias="range")] = None,
308
+ ) -> StreamingResponse:
309
+ byte_range = _parse_range(range_header)
310
+ backend = get_backend()
311
+ try:
312
+ async with db.as_role(_claims_role(claims), claims) as conn:
313
+ obj, stream = await service.download_object(
314
+ conn,
315
+ backend,
316
+ bucket_name=bucket,
317
+ path=path,
318
+ byte_range=byte_range,
319
+ )
320
+ except service.StorageError as exc:
321
+ raise _storage_error(exc) from exc
322
+ return _stream_response(stream, filename=obj.name.rsplit("/", 1)[-1])
323
+
324
+
325
+ @router.delete("/object/{bucket}/{path:path}", status_code=204)
326
+ async def delete_object(
327
+ bucket: str,
328
+ path: str,
329
+ claims: Annotated[dict[str, Any], Depends(_current_claims)],
330
+ ) -> None:
331
+ backend = get_backend()
332
+ try:
333
+ async with db.as_role(_claims_role(claims), claims) as conn:
334
+ await service.delete_object(
335
+ conn,
336
+ backend,
337
+ bucket_name=bucket,
338
+ path=path,
339
+ )
340
+ except service.StorageError as exc:
341
+ raise _storage_error(exc) from exc
@@ -0,0 +1,50 @@
1
+ from datetime import datetime
2
+ from uuid import UUID
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class CreateBucketRequest(BaseModel):
8
+ name: str = Field(min_length=1, max_length=63, pattern=r"^[a-z0-9][a-z0-9_-]{0,62}$")
9
+ public: bool = False
10
+ file_size_limit: int | None = Field(default=None, ge=1)
11
+ allowed_mime_types: list[str] | None = None
12
+
13
+
14
+ class BucketResponse(BaseModel):
15
+ id: UUID
16
+ name: str
17
+ owner: UUID | None
18
+ public: bool
19
+ file_size_limit: int | None
20
+ allowed_mime_types: list[str] | None
21
+ created_at: datetime
22
+ updated_at: datetime
23
+
24
+
25
+ class ObjectResponse(BaseModel):
26
+ id: UUID
27
+ bucket_id: UUID
28
+ bucket: str
29
+ name: str
30
+ owner: UUID
31
+ size: int
32
+ mime_type: str | None
33
+ etag: str | None
34
+ created_at: datetime
35
+ updated_at: datetime
36
+
37
+
38
+ class SignedUrlRequest(BaseModel):
39
+ expires_in: int | None = Field(
40
+ default=None,
41
+ ge=1,
42
+ description="Lifetime of the signed URL in seconds. Defaults to settings.",
43
+ )
44
+
45
+
46
+ class SignedUrlResponse(BaseModel):
47
+ signed_url: str
48
+ token: str
49
+ expires_at: datetime
50
+ expires_in: int