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.
- svc_infra/__init__.py +58 -2
- svc_infra/apf_payments/README.md +732 -0
- svc_infra/apf_payments/models.py +133 -42
- svc_infra/apf_payments/provider/__init__.py +4 -0
- svc_infra/apf_payments/provider/aiydan.py +871 -0
- svc_infra/apf_payments/provider/base.py +30 -9
- svc_infra/apf_payments/provider/stripe.py +156 -62
- svc_infra/apf_payments/schemas.py +19 -10
- svc_infra/apf_payments/service.py +211 -68
- svc_infra/apf_payments/settings.py +27 -3
- svc_infra/api/__init__.py +61 -0
- svc_infra/api/fastapi/__init__.py +15 -0
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +245 -0
- svc_infra/api/fastapi/apf_payments/router.py +145 -46
- svc_infra/api/fastapi/apf_payments/setup.py +26 -8
- svc_infra/api/fastapi/auth/__init__.py +65 -0
- svc_infra/api/fastapi/auth/_cookies.py +6 -2
- svc_infra/api/fastapi/auth/add.py +27 -14
- svc_infra/api/fastapi/auth/gaurd.py +104 -13
- svc_infra/api/fastapi/auth/mfa/models.py +3 -1
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +10 -6
- svc_infra/api/fastapi/auth/mfa/router.py +15 -8
- svc_infra/api/fastapi/auth/mfa/security.py +1 -2
- svc_infra/api/fastapi/auth/mfa/utils.py +2 -1
- svc_infra/api/fastapi/auth/mfa/verify.py +9 -2
- svc_infra/api/fastapi/auth/policy.py +0 -1
- svc_infra/api/fastapi/auth/providers.py +3 -1
- svc_infra/api/fastapi/auth/routers/apikey_router.py +6 -6
- svc_infra/api/fastapi/auth/routers/oauth_router.py +214 -75
- svc_infra/api/fastapi/auth/routers/session_router.py +67 -0
- svc_infra/api/fastapi/auth/security.py +31 -10
- svc_infra/api/fastapi/auth/sender.py +8 -1
- svc_infra/api/fastapi/auth/settings.py +2 -0
- svc_infra/api/fastapi/auth/state.py +3 -1
- svc_infra/api/fastapi/auth/ws_security.py +275 -0
- svc_infra/api/fastapi/billing/router.py +73 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/cache/add.py +9 -5
- svc_infra/api/fastapi/db/__init__.py +5 -1
- svc_infra/api/fastapi/db/http.py +3 -1
- svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
- svc_infra/api/fastapi/db/nosql/mongo/add.py +47 -32
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +30 -11
- svc_infra/api/fastapi/db/sql/__init__.py +5 -1
- svc_infra/api/fastapi/db/sql/add.py +71 -26
- svc_infra/api/fastapi/db/sql/crud_router.py +210 -22
- svc_infra/api/fastapi/db/sql/health.py +3 -1
- svc_infra/api/fastapi/db/sql/session.py +18 -0
- svc_infra/api/fastapi/db/sql/users.py +29 -5
- svc_infra/api/fastapi/dependencies/ratelimit.py +130 -0
- svc_infra/api/fastapi/docs/add.py +173 -0
- svc_infra/api/fastapi/docs/landing.py +4 -2
- svc_infra/api/fastapi/docs/scoped.py +62 -15
- svc_infra/api/fastapi/dual/__init__.py +12 -2
- svc_infra/api/fastapi/dual/dualize.py +1 -1
- svc_infra/api/fastapi/dual/protected.py +126 -4
- svc_infra/api/fastapi/dual/public.py +25 -0
- svc_infra/api/fastapi/dual/router.py +40 -13
- svc_infra/api/fastapi/dx.py +33 -2
- svc_infra/api/fastapi/ease.py +10 -2
- svc_infra/api/fastapi/http/concurrency.py +2 -1
- svc_infra/api/fastapi/http/conditional.py +3 -1
- svc_infra/api/fastapi/middleware/debug.py +4 -1
- svc_infra/api/fastapi/middleware/errors/catchall.py +6 -2
- svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -1
- svc_infra/api/fastapi/middleware/errors/handlers.py +54 -8
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +104 -0
- svc_infra/api/fastapi/middleware/idempotency.py +197 -70
- svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
- svc_infra/api/fastapi/middleware/optimistic_lock.py +42 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +143 -31
- svc_infra/api/fastapi/middleware/ratelimit_store.py +111 -0
- svc_infra/api/fastapi/middleware/request_id.py +27 -11
- svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
- svc_infra/api/fastapi/middleware/timeout.py +177 -0
- svc_infra/api/fastapi/openapi/apply.py +5 -3
- svc_infra/api/fastapi/openapi/conventions.py +9 -2
- svc_infra/api/fastapi/openapi/mutators.py +165 -20
- svc_infra/api/fastapi/openapi/pipeline.py +1 -1
- svc_infra/api/fastapi/openapi/security.py +3 -1
- svc_infra/api/fastapi/ops/add.py +75 -0
- svc_infra/api/fastapi/pagination.py +47 -20
- svc_infra/api/fastapi/routers/__init__.py +43 -15
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +188 -56
- svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra/api/fastapi/tenancy/context.py +112 -0
- svc_infra/api/fastapi/versioned.py +101 -0
- svc_infra/app/README.md +5 -5
- svc_infra/app/__init__.py +3 -1
- svc_infra/app/env.py +69 -1
- svc_infra/app/logging/add.py +9 -2
- svc_infra/app/logging/formats.py +12 -5
- svc_infra/billing/__init__.py +23 -0
- svc_infra/billing/async_service.py +147 -0
- svc_infra/billing/jobs.py +241 -0
- svc_infra/billing/models.py +177 -0
- svc_infra/billing/quotas.py +103 -0
- svc_infra/billing/schemas.py +36 -0
- svc_infra/billing/service.py +123 -0
- svc_infra/bundled_docs/README.md +5 -0
- svc_infra/bundled_docs/__init__.py +1 -0
- svc_infra/bundled_docs/getting-started.md +6 -0
- svc_infra/cache/__init__.py +9 -0
- svc_infra/cache/add.py +170 -0
- svc_infra/cache/backend.py +7 -6
- svc_infra/cache/decorators.py +81 -15
- svc_infra/cache/demo.py +2 -2
- svc_infra/cache/keys.py +24 -4
- svc_infra/cache/recache.py +26 -14
- svc_infra/cache/resources.py +14 -5
- svc_infra/cache/tags.py +19 -44
- svc_infra/cache/utils.py +3 -1
- svc_infra/cli/__init__.py +52 -8
- svc_infra/cli/__main__.py +4 -0
- svc_infra/cli/cmds/__init__.py +39 -2
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +7 -4
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +7 -5
- svc_infra/cli/cmds/db/ops_cmds.py +270 -0
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +103 -18
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +88 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
- svc_infra/cli/cmds/docs/docs_cmds.py +142 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +116 -0
- svc_infra/cli/cmds/health/__init__.py +179 -0
- svc_infra/cli/cmds/health/health_cmds.py +8 -0
- svc_infra/cli/cmds/help.py +4 -0
- svc_infra/cli/cmds/jobs/__init__.py +1 -0
- svc_infra/cli/cmds/jobs/jobs_cmds.py +47 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +36 -15
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +112 -0
- svc_infra/cli/foundation/runner.py +6 -2
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +58 -0
- svc_infra/data/erasure.py +45 -0
- svc_infra/data/fixtures.py +42 -0
- svc_infra/data/retention.py +61 -0
- svc_infra/db/__init__.py +15 -0
- svc_infra/db/crud_schema.py +9 -9
- svc_infra/db/inbox.py +67 -0
- svc_infra/db/nosql/__init__.py +3 -0
- svc_infra/db/nosql/core.py +30 -9
- svc_infra/db/nosql/indexes.py +3 -1
- svc_infra/db/nosql/management.py +1 -1
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/nosql/mongo/client.py +19 -2
- svc_infra/db/nosql/mongo/settings.py +6 -2
- svc_infra/db/nosql/repository.py +35 -15
- svc_infra/db/nosql/resource.py +20 -3
- svc_infra/db/nosql/scaffold.py +9 -3
- svc_infra/db/nosql/service.py +3 -1
- svc_infra/db/nosql/types.py +6 -2
- svc_infra/db/ops.py +384 -0
- svc_infra/db/outbox.py +108 -0
- svc_infra/db/sql/apikey.py +37 -9
- svc_infra/db/sql/authref.py +9 -3
- svc_infra/db/sql/constants.py +12 -8
- svc_infra/db/sql/core.py +2 -2
- svc_infra/db/sql/management.py +11 -8
- svc_infra/db/sql/repository.py +99 -26
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/scaffold.py +6 -2
- svc_infra/db/sql/service.py +15 -5
- svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
- svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
- svc_infra/db/sql/tenant.py +88 -0
- svc_infra/db/sql/uniq_hooks.py +9 -3
- svc_infra/db/sql/utils.py +138 -51
- svc_infra/db/sql/versioning.py +14 -0
- svc_infra/deploy/__init__.py +538 -0
- svc_infra/documents/__init__.py +100 -0
- svc_infra/documents/add.py +264 -0
- svc_infra/documents/ease.py +233 -0
- svc_infra/documents/models.py +114 -0
- svc_infra/documents/storage.py +264 -0
- svc_infra/dx/add.py +65 -0
- svc_infra/dx/changelog.py +74 -0
- svc_infra/dx/checks.py +68 -0
- svc_infra/exceptions.py +141 -0
- svc_infra/health/__init__.py +864 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +105 -0
- svc_infra/jobs/builtins/outbox_processor.py +40 -0
- svc_infra/jobs/builtins/webhook_delivery.py +95 -0
- svc_infra/jobs/easy.py +33 -0
- svc_infra/jobs/loader.py +50 -0
- svc_infra/jobs/queue.py +116 -0
- svc_infra/jobs/redis_queue.py +256 -0
- svc_infra/jobs/runner.py +79 -0
- svc_infra/jobs/scheduler.py +53 -0
- svc_infra/jobs/worker.py +40 -0
- svc_infra/loaders/__init__.py +186 -0
- svc_infra/loaders/base.py +142 -0
- svc_infra/loaders/github.py +311 -0
- svc_infra/loaders/models.py +147 -0
- svc_infra/loaders/url.py +235 -0
- svc_infra/logging/__init__.py +374 -0
- svc_infra/mcp/svc_infra_mcp.py +91 -33
- svc_infra/obs/README.md +2 -0
- svc_infra/obs/add.py +65 -9
- svc_infra/obs/cloud_dash.py +2 -1
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/obs/metrics/__init__.py +52 -0
- svc_infra/obs/metrics/asgi.py +13 -7
- svc_infra/obs/metrics/http.py +9 -5
- svc_infra/obs/metrics/sqlalchemy.py +13 -9
- svc_infra/obs/metrics.py +53 -0
- svc_infra/obs/settings.py +6 -2
- svc_infra/security/add.py +217 -0
- svc_infra/security/audit.py +212 -0
- svc_infra/security/audit_service.py +74 -0
- svc_infra/security/headers.py +52 -0
- svc_infra/security/hibp.py +101 -0
- svc_infra/security/jwt_rotation.py +105 -0
- svc_infra/security/lockout.py +102 -0
- svc_infra/security/models.py +287 -0
- svc_infra/security/oauth_models.py +73 -0
- svc_infra/security/org_invites.py +130 -0
- svc_infra/security/passwords.py +79 -0
- svc_infra/security/permissions.py +171 -0
- svc_infra/security/session.py +98 -0
- svc_infra/security/signed_cookies.py +100 -0
- svc_infra/storage/__init__.py +93 -0
- svc_infra/storage/add.py +253 -0
- svc_infra/storage/backends/__init__.py +11 -0
- svc_infra/storage/backends/local.py +339 -0
- svc_infra/storage/backends/memory.py +216 -0
- svc_infra/storage/backends/s3.py +353 -0
- svc_infra/storage/base.py +239 -0
- svc_infra/storage/easy.py +185 -0
- svc_infra/storage/settings.py +195 -0
- svc_infra/testing/__init__.py +685 -0
- svc_infra/utils.py +7 -3
- svc_infra/webhooks/__init__.py +69 -0
- svc_infra/webhooks/add.py +339 -0
- svc_infra/webhooks/encryption.py +115 -0
- svc_infra/webhooks/fastapi.py +39 -0
- svc_infra/webhooks/router.py +55 -0
- svc_infra/webhooks/service.py +70 -0
- svc_infra/webhooks/signing.py +34 -0
- svc_infra/websocket/__init__.py +79 -0
- svc_infra/websocket/add.py +140 -0
- svc_infra/websocket/client.py +282 -0
- svc_infra/websocket/config.py +69 -0
- svc_infra/websocket/easy.py +76 -0
- svc_infra/websocket/exceptions.py +61 -0
- svc_infra/websocket/manager.py +344 -0
- svc_infra/websocket/models.py +49 -0
- svc_infra-0.1.706.dist-info/LICENSE +21 -0
- svc_infra-0.1.706.dist-info/METADATA +356 -0
- svc_infra-0.1.706.dist-info/RECORD +357 -0
- svc_infra-0.1.589.dist-info/METADATA +0 -79
- svc_infra-0.1.589.dist-info/RECORD +0 -234
- {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/WHEEL +0 -0
- {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
|
+
)
|