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,263 @@
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, 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
+
53
+ return cast("DocumentManager", app.state.documents)
54
+
55
+
56
+ def add_documents(
57
+ app: FastAPI,
58
+ storage_backend: StorageBackend | None = None,
59
+ prefix: str = "/documents",
60
+ tags: list[str] | None = None,
61
+ ) -> DocumentManager:
62
+ """
63
+ Add document management endpoints to FastAPI app.
64
+
65
+ Mounts 4 endpoints:
66
+ 1. POST /documents/upload - Upload new document
67
+ 2. GET /documents/{document_id} - Get document metadata
68
+ 3. GET /documents/list - List user's documents
69
+ 4. DELETE /documents/{document_id} - Delete document
70
+
71
+ Args:
72
+ app: FastAPI application
73
+ storage_backend: Storage backend (auto-detected if None)
74
+ prefix: URL prefix for document endpoints (default: /documents)
75
+ tags: OpenAPI tags for documentation
76
+
77
+ Returns:
78
+ Document manager instance for programmatic access
79
+
80
+ Examples:
81
+ >>> from fastapi import FastAPI
82
+ >>> from svc_infra.documents import add_documents
83
+ >>>
84
+ >>> app = FastAPI()
85
+ >>> manager = add_documents(app)
86
+ >>>
87
+ >>> # Endpoints available at /documents/*
88
+ >>> # Use manager programmatically:
89
+ >>> doc = manager.upload("user_123", file_bytes, "file.pdf")
90
+
91
+ Notes:
92
+ - All routes require user authentication (uses dual router pattern)
93
+ - Stores manager on app.state.documents for route access
94
+ - Storage backend auto-detected from environment if not provided
95
+ """
96
+ from svc_infra.api.fastapi.dual.protected import user_router
97
+
98
+ from .ease import easy_documents
99
+
100
+ # Create manager with storage backend
101
+ manager = easy_documents(storage_backend)
102
+
103
+ # Store manager on app state
104
+ app.state.documents = manager
105
+
106
+ # Create protected router for document endpoints (requires user authentication)
107
+ router = user_router(prefix=prefix, tags=tags or ["Documents"])
108
+
109
+ # Route 1: Upload document
110
+ @router.post("/upload", response_model=Document)
111
+ async def upload_document(request: Request) -> Document:
112
+ """
113
+ Upload a document.
114
+
115
+ Args:
116
+ request: FastAPI request with form data
117
+ - user_id (required): User uploading the document
118
+ - file (required): File to upload
119
+ - Any additional fields become document metadata
120
+
121
+ Returns:
122
+ Document metadata with storage information
123
+
124
+ Examples:
125
+ ```bash
126
+ curl -X POST http://localhost:8000/documents/upload \\
127
+ -F "user_id=user_123" \\
128
+ -F "file=@contract.pdf" \\
129
+ -F "category=legal" \\
130
+ -F "year=2024"
131
+ ```
132
+ """
133
+ # Parse form data
134
+ form = await request.form()
135
+
136
+ # Extract required fields
137
+ user_id = form.get("user_id")
138
+ file = form.get("file")
139
+
140
+ if not user_id or not isinstance(user_id, str):
141
+ raise HTTPException(status_code=422, detail="user_id is required")
142
+
143
+ # NOTE: request.form() yields Starlette's UploadFile, not FastAPI's wrapper.
144
+ from starlette.datastructures import UploadFile as StarletteUploadFile
145
+
146
+ if not file or not isinstance(file, StarletteUploadFile):
147
+ raise HTTPException(status_code=422, detail="file is required")
148
+
149
+ # Read file content
150
+ file_content = await file.read()
151
+
152
+ # Build metadata from all other form fields
153
+ metadata = {}
154
+ for key, value in form.items():
155
+ if key not in ("user_id", "file"):
156
+ metadata[key] = value
157
+
158
+ # Upload document
159
+ doc = await manager.upload(
160
+ user_id=user_id,
161
+ file=file_content,
162
+ filename=file.filename or "unnamed",
163
+ metadata=metadata,
164
+ content_type=file.content_type,
165
+ )
166
+
167
+ return doc
168
+
169
+ # Route 2: List user's documents (must come before /{document_id} to avoid conflicts)
170
+ @router.get("/list")
171
+ async def list_user_documents(
172
+ user_id: str,
173
+ limit: int = 100,
174
+ offset: int = 0,
175
+ ) -> dict:
176
+ """
177
+ List user's documents with pagination.
178
+
179
+ Args:
180
+ user_id: User identifier
181
+ limit: Maximum number of documents (default: 100)
182
+ offset: Number of documents to skip (default: 0)
183
+
184
+ Returns:
185
+ Dict with "documents", "total", "limit", "offset" keys
186
+
187
+ Examples:
188
+ ```bash
189
+ # Get first page
190
+ curl "http://localhost:8000/documents/list?user_id=user_123&limit=20"
191
+
192
+ # Get second page
193
+ curl "http://localhost:8000/documents/list?user_id=user_123&limit=20&offset=20"
194
+ ```
195
+ """
196
+ # Get all docs for total count (before pagination)
197
+ all_docs = manager.list(user_id=user_id, limit=999999, offset=0)
198
+ total_count = len(all_docs)
199
+
200
+ # Get paginated docs
201
+ docs = manager.list(user_id=user_id, limit=limit, offset=offset)
202
+
203
+ return {
204
+ "documents": docs,
205
+ "total": total_count,
206
+ "limit": limit,
207
+ "offset": offset,
208
+ }
209
+
210
+ # Route 3: Get document metadata
211
+ @router.get("/{document_id}", response_model=Document)
212
+ async def get_document_metadata(document_id: str) -> Document:
213
+ """
214
+ Get document metadata.
215
+
216
+ Args:
217
+ document_id: Document identifier
218
+
219
+ Returns:
220
+ Document metadata
221
+
222
+ Raises:
223
+ HTTPException: 404 if document not found
224
+
225
+ Examples:
226
+ ```bash
227
+ curl http://localhost:8000/documents/doc_abc123
228
+ ```
229
+ """
230
+ doc = manager.get(document_id)
231
+ if not doc:
232
+ raise HTTPException(status_code=404, detail="Document not found")
233
+ return doc
234
+
235
+ # Route 4: Delete document
236
+ @router.delete("/{document_id}", status_code=204)
237
+ async def delete_document_route(document_id: str) -> Response:
238
+ """
239
+ Delete a document and its file content.
240
+
241
+ Args:
242
+ document_id: Document identifier
243
+
244
+ Returns:
245
+ 204 No Content on success
246
+
247
+ Raises:
248
+ HTTPException: 404 if document not found
249
+
250
+ Examples:
251
+ ```bash
252
+ curl -X DELETE http://localhost:8000/documents/doc_abc123
253
+ ```
254
+ """
255
+ success = await manager.delete(document_id)
256
+ if not success:
257
+ raise HTTPException(status_code=404, detail="Document not found")
258
+ return Response(status_code=204)
259
+
260
+ # Mount router
261
+ app.include_router(router)
262
+
263
+ 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
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: dict | None = None,
83
+ content_type: str | None = 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) -> Document | None:
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: StorageBackend | None = 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
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: str | None = 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
+ )