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.
- svc_infra/__init__.py +58 -2
- svc_infra/apf_payments/models.py +68 -38
- svc_infra/apf_payments/provider/__init__.py +2 -2
- svc_infra/apf_payments/provider/aiydan.py +39 -23
- svc_infra/apf_payments/provider/base.py +8 -3
- svc_infra/apf_payments/provider/registry.py +3 -5
- svc_infra/apf_payments/provider/stripe.py +74 -52
- svc_infra/apf_payments/schemas.py +84 -83
- svc_infra/apf_payments/service.py +27 -16
- svc_infra/apf_payments/settings.py +12 -11
- svc_infra/api/__init__.py +61 -0
- svc_infra/api/fastapi/__init__.py +34 -0
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +240 -0
- svc_infra/api/fastapi/apf_payments/router.py +94 -73
- svc_infra/api/fastapi/apf_payments/setup.py +10 -9
- svc_infra/api/fastapi/auth/__init__.py +65 -0
- svc_infra/api/fastapi/auth/_cookies.py +1 -3
- svc_infra/api/fastapi/auth/add.py +14 -15
- svc_infra/api/fastapi/auth/gaurd.py +32 -20
- svc_infra/api/fastapi/auth/mfa/models.py +3 -4
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +13 -9
- svc_infra/api/fastapi/auth/mfa/router.py +9 -8
- svc_infra/api/fastapi/auth/mfa/security.py +4 -7
- svc_infra/api/fastapi/auth/mfa/utils.py +5 -3
- svc_infra/api/fastapi/auth/policy.py +0 -1
- svc_infra/api/fastapi/auth/providers.py +3 -3
- svc_infra/api/fastapi/auth/routers/apikey_router.py +19 -21
- svc_infra/api/fastapi/auth/routers/oauth_router.py +98 -52
- svc_infra/api/fastapi/auth/routers/session_router.py +6 -5
- svc_infra/api/fastapi/auth/security.py +25 -15
- svc_infra/api/fastapi/auth/sender.py +5 -0
- svc_infra/api/fastapi/auth/settings.py +18 -19
- svc_infra/api/fastapi/auth/state.py +5 -4
- svc_infra/api/fastapi/auth/ws_security.py +275 -0
- svc_infra/api/fastapi/billing/router.py +71 -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 +10 -9
- svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
- svc_infra/api/fastapi/db/nosql/mongo/add.py +35 -30
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +39 -21
- svc_infra/api/fastapi/db/sql/__init__.py +5 -1
- svc_infra/api/fastapi/db/sql/add.py +62 -25
- svc_infra/api/fastapi/db/sql/crud_router.py +205 -30
- svc_infra/api/fastapi/db/sql/session.py +19 -2
- svc_infra/api/fastapi/db/sql/users.py +18 -9
- svc_infra/api/fastapi/dependencies/ratelimit.py +76 -14
- svc_infra/api/fastapi/docs/add.py +163 -0
- svc_infra/api/fastapi/docs/landing.py +6 -6
- svc_infra/api/fastapi/docs/scoped.py +75 -36
- svc_infra/api/fastapi/dual/__init__.py +12 -2
- svc_infra/api/fastapi/dual/dualize.py +2 -2
- svc_infra/api/fastapi/dual/protected.py +123 -10
- svc_infra/api/fastapi/dual/public.py +25 -0
- svc_infra/api/fastapi/dual/router.py +18 -8
- svc_infra/api/fastapi/dx.py +33 -2
- svc_infra/api/fastapi/ease.py +59 -7
- svc_infra/api/fastapi/http/concurrency.py +2 -1
- svc_infra/api/fastapi/http/conditional.py +2 -2
- svc_infra/api/fastapi/middleware/debug.py +4 -1
- svc_infra/api/fastapi/middleware/errors/exceptions.py +2 -5
- svc_infra/api/fastapi/middleware/errors/handlers.py +50 -10
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +95 -0
- svc_infra/api/fastapi/middleware/idempotency.py +190 -68
- svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
- svc_infra/api/fastapi/middleware/optimistic_lock.py +39 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +125 -28
- svc_infra/api/fastapi/middleware/ratelimit_store.py +45 -13
- svc_infra/api/fastapi/middleware/request_id.py +24 -10
- svc_infra/api/fastapi/middleware/request_size_limit.py +3 -3
- svc_infra/api/fastapi/middleware/timeout.py +176 -0
- svc_infra/api/fastapi/object_router.py +1060 -0
- svc_infra/api/fastapi/openapi/apply.py +4 -3
- svc_infra/api/fastapi/openapi/conventions.py +13 -6
- svc_infra/api/fastapi/openapi/mutators.py +144 -17
- svc_infra/api/fastapi/openapi/pipeline.py +2 -2
- svc_infra/api/fastapi/openapi/responses.py +4 -6
- svc_infra/api/fastapi/openapi/security.py +1 -1
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +47 -32
- svc_infra/api/fastapi/routers/__init__.py +16 -10
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +167 -54
- svc_infra/api/fastapi/tenancy/add.py +20 -0
- svc_infra/api/fastapi/tenancy/context.py +113 -0
- svc_infra/api/fastapi/versioned.py +102 -0
- svc_infra/app/README.md +5 -5
- svc_infra/app/__init__.py +3 -1
- svc_infra/app/env.py +70 -4
- svc_infra/app/logging/add.py +10 -2
- svc_infra/app/logging/filter.py +1 -1
- svc_infra/app/logging/formats.py +13 -5
- svc_infra/app/root.py +3 -3
- svc_infra/billing/__init__.py +40 -0
- svc_infra/billing/async_service.py +167 -0
- svc_infra/billing/jobs.py +231 -0
- svc_infra/billing/models.py +146 -0
- svc_infra/billing/quotas.py +101 -0
- svc_infra/billing/schemas.py +34 -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 +21 -5
- svc_infra/cache/add.py +167 -0
- svc_infra/cache/backend.py +9 -7
- svc_infra/cache/decorators.py +75 -20
- svc_infra/cache/demo.py +2 -2
- svc_infra/cache/keys.py +26 -6
- svc_infra/cache/recache.py +26 -27
- svc_infra/cache/resources.py +6 -5
- svc_infra/cache/tags.py +19 -44
- svc_infra/cache/ttl.py +2 -3
- svc_infra/cache/utils.py +4 -3
- svc_infra/cli/__init__.py +44 -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 +18 -14
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +9 -10
- svc_infra/cli/cmds/db/ops_cmds.py +267 -0
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +97 -29
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +13 -13
- svc_infra/cli/cmds/docs/docs_cmds.py +139 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +110 -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 +42 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +31 -13
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
- svc_infra/cli/foundation/runner.py +4 -5
- svc_infra/cli/foundation/typer_bootstrap.py +1 -2
- svc_infra/data/__init__.py +83 -0
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +56 -0
- svc_infra/data/erasure.py +46 -0
- svc_infra/data/fixtures.py +42 -0
- svc_infra/data/retention.py +56 -0
- svc_infra/db/__init__.py +15 -0
- svc_infra/db/crud_schema.py +14 -13
- svc_infra/db/inbox.py +67 -0
- svc_infra/db/nosql/__init__.py +2 -0
- svc_infra/db/nosql/constants.py +1 -1
- svc_infra/db/nosql/core.py +19 -5
- svc_infra/db/nosql/indexes.py +12 -9
- svc_infra/db/nosql/management.py +4 -4
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/nosql/mongo/client.py +21 -4
- svc_infra/db/nosql/mongo/settings.py +1 -1
- svc_infra/db/nosql/repository.py +46 -27
- svc_infra/db/nosql/resource.py +28 -16
- svc_infra/db/nosql/scaffold.py +14 -12
- svc_infra/db/nosql/service.py +2 -1
- svc_infra/db/nosql/service_with_hooks.py +4 -3
- svc_infra/db/nosql/utils.py +4 -4
- svc_infra/db/ops.py +380 -0
- svc_infra/db/outbox.py +105 -0
- svc_infra/db/sql/apikey.py +34 -15
- svc_infra/db/sql/authref.py +8 -6
- svc_infra/db/sql/constants.py +5 -1
- svc_infra/db/sql/core.py +13 -13
- svc_infra/db/sql/management.py +5 -6
- svc_infra/db/sql/repository.py +92 -26
- svc_infra/db/sql/resource.py +18 -12
- svc_infra/db/sql/scaffold.py +11 -11
- svc_infra/db/sql/service.py +2 -1
- svc_infra/db/sql/service_with_hooks.py +4 -3
- 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 +80 -0
- svc_infra/db/sql/uniq.py +8 -7
- svc_infra/db/sql/uniq_hooks.py +12 -11
- svc_infra/db/sql/utils.py +105 -47
- svc_infra/db/sql/versioning.py +14 -0
- svc_infra/db/utils.py +3 -3
- svc_infra/deploy/__init__.py +531 -0
- svc_infra/documents/__init__.py +100 -0
- svc_infra/documents/add.py +263 -0
- svc_infra/documents/ease.py +233 -0
- svc_infra/documents/models.py +114 -0
- svc_infra/documents/storage.py +262 -0
- svc_infra/dx/__init__.py +58 -0
- svc_infra/dx/add.py +63 -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 +863 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +101 -0
- svc_infra/jobs/__init__.py +79 -0
- svc_infra/jobs/builtins/outbox_processor.py +38 -0
- svc_infra/jobs/builtins/webhook_delivery.py +93 -0
- svc_infra/jobs/easy.py +33 -0
- svc_infra/jobs/loader.py +49 -0
- svc_infra/jobs/queue.py +106 -0
- svc_infra/jobs/redis_queue.py +242 -0
- svc_infra/jobs/runner.py +75 -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 +143 -0
- svc_infra/loaders/github.py +309 -0
- svc_infra/loaders/models.py +147 -0
- svc_infra/loaders/url.py +229 -0
- svc_infra/logging/__init__.py +375 -0
- svc_infra/mcp/__init__.py +82 -0
- svc_infra/mcp/svc_infra_mcp.py +91 -33
- svc_infra/obs/README.md +2 -0
- svc_infra/obs/add.py +68 -11
- svc_infra/obs/cloud_dash.py +2 -1
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/obs/metrics/__init__.py +6 -7
- svc_infra/obs/metrics/asgi.py +8 -7
- svc_infra/obs/metrics/base.py +13 -13
- svc_infra/obs/metrics/http.py +3 -3
- svc_infra/obs/metrics/sqlalchemy.py +14 -13
- svc_infra/obs/metrics.py +9 -8
- svc_infra/resilience/__init__.py +44 -0
- svc_infra/resilience/circuit_breaker.py +328 -0
- svc_infra/resilience/retry.py +289 -0
- svc_infra/security/__init__.py +167 -0
- svc_infra/security/add.py +213 -0
- svc_infra/security/audit.py +97 -18
- svc_infra/security/audit_service.py +10 -9
- svc_infra/security/headers.py +15 -2
- svc_infra/security/hibp.py +14 -7
- svc_infra/security/jwt_rotation.py +78 -29
- svc_infra/security/lockout.py +23 -16
- svc_infra/security/models.py +77 -44
- svc_infra/security/oauth_models.py +73 -0
- svc_infra/security/org_invites.py +12 -12
- svc_infra/security/passwords.py +3 -3
- svc_infra/security/permissions.py +31 -7
- svc_infra/security/session.py +7 -8
- svc_infra/security/signed_cookies.py +26 -6
- svc_infra/storage/__init__.py +93 -0
- svc_infra/storage/add.py +250 -0
- svc_infra/storage/backends/__init__.py +11 -0
- svc_infra/storage/backends/local.py +331 -0
- svc_infra/storage/backends/memory.py +213 -0
- svc_infra/storage/backends/s3.py +334 -0
- svc_infra/storage/base.py +239 -0
- svc_infra/storage/easy.py +181 -0
- svc_infra/storage/settings.py +193 -0
- svc_infra/testing/__init__.py +682 -0
- svc_infra/utils.py +170 -5
- svc_infra/webhooks/__init__.py +69 -0
- svc_infra/webhooks/add.py +327 -0
- svc_infra/webhooks/encryption.py +115 -0
- svc_infra/webhooks/fastapi.py +37 -0
- svc_infra/webhooks/router.py +55 -0
- svc_infra/webhooks/service.py +69 -0
- svc_infra/webhooks/signing.py +34 -0
- svc_infra/websocket/__init__.py +79 -0
- svc_infra/websocket/add.py +139 -0
- svc_infra/websocket/client.py +283 -0
- svc_infra/websocket/config.py +57 -0
- svc_infra/websocket/easy.py +76 -0
- svc_infra/websocket/exceptions.py +61 -0
- svc_infra/websocket/manager.py +343 -0
- svc_infra/websocket/models.py +49 -0
- svc_infra-1.1.0.dist-info/LICENSE +21 -0
- svc_infra-1.1.0.dist-info/METADATA +362 -0
- svc_infra-1.1.0.dist-info/RECORD +364 -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-1.1.0.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
svc_infra/__init__.py
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+
"""svc-infra: Service Infrastructure Toolkit.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A comprehensive backend infrastructure library providing:
|
|
4
|
+
- API framework (FastAPI scaffolding, dual routers, auth)
|
|
5
|
+
- Database (SQL/Mongo, migrations, repositories)
|
|
6
|
+
- Caching (Redis, decorators)
|
|
7
|
+
- Jobs (background tasks, queues)
|
|
8
|
+
- Webhooks (delivery, subscriptions)
|
|
9
|
+
- Billing (Stripe/Adyen integration)
|
|
10
|
+
- Observability (logging, metrics)
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
from svc_infra.api.fastapi import easy_service_app
|
|
14
|
+
from svc_infra.api.fastapi.auth import add_auth_users
|
|
15
|
+
|
|
16
|
+
app = easy_service_app(name="MyAPI")
|
|
17
|
+
add_auth_users(app)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
# Core modules (lazy import pattern for optional dependencies)
|
|
23
|
+
from . import api, app, cache, db, jobs, webhooks
|
|
24
|
+
|
|
25
|
+
# Base exception
|
|
26
|
+
from .exceptions import SvcInfraError
|
|
27
|
+
|
|
28
|
+
# Content Loaders
|
|
29
|
+
from .loaders import (
|
|
30
|
+
BaseLoader,
|
|
31
|
+
GitHubLoader,
|
|
32
|
+
LoadedContent,
|
|
33
|
+
URLLoader,
|
|
34
|
+
load_github,
|
|
35
|
+
load_github_sync,
|
|
36
|
+
load_url,
|
|
37
|
+
load_url_sync,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Core modules
|
|
42
|
+
"api",
|
|
43
|
+
"app",
|
|
44
|
+
"cache",
|
|
45
|
+
"db",
|
|
46
|
+
"jobs",
|
|
47
|
+
"webhooks",
|
|
48
|
+
# Base exception
|
|
49
|
+
"SvcInfraError",
|
|
50
|
+
# Loaders
|
|
51
|
+
"BaseLoader",
|
|
52
|
+
"GitHubLoader",
|
|
53
|
+
"LoadedContent",
|
|
54
|
+
"URLLoader",
|
|
55
|
+
"load_github",
|
|
56
|
+
"load_github_sync",
|
|
57
|
+
"load_url",
|
|
58
|
+
"load_url_sync",
|
|
59
|
+
]
|
svc_infra/apf_payments/models.py
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
|
-
from sqlalchemy import
|
|
5
|
+
from sqlalchemy import (
|
|
6
|
+
JSON,
|
|
7
|
+
Boolean,
|
|
8
|
+
DateTime,
|
|
9
|
+
Index,
|
|
10
|
+
Numeric,
|
|
11
|
+
String,
|
|
12
|
+
UniqueConstraint,
|
|
13
|
+
text,
|
|
14
|
+
)
|
|
7
15
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
8
16
|
|
|
9
17
|
from svc_infra.db.sql.authref import user_fk_constraint, user_id_type
|
|
@@ -21,7 +29,7 @@ class PayCustomer(ModelBase):
|
|
|
21
29
|
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
22
30
|
|
|
23
31
|
# Always typed to match the actual auth PK; FK is enforced at table level
|
|
24
|
-
user_id: Mapped[
|
|
32
|
+
user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
|
|
25
33
|
|
|
26
34
|
provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
|
|
27
35
|
provider_customer_id: Mapped[str] = mapped_column(
|
|
@@ -29,7 +37,9 @@ class PayCustomer(ModelBase):
|
|
|
29
37
|
)
|
|
30
38
|
|
|
31
39
|
created_at: Mapped[datetime] = mapped_column(
|
|
32
|
-
DateTime(timezone=True),
|
|
40
|
+
DateTime(timezone=True),
|
|
41
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
42
|
+
nullable=False,
|
|
33
43
|
)
|
|
34
44
|
|
|
35
45
|
__table_args__ = (
|
|
@@ -46,7 +56,7 @@ class PayIntent(ModelBase):
|
|
|
46
56
|
|
|
47
57
|
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
48
58
|
|
|
49
|
-
user_id: Mapped[
|
|
59
|
+
user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
|
|
50
60
|
|
|
51
61
|
provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
|
|
52
62
|
provider_intent_id: Mapped[str] = mapped_column(
|
|
@@ -55,11 +65,13 @@ class PayIntent(ModelBase):
|
|
|
55
65
|
amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False) # minor units
|
|
56
66
|
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
57
67
|
status: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
|
|
58
|
-
client_secret: Mapped[
|
|
68
|
+
client_secret: Mapped[str | None] = mapped_column(String(255))
|
|
59
69
|
created_at: Mapped[datetime] = mapped_column(
|
|
60
|
-
DateTime(timezone=True),
|
|
70
|
+
DateTime(timezone=True),
|
|
71
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
72
|
+
nullable=False,
|
|
61
73
|
)
|
|
62
|
-
confirmed_at: Mapped[
|
|
74
|
+
confirmed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
63
75
|
captured: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
64
76
|
|
|
65
77
|
__table_args__ = (
|
|
@@ -82,7 +94,9 @@ class PayEvent(ModelBase):
|
|
|
82
94
|
)
|
|
83
95
|
type: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
84
96
|
received_at: Mapped[datetime] = mapped_column(
|
|
85
|
-
DateTime(timezone=True),
|
|
97
|
+
DateTime(timezone=True),
|
|
98
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
99
|
+
nullable=False,
|
|
86
100
|
)
|
|
87
101
|
payload_json: Mapped[dict] = mapped_column(JSON, nullable=False) # compact JSON string
|
|
88
102
|
|
|
@@ -102,8 +116,8 @@ class LedgerEntry(ModelBase):
|
|
|
102
116
|
)
|
|
103
117
|
|
|
104
118
|
provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
|
|
105
|
-
provider_ref: Mapped[
|
|
106
|
-
user_id: Mapped[
|
|
119
|
+
provider_ref: Mapped[str | None] = mapped_column(String(128), index=True)
|
|
120
|
+
user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
|
|
107
121
|
amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
108
122
|
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
109
123
|
kind: Mapped[str] = mapped_column(String(24), nullable=False) # payment|refund|fee|payout...
|
|
@@ -130,19 +144,21 @@ class PayPaymentMethod(ModelBase):
|
|
|
130
144
|
|
|
131
145
|
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
132
146
|
|
|
133
|
-
user_id: Mapped[
|
|
147
|
+
user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
|
|
134
148
|
provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
|
|
135
149
|
provider_customer_id: Mapped[str] = mapped_column(String(128), index=True, nullable=False)
|
|
136
150
|
provider_method_id: Mapped[str] = mapped_column(
|
|
137
151
|
String(128), unique=True, index=True, nullable=False
|
|
138
152
|
)
|
|
139
|
-
brand: Mapped[
|
|
140
|
-
last4: Mapped[
|
|
141
|
-
exp_month: Mapped[
|
|
142
|
-
exp_year: Mapped[
|
|
153
|
+
brand: Mapped[str | None] = mapped_column(String(32))
|
|
154
|
+
last4: Mapped[str | None] = mapped_column(String(8))
|
|
155
|
+
exp_month: Mapped[int | None] = mapped_column(Numeric(2, 0))
|
|
156
|
+
exp_year: Mapped[int | None] = mapped_column(Numeric(4, 0))
|
|
143
157
|
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
144
158
|
created_at: Mapped[datetime] = mapped_column(
|
|
145
|
-
DateTime(timezone=True),
|
|
159
|
+
DateTime(timezone=True),
|
|
160
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
161
|
+
nullable=False,
|
|
146
162
|
)
|
|
147
163
|
|
|
148
164
|
__table_args__ = (
|
|
@@ -170,7 +186,9 @@ class PayProduct(ModelBase):
|
|
|
170
186
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
171
187
|
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
172
188
|
created_at: Mapped[datetime] = mapped_column(
|
|
173
|
-
DateTime(timezone=True),
|
|
189
|
+
DateTime(timezone=True),
|
|
190
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
191
|
+
nullable=False,
|
|
174
192
|
)
|
|
175
193
|
|
|
176
194
|
|
|
@@ -188,11 +206,13 @@ class PayPrice(ModelBase):
|
|
|
188
206
|
provider_product_id: Mapped[str] = mapped_column(String(128), index=True, nullable=False)
|
|
189
207
|
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
190
208
|
unit_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False) # minor units
|
|
191
|
-
interval: Mapped[
|
|
192
|
-
trial_days: Mapped[
|
|
209
|
+
interval: Mapped[str | None] = mapped_column(String(16)) # month|year|week|day
|
|
210
|
+
trial_days: Mapped[int | None] = mapped_column(Numeric(5, 0))
|
|
193
211
|
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
194
212
|
created_at: Mapped[datetime] = mapped_column(
|
|
195
|
-
DateTime(timezone=True),
|
|
213
|
+
DateTime(timezone=True),
|
|
214
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
215
|
+
nullable=False,
|
|
196
216
|
)
|
|
197
217
|
|
|
198
218
|
|
|
@@ -203,7 +223,7 @@ class PaySubscription(ModelBase):
|
|
|
203
223
|
|
|
204
224
|
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
205
225
|
|
|
206
|
-
user_id: Mapped[
|
|
226
|
+
user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
|
|
207
227
|
provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
|
|
208
228
|
provider_customer_id: Mapped[str] = mapped_column(String(128), index=True, nullable=False)
|
|
209
229
|
provider_subscription_id: Mapped[str] = mapped_column(
|
|
@@ -215,9 +235,11 @@ class PaySubscription(ModelBase):
|
|
|
215
235
|
) # active|trialing|canceled|past_due|incomplete
|
|
216
236
|
quantity: Mapped[int] = mapped_column(Numeric(10, 0), default=1, nullable=False)
|
|
217
237
|
cancel_at_period_end: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
218
|
-
current_period_end: Mapped[
|
|
238
|
+
current_period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
219
239
|
created_at: Mapped[datetime] = mapped_column(
|
|
220
|
-
DateTime(timezone=True),
|
|
240
|
+
DateTime(timezone=True),
|
|
241
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
242
|
+
nullable=False,
|
|
221
243
|
)
|
|
222
244
|
|
|
223
245
|
__table_args__ = (
|
|
@@ -238,7 +260,7 @@ class PayInvoice(ModelBase):
|
|
|
238
260
|
|
|
239
261
|
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
240
262
|
|
|
241
|
-
user_id: Mapped[
|
|
263
|
+
user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
|
|
242
264
|
provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
|
|
243
265
|
provider_invoice_id: Mapped[str] = mapped_column(
|
|
244
266
|
String(128), unique=True, index=True, nullable=False
|
|
@@ -249,10 +271,12 @@ class PayInvoice(ModelBase):
|
|
|
249
271
|
) # draft|open|paid|void|uncollectible
|
|
250
272
|
amount_due: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
251
273
|
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
252
|
-
hosted_invoice_url: Mapped[
|
|
253
|
-
pdf_url: Mapped[
|
|
274
|
+
hosted_invoice_url: Mapped[str | None] = mapped_column(String(255))
|
|
275
|
+
pdf_url: Mapped[str | None] = mapped_column(String(255))
|
|
254
276
|
created_at: Mapped[datetime] = mapped_column(
|
|
255
|
-
DateTime(timezone=True),
|
|
277
|
+
DateTime(timezone=True),
|
|
278
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
279
|
+
nullable=False,
|
|
256
280
|
)
|
|
257
281
|
|
|
258
282
|
__table_args__ = (
|
|
@@ -273,7 +297,7 @@ class PaySetupIntent(ModelBase):
|
|
|
273
297
|
|
|
274
298
|
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
275
299
|
|
|
276
|
-
user_id: Mapped[
|
|
300
|
+
user_id: Mapped[str | None] = mapped_column(user_id_type(), index=True, nullable=True)
|
|
277
301
|
provider: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
|
|
278
302
|
provider_setup_intent_id: Mapped[str] = mapped_column(
|
|
279
303
|
String(128), unique=True, index=True, nullable=False
|
|
@@ -281,9 +305,11 @@ class PaySetupIntent(ModelBase):
|
|
|
281
305
|
status: Mapped[str] = mapped_column(
|
|
282
306
|
String(32), index=True, nullable=False
|
|
283
307
|
) # requires_action|succeeded|canceled|processing
|
|
284
|
-
client_secret: Mapped[
|
|
308
|
+
client_secret: Mapped[str | None] = mapped_column(String(255))
|
|
285
309
|
created_at: Mapped[datetime] = mapped_column(
|
|
286
|
-
DateTime(timezone=True),
|
|
310
|
+
DateTime(timezone=True),
|
|
311
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
312
|
+
nullable=False,
|
|
287
313
|
)
|
|
288
314
|
|
|
289
315
|
__table_args__ = (
|
|
@@ -303,16 +329,18 @@ class PayDispute(ModelBase):
|
|
|
303
329
|
provider_dispute_id: Mapped[str] = mapped_column(
|
|
304
330
|
String(128), unique=True, index=True, nullable=False
|
|
305
331
|
)
|
|
306
|
-
provider_charge_id: Mapped[
|
|
332
|
+
provider_charge_id: Mapped[str | None] = mapped_column(String(128), index=True)
|
|
307
333
|
amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
308
334
|
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
309
|
-
reason: Mapped[
|
|
335
|
+
reason: Mapped[str | None] = mapped_column(String(64))
|
|
310
336
|
status: Mapped[str] = mapped_column(
|
|
311
337
|
String(32), index=True, nullable=False
|
|
312
338
|
) # needs_response|under_review|won|lost|warning_closed
|
|
313
|
-
evidence_due_by: Mapped[
|
|
339
|
+
evidence_due_by: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
314
340
|
created_at: Mapped[datetime] = mapped_column(
|
|
315
|
-
DateTime(timezone=True),
|
|
341
|
+
DateTime(timezone=True),
|
|
342
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
343
|
+
nullable=False,
|
|
316
344
|
)
|
|
317
345
|
|
|
318
346
|
|
|
@@ -332,8 +360,10 @@ class PayPayout(ModelBase):
|
|
|
332
360
|
status: Mapped[str] = mapped_column(
|
|
333
361
|
String(32), index=True, nullable=False
|
|
334
362
|
) # pending|in_transit|paid|canceled|failed
|
|
335
|
-
arrival_date: Mapped[
|
|
336
|
-
type: Mapped[
|
|
363
|
+
arrival_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
364
|
+
type: Mapped[str | None] = mapped_column(String(32)) # bank_account|card|...
|
|
337
365
|
created_at: Mapped[datetime] = mapped_column(
|
|
338
|
-
DateTime(timezone=True),
|
|
366
|
+
DateTime(timezone=True),
|
|
367
|
+
server_default=text("CURRENT_TIMESTAMP"),
|
|
368
|
+
nullable=False,
|
|
339
369
|
)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
from
|
|
5
|
-
from
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from datetime import UTC, date, datetime
|
|
6
|
+
from typing import Any, Literal, cast
|
|
6
7
|
|
|
7
8
|
from svc_infra.apf_payments.schemas import (
|
|
8
9
|
BalanceAmount,
|
|
@@ -43,9 +44,9 @@ from svc_infra.apf_payments.settings import get_payments_settings
|
|
|
43
44
|
from .base import ProviderAdapter
|
|
44
45
|
|
|
45
46
|
try: # pragma: no cover - optional dependency
|
|
46
|
-
import aiydan
|
|
47
|
+
import aiydan
|
|
47
48
|
except Exception: # pragma: no cover - handled at runtime
|
|
48
|
-
aiydan = None
|
|
49
|
+
aiydan = None
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
async def _maybe_await(result: Any) -> Any:
|
|
@@ -62,24 +63,25 @@ def _coerce_id(data: dict[str, Any], *candidates: str) -> str:
|
|
|
62
63
|
raise RuntimeError(f"Aiydan payload missing id fields: {candidates}")
|
|
63
64
|
|
|
64
65
|
|
|
65
|
-
def _ensure_utc_isoformat(value: Any) ->
|
|
66
|
+
def _ensure_utc_isoformat(value: Any) -> str | None:
|
|
66
67
|
if value is None:
|
|
67
68
|
return None
|
|
68
69
|
if isinstance(value, str):
|
|
69
70
|
return value
|
|
70
71
|
if isinstance(value, datetime):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
dt: datetime = value
|
|
73
|
+
if dt.tzinfo is None:
|
|
74
|
+
dt = dt.replace(tzinfo=UTC)
|
|
75
|
+
return dt.astimezone(UTC).isoformat()
|
|
74
76
|
if isinstance(value, date):
|
|
75
|
-
return datetime(value.year, value.month, value.day, tzinfo=
|
|
77
|
+
return datetime(value.year, value.month, value.day, tzinfo=UTC).isoformat()
|
|
76
78
|
try:
|
|
77
79
|
parsed = datetime.fromisoformat(str(value))
|
|
78
80
|
if parsed.tzinfo is None:
|
|
79
|
-
parsed = parsed.replace(tzinfo=
|
|
80
|
-
return parsed.astimezone(
|
|
81
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
82
|
+
return parsed.astimezone(UTC).isoformat()
|
|
81
83
|
except Exception:
|
|
82
|
-
return str(value)
|
|
84
|
+
return cast("str", str(value)) # Cast needed since value is Any
|
|
83
85
|
|
|
84
86
|
|
|
85
87
|
def _customer_to_out(data: dict[str, Any]) -> CustomerOut:
|
|
@@ -208,12 +210,15 @@ def _invoice_line_item_to_out(data: dict[str, Any]) -> InvoiceLineItemOut:
|
|
|
208
210
|
price = data.get("price") or {}
|
|
209
211
|
if not isinstance(price, dict):
|
|
210
212
|
price = {"id": getattr(price, "id", None)}
|
|
213
|
+
quantity = int(data.get("quantity", 0) or 0)
|
|
214
|
+
unit_amount = int(data.get("unit_amount", 0) or 0)
|
|
215
|
+
amount = int(data.get("amount", unit_amount * quantity) or 0)
|
|
211
216
|
return InvoiceLineItemOut(
|
|
212
217
|
id=line_id,
|
|
213
218
|
description=data.get("description"),
|
|
214
219
|
currency=str(data.get("currency", price.get("currency", ""))).upper(),
|
|
215
|
-
quantity=
|
|
216
|
-
|
|
220
|
+
quantity=quantity,
|
|
221
|
+
amount=amount,
|
|
217
222
|
provider_price_id=price.get("id"),
|
|
218
223
|
)
|
|
219
224
|
|
|
@@ -268,6 +273,10 @@ def _payout_to_out(data: dict[str, Any]) -> PayoutOut:
|
|
|
268
273
|
|
|
269
274
|
|
|
270
275
|
def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
|
|
276
|
+
action_raw = data.get("action")
|
|
277
|
+
action: Literal["increment", "set"] | None = None
|
|
278
|
+
if action_raw in ("increment", "set"):
|
|
279
|
+
action = cast("Literal['increment', 'set']", action_raw)
|
|
271
280
|
return UsageRecordOut(
|
|
272
281
|
id=str(data.get("id")),
|
|
273
282
|
quantity=int(data.get("quantity", 0) or 0),
|
|
@@ -278,7 +287,7 @@ def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
|
|
|
278
287
|
provider_price_id=(
|
|
279
288
|
str(data.get("provider_price_id")) if data.get("provider_price_id") else None
|
|
280
289
|
),
|
|
281
|
-
action=
|
|
290
|
+
action=action,
|
|
282
291
|
)
|
|
283
292
|
|
|
284
293
|
|
|
@@ -315,33 +324,35 @@ def _balance_snapshot_to_out(data: dict[str, Any]) -> BalanceSnapshotOut:
|
|
|
315
324
|
|
|
316
325
|
def _ensure_sequence(result: Any) -> Sequence[dict[str, Any]]:
|
|
317
326
|
if isinstance(result, Sequence):
|
|
318
|
-
return result
|
|
327
|
+
return result
|
|
319
328
|
if isinstance(result, dict):
|
|
320
329
|
items = result.get("items")
|
|
321
330
|
if isinstance(items, Sequence):
|
|
322
|
-
return items
|
|
331
|
+
return items
|
|
323
332
|
raise RuntimeError("Expected sequence payload from Aiydan client")
|
|
324
333
|
|
|
325
334
|
|
|
326
|
-
def _ensure_list_response(
|
|
335
|
+
def _ensure_list_response(
|
|
336
|
+
result: Any,
|
|
337
|
+
) -> tuple[Sequence[dict[str, Any]], str | None]:
|
|
327
338
|
if isinstance(result, tuple) and len(result) == 2:
|
|
328
339
|
items, cursor = result
|
|
329
340
|
if isinstance(items, Sequence) or items is None:
|
|
330
|
-
return (items or []), cursor
|
|
341
|
+
return (items or []), cursor
|
|
331
342
|
if isinstance(result, dict):
|
|
332
343
|
items = result.get("items")
|
|
333
344
|
cursor = result.get("next_cursor") or result.get("cursor")
|
|
334
345
|
if isinstance(items, Sequence):
|
|
335
346
|
return items, cursor
|
|
336
347
|
if isinstance(result, Sequence):
|
|
337
|
-
return result, None
|
|
348
|
+
return result, None
|
|
338
349
|
raise RuntimeError("Expected iterable response from Aiydan client")
|
|
339
350
|
|
|
340
351
|
|
|
341
352
|
class AiydanAdapter(ProviderAdapter):
|
|
342
353
|
name = "aiydan"
|
|
343
354
|
|
|
344
|
-
def __init__(self, *, client:
|
|
355
|
+
def __init__(self, *, client: Any | None = None):
|
|
345
356
|
settings = get_payments_settings()
|
|
346
357
|
cfg = settings.aiydan
|
|
347
358
|
if client is not None:
|
|
@@ -777,7 +788,12 @@ class AiydanAdapter(ProviderAdapter):
|
|
|
777
788
|
return _intent_to_out(result)
|
|
778
789
|
|
|
779
790
|
async def list_customers(
|
|
780
|
-
self,
|
|
791
|
+
self,
|
|
792
|
+
*,
|
|
793
|
+
provider: str | None,
|
|
794
|
+
user_id: str | None,
|
|
795
|
+
limit: int,
|
|
796
|
+
cursor: str | None,
|
|
781
797
|
) -> tuple[list[CustomerOut], str | None]:
|
|
782
798
|
result = await _maybe_await(
|
|
783
799
|
self._client.list_customers(
|
|
@@ -790,7 +806,7 @@ class AiydanAdapter(ProviderAdapter):
|
|
|
790
806
|
items, next_cursor = _ensure_list_response(result)
|
|
791
807
|
return [_customer_to_out(item) for item in items], next_cursor
|
|
792
808
|
|
|
793
|
-
async def get_customer(self, provider_customer_id: str) ->
|
|
809
|
+
async def get_customer(self, provider_customer_id: str) -> CustomerOut | None:
|
|
794
810
|
result = await _maybe_await(self._client.get_customer(provider_customer_id))
|
|
795
811
|
if result is None:
|
|
796
812
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, Protocol
|
|
4
4
|
|
|
5
5
|
from ..schemas import (
|
|
6
6
|
BalanceSnapshotOut,
|
|
@@ -186,12 +186,17 @@ class ProviderAdapter(Protocol):
|
|
|
186
186
|
|
|
187
187
|
# --- Customers ---
|
|
188
188
|
async def list_customers(
|
|
189
|
-
self,
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
provider: str | None,
|
|
192
|
+
user_id: str | None,
|
|
193
|
+
limit: int,
|
|
194
|
+
cursor: str | None,
|
|
190
195
|
) -> tuple[list[CustomerOut], str | None]:
|
|
191
196
|
"""Optional: if not implemented, the service will list from local DB."""
|
|
192
197
|
pass
|
|
193
198
|
|
|
194
|
-
async def get_customer(self, provider_customer_id: str) ->
|
|
199
|
+
async def get_customer(self, provider_customer_id: str) -> CustomerOut | None:
|
|
195
200
|
pass
|
|
196
201
|
|
|
197
202
|
# --- Products / Prices ---
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Dict, Optional
|
|
4
|
-
|
|
5
3
|
from ..settings import get_payments_settings
|
|
6
4
|
from .base import ProviderAdapter
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
class ProviderRegistry:
|
|
10
8
|
def __init__(self):
|
|
11
|
-
self._adapters:
|
|
9
|
+
self._adapters: dict[str, ProviderAdapter] = {}
|
|
12
10
|
|
|
13
11
|
def register(self, adapter: ProviderAdapter):
|
|
14
12
|
self._adapters[adapter.name] = adapter
|
|
15
13
|
|
|
16
|
-
def get(self, name:
|
|
14
|
+
def get(self, name: str | None = None) -> ProviderAdapter:
|
|
17
15
|
settings = get_payments_settings()
|
|
18
16
|
key = (name or settings.default_provider).lower()
|
|
19
17
|
if key not in self._adapters:
|
|
@@ -21,7 +19,7 @@ class ProviderRegistry:
|
|
|
21
19
|
return self._adapters[key]
|
|
22
20
|
|
|
23
21
|
|
|
24
|
-
_REGISTRY:
|
|
22
|
+
_REGISTRY: ProviderRegistry | None = None
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
def get_provider_registry() -> ProviderRegistry:
|