svc-infra 0.1.589__py3-none-any.whl → 0.1.706__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 (260) hide show
  1. svc_infra/__init__.py +58 -2
  2. svc_infra/apf_payments/README.md +732 -0
  3. svc_infra/apf_payments/models.py +133 -42
  4. svc_infra/apf_payments/provider/__init__.py +4 -0
  5. svc_infra/apf_payments/provider/aiydan.py +871 -0
  6. svc_infra/apf_payments/provider/base.py +30 -9
  7. svc_infra/apf_payments/provider/stripe.py +156 -62
  8. svc_infra/apf_payments/schemas.py +19 -10
  9. svc_infra/apf_payments/service.py +211 -68
  10. svc_infra/apf_payments/settings.py +27 -3
  11. svc_infra/api/__init__.py +61 -0
  12. svc_infra/api/fastapi/__init__.py +15 -0
  13. svc_infra/api/fastapi/admin/__init__.py +3 -0
  14. svc_infra/api/fastapi/admin/add.py +245 -0
  15. svc_infra/api/fastapi/apf_payments/router.py +145 -46
  16. svc_infra/api/fastapi/apf_payments/setup.py +26 -8
  17. svc_infra/api/fastapi/auth/__init__.py +65 -0
  18. svc_infra/api/fastapi/auth/_cookies.py +6 -2
  19. svc_infra/api/fastapi/auth/add.py +27 -14
  20. svc_infra/api/fastapi/auth/gaurd.py +104 -13
  21. svc_infra/api/fastapi/auth/mfa/models.py +3 -1
  22. svc_infra/api/fastapi/auth/mfa/pre_auth.py +10 -6
  23. svc_infra/api/fastapi/auth/mfa/router.py +15 -8
  24. svc_infra/api/fastapi/auth/mfa/security.py +1 -2
  25. svc_infra/api/fastapi/auth/mfa/utils.py +2 -1
  26. svc_infra/api/fastapi/auth/mfa/verify.py +9 -2
  27. svc_infra/api/fastapi/auth/policy.py +0 -1
  28. svc_infra/api/fastapi/auth/providers.py +3 -1
  29. svc_infra/api/fastapi/auth/routers/apikey_router.py +6 -6
  30. svc_infra/api/fastapi/auth/routers/oauth_router.py +214 -75
  31. svc_infra/api/fastapi/auth/routers/session_router.py +67 -0
  32. svc_infra/api/fastapi/auth/security.py +31 -10
  33. svc_infra/api/fastapi/auth/sender.py +8 -1
  34. svc_infra/api/fastapi/auth/settings.py +2 -0
  35. svc_infra/api/fastapi/auth/state.py +3 -1
  36. svc_infra/api/fastapi/auth/ws_security.py +275 -0
  37. svc_infra/api/fastapi/billing/router.py +73 -0
  38. svc_infra/api/fastapi/billing/setup.py +19 -0
  39. svc_infra/api/fastapi/cache/add.py +9 -5
  40. svc_infra/api/fastapi/db/__init__.py +5 -1
  41. svc_infra/api/fastapi/db/http.py +3 -1
  42. svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
  43. svc_infra/api/fastapi/db/nosql/mongo/add.py +47 -32
  44. svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +30 -11
  45. svc_infra/api/fastapi/db/sql/__init__.py +5 -1
  46. svc_infra/api/fastapi/db/sql/add.py +71 -26
  47. svc_infra/api/fastapi/db/sql/crud_router.py +210 -22
  48. svc_infra/api/fastapi/db/sql/health.py +3 -1
  49. svc_infra/api/fastapi/db/sql/session.py +18 -0
  50. svc_infra/api/fastapi/db/sql/users.py +29 -5
  51. svc_infra/api/fastapi/dependencies/ratelimit.py +130 -0
  52. svc_infra/api/fastapi/docs/add.py +173 -0
  53. svc_infra/api/fastapi/docs/landing.py +4 -2
  54. svc_infra/api/fastapi/docs/scoped.py +62 -15
  55. svc_infra/api/fastapi/dual/__init__.py +12 -2
  56. svc_infra/api/fastapi/dual/dualize.py +1 -1
  57. svc_infra/api/fastapi/dual/protected.py +126 -4
  58. svc_infra/api/fastapi/dual/public.py +25 -0
  59. svc_infra/api/fastapi/dual/router.py +40 -13
  60. svc_infra/api/fastapi/dx.py +33 -2
  61. svc_infra/api/fastapi/ease.py +10 -2
  62. svc_infra/api/fastapi/http/concurrency.py +2 -1
  63. svc_infra/api/fastapi/http/conditional.py +3 -1
  64. svc_infra/api/fastapi/middleware/debug.py +4 -1
  65. svc_infra/api/fastapi/middleware/errors/catchall.py +6 -2
  66. svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -1
  67. svc_infra/api/fastapi/middleware/errors/handlers.py +54 -8
  68. svc_infra/api/fastapi/middleware/graceful_shutdown.py +104 -0
  69. svc_infra/api/fastapi/middleware/idempotency.py +197 -70
  70. svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
  71. svc_infra/api/fastapi/middleware/optimistic_lock.py +42 -0
  72. svc_infra/api/fastapi/middleware/ratelimit.py +143 -31
  73. svc_infra/api/fastapi/middleware/ratelimit_store.py +111 -0
  74. svc_infra/api/fastapi/middleware/request_id.py +27 -11
  75. svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
  76. svc_infra/api/fastapi/middleware/timeout.py +177 -0
  77. svc_infra/api/fastapi/openapi/apply.py +5 -3
  78. svc_infra/api/fastapi/openapi/conventions.py +9 -2
  79. svc_infra/api/fastapi/openapi/mutators.py +165 -20
  80. svc_infra/api/fastapi/openapi/pipeline.py +1 -1
  81. svc_infra/api/fastapi/openapi/security.py +3 -1
  82. svc_infra/api/fastapi/ops/add.py +75 -0
  83. svc_infra/api/fastapi/pagination.py +47 -20
  84. svc_infra/api/fastapi/routers/__init__.py +43 -15
  85. svc_infra/api/fastapi/routers/ping.py +1 -0
  86. svc_infra/api/fastapi/setup.py +188 -56
  87. svc_infra/api/fastapi/tenancy/add.py +19 -0
  88. svc_infra/api/fastapi/tenancy/context.py +112 -0
  89. svc_infra/api/fastapi/versioned.py +101 -0
  90. svc_infra/app/README.md +5 -5
  91. svc_infra/app/__init__.py +3 -1
  92. svc_infra/app/env.py +69 -1
  93. svc_infra/app/logging/add.py +9 -2
  94. svc_infra/app/logging/formats.py +12 -5
  95. svc_infra/billing/__init__.py +23 -0
  96. svc_infra/billing/async_service.py +147 -0
  97. svc_infra/billing/jobs.py +241 -0
  98. svc_infra/billing/models.py +177 -0
  99. svc_infra/billing/quotas.py +103 -0
  100. svc_infra/billing/schemas.py +36 -0
  101. svc_infra/billing/service.py +123 -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 +9 -0
  106. svc_infra/cache/add.py +170 -0
  107. svc_infra/cache/backend.py +7 -6
  108. svc_infra/cache/decorators.py +81 -15
  109. svc_infra/cache/demo.py +2 -2
  110. svc_infra/cache/keys.py +24 -4
  111. svc_infra/cache/recache.py +26 -14
  112. svc_infra/cache/resources.py +14 -5
  113. svc_infra/cache/tags.py +19 -44
  114. svc_infra/cache/utils.py +3 -1
  115. svc_infra/cli/__init__.py +52 -8
  116. svc_infra/cli/__main__.py +4 -0
  117. svc_infra/cli/cmds/__init__.py +39 -2
  118. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +7 -4
  119. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +7 -5
  120. svc_infra/cli/cmds/db/ops_cmds.py +270 -0
  121. svc_infra/cli/cmds/db/sql/alembic_cmds.py +103 -18
  122. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +88 -0
  123. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
  124. svc_infra/cli/cmds/docs/docs_cmds.py +142 -0
  125. svc_infra/cli/cmds/dx/__init__.py +12 -0
  126. svc_infra/cli/cmds/dx/dx_cmds.py +116 -0
  127. svc_infra/cli/cmds/health/__init__.py +179 -0
  128. svc_infra/cli/cmds/health/health_cmds.py +8 -0
  129. svc_infra/cli/cmds/help.py +4 -0
  130. svc_infra/cli/cmds/jobs/__init__.py +1 -0
  131. svc_infra/cli/cmds/jobs/jobs_cmds.py +47 -0
  132. svc_infra/cli/cmds/obs/obs_cmds.py +36 -15
  133. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  134. svc_infra/cli/cmds/sdk/sdk_cmds.py +112 -0
  135. svc_infra/cli/foundation/runner.py +6 -2
  136. svc_infra/data/add.py +61 -0
  137. svc_infra/data/backup.py +58 -0
  138. svc_infra/data/erasure.py +45 -0
  139. svc_infra/data/fixtures.py +42 -0
  140. svc_infra/data/retention.py +61 -0
  141. svc_infra/db/__init__.py +15 -0
  142. svc_infra/db/crud_schema.py +9 -9
  143. svc_infra/db/inbox.py +67 -0
  144. svc_infra/db/nosql/__init__.py +3 -0
  145. svc_infra/db/nosql/core.py +30 -9
  146. svc_infra/db/nosql/indexes.py +3 -1
  147. svc_infra/db/nosql/management.py +1 -1
  148. svc_infra/db/nosql/mongo/README.md +13 -13
  149. svc_infra/db/nosql/mongo/client.py +19 -2
  150. svc_infra/db/nosql/mongo/settings.py +6 -2
  151. svc_infra/db/nosql/repository.py +35 -15
  152. svc_infra/db/nosql/resource.py +20 -3
  153. svc_infra/db/nosql/scaffold.py +9 -3
  154. svc_infra/db/nosql/service.py +3 -1
  155. svc_infra/db/nosql/types.py +6 -2
  156. svc_infra/db/ops.py +384 -0
  157. svc_infra/db/outbox.py +108 -0
  158. svc_infra/db/sql/apikey.py +37 -9
  159. svc_infra/db/sql/authref.py +9 -3
  160. svc_infra/db/sql/constants.py +12 -8
  161. svc_infra/db/sql/core.py +2 -2
  162. svc_infra/db/sql/management.py +11 -8
  163. svc_infra/db/sql/repository.py +99 -26
  164. svc_infra/db/sql/resource.py +5 -0
  165. svc_infra/db/sql/scaffold.py +6 -2
  166. svc_infra/db/sql/service.py +15 -5
  167. svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
  168. svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
  169. svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
  170. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
  171. svc_infra/db/sql/tenant.py +88 -0
  172. svc_infra/db/sql/uniq_hooks.py +9 -3
  173. svc_infra/db/sql/utils.py +138 -51
  174. svc_infra/db/sql/versioning.py +14 -0
  175. svc_infra/deploy/__init__.py +538 -0
  176. svc_infra/documents/__init__.py +100 -0
  177. svc_infra/documents/add.py +264 -0
  178. svc_infra/documents/ease.py +233 -0
  179. svc_infra/documents/models.py +114 -0
  180. svc_infra/documents/storage.py +264 -0
  181. svc_infra/dx/add.py +65 -0
  182. svc_infra/dx/changelog.py +74 -0
  183. svc_infra/dx/checks.py +68 -0
  184. svc_infra/exceptions.py +141 -0
  185. svc_infra/health/__init__.py +864 -0
  186. svc_infra/http/__init__.py +13 -0
  187. svc_infra/http/client.py +105 -0
  188. svc_infra/jobs/builtins/outbox_processor.py +40 -0
  189. svc_infra/jobs/builtins/webhook_delivery.py +95 -0
  190. svc_infra/jobs/easy.py +33 -0
  191. svc_infra/jobs/loader.py +50 -0
  192. svc_infra/jobs/queue.py +116 -0
  193. svc_infra/jobs/redis_queue.py +256 -0
  194. svc_infra/jobs/runner.py +79 -0
  195. svc_infra/jobs/scheduler.py +53 -0
  196. svc_infra/jobs/worker.py +40 -0
  197. svc_infra/loaders/__init__.py +186 -0
  198. svc_infra/loaders/base.py +142 -0
  199. svc_infra/loaders/github.py +311 -0
  200. svc_infra/loaders/models.py +147 -0
  201. svc_infra/loaders/url.py +235 -0
  202. svc_infra/logging/__init__.py +374 -0
  203. svc_infra/mcp/svc_infra_mcp.py +91 -33
  204. svc_infra/obs/README.md +2 -0
  205. svc_infra/obs/add.py +65 -9
  206. svc_infra/obs/cloud_dash.py +2 -1
  207. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  208. svc_infra/obs/metrics/__init__.py +52 -0
  209. svc_infra/obs/metrics/asgi.py +13 -7
  210. svc_infra/obs/metrics/http.py +9 -5
  211. svc_infra/obs/metrics/sqlalchemy.py +13 -9
  212. svc_infra/obs/metrics.py +53 -0
  213. svc_infra/obs/settings.py +6 -2
  214. svc_infra/security/add.py +217 -0
  215. svc_infra/security/audit.py +212 -0
  216. svc_infra/security/audit_service.py +74 -0
  217. svc_infra/security/headers.py +52 -0
  218. svc_infra/security/hibp.py +101 -0
  219. svc_infra/security/jwt_rotation.py +105 -0
  220. svc_infra/security/lockout.py +102 -0
  221. svc_infra/security/models.py +287 -0
  222. svc_infra/security/oauth_models.py +73 -0
  223. svc_infra/security/org_invites.py +130 -0
  224. svc_infra/security/passwords.py +79 -0
  225. svc_infra/security/permissions.py +171 -0
  226. svc_infra/security/session.py +98 -0
  227. svc_infra/security/signed_cookies.py +100 -0
  228. svc_infra/storage/__init__.py +93 -0
  229. svc_infra/storage/add.py +253 -0
  230. svc_infra/storage/backends/__init__.py +11 -0
  231. svc_infra/storage/backends/local.py +339 -0
  232. svc_infra/storage/backends/memory.py +216 -0
  233. svc_infra/storage/backends/s3.py +353 -0
  234. svc_infra/storage/base.py +239 -0
  235. svc_infra/storage/easy.py +185 -0
  236. svc_infra/storage/settings.py +195 -0
  237. svc_infra/testing/__init__.py +685 -0
  238. svc_infra/utils.py +7 -3
  239. svc_infra/webhooks/__init__.py +69 -0
  240. svc_infra/webhooks/add.py +339 -0
  241. svc_infra/webhooks/encryption.py +115 -0
  242. svc_infra/webhooks/fastapi.py +39 -0
  243. svc_infra/webhooks/router.py +55 -0
  244. svc_infra/webhooks/service.py +70 -0
  245. svc_infra/webhooks/signing.py +34 -0
  246. svc_infra/websocket/__init__.py +79 -0
  247. svc_infra/websocket/add.py +140 -0
  248. svc_infra/websocket/client.py +282 -0
  249. svc_infra/websocket/config.py +69 -0
  250. svc_infra/websocket/easy.py +76 -0
  251. svc_infra/websocket/exceptions.py +61 -0
  252. svc_infra/websocket/manager.py +344 -0
  253. svc_infra/websocket/models.py +49 -0
  254. svc_infra-0.1.706.dist-info/LICENSE +21 -0
  255. svc_infra-0.1.706.dist-info/METADATA +356 -0
  256. svc_infra-0.1.706.dist-info/RECORD +357 -0
  257. svc_infra-0.1.589.dist-info/METADATA +0 -79
  258. svc_infra-0.1.589.dist-info/RECORD +0 -234
  259. {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/WHEEL +0 -0
  260. {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,264 @@
1
+ """
2
+ FastAPI integration for document management.
3
+
4
+ Mounts document endpoints with authentication and storage backend integration.
5
+ Uses svc-infra's dual router pattern for public/protected routes.
6
+
7
+ Quick Start:
8
+ >>> from fastapi import FastAPI
9
+ >>> from svc_infra.documents import add_documents
10
+ >>>
11
+ >>> app = FastAPI()
12
+ >>> manager = add_documents(app)
13
+ >>>
14
+ >>> # Documents available at:
15
+ >>> # POST /documents/upload (protected)
16
+ >>> # GET /documents/{document_id} (protected)
17
+ >>> # GET /documents/list (protected)
18
+ >>> # DELETE /documents/{document_id} (protected)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import TYPE_CHECKING, Optional, cast
24
+
25
+ from fastapi import HTTPException, Request, Response
26
+
27
+ from svc_infra.documents.models import Document
28
+
29
+ if TYPE_CHECKING:
30
+ from fastapi import FastAPI
31
+
32
+ from svc_infra.storage.base import StorageBackend
33
+
34
+ from .ease import DocumentManager
35
+
36
+
37
+ def get_documents_manager(app: "FastAPI") -> "DocumentManager":
38
+ """
39
+ Dependency to get document manager from app state.
40
+
41
+ Args:
42
+ app: FastAPI application
43
+
44
+ Returns:
45
+ Document manager instance
46
+
47
+ Raises:
48
+ RuntimeError: If add_documents() has not been called
49
+ """
50
+ if not hasattr(app.state, "documents"):
51
+ raise RuntimeError("Documents not configured. Call add_documents(app) first.")
52
+ from .ease import DocumentManager
53
+
54
+ return cast(DocumentManager, app.state.documents)
55
+
56
+
57
+ def add_documents(
58
+ app: "FastAPI",
59
+ storage_backend: Optional["StorageBackend"] = None,
60
+ prefix: str = "/documents",
61
+ tags: Optional[list[str]] = None,
62
+ ) -> "DocumentManager":
63
+ """
64
+ Add document management endpoints to FastAPI app.
65
+
66
+ Mounts 4 endpoints:
67
+ 1. POST /documents/upload - Upload new document
68
+ 2. GET /documents/{document_id} - Get document metadata
69
+ 3. GET /documents/list - List user's documents
70
+ 4. DELETE /documents/{document_id} - Delete document
71
+
72
+ Args:
73
+ app: FastAPI application
74
+ storage_backend: Storage backend (auto-detected if None)
75
+ prefix: URL prefix for document endpoints (default: /documents)
76
+ tags: OpenAPI tags for documentation
77
+
78
+ Returns:
79
+ Document manager instance for programmatic access
80
+
81
+ Examples:
82
+ >>> from fastapi import FastAPI
83
+ >>> from svc_infra.documents import add_documents
84
+ >>>
85
+ >>> app = FastAPI()
86
+ >>> manager = add_documents(app)
87
+ >>>
88
+ >>> # Endpoints available at /documents/*
89
+ >>> # Use manager programmatically:
90
+ >>> doc = manager.upload("user_123", file_bytes, "file.pdf")
91
+
92
+ Notes:
93
+ - All routes require user authentication (uses dual router pattern)
94
+ - Stores manager on app.state.documents for route access
95
+ - Storage backend auto-detected from environment if not provided
96
+ """
97
+ from svc_infra.api.fastapi.dual.protected import user_router
98
+
99
+ from .ease import easy_documents
100
+
101
+ # Create manager with storage backend
102
+ manager = easy_documents(storage_backend)
103
+
104
+ # Store manager on app state
105
+ app.state.documents = manager
106
+
107
+ # Create protected router for document endpoints (requires user authentication)
108
+ router = user_router(prefix=prefix, tags=tags or ["Documents"])
109
+
110
+ # Route 1: Upload document
111
+ @router.post("/upload", response_model=Document)
112
+ async def upload_document(request: Request) -> Document:
113
+ """
114
+ Upload a document.
115
+
116
+ Args:
117
+ request: FastAPI request with form data
118
+ - user_id (required): User uploading the document
119
+ - file (required): File to upload
120
+ - Any additional fields become document metadata
121
+
122
+ Returns:
123
+ Document metadata with storage information
124
+
125
+ Examples:
126
+ ```bash
127
+ curl -X POST http://localhost:8000/documents/upload \\
128
+ -F "user_id=user_123" \\
129
+ -F "file=@contract.pdf" \\
130
+ -F "category=legal" \\
131
+ -F "year=2024"
132
+ ```
133
+ """
134
+ # Parse form data
135
+ form = await request.form()
136
+
137
+ # Extract required fields
138
+ user_id = form.get("user_id")
139
+ file = form.get("file")
140
+
141
+ if not user_id or not isinstance(user_id, str):
142
+ raise HTTPException(status_code=422, detail="user_id is required")
143
+
144
+ # NOTE: request.form() yields Starlette's UploadFile, not FastAPI's wrapper.
145
+ from starlette.datastructures import UploadFile as StarletteUploadFile
146
+
147
+ if not file or not isinstance(file, StarletteUploadFile):
148
+ raise HTTPException(status_code=422, detail="file is required")
149
+
150
+ # Read file content
151
+ file_content = await file.read()
152
+
153
+ # Build metadata from all other form fields
154
+ metadata = {}
155
+ for key, value in form.items():
156
+ if key not in ("user_id", "file"):
157
+ metadata[key] = value
158
+
159
+ # Upload document
160
+ doc = await manager.upload(
161
+ user_id=user_id,
162
+ file=file_content,
163
+ filename=file.filename or "unnamed",
164
+ metadata=metadata,
165
+ content_type=file.content_type,
166
+ )
167
+
168
+ return doc
169
+
170
+ # Route 2: List user's documents (must come before /{document_id} to avoid conflicts)
171
+ @router.get("/list")
172
+ async def list_user_documents(
173
+ user_id: str,
174
+ limit: int = 100,
175
+ offset: int = 0,
176
+ ) -> dict:
177
+ """
178
+ List user's documents with pagination.
179
+
180
+ Args:
181
+ user_id: User identifier
182
+ limit: Maximum number of documents (default: 100)
183
+ offset: Number of documents to skip (default: 0)
184
+
185
+ Returns:
186
+ Dict with "documents", "total", "limit", "offset" keys
187
+
188
+ Examples:
189
+ ```bash
190
+ # Get first page
191
+ curl "http://localhost:8000/documents/list?user_id=user_123&limit=20"
192
+
193
+ # Get second page
194
+ curl "http://localhost:8000/documents/list?user_id=user_123&limit=20&offset=20"
195
+ ```
196
+ """
197
+ # Get all docs for total count (before pagination)
198
+ all_docs = manager.list(user_id=user_id, limit=999999, offset=0)
199
+ total_count = len(all_docs)
200
+
201
+ # Get paginated docs
202
+ docs = manager.list(user_id=user_id, limit=limit, offset=offset)
203
+
204
+ return {
205
+ "documents": docs,
206
+ "total": total_count,
207
+ "limit": limit,
208
+ "offset": offset,
209
+ }
210
+
211
+ # Route 3: Get document metadata
212
+ @router.get("/{document_id}", response_model=Document)
213
+ async def get_document_metadata(document_id: str) -> Document:
214
+ """
215
+ Get document metadata.
216
+
217
+ Args:
218
+ document_id: Document identifier
219
+
220
+ Returns:
221
+ Document metadata
222
+
223
+ Raises:
224
+ HTTPException: 404 if document not found
225
+
226
+ Examples:
227
+ ```bash
228
+ curl http://localhost:8000/documents/doc_abc123
229
+ ```
230
+ """
231
+ doc = manager.get(document_id)
232
+ if not doc:
233
+ raise HTTPException(status_code=404, detail="Document not found")
234
+ return doc
235
+
236
+ # Route 4: Delete document
237
+ @router.delete("/{document_id}", status_code=204)
238
+ async def delete_document_route(document_id: str) -> Response:
239
+ """
240
+ Delete a document and its file content.
241
+
242
+ Args:
243
+ document_id: Document identifier
244
+
245
+ Returns:
246
+ 204 No Content on success
247
+
248
+ Raises:
249
+ HTTPException: 404 if document not found
250
+
251
+ Examples:
252
+ ```bash
253
+ curl -X DELETE http://localhost:8000/documents/doc_abc123
254
+ ```
255
+ """
256
+ success = await manager.delete(document_id)
257
+ if not success:
258
+ raise HTTPException(status_code=404, detail="Document not found")
259
+ return Response(status_code=204)
260
+
261
+ # Mount router
262
+ app.include_router(router)
263
+
264
+ return manager
@@ -0,0 +1,233 @@
1
+ """
2
+ Easy builder for document management.
3
+
4
+ Provides a simple interface for document operations with automatic
5
+ storage backend integration.
6
+
7
+ Quick Start:
8
+ >>> from svc_infra.documents import easy_documents
9
+ >>>
10
+ >>> # Create manager with auto-detected storage
11
+ >>> manager = easy_documents()
12
+ >>>
13
+ >>> # Upload document
14
+ >>> doc = manager.upload(
15
+ ... user_id="user_123",
16
+ ... file=file_bytes,
17
+ ... filename="document.pdf",
18
+ ... metadata={"category": "legal"}
19
+ ... )
20
+ >>>
21
+ >>> # Download document
22
+ >>> file_data = manager.download(doc.id)
23
+ >>>
24
+ >>> # List user's documents
25
+ >>> docs = manager.list(user_id="user_123")
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import TYPE_CHECKING, Dict, List, Optional
31
+
32
+ if TYPE_CHECKING:
33
+ from svc_infra.storage.base import StorageBackend
34
+
35
+ from .models import Document
36
+
37
+
38
+ class DocumentManager:
39
+ """
40
+ Document manager for upload, download, and metadata operations.
41
+
42
+ This class provides a convenient interface for all document operations,
43
+ automatically handling storage backend integration.
44
+
45
+ Attributes:
46
+ storage: Storage backend instance (S3, local, memory)
47
+
48
+ Examples:
49
+ >>> from svc_infra.storage import easy_storage
50
+ >>> from svc_infra.documents import DocumentManager
51
+ >>>
52
+ >>> storage = easy_storage()
53
+ >>> manager = DocumentManager(storage)
54
+ >>>
55
+ >>> # Upload
56
+ >>> doc = manager.upload("user_123", file_bytes, "contract.pdf")
57
+ >>>
58
+ >>> # Download
59
+ >>> file_data = manager.download(doc.id)
60
+ >>>
61
+ >>> # List
62
+ >>> docs = manager.list("user_123")
63
+ >>>
64
+ >>> # Delete
65
+ >>> manager.delete(doc.id)
66
+ """
67
+
68
+ def __init__(self, storage: "StorageBackend"):
69
+ """
70
+ Initialize document manager.
71
+
72
+ Args:
73
+ storage: Storage backend instance
74
+ """
75
+ self.storage = storage
76
+
77
+ async def upload(
78
+ self,
79
+ user_id: str,
80
+ file: bytes,
81
+ filename: str,
82
+ metadata: Optional[Dict] = None,
83
+ content_type: Optional[str] = None,
84
+ ) -> "Document":
85
+ """
86
+ Upload a document.
87
+
88
+ Args:
89
+ user_id: User uploading the document
90
+ file: File content as bytes
91
+ filename: Original filename
92
+ metadata: Optional custom metadata
93
+ content_type: Optional MIME type
94
+
95
+ Returns:
96
+ Document with storage information
97
+
98
+ Examples:
99
+ >>> doc = await manager.upload(
100
+ ... user_id="user_123",
101
+ ... file=pdf_bytes,
102
+ ... filename="contract.pdf",
103
+ ... metadata={"category": "legal", "year": 2024}
104
+ ... )
105
+ """
106
+ from .storage import upload_document
107
+
108
+ return await upload_document(
109
+ storage=self.storage,
110
+ user_id=user_id,
111
+ file=file,
112
+ filename=filename,
113
+ metadata=metadata,
114
+ content_type=content_type,
115
+ )
116
+
117
+ async def download(self, document_id: str) -> bytes:
118
+ """
119
+ Download a document by ID.
120
+
121
+ Args:
122
+ document_id: Document identifier
123
+
124
+ Returns:
125
+ Document file content
126
+
127
+ Examples:
128
+ >>> file_data = await manager.download("doc_abc123")
129
+ >>> with open("file.pdf", "wb") as f:
130
+ ... f.write(file_data)
131
+ """
132
+ from .storage import download_document
133
+
134
+ return await download_document(self.storage, document_id)
135
+
136
+ def get(self, document_id: str) -> Optional["Document"]:
137
+ """
138
+ Get document metadata by ID.
139
+
140
+ Args:
141
+ document_id: Document identifier
142
+
143
+ Returns:
144
+ Document metadata or None if not found
145
+
146
+ Examples:
147
+ >>> doc = manager.get("doc_abc123")
148
+ >>> if doc:
149
+ ... print(doc.filename, doc.file_size)
150
+ """
151
+ from .storage import get_document
152
+
153
+ return get_document(document_id)
154
+
155
+ async def delete(self, document_id: str) -> bool:
156
+ """
157
+ Delete a document.
158
+
159
+ Args:
160
+ document_id: Document identifier
161
+
162
+ Returns:
163
+ True if deleted, False if not found
164
+
165
+ Examples:
166
+ >>> success = await manager.delete("doc_abc123")
167
+ """
168
+ from .storage import delete_document
169
+
170
+ return await delete_document(self.storage, document_id)
171
+
172
+ def list(
173
+ self,
174
+ user_id: str,
175
+ limit: int = 100,
176
+ offset: int = 0,
177
+ ) -> List["Document"]:
178
+ """
179
+ List user's documents.
180
+
181
+ Args:
182
+ user_id: User identifier
183
+ limit: Maximum number of documents
184
+ offset: Number of documents to skip
185
+
186
+ Returns:
187
+ List of documents
188
+
189
+ Examples:
190
+ >>> # Get all documents
191
+ >>> docs = manager.list("user_123")
192
+ >>>
193
+ >>> # Paginated
194
+ >>> docs = manager.list("user_123", limit=20, offset=20)
195
+ """
196
+ from .storage import list_documents
197
+
198
+ return list_documents(user_id, limit, offset)
199
+
200
+
201
+ def easy_documents(storage: Optional["StorageBackend"] = None) -> DocumentManager:
202
+ """
203
+ Create a document manager with auto-configured storage.
204
+
205
+ Args:
206
+ storage: Optional storage backend (auto-detected if not provided)
207
+
208
+ Returns:
209
+ Document manager instance
210
+
211
+ Examples:
212
+ >>> # Auto-detect storage from environment
213
+ >>> manager = easy_documents()
214
+ >>>
215
+ >>> # Explicit storage backend
216
+ >>> from svc_infra.storage import easy_storage
217
+ >>> storage = easy_storage(backend="s3")
218
+ >>> manager = easy_documents(storage)
219
+ >>>
220
+ >>> # Use the manager
221
+ >>> doc = manager.upload("user_123", file_bytes, "file.pdf")
222
+
223
+ Notes:
224
+ - If storage is None, uses easy_storage() to auto-detect backend
225
+ - Auto-detection checks for Railway, S3, GCS credentials
226
+ - Falls back to MemoryBackend if no credentials found
227
+ """
228
+ if storage is None:
229
+ from svc_infra.storage import easy_storage
230
+
231
+ storage = easy_storage()
232
+
233
+ return DocumentManager(storage)
@@ -0,0 +1,114 @@
1
+ """
2
+ Generic document models for file management.
3
+
4
+ This module provides domain-agnostic document metadata models that work with
5
+ any type of file (PDFs, images, videos, etc.). For domain-specific extensions
6
+ (e.g., tax forms, medical records), see implementation examples in fin-infra.
7
+
8
+ Quick Start:
9
+ >>> from svc_infra.documents import Document
10
+ >>>
11
+ >>> doc = Document(
12
+ ... id="doc_abc123",
13
+ ... user_id="user_123",
14
+ ... filename="contract.pdf",
15
+ ... file_size=524288,
16
+ ... storage_path="documents/user_123/doc_abc123.pdf",
17
+ ... content_type="application/pdf",
18
+ ... metadata={"category": "legal", "year": 2024}
19
+ ... )
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from datetime import datetime
25
+ from typing import Any, Dict, Optional
26
+
27
+ from pydantic import BaseModel, ConfigDict, Field
28
+
29
+
30
+ class Document(BaseModel):
31
+ """
32
+ Generic document metadata and storage information.
33
+
34
+ This is a base model for any type of document. Domain-specific applications
35
+ should extend this model with additional fields as needed.
36
+
37
+ Attributes:
38
+ id: Unique document identifier (e.g., "doc_abc123")
39
+ user_id: User who owns/uploaded the document
40
+ filename: Original filename
41
+ file_size: File size in bytes
42
+ upload_date: When document was uploaded (UTC)
43
+ storage_path: Storage backend path/key
44
+ content_type: MIME type (e.g., "application/pdf", "image/jpeg")
45
+ checksum: Optional file checksum for integrity validation
46
+ metadata: Flexible metadata dictionary for custom fields
47
+
48
+ Examples:
49
+ >>> # Legal document
50
+ >>> doc = Document(
51
+ ... id="doc_abc123",
52
+ ... user_id="user_123",
53
+ ... filename="employment_contract.pdf",
54
+ ... file_size=524288,
55
+ ... storage_path="documents/user_123/2024/legal/doc_abc123.pdf",
56
+ ... content_type="application/pdf",
57
+ ... metadata={"category": "legal", "type": "contract", "year": 2024}
58
+ ... )
59
+ >>>
60
+ >>> # Medical record
61
+ >>> doc = Document(
62
+ ... id="doc_def456",
63
+ ... user_id="patient_789",
64
+ ... filename="lab_results.pdf",
65
+ ... file_size=102400,
66
+ ... storage_path="documents/patient_789/2024/medical/doc_def456.pdf",
67
+ ... content_type="application/pdf",
68
+ ... metadata={"category": "medical", "test_type": "blood_work", "date": "2024-11-18"}
69
+ ... )
70
+ >>>
71
+ >>> # Invoice
72
+ >>> doc = Document(
73
+ ... id="doc_ghi789",
74
+ ... user_id="company_456",
75
+ ... filename="invoice_2024_11.pdf",
76
+ ... file_size=256000,
77
+ ... storage_path="documents/company_456/invoices/doc_ghi789.pdf",
78
+ ... content_type="application/pdf",
79
+ ... metadata={"category": "invoice", "amount": 1500.00, "month": "2024-11"}
80
+ ... )
81
+ """
82
+
83
+ model_config = ConfigDict(
84
+ json_schema_extra={
85
+ "example": {
86
+ "id": "doc_abc123",
87
+ "user_id": "user_123",
88
+ "filename": "contract.pdf",
89
+ "file_size": 524288,
90
+ "upload_date": "2025-11-18T14:30:00Z",
91
+ "storage_path": "documents/user_123/2024/doc_abc123.pdf",
92
+ "content_type": "application/pdf",
93
+ "checksum": "sha256:abc123...",
94
+ "metadata": {"category": "legal", "year": 2024, "tags": ["important"]},
95
+ }
96
+ }
97
+ )
98
+
99
+ id: str = Field(..., description="Unique document identifier")
100
+ user_id: str = Field(..., description="User who owns this document")
101
+ filename: str = Field(..., description="Original filename")
102
+ file_size: int = Field(..., description="File size in bytes", ge=0)
103
+ upload_date: datetime = Field(
104
+ default_factory=datetime.utcnow, description="Upload timestamp (UTC)"
105
+ )
106
+ storage_path: str = Field(..., description="Storage backend path/key")
107
+ content_type: str = Field(..., description="MIME type (e.g., application/pdf)")
108
+ checksum: Optional[str] = Field(
109
+ None, description="File checksum for integrity validation (e.g., sha256:...)"
110
+ )
111
+ metadata: Dict[str, Any] = Field(
112
+ default_factory=dict,
113
+ description="Flexible metadata for custom fields (category, tags, dates, etc.)",
114
+ )