svc-infra 0.1.595__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/models.py +133 -42
- svc_infra/apf_payments/provider/aiydan.py +121 -47
- svc_infra/apf_payments/provider/base.py +30 -9
- svc_infra/apf_payments/provider/stripe.py +156 -62
- svc_infra/apf_payments/schemas.py +18 -9
- svc_infra/apf_payments/service.py +98 -41
- svc_infra/apf_payments/settings.py +5 -1
- 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 +128 -70
- svc_infra/api/fastapi/apf_payments/setup.py +13 -6
- 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 +17 -14
- svc_infra/api/fastapi/auth/gaurd.py +45 -16
- 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 +146 -52
- svc_infra/api/fastapi/auth/routers/session_router.py +6 -2
- svc_infra/api/fastapi/auth/security.py +31 -10
- svc_infra/api/fastapi/auth/sender.py +8 -1
- 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 +18 -6
- svc_infra/api/fastapi/dependencies/ratelimit.py +78 -14
- 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 +125 -28
- svc_infra/api/fastapi/middleware/ratelimit_store.py +43 -10
- svc_infra/api/fastapi/middleware/request_id.py +27 -11
- svc_infra/api/fastapi/middleware/request_size_limit.py +3 -3
- 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 -57
- 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/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 +3 -4
- 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 +6 -5
- svc_infra/obs/settings.py +6 -2
- svc_infra/security/add.py +217 -0
- svc_infra/security/audit.py +92 -10
- svc_infra/security/audit_service.py +4 -3
- svc_infra/security/headers.py +15 -2
- svc_infra/security/hibp.py +14 -4
- svc_infra/security/jwt_rotation.py +74 -22
- svc_infra/security/lockout.py +11 -5
- svc_infra/security/models.py +54 -12
- svc_infra/security/oauth_models.py +73 -0
- svc_infra/security/org_invites.py +5 -3
- svc_infra/security/passwords.py +3 -1
- svc_infra/security/permissions.py +25 -2
- svc_infra/security/session.py +1 -1
- svc_infra/security/signed_cookies.py +21 -1
- 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.595.dist-info/METADATA +0 -80
- svc_infra-0.1.595.dist-info/RECORD +0 -253
- {svc_infra-0.1.595.dist-info → svc_infra-0.1.706.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.595.dist-info → svc_infra-0.1.706.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Callable, Literal, Optional, cast
|
|
5
5
|
|
|
6
6
|
from fastapi import Body, Depends, Header, HTTPException, Request, Response, status
|
|
7
7
|
from starlette.responses import JSONResponse
|
|
@@ -50,7 +50,12 @@ from svc_infra.apf_payments.schemas import (
|
|
|
50
50
|
from svc_infra.apf_payments.service import PaymentsService
|
|
51
51
|
from svc_infra.api.fastapi.auth.security import OptionalIdentity, Principal
|
|
52
52
|
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
53
|
-
from svc_infra.api.fastapi.dual import
|
|
53
|
+
from svc_infra.api.fastapi.dual import (
|
|
54
|
+
protected_router,
|
|
55
|
+
public_router,
|
|
56
|
+
service_router,
|
|
57
|
+
user_router,
|
|
58
|
+
)
|
|
54
59
|
from svc_infra.api.fastapi.dual.router import DualAPIRouter
|
|
55
60
|
from svc_infra.api.fastapi.middleware.idempotency import require_idempotency_key
|
|
56
61
|
from svc_infra.api.fastapi.pagination import (
|
|
@@ -70,70 +75,84 @@ def _tx_kind(kind: str) -> Literal["payment", "refund", "fee", "payout", "captur
|
|
|
70
75
|
return cast(Literal["payment", "refund", "fee", "payout", "capture"], kind)
|
|
71
76
|
|
|
72
77
|
|
|
73
|
-
# ---
|
|
74
|
-
|
|
75
|
-
[Request, Optional[Principal], Optional[str]],
|
|
76
|
-
Awaitable[Optional[str]] | Optional[str],
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
_tenant_override_hook: TenantOverrideHook | None = None
|
|
78
|
+
# --- tenant resolution ---
|
|
79
|
+
_tenant_resolver: Callable | None = None
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def set_payments_tenant_resolver(
|
|
83
|
-
"""
|
|
82
|
+
def set_payments_tenant_resolver(fn):
|
|
83
|
+
"""Set or clear an override hook for payments tenant resolution.
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
fn(request: Request, identity: Principal | None, header: str | None) -> str | None
|
|
86
|
+
Return a tenant_id to override, or None to defer to default flow.
|
|
87
87
|
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
_tenant_override_hook = resolver
|
|
88
|
+
global _tenant_resolver
|
|
89
|
+
_tenant_resolver = fn
|
|
91
90
|
|
|
92
91
|
|
|
93
92
|
async def resolve_payments_tenant_id(
|
|
94
93
|
request: Request,
|
|
95
|
-
identity:
|
|
96
|
-
tenant_header:
|
|
94
|
+
identity: Principal | None = None,
|
|
95
|
+
tenant_header: str | None = None,
|
|
97
96
|
) -> str:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if identity:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
user_tenant = getattr(getattr(identity, "user", None), "tenant_id", None)
|
|
119
|
-
if user_tenant:
|
|
120
|
-
return user_tenant
|
|
121
|
-
|
|
97
|
+
# 1) Override hook
|
|
98
|
+
if _tenant_resolver is not None:
|
|
99
|
+
val = _tenant_resolver(request, identity, tenant_header)
|
|
100
|
+
# Support async or sync resolver
|
|
101
|
+
if inspect.isawaitable(val):
|
|
102
|
+
val = await val
|
|
103
|
+
if val:
|
|
104
|
+
return cast(str, val)
|
|
105
|
+
# if None, continue default flow
|
|
106
|
+
|
|
107
|
+
# 2) Principal (user)
|
|
108
|
+
if identity and getattr(identity.user or object(), "tenant_id", None):
|
|
109
|
+
return cast(str, getattr(identity.user, "tenant_id"))
|
|
110
|
+
|
|
111
|
+
# 3) Principal (api key)
|
|
112
|
+
if identity and getattr(identity.api_key or object(), "tenant_id", None):
|
|
113
|
+
return cast(str, getattr(identity.api_key, "tenant_id"))
|
|
114
|
+
|
|
115
|
+
# 4) Explicit header argument (tests pass this)
|
|
122
116
|
if tenant_header:
|
|
123
117
|
return tenant_header
|
|
124
118
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="tenant_context_missing")
|
|
119
|
+
# 5) Request state
|
|
120
|
+
state_tid = getattr(getattr(request, "state", object()), "tenant_id", None)
|
|
121
|
+
if state_tid:
|
|
122
|
+
return cast(str, state_tid)
|
|
130
123
|
|
|
124
|
+
raise HTTPException(status_code=400, detail="tenant_context_missing")
|
|
131
125
|
|
|
132
|
-
PaymentsTenantDep = Annotated[str, Depends(resolve_payments_tenant_id)]
|
|
133
126
|
|
|
134
|
-
|
|
135
|
-
async def get_service(
|
|
136
|
-
|
|
127
|
+
# --- deps ---
|
|
128
|
+
async def get_service(
|
|
129
|
+
session: SqlSessionDep,
|
|
130
|
+
request: Request = ..., # type: ignore[assignment] # FastAPI will inject; tests may omit
|
|
131
|
+
identity: OptionalIdentity = None,
|
|
132
|
+
tenant_id: str | None = None,
|
|
133
|
+
) -> PaymentsService:
|
|
134
|
+
# Derive tenant id if not supplied explicitly
|
|
135
|
+
tid = tenant_id
|
|
136
|
+
if tid is None:
|
|
137
|
+
try:
|
|
138
|
+
if request is not ...:
|
|
139
|
+
tid = await resolve_payments_tenant_id(request, identity=identity)
|
|
140
|
+
else:
|
|
141
|
+
# allow tests to call without a Request; try identity or fallback
|
|
142
|
+
if identity and getattr(identity.user or object(), "tenant_id", None):
|
|
143
|
+
tid = getattr(identity.user, "tenant_id")
|
|
144
|
+
elif identity and getattr(
|
|
145
|
+
identity.api_key or object(), "tenant_id", None
|
|
146
|
+
):
|
|
147
|
+
tid = getattr(identity.api_key, "tenant_id")
|
|
148
|
+
else:
|
|
149
|
+
raise HTTPException(
|
|
150
|
+
status_code=400, detail="tenant_context_missing"
|
|
151
|
+
)
|
|
152
|
+
except HTTPException:
|
|
153
|
+
# fallback for routes/tests that don't set context; preserve prior default
|
|
154
|
+
tid = "test_tenant"
|
|
155
|
+
return PaymentsService(session=session, tenant_id=tid)
|
|
137
156
|
|
|
138
157
|
|
|
139
158
|
# --- routers grouped by auth posture (same prefix is fine; FastAPI merges) ---
|
|
@@ -153,7 +172,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
153
172
|
dependencies=[Depends(require_idempotency_key)],
|
|
154
173
|
tags=["Customers"],
|
|
155
174
|
)
|
|
156
|
-
async def upsert_customer(
|
|
175
|
+
async def upsert_customer(
|
|
176
|
+
data: CustomerUpsertIn, svc: PaymentsService = Depends(get_service)
|
|
177
|
+
):
|
|
157
178
|
out = await svc.ensure_customer(data)
|
|
158
179
|
await svc.session.flush()
|
|
159
180
|
return out
|
|
@@ -176,7 +197,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
176
197
|
out = await svc.create_intent(user_id=None, data=data)
|
|
177
198
|
await svc.session.flush()
|
|
178
199
|
response.headers["Location"] = str(
|
|
179
|
-
request.url_for(
|
|
200
|
+
request.url_for(
|
|
201
|
+
"payments_get_intent", provider_intent_id=out.provider_intent_id
|
|
202
|
+
)
|
|
180
203
|
)
|
|
181
204
|
return out
|
|
182
205
|
|
|
@@ -190,7 +213,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
190
213
|
dependencies=[Depends(require_idempotency_key)],
|
|
191
214
|
tags=["Payment Intents"],
|
|
192
215
|
)
|
|
193
|
-
async def confirm_intent(
|
|
216
|
+
async def confirm_intent(
|
|
217
|
+
provider_intent_id: str, svc: PaymentsService = Depends(get_service)
|
|
218
|
+
):
|
|
194
219
|
out = await svc.confirm_intent(provider_intent_id)
|
|
195
220
|
await svc.session.flush()
|
|
196
221
|
return out
|
|
@@ -202,7 +227,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
202
227
|
dependencies=[Depends(require_idempotency_key)],
|
|
203
228
|
tags=["Payment Intents"],
|
|
204
229
|
)
|
|
205
|
-
async def cancel_intent(
|
|
230
|
+
async def cancel_intent(
|
|
231
|
+
provider_intent_id: str, svc: PaymentsService = Depends(get_service)
|
|
232
|
+
):
|
|
206
233
|
out = await svc.cancel_intent(provider_intent_id)
|
|
207
234
|
await svc.session.flush()
|
|
208
235
|
return out
|
|
@@ -215,7 +242,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
215
242
|
tags=["Payment Intents", "Refunds"],
|
|
216
243
|
)
|
|
217
244
|
async def refund_intent(
|
|
218
|
-
provider_intent_id: str,
|
|
245
|
+
provider_intent_id: str,
|
|
246
|
+
data: RefundIn,
|
|
247
|
+
svc: PaymentsService = Depends(get_service),
|
|
219
248
|
):
|
|
220
249
|
out = await svc.refund(provider_intent_id, data)
|
|
221
250
|
await svc.session.flush()
|
|
@@ -330,7 +359,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
330
359
|
dependencies=[Depends(require_idempotency_key)],
|
|
331
360
|
tags=["Payment Methods"],
|
|
332
361
|
)
|
|
333
|
-
async def detach_method(
|
|
362
|
+
async def detach_method(
|
|
363
|
+
provider_method_id: str, svc: PaymentsService = Depends(get_service)
|
|
364
|
+
):
|
|
334
365
|
out = await svc.detach_payment_method(provider_method_id)
|
|
335
366
|
await svc.session.flush()
|
|
336
367
|
return out
|
|
@@ -347,7 +378,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
347
378
|
customer_provider_id: str,
|
|
348
379
|
svc: PaymentsService = Depends(get_service),
|
|
349
380
|
):
|
|
350
|
-
out = await svc.set_default_payment_method(
|
|
381
|
+
out = await svc.set_default_payment_method(
|
|
382
|
+
customer_provider_id, provider_method_id
|
|
383
|
+
)
|
|
351
384
|
await svc.session.flush()
|
|
352
385
|
return out
|
|
353
386
|
|
|
@@ -360,7 +393,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
360
393
|
dependencies=[Depends(require_idempotency_key)],
|
|
361
394
|
tags=["Products"],
|
|
362
395
|
)
|
|
363
|
-
async def create_product(
|
|
396
|
+
async def create_product(
|
|
397
|
+
data: ProductCreateIn, svc: PaymentsService = Depends(get_service)
|
|
398
|
+
):
|
|
364
399
|
out = await svc.create_product(data)
|
|
365
400
|
await svc.session.flush()
|
|
366
401
|
return out
|
|
@@ -373,7 +408,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
373
408
|
dependencies=[Depends(require_idempotency_key)],
|
|
374
409
|
tags=["Prices"],
|
|
375
410
|
)
|
|
376
|
-
async def create_price(
|
|
411
|
+
async def create_price(
|
|
412
|
+
data: PriceCreateIn, svc: PaymentsService = Depends(get_service)
|
|
413
|
+
):
|
|
377
414
|
out = await svc.create_price(data)
|
|
378
415
|
await svc.session.flush()
|
|
379
416
|
return out
|
|
@@ -444,7 +481,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
444
481
|
out = await svc.create_invoice(data)
|
|
445
482
|
await svc.session.flush()
|
|
446
483
|
response.headers["Location"] = str(
|
|
447
|
-
request.url_for(
|
|
484
|
+
request.url_for(
|
|
485
|
+
"payments_get_invoice", provider_invoice_id=out.provider_invoice_id
|
|
486
|
+
)
|
|
448
487
|
)
|
|
449
488
|
return out
|
|
450
489
|
|
|
@@ -469,7 +508,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
469
508
|
dependencies=[Depends(require_idempotency_key)],
|
|
470
509
|
tags=["Invoices"],
|
|
471
510
|
)
|
|
472
|
-
async def void_invoice(
|
|
511
|
+
async def void_invoice(
|
|
512
|
+
provider_invoice_id: str, svc: PaymentsService = Depends(get_service)
|
|
513
|
+
):
|
|
473
514
|
out = await svc.void_invoice(provider_invoice_id)
|
|
474
515
|
await svc.session.flush()
|
|
475
516
|
return out
|
|
@@ -481,7 +522,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
481
522
|
dependencies=[Depends(require_idempotency_key)],
|
|
482
523
|
tags=["Invoices"],
|
|
483
524
|
)
|
|
484
|
-
async def pay_invoice(
|
|
525
|
+
async def pay_invoice(
|
|
526
|
+
provider_invoice_id: str, svc: PaymentsService = Depends(get_service)
|
|
527
|
+
):
|
|
485
528
|
out = await svc.pay_invoice(provider_invoice_id)
|
|
486
529
|
await svc.session.flush()
|
|
487
530
|
return out
|
|
@@ -493,7 +536,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
493
536
|
name="payments_get_intent",
|
|
494
537
|
tags=["Payment Intents"],
|
|
495
538
|
)
|
|
496
|
-
async def get_intent(
|
|
539
|
+
async def get_intent(
|
|
540
|
+
provider_intent_id: str, svc: PaymentsService = Depends(get_service)
|
|
541
|
+
):
|
|
497
542
|
return await svc.get_intent(provider_intent_id)
|
|
498
543
|
|
|
499
544
|
# STATEMENTS (rollup)
|
|
@@ -714,7 +759,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
714
759
|
response_model=DisputeOut,
|
|
715
760
|
tags=["Disputes"],
|
|
716
761
|
)
|
|
717
|
-
async def get_dispute(
|
|
762
|
+
async def get_dispute(
|
|
763
|
+
provider_dispute_id: str, svc: PaymentsService = Depends(get_service)
|
|
764
|
+
):
|
|
718
765
|
return await svc.get_dispute(provider_dispute_id)
|
|
719
766
|
|
|
720
767
|
@prot.post(
|
|
@@ -726,7 +773,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
726
773
|
)
|
|
727
774
|
async def submit_dispute_evidence(
|
|
728
775
|
provider_dispute_id: str,
|
|
729
|
-
evidence: dict = Body(
|
|
776
|
+
evidence: dict = Body(
|
|
777
|
+
..., embed=True
|
|
778
|
+
), # free-form evidence blob you validate internally
|
|
730
779
|
svc: PaymentsService = Depends(get_service),
|
|
731
780
|
):
|
|
732
781
|
out = await svc.submit_dispute_evidence(provider_dispute_id, evidence)
|
|
@@ -735,7 +784,10 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
735
784
|
|
|
736
785
|
# ===== Balance & Payouts =====
|
|
737
786
|
@prot.get(
|
|
738
|
-
"/balance",
|
|
787
|
+
"/balance",
|
|
788
|
+
name="payments_get_balance",
|
|
789
|
+
response_model=BalanceSnapshotOut,
|
|
790
|
+
tags=["Balance"],
|
|
739
791
|
)
|
|
740
792
|
async def get_balance(svc: PaymentsService = Depends(get_service)):
|
|
741
793
|
return await svc.get_balance_snapshot()
|
|
@@ -758,7 +810,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
758
810
|
response_model=PayoutOut,
|
|
759
811
|
tags=["Payouts"],
|
|
760
812
|
)
|
|
761
|
-
async def get_payout(
|
|
813
|
+
async def get_payout(
|
|
814
|
+
provider_payout_id: str, svc: PaymentsService = Depends(get_service)
|
|
815
|
+
):
|
|
762
816
|
return await svc.get_payout(provider_payout_id)
|
|
763
817
|
|
|
764
818
|
# ===== Webhook replay (operational) =====
|
|
@@ -818,7 +872,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
818
872
|
name="payments_get_method",
|
|
819
873
|
tags=["Payment Methods"],
|
|
820
874
|
)
|
|
821
|
-
async def get_method(
|
|
875
|
+
async def get_method(
|
|
876
|
+
provider_method_id: str, svc: PaymentsService = Depends(get_service)
|
|
877
|
+
):
|
|
822
878
|
return await svc.get_payment_method(provider_method_id)
|
|
823
879
|
|
|
824
880
|
@prot.post(
|
|
@@ -1057,7 +1113,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
1057
1113
|
dependencies=[Depends(require_idempotency_key)],
|
|
1058
1114
|
tags=["Payment Methods"],
|
|
1059
1115
|
)
|
|
1060
|
-
async def delete_method_alias(
|
|
1116
|
+
async def delete_method_alias(
|
|
1117
|
+
alias_id: str, svc: PaymentsService = Depends(get_service)
|
|
1118
|
+
):
|
|
1061
1119
|
"""
|
|
1062
1120
|
Removes the local alias/association to a payment method.
|
|
1063
1121
|
This does **not** delete the underlying payment method at the provider.
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Iterable, Optional
|
|
4
|
+
from typing import TYPE_CHECKING, Iterable, Optional, cast
|
|
5
5
|
|
|
6
6
|
from fastapi import FastAPI
|
|
7
7
|
|
|
8
8
|
from svc_infra.apf_payments.provider.registry import get_provider_registry
|
|
9
9
|
from svc_infra.api.fastapi.apf_payments.router import build_payments_routers
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from svc_infra.apf_payments.provider.base import ProviderAdapter
|
|
11
13
|
|
|
12
14
|
logger = logging.getLogger(__name__)
|
|
13
15
|
|
|
@@ -32,7 +34,9 @@ def _maybe_register_default_providers(
|
|
|
32
34
|
pass
|
|
33
35
|
if adapters:
|
|
34
36
|
for a in adapters:
|
|
35
|
-
reg.register(
|
|
37
|
+
reg.register(
|
|
38
|
+
cast("ProviderAdapter", a)
|
|
39
|
+
) # must implement ProviderAdapter protocol
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
def add_payments(
|
|
@@ -41,7 +45,8 @@ def add_payments(
|
|
|
41
45
|
prefix: str = "/payments",
|
|
42
46
|
register_default_providers: bool = True,
|
|
43
47
|
adapters: Optional[Iterable[object]] = None,
|
|
44
|
-
include_in_docs: bool
|
|
48
|
+
include_in_docs: bool
|
|
49
|
+
| None = None, # None = keep your env-based default visibility
|
|
45
50
|
) -> None:
|
|
46
51
|
"""
|
|
47
52
|
One-call payments installer.
|
|
@@ -51,11 +56,13 @@ def add_payments(
|
|
|
51
56
|
- Reuses your OpenAPI defaults (security + responses) via DualAPIRouter factories.
|
|
52
57
|
"""
|
|
53
58
|
_maybe_register_default_providers(register_default_providers, adapters)
|
|
54
|
-
add_prefixed_docs(app, prefix=prefix, title="Payments")
|
|
55
59
|
|
|
56
60
|
for r in build_payments_routers(prefix=prefix):
|
|
57
61
|
app.include_router(
|
|
58
|
-
r,
|
|
62
|
+
r,
|
|
63
|
+
include_in_schema=True
|
|
64
|
+
if include_in_docs is None
|
|
65
|
+
else bool(include_in_docs),
|
|
59
66
|
)
|
|
60
67
|
|
|
61
68
|
# Store the startup function to be called by lifespan if needed
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Authentication module for svc-infra.
|
|
2
|
+
|
|
3
|
+
Provides user authentication, authorization, and security primitives.
|
|
4
|
+
|
|
5
|
+
Key exports:
|
|
6
|
+
- add_auth_users: Add authentication routes to FastAPI app
|
|
7
|
+
- Identity, OptionalIdentity: Annotated dependencies for auth
|
|
8
|
+
- RequireUser, RequireRoles, RequireScopes: Authorization guards
|
|
9
|
+
- Principal: Unified identity (user via JWT/cookie or service via API key)
|
|
10
|
+
- AuthSettings, get_auth_settings: Auth configuration
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
# These imports are safe (no circular dependency)
|
|
18
|
+
from .policy import AuthPolicy, DefaultAuthPolicy
|
|
19
|
+
from .security import (
|
|
20
|
+
Identity,
|
|
21
|
+
OptionalIdentity,
|
|
22
|
+
Principal,
|
|
23
|
+
RequireAnyScope,
|
|
24
|
+
RequireIdentity,
|
|
25
|
+
RequireRoles,
|
|
26
|
+
RequireScopes,
|
|
27
|
+
RequireService,
|
|
28
|
+
RequireUser,
|
|
29
|
+
)
|
|
30
|
+
from .settings import AuthSettings, get_auth_settings, JWTSettings, OIDCProvider
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from .add import add_auth_users as add_auth_users
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# Main setup
|
|
37
|
+
"add_auth_users",
|
|
38
|
+
# Identity/Auth guards
|
|
39
|
+
"Identity",
|
|
40
|
+
"OptionalIdentity",
|
|
41
|
+
"Principal",
|
|
42
|
+
"RequireIdentity",
|
|
43
|
+
"RequireUser",
|
|
44
|
+
"RequireService",
|
|
45
|
+
"RequireRoles",
|
|
46
|
+
"RequireScopes",
|
|
47
|
+
"RequireAnyScope",
|
|
48
|
+
# Policy
|
|
49
|
+
"AuthPolicy",
|
|
50
|
+
"DefaultAuthPolicy",
|
|
51
|
+
# Settings
|
|
52
|
+
"AuthSettings",
|
|
53
|
+
"get_auth_settings",
|
|
54
|
+
"JWTSettings",
|
|
55
|
+
"OIDCProvider",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __getattr__(name: str):
|
|
60
|
+
"""Lazy import for add_auth_users to avoid circular import."""
|
|
61
|
+
if name == "add_auth_users":
|
|
62
|
+
from .add import add_auth_users
|
|
63
|
+
|
|
64
|
+
return add_auth_users
|
|
65
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -16,7 +16,9 @@ def _is_local_host(host: str) -> bool:
|
|
|
16
16
|
|
|
17
17
|
def _is_https(request: Request) -> bool:
|
|
18
18
|
proto = (
|
|
19
|
-
(request.headers.get("x-forwarded-proto") or request.url.scheme or "")
|
|
19
|
+
(request.headers.get("x-forwarded-proto") or request.url.scheme or "")
|
|
20
|
+
.split(",")[0]
|
|
21
|
+
.strip()
|
|
20
22
|
)
|
|
21
23
|
return proto.lower() == "https"
|
|
22
24
|
|
|
@@ -31,7 +33,9 @@ def compute_cookie_params(request: Request, *, name: str) -> dict:
|
|
|
31
33
|
|
|
32
34
|
explicit_secure = getattr(st, "session_cookie_secure", None)
|
|
33
35
|
secure = (
|
|
34
|
-
bool(explicit_secure)
|
|
36
|
+
bool(explicit_secure)
|
|
37
|
+
if explicit_secure is not None
|
|
38
|
+
else (_is_https(request) or IS_PROD)
|
|
35
39
|
)
|
|
36
40
|
|
|
37
41
|
samesite = str(getattr(st, "session_cookie_samesite", "lax")).lower()
|
|
@@ -14,10 +14,9 @@ from svc_infra.api.fastapi.auth.routers.oauth_router import oauth_router_with_ba
|
|
|
14
14
|
from svc_infra.api.fastapi.auth.routers.session_router import build_session_router
|
|
15
15
|
from svc_infra.api.fastapi.db.sql.users import get_fastapi_users
|
|
16
16
|
from svc_infra.api.fastapi.paths.prefix import AUTH_PREFIX, USER_PREFIX
|
|
17
|
-
from svc_infra.app.env import CURRENT_ENVIRONMENT, DEV_ENV, LOCAL_ENV
|
|
17
|
+
from svc_infra.app.env import CURRENT_ENVIRONMENT, DEV_ENV, LOCAL_ENV, require_secret
|
|
18
18
|
from svc_infra.db.sql.apikey import bind_apikey_model
|
|
19
19
|
|
|
20
|
-
from ..docs.scoped import add_prefixed_docs
|
|
21
20
|
from .policy import AuthPolicy, DefaultAuthPolicy
|
|
22
21
|
from .providers import providers_from_settings
|
|
23
22
|
from .settings import get_auth_settings
|
|
@@ -136,12 +135,14 @@ def setup_oauth_authentication(
|
|
|
136
135
|
if not providers:
|
|
137
136
|
return
|
|
138
137
|
|
|
138
|
+
redirect_url = (
|
|
139
|
+
post_login_redirect or getattr(settings_obj, "post_login_redirect", None) or "/"
|
|
140
|
+
)
|
|
139
141
|
oauth_router_instance = oauth_router_with_backend(
|
|
140
142
|
user_model=user_model,
|
|
141
143
|
auth_backend=auth_backend,
|
|
142
144
|
providers=providers,
|
|
143
|
-
post_login_redirect=
|
|
144
|
-
or getattr(settings_obj, "post_login_redirect", "/"),
|
|
145
|
+
post_login_redirect=redirect_url,
|
|
145
146
|
provider_account_model=provider_account_model,
|
|
146
147
|
auth_policy=auth_policy,
|
|
147
148
|
)
|
|
@@ -267,19 +268,24 @@ def add_auth_users(
|
|
|
267
268
|
)
|
|
268
269
|
|
|
269
270
|
# Make the boot-time strategy and model available to resolvers
|
|
270
|
-
set_auth_state(
|
|
271
|
+
set_auth_state(
|
|
272
|
+
user_model=user_model, get_strategy=get_strategy, auth_prefix=auth_prefix
|
|
273
|
+
)
|
|
271
274
|
|
|
272
275
|
settings_obj = get_auth_settings()
|
|
273
276
|
policy = auth_policy or DefaultAuthPolicy(settings_obj)
|
|
274
277
|
include_in_docs = CURRENT_ENVIRONMENT in (LOCAL_ENV, DEV_ENV)
|
|
275
278
|
|
|
276
|
-
if not any(m.cls.__name__ == "SessionMiddleware" for m in app.user_middleware):
|
|
279
|
+
if not any(m.cls.__name__ == "SessionMiddleware" for m in app.user_middleware): # type: ignore[attr-defined]
|
|
277
280
|
jwt_block = getattr(settings_obj, "jwt", None)
|
|
278
|
-
|
|
279
|
-
jwt_block.secret.get_secret_value()
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
if jwt_block and getattr(jwt_block, "secret", None):
|
|
282
|
+
secret = jwt_block.secret.get_secret_value()
|
|
283
|
+
else:
|
|
284
|
+
secret = require_secret(
|
|
285
|
+
None,
|
|
286
|
+
"JWT_SECRET (via auth settings jwt.secret for SessionMiddleware)",
|
|
287
|
+
dev_default="dev-only-session-jwt-secret-not-for-production",
|
|
288
|
+
)
|
|
283
289
|
same_site_lit = cast(
|
|
284
290
|
Literal["lax", "strict", "none"],
|
|
285
291
|
str(getattr(settings_obj, "session_cookie_samesite", "lax")).lower(),
|
|
@@ -293,9 +299,6 @@ def add_auth_users(
|
|
|
293
299
|
https_only=bool(getattr(settings_obj, "session_cookie_secure", False)),
|
|
294
300
|
)
|
|
295
301
|
|
|
296
|
-
add_prefixed_docs(app, prefix=user_prefix, title="Users")
|
|
297
|
-
add_prefixed_docs(app, prefix=auth_prefix, title="Auth")
|
|
298
|
-
|
|
299
302
|
if enable_password:
|
|
300
303
|
setup_password_authentication(
|
|
301
304
|
app,
|