svc-infra 0.1.706__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/apf_payments/models.py +47 -108
- svc_infra/apf_payments/provider/__init__.py +2 -2
- svc_infra/apf_payments/provider/aiydan.py +42 -100
- svc_infra/apf_payments/provider/base.py +10 -26
- svc_infra/apf_payments/provider/registry.py +3 -5
- svc_infra/apf_payments/provider/stripe.py +63 -135
- svc_infra/apf_payments/schemas.py +82 -90
- svc_infra/apf_payments/service.py +40 -86
- svc_infra/apf_payments/settings.py +10 -13
- svc_infra/api/__init__.py +13 -13
- svc_infra/api/fastapi/__init__.py +19 -0
- svc_infra/api/fastapi/admin/add.py +13 -18
- svc_infra/api/fastapi/apf_payments/router.py +47 -84
- svc_infra/api/fastapi/apf_payments/setup.py +7 -13
- svc_infra/api/fastapi/auth/__init__.py +1 -1
- svc_infra/api/fastapi/auth/_cookies.py +3 -9
- svc_infra/api/fastapi/auth/add.py +4 -8
- svc_infra/api/fastapi/auth/gaurd.py +9 -26
- svc_infra/api/fastapi/auth/mfa/models.py +4 -7
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +3 -3
- svc_infra/api/fastapi/auth/mfa/router.py +9 -15
- svc_infra/api/fastapi/auth/mfa/security.py +3 -5
- svc_infra/api/fastapi/auth/mfa/utils.py +3 -2
- svc_infra/api/fastapi/auth/mfa/verify.py +2 -9
- svc_infra/api/fastapi/auth/providers.py +4 -6
- svc_infra/api/fastapi/auth/routers/apikey_router.py +16 -18
- svc_infra/api/fastapi/auth/routers/oauth_router.py +37 -85
- svc_infra/api/fastapi/auth/routers/session_router.py +3 -6
- svc_infra/api/fastapi/auth/security.py +17 -28
- svc_infra/api/fastapi/auth/sender.py +1 -3
- svc_infra/api/fastapi/auth/settings.py +18 -19
- svc_infra/api/fastapi/auth/state.py +6 -7
- svc_infra/api/fastapi/auth/ws_security.py +2 -2
- svc_infra/api/fastapi/billing/router.py +6 -8
- svc_infra/api/fastapi/db/http.py +10 -11
- svc_infra/api/fastapi/db/nosql/mongo/add.py +5 -15
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +14 -15
- svc_infra/api/fastapi/db/sql/add.py +6 -14
- svc_infra/api/fastapi/db/sql/crud_router.py +27 -40
- svc_infra/api/fastapi/db/sql/health.py +1 -3
- svc_infra/api/fastapi/db/sql/session.py +4 -5
- svc_infra/api/fastapi/db/sql/users.py +8 -11
- svc_infra/api/fastapi/dependencies/ratelimit.py +4 -6
- svc_infra/api/fastapi/docs/add.py +13 -23
- svc_infra/api/fastapi/docs/landing.py +6 -8
- svc_infra/api/fastapi/docs/scoped.py +34 -42
- svc_infra/api/fastapi/dual/dualize.py +1 -1
- svc_infra/api/fastapi/dual/protected.py +12 -21
- svc_infra/api/fastapi/dual/router.py +14 -31
- svc_infra/api/fastapi/ease.py +57 -13
- svc_infra/api/fastapi/http/conditional.py +3 -5
- svc_infra/api/fastapi/middleware/errors/catchall.py +2 -6
- svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -4
- svc_infra/api/fastapi/middleware/errors/handlers.py +12 -18
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +4 -13
- svc_infra/api/fastapi/middleware/idempotency.py +11 -16
- svc_infra/api/fastapi/middleware/idempotency_store.py +14 -14
- svc_infra/api/fastapi/middleware/optimistic_lock.py +5 -8
- svc_infra/api/fastapi/middleware/ratelimit.py +8 -8
- svc_infra/api/fastapi/middleware/ratelimit_store.py +7 -8
- svc_infra/api/fastapi/middleware/request_id.py +1 -3
- svc_infra/api/fastapi/middleware/timeout.py +9 -10
- svc_infra/api/fastapi/object_router.py +1060 -0
- svc_infra/api/fastapi/openapi/apply.py +5 -6
- svc_infra/api/fastapi/openapi/conventions.py +4 -4
- svc_infra/api/fastapi/openapi/mutators.py +13 -31
- 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 -3
- svc_infra/api/fastapi/ops/add.py +7 -9
- svc_infra/api/fastapi/pagination.py +25 -37
- svc_infra/api/fastapi/routers/__init__.py +16 -38
- svc_infra/api/fastapi/setup.py +13 -31
- svc_infra/api/fastapi/tenancy/add.py +3 -2
- svc_infra/api/fastapi/tenancy/context.py +8 -7
- svc_infra/api/fastapi/versioned.py +3 -2
- svc_infra/app/env.py +5 -7
- svc_infra/app/logging/add.py +2 -1
- svc_infra/app/logging/filter.py +1 -1
- svc_infra/app/logging/formats.py +3 -2
- svc_infra/app/root.py +3 -3
- svc_infra/billing/__init__.py +19 -2
- svc_infra/billing/async_service.py +27 -7
- svc_infra/billing/jobs.py +23 -33
- svc_infra/billing/models.py +21 -52
- svc_infra/billing/quotas.py +5 -7
- svc_infra/billing/schemas.py +4 -6
- svc_infra/cache/__init__.py +12 -5
- svc_infra/cache/add.py +6 -9
- svc_infra/cache/backend.py +6 -5
- svc_infra/cache/decorators.py +17 -28
- svc_infra/cache/keys.py +2 -2
- svc_infra/cache/recache.py +22 -35
- svc_infra/cache/resources.py +8 -16
- svc_infra/cache/ttl.py +2 -3
- svc_infra/cache/utils.py +5 -6
- svc_infra/cli/__init__.py +4 -12
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +11 -10
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +6 -9
- svc_infra/cli/cmds/db/ops_cmds.py +3 -6
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +24 -41
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +9 -17
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +10 -10
- svc_infra/cli/cmds/docs/docs_cmds.py +7 -10
- svc_infra/cli/cmds/dx/dx_cmds.py +5 -11
- svc_infra/cli/cmds/jobs/jobs_cmds.py +2 -7
- svc_infra/cli/cmds/obs/obs_cmds.py +4 -7
- svc_infra/cli/cmds/sdk/sdk_cmds.py +5 -15
- svc_infra/cli/foundation/runner.py +6 -11
- svc_infra/cli/foundation/typer_bootstrap.py +1 -2
- svc_infra/data/__init__.py +83 -0
- svc_infra/data/add.py +5 -5
- svc_infra/data/backup.py +8 -10
- svc_infra/data/erasure.py +3 -2
- svc_infra/data/fixtures.py +3 -3
- svc_infra/data/retention.py +8 -13
- svc_infra/db/crud_schema.py +9 -8
- svc_infra/db/nosql/__init__.py +0 -1
- svc_infra/db/nosql/constants.py +1 -1
- svc_infra/db/nosql/core.py +7 -14
- svc_infra/db/nosql/indexes.py +11 -10
- svc_infra/db/nosql/management.py +3 -3
- svc_infra/db/nosql/mongo/client.py +3 -3
- svc_infra/db/nosql/mongo/settings.py +2 -6
- svc_infra/db/nosql/repository.py +27 -28
- svc_infra/db/nosql/resource.py +15 -20
- svc_infra/db/nosql/scaffold.py +13 -17
- svc_infra/db/nosql/service.py +3 -4
- svc_infra/db/nosql/service_with_hooks.py +4 -3
- svc_infra/db/nosql/types.py +2 -6
- svc_infra/db/nosql/utils.py +4 -4
- svc_infra/db/ops.py +14 -18
- svc_infra/db/outbox.py +15 -18
- svc_infra/db/sql/apikey.py +12 -21
- svc_infra/db/sql/authref.py +3 -7
- svc_infra/db/sql/constants.py +9 -9
- svc_infra/db/sql/core.py +11 -11
- svc_infra/db/sql/management.py +2 -6
- svc_infra/db/sql/repository.py +17 -24
- svc_infra/db/sql/resource.py +14 -13
- svc_infra/db/sql/scaffold.py +13 -17
- svc_infra/db/sql/service.py +7 -16
- svc_infra/db/sql/service_with_hooks.py +4 -3
- svc_infra/db/sql/tenant.py +6 -14
- svc_infra/db/sql/uniq.py +8 -7
- svc_infra/db/sql/uniq_hooks.py +14 -19
- svc_infra/db/sql/utils.py +24 -53
- svc_infra/db/utils.py +3 -3
- svc_infra/deploy/__init__.py +8 -15
- svc_infra/documents/add.py +7 -8
- svc_infra/documents/ease.py +8 -8
- svc_infra/documents/models.py +3 -3
- svc_infra/documents/storage.py +11 -13
- svc_infra/dx/__init__.py +58 -0
- svc_infra/dx/add.py +1 -3
- svc_infra/dx/changelog.py +2 -2
- svc_infra/dx/checks.py +1 -1
- svc_infra/health/__init__.py +15 -16
- svc_infra/http/client.py +10 -14
- svc_infra/jobs/__init__.py +79 -0
- svc_infra/jobs/builtins/outbox_processor.py +3 -5
- svc_infra/jobs/builtins/webhook_delivery.py +1 -3
- svc_infra/jobs/loader.py +4 -5
- svc_infra/jobs/queue.py +14 -24
- svc_infra/jobs/redis_queue.py +20 -34
- svc_infra/jobs/runner.py +7 -11
- svc_infra/jobs/scheduler.py +5 -5
- svc_infra/jobs/worker.py +1 -1
- svc_infra/loaders/base.py +5 -4
- svc_infra/loaders/github.py +1 -3
- svc_infra/loaders/url.py +3 -9
- svc_infra/logging/__init__.py +7 -6
- svc_infra/mcp/__init__.py +82 -0
- svc_infra/mcp/svc_infra_mcp.py +2 -2
- svc_infra/obs/add.py +4 -3
- svc_infra/obs/cloud_dash.py +1 -1
- svc_infra/obs/metrics/__init__.py +3 -3
- svc_infra/obs/metrics/asgi.py +9 -14
- svc_infra/obs/metrics/base.py +13 -13
- svc_infra/obs/metrics/http.py +5 -9
- svc_infra/obs/metrics/sqlalchemy.py +9 -12
- svc_infra/obs/metrics.py +3 -3
- svc_infra/obs/settings.py +2 -6
- 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 +5 -9
- svc_infra/security/audit.py +14 -17
- svc_infra/security/audit_service.py +9 -9
- svc_infra/security/hibp.py +3 -6
- svc_infra/security/jwt_rotation.py +7 -10
- svc_infra/security/lockout.py +12 -11
- svc_infra/security/models.py +37 -46
- svc_infra/security/oauth_models.py +8 -8
- svc_infra/security/org_invites.py +11 -13
- svc_infra/security/passwords.py +4 -6
- svc_infra/security/permissions.py +8 -7
- svc_infra/security/session.py +6 -7
- svc_infra/security/signed_cookies.py +9 -9
- svc_infra/storage/add.py +5 -8
- svc_infra/storage/backends/local.py +13 -21
- svc_infra/storage/backends/memory.py +4 -7
- svc_infra/storage/backends/s3.py +17 -36
- svc_infra/storage/base.py +2 -2
- svc_infra/storage/easy.py +4 -8
- svc_infra/storage/settings.py +16 -18
- svc_infra/testing/__init__.py +36 -39
- svc_infra/utils.py +169 -8
- svc_infra/webhooks/__init__.py +1 -1
- svc_infra/webhooks/add.py +17 -29
- svc_infra/webhooks/encryption.py +2 -2
- svc_infra/webhooks/fastapi.py +2 -4
- svc_infra/webhooks/router.py +3 -3
- svc_infra/webhooks/service.py +5 -6
- svc_infra/webhooks/signing.py +5 -5
- svc_infra/websocket/add.py +2 -3
- svc_infra/websocket/client.py +3 -2
- svc_infra/websocket/config.py +6 -18
- svc_infra/websocket/manager.py +9 -10
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/METADATA +11 -5
- svc_infra-1.1.0.dist-info/RECORD +364 -0
- svc_infra/billing/service.py +0 -123
- svc_infra-0.1.706.dist-info/RECORD +0 -357
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/LICENSE +0 -0
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.706.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
svc_infra/db/sql/apikey.py
CHANGED
|
@@ -4,8 +4,7 @@ import hashlib
|
|
|
4
4
|
import hmac
|
|
5
5
|
import os
|
|
6
6
|
import uuid
|
|
7
|
-
from datetime import
|
|
8
|
-
from typing import Optional, Type
|
|
7
|
+
from datetime import UTC, datetime
|
|
9
8
|
|
|
10
9
|
from sqlalchemy import (
|
|
11
10
|
JSON,
|
|
@@ -40,24 +39,22 @@ def _hmac_sha256(s: str) -> str:
|
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def _now() -> datetime:
|
|
43
|
-
return datetime.now(
|
|
42
|
+
return datetime.now(UTC)
|
|
44
43
|
|
|
45
44
|
|
|
46
45
|
# -------------------- Factory & registry --------------------
|
|
47
46
|
|
|
48
|
-
_ApiKeyModel:
|
|
47
|
+
_ApiKeyModel: type | None = None
|
|
49
48
|
|
|
50
49
|
|
|
51
50
|
def get_apikey_model() -> type:
|
|
52
51
|
"""Return the bound ApiKey model (or raise if not enabled)."""
|
|
53
52
|
if _ApiKeyModel is None:
|
|
54
|
-
raise RuntimeError(
|
|
55
|
-
"ApiKey model is not enabled. Call bind_apikey_model(...) first."
|
|
56
|
-
)
|
|
53
|
+
raise RuntimeError("ApiKey model is not enabled. Call bind_apikey_model(...) first.")
|
|
57
54
|
return _ApiKeyModel
|
|
58
55
|
|
|
59
56
|
|
|
60
|
-
def bind_apikey_model(user_model:
|
|
57
|
+
def bind_apikey_model(user_model: type[ModelBase], *, table_name: str = "api_keys") -> type:
|
|
61
58
|
"""
|
|
62
59
|
Create and register an ApiKey model bound to the provided user_model and table name.
|
|
63
60
|
Call this once during app boot (e.g., inside add_auth_users when enable_api_keys=True).
|
|
@@ -66,12 +63,10 @@ def bind_apikey_model(user_model: Type, *, table_name: str = "api_keys") -> type
|
|
|
66
63
|
class ApiKey(ModelBase):
|
|
67
64
|
__tablename__ = table_name
|
|
68
65
|
|
|
69
|
-
id: Mapped[uuid.UUID] = mapped_column(
|
|
70
|
-
GUID(), primary_key=True, default=uuid.uuid4
|
|
71
|
-
)
|
|
66
|
+
id: Mapped[uuid.UUID] = mapped_column(GUID(), primary_key=True, default=uuid.uuid4)
|
|
72
67
|
|
|
73
68
|
@declared_attr
|
|
74
|
-
def user_id(cls) -> Mapped[uuid.UUID | None]:
|
|
69
|
+
def user_id(cls) -> Mapped[uuid.UUID | None]:
|
|
75
70
|
return mapped_column(
|
|
76
71
|
GUID(),
|
|
77
72
|
ForeignKey(f"{user_model.__tablename__}.id", ondelete="SET NULL"),
|
|
@@ -80,7 +75,7 @@ def bind_apikey_model(user_model: Type, *, table_name: str = "api_keys") -> type
|
|
|
80
75
|
)
|
|
81
76
|
|
|
82
77
|
@declared_attr
|
|
83
|
-
def user(cls):
|
|
78
|
+
def user(cls):
|
|
84
79
|
return relationship(user_model.__name__, lazy="selectin")
|
|
85
80
|
|
|
86
81
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
@@ -89,9 +84,7 @@ def bind_apikey_model(user_model: Type, *, table_name: str = "api_keys") -> type
|
|
|
89
84
|
key_prefix: Mapped[str] = mapped_column(String(12), index=True, nullable=False)
|
|
90
85
|
key_hash: Mapped[str] = mapped_column(String(64), nullable=False) # hex sha256
|
|
91
86
|
|
|
92
|
-
scopes: Mapped[list[str]] = mapped_column(
|
|
93
|
-
MutableList.as_mutable(JSON), default=list
|
|
94
|
-
)
|
|
87
|
+
scopes: Mapped[list[str]] = mapped_column(MutableList.as_mutable(JSON), default=list)
|
|
95
88
|
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
96
89
|
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
97
90
|
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
@@ -125,9 +118,7 @@ def bind_apikey_model(user_model: Type, *, table_name: str = "api_keys") -> type
|
|
|
125
118
|
import secrets
|
|
126
119
|
|
|
127
120
|
prefix = secrets.token_urlsafe(6).replace("-", "").replace("_", "")[:8]
|
|
128
|
-
rand = (
|
|
129
|
-
base64.urlsafe_b64encode(secrets.token_bytes(24)).decode().rstrip("=")
|
|
130
|
-
)
|
|
121
|
+
rand = base64.urlsafe_b64encode(secrets.token_bytes(24)).decode().rstrip("=")
|
|
131
122
|
plaintext = f"ak_{prefix}_{rand}"
|
|
132
123
|
return plaintext, prefix, _hmac_sha256(plaintext)
|
|
133
124
|
|
|
@@ -143,7 +134,7 @@ def bind_apikey_model(user_model: Type, *, table_name: str = "api_keys") -> type
|
|
|
143
134
|
return ApiKey
|
|
144
135
|
|
|
145
136
|
|
|
146
|
-
def try_autobind_apikey_model(*, require_env: bool = False) ->
|
|
137
|
+
def try_autobind_apikey_model(*, require_env: bool = False) -> type | None:
|
|
147
138
|
"""
|
|
148
139
|
If API keys aren’t bound yet, try to discover the User model and bind.
|
|
149
140
|
- If require_env=True, only bind when AUTH_ENABLE_API_KEYS is truthy.
|
|
@@ -161,7 +152,7 @@ def try_autobind_apikey_model(*, require_env: bool = False) -> Optional[type]:
|
|
|
161
152
|
from svc_infra.db.sql.base import ModelBase
|
|
162
153
|
|
|
163
154
|
# SQLAlchemy 2.x: iterate registry mappers to get mapped classes
|
|
164
|
-
for mapper in list(
|
|
155
|
+
for mapper in list(ModelBase.registry.mappers):
|
|
165
156
|
cls = mapper.class_
|
|
166
157
|
if getattr(cls, "__svc_infra_auth_user__", False):
|
|
167
158
|
return bind_apikey_model(cls) # binds and returns ApiKey
|
svc_infra/db/sql/authref.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Optional, Tuple
|
|
4
|
-
|
|
5
3
|
from sqlalchemy import ForeignKeyConstraint
|
|
6
4
|
from sqlalchemy.sql.type_api import TypeEngine
|
|
7
5
|
|
|
@@ -9,7 +7,7 @@ from svc_infra.db.sql.base import ModelBase
|
|
|
9
7
|
from svc_infra.db.sql.types import GUID
|
|
10
8
|
|
|
11
9
|
|
|
12
|
-
def _find_auth_mapper() ->
|
|
10
|
+
def _find_auth_mapper() -> tuple[str, TypeEngine, str] | None:
|
|
13
11
|
"""
|
|
14
12
|
Returns (table_name, pk_sqlatype, pk_name) for the auth user model.
|
|
15
13
|
Looks for any mapped class with __svc_infra_auth_user__ = True that
|
|
@@ -36,7 +34,7 @@ def _find_auth_mapper() -> Optional[Tuple[str, TypeEngine, str]]:
|
|
|
36
34
|
return None
|
|
37
35
|
|
|
38
36
|
|
|
39
|
-
def resolve_auth_table_pk() ->
|
|
37
|
+
def resolve_auth_table_pk() -> tuple[str, TypeEngine, str]:
|
|
40
38
|
"""
|
|
41
39
|
Single source of truth for the auth table and PK.
|
|
42
40
|
Falls back to ('users', GUID(), 'id') if nothing is marked.
|
|
@@ -62,6 +60,4 @@ def user_fk_constraint(
|
|
|
62
60
|
Returns a table-level ForeignKeyConstraint([...], [<auth_table>.<pk>]) for the given column.
|
|
63
61
|
"""
|
|
64
62
|
table, _pk_type, pk_name = resolve_auth_table_pk()
|
|
65
|
-
return ForeignKeyConstraint(
|
|
66
|
-
[column_name], [f"{table}.{pk_name}"], ondelete=ondelete
|
|
67
|
-
)
|
|
63
|
+
return ForeignKeyConstraint([column_name], [f"{table}.{pk_name}"], ondelete=ondelete)
|
svc_infra/db/sql/constants.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Sequence
|
|
5
5
|
|
|
6
6
|
# Environment variable names to look up for DB URL
|
|
7
7
|
# Order matters: svc-infra canonical names first, then common PaaS names
|
|
@@ -22,16 +22,16 @@ try:
|
|
|
22
22
|
import importlib.resources as pkg
|
|
23
23
|
|
|
24
24
|
_tmpl_pkg = pkg.files("svc_infra.db.sql.templates.setup")
|
|
25
|
-
ALEMBIC_INI_TEMPLATE = _tmpl_pkg.joinpath("alembic.ini.tmpl").read_text(
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
ALEMBIC_SCRIPT_TEMPLATE = _tmpl_pkg.joinpath("script.py.mako.tmpl").read_text(
|
|
29
|
-
encoding="utf-8"
|
|
30
|
-
)
|
|
25
|
+
ALEMBIC_INI_TEMPLATE = _tmpl_pkg.joinpath("alembic.ini.tmpl").read_text(encoding="utf-8")
|
|
26
|
+
ALEMBIC_SCRIPT_TEMPLATE = _tmpl_pkg.joinpath("script.py.mako.tmpl").read_text(encoding="utf-8")
|
|
31
27
|
except Exception:
|
|
32
28
|
# Fallbacks (should not normally happen). Provide minimal safe defaults.
|
|
33
|
-
ALEMBIC_INI_TEMPLATE =
|
|
34
|
-
|
|
29
|
+
ALEMBIC_INI_TEMPLATE = (
|
|
30
|
+
"""[alembic]\nscript_location = {script_location}\nsqlalchemy.url = {sqlalchemy_url}\n"""
|
|
31
|
+
)
|
|
32
|
+
ALEMBIC_INI_TEMPLATE = (
|
|
33
|
+
"""[alembic]\nscript_location = {script_location}\nsqlalchemy.url = {sqlalchemy_url}\n"""
|
|
34
|
+
)
|
|
35
35
|
ALEMBIC_SCRIPT_TEMPLATE = '"""${message}"""\nfrom alembic import op\nimport sqlalchemy as sa\n\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\ndef upgrade():\n ${upgrades if upgrades else "pass"}\n\n\ndef downgrade():\n ${downgrades if downgrades else "pass"}\n'
|
|
36
36
|
__all__ = [
|
|
37
37
|
"DEFAULT_DB_ENV_VARS",
|
svc_infra/db/sql/core.py
CHANGED
|
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import io
|
|
5
5
|
import os
|
|
6
|
+
from collections.abc import Sequence
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import Optional, Sequence
|
|
9
9
|
|
|
10
10
|
from alembic import command
|
|
11
11
|
from alembic.config import Config
|
|
@@ -30,7 +30,7 @@ from svc_infra.db.sql.utils import (
|
|
|
30
30
|
def init_alembic(
|
|
31
31
|
*,
|
|
32
32
|
script_location: str = "migrations",
|
|
33
|
-
discover_packages:
|
|
33
|
+
discover_packages: Sequence[str] | None = None,
|
|
34
34
|
overwrite: bool = False,
|
|
35
35
|
) -> Path:
|
|
36
36
|
"""
|
|
@@ -157,7 +157,7 @@ def revision(
|
|
|
157
157
|
def upgrade(
|
|
158
158
|
revision_target: str = "head",
|
|
159
159
|
*,
|
|
160
|
-
database_url:
|
|
160
|
+
database_url: str | None = None,
|
|
161
161
|
) -> dict:
|
|
162
162
|
"""
|
|
163
163
|
Apply migrations forward.
|
|
@@ -181,7 +181,7 @@ def upgrade(
|
|
|
181
181
|
def downgrade(
|
|
182
182
|
*,
|
|
183
183
|
revision_target: str = "-1",
|
|
184
|
-
database_url:
|
|
184
|
+
database_url: str | None = None,
|
|
185
185
|
) -> dict:
|
|
186
186
|
"""Revert migrations down to the specified revision or relative step.
|
|
187
187
|
|
|
@@ -203,7 +203,7 @@ def downgrade(
|
|
|
203
203
|
def current(
|
|
204
204
|
verbose: bool = False,
|
|
205
205
|
*,
|
|
206
|
-
database_url:
|
|
206
|
+
database_url: str | None = None,
|
|
207
207
|
) -> dict:
|
|
208
208
|
"""Print the current database revision(s)."""
|
|
209
209
|
root = prepare_env()
|
|
@@ -224,7 +224,7 @@ def current(
|
|
|
224
224
|
def history(
|
|
225
225
|
*,
|
|
226
226
|
verbose: bool = False,
|
|
227
|
-
database_url:
|
|
227
|
+
database_url: str | None = None,
|
|
228
228
|
) -> dict:
|
|
229
229
|
"""Show the migration history for this project."""
|
|
230
230
|
root = prepare_env()
|
|
@@ -245,7 +245,7 @@ def history(
|
|
|
245
245
|
def stamp(
|
|
246
246
|
*,
|
|
247
247
|
revision_target: str = "head",
|
|
248
|
-
database_url:
|
|
248
|
+
database_url: str | None = None,
|
|
249
249
|
) -> dict:
|
|
250
250
|
"""Set the current database revision without running migrations. Useful for marking an existing database as up-to-date."""
|
|
251
251
|
root = prepare_env()
|
|
@@ -262,8 +262,8 @@ def stamp(
|
|
|
262
262
|
|
|
263
263
|
def merge_heads(
|
|
264
264
|
*,
|
|
265
|
-
message:
|
|
266
|
-
database_url:
|
|
265
|
+
message: str | None = None,
|
|
266
|
+
database_url: str | None = None,
|
|
267
267
|
) -> dict:
|
|
268
268
|
"""Create a merge revision that joins multiple migration heads."""
|
|
269
269
|
root = prepare_env()
|
|
@@ -309,8 +309,8 @@ def setup_and_migrate(
|
|
|
309
309
|
create_followup_revision: bool = True,
|
|
310
310
|
initial_message: str = "initial schema",
|
|
311
311
|
followup_message: str = "autogen",
|
|
312
|
-
database_url:
|
|
313
|
-
discover_packages:
|
|
312
|
+
database_url: str | None = None,
|
|
313
|
+
discover_packages: Sequence[str] | None = None,
|
|
314
314
|
) -> dict:
|
|
315
315
|
"""
|
|
316
316
|
Ensure DB + Alembic are ready and up-to-date.
|
svc_infra/db/sql/management.py
CHANGED
|
@@ -100,13 +100,9 @@ def make_crud_schemas(
|
|
|
100
100
|
name=name,
|
|
101
101
|
typ=T,
|
|
102
102
|
required_for_create=bool(
|
|
103
|
-
is_required
|
|
104
|
-
and name not in explicit_excludes
|
|
105
|
-
and not _exclude_from_create(col)
|
|
106
|
-
),
|
|
107
|
-
exclude_from_create=bool(
|
|
108
|
-
name in explicit_excludes or _exclude_from_create(col)
|
|
103
|
+
is_required and name not in explicit_excludes and not _exclude_from_create(col)
|
|
109
104
|
),
|
|
105
|
+
exclude_from_create=bool(name in explicit_excludes or _exclude_from_create(col)),
|
|
110
106
|
exclude_from_read=bool(name in read_ex),
|
|
111
107
|
exclude_from_update=bool(name in update_ex),
|
|
112
108
|
)
|
svc_infra/db/sql/repository.py
CHANGED
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import logging
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Iterable, Sequence
|
|
6
|
+
from typing import Any, cast
|
|
6
7
|
|
|
7
8
|
from sqlalchemy import Select, String, and_, func, or_, select
|
|
8
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
@@ -33,14 +34,14 @@ class SqlRepository:
|
|
|
33
34
|
soft_delete: bool = False,
|
|
34
35
|
soft_delete_field: str = "deleted_at",
|
|
35
36
|
soft_delete_flag_field: str | None = None,
|
|
36
|
-
immutable_fields:
|
|
37
|
+
immutable_fields: set[str] | None = None,
|
|
37
38
|
):
|
|
38
39
|
self.model = model
|
|
39
40
|
self.id_attr = id_attr
|
|
40
41
|
self.soft_delete = soft_delete
|
|
41
42
|
self.soft_delete_field = soft_delete_field
|
|
42
43
|
self.soft_delete_flag_field = soft_delete_flag_field
|
|
43
|
-
self.immutable_fields:
|
|
44
|
+
self.immutable_fields: set[str] = set(
|
|
44
45
|
immutable_fields or {"id", "created_at", "updated_at"}
|
|
45
46
|
)
|
|
46
47
|
|
|
@@ -48,7 +49,7 @@ class SqlRepository:
|
|
|
48
49
|
return {c.key for c in class_mapper(self.model).columns}
|
|
49
50
|
|
|
50
51
|
def _id_column(self) -> InstrumentedAttribute[Any]:
|
|
51
|
-
return cast(InstrumentedAttribute[Any], getattr(self.model, self.id_attr))
|
|
52
|
+
return cast("InstrumentedAttribute[Any]", getattr(self.model, self.id_attr))
|
|
52
53
|
|
|
53
54
|
def _base_select(self) -> Select:
|
|
54
55
|
stmt = select(self.model)
|
|
@@ -56,12 +57,8 @@ class SqlRepository:
|
|
|
56
57
|
# Filter out soft-deleted rows by timestamp and/or active flag
|
|
57
58
|
if hasattr(self.model, self.soft_delete_field):
|
|
58
59
|
stmt = stmt.where(getattr(self.model, self.soft_delete_field).is_(None))
|
|
59
|
-
if self.soft_delete_flag_field and hasattr(
|
|
60
|
-
self.model, self.soft_delete_flag_field
|
|
61
|
-
):
|
|
62
|
-
stmt = stmt.where(
|
|
63
|
-
getattr(self.model, self.soft_delete_flag_field).is_(True)
|
|
64
|
-
)
|
|
60
|
+
if self.soft_delete_flag_field and hasattr(self.model, self.soft_delete_flag_field):
|
|
61
|
+
stmt = stmt.where(getattr(self.model, self.soft_delete_flag_field).is_(True))
|
|
65
62
|
return stmt
|
|
66
63
|
|
|
67
64
|
# basic ops
|
|
@@ -72,8 +69,8 @@ class SqlRepository:
|
|
|
72
69
|
*,
|
|
73
70
|
limit: int,
|
|
74
71
|
offset: int,
|
|
75
|
-
order_by:
|
|
76
|
-
where:
|
|
72
|
+
order_by: Sequence[Any] | None = None,
|
|
73
|
+
where: Sequence[Any] | None = None,
|
|
77
74
|
) -> Sequence[Any]:
|
|
78
75
|
stmt = self._base_select()
|
|
79
76
|
if where:
|
|
@@ -84,9 +81,7 @@ class SqlRepository:
|
|
|
84
81
|
result = (await session.execute(stmt)).scalars().all()
|
|
85
82
|
return list(result)
|
|
86
83
|
|
|
87
|
-
async def count(
|
|
88
|
-
self, session: AsyncSession, *, where: Optional[Sequence[Any]] = None
|
|
89
|
-
) -> int:
|
|
84
|
+
async def count(self, session: AsyncSession, *, where: Sequence[Any] | None = None) -> int:
|
|
90
85
|
base = self._base_select()
|
|
91
86
|
if where:
|
|
92
87
|
base = base.where(and_(*where))
|
|
@@ -98,7 +93,7 @@ class SqlRepository:
|
|
|
98
93
|
session: AsyncSession,
|
|
99
94
|
id_value: Any,
|
|
100
95
|
*,
|
|
101
|
-
where:
|
|
96
|
+
where: Sequence[Any] | None = None,
|
|
102
97
|
) -> Any | None:
|
|
103
98
|
# honors soft-delete if configured
|
|
104
99
|
stmt = self._base_select().where(self._id_column() == id_value)
|
|
@@ -121,7 +116,7 @@ class SqlRepository:
|
|
|
121
116
|
id_value: Any,
|
|
122
117
|
data: dict[str, Any],
|
|
123
118
|
*,
|
|
124
|
-
where:
|
|
119
|
+
where: Sequence[Any] | None = None,
|
|
125
120
|
) -> Any | None:
|
|
126
121
|
obj = await self.get(session, id_value, where=where)
|
|
127
122
|
if not obj:
|
|
@@ -139,7 +134,7 @@ class SqlRepository:
|
|
|
139
134
|
session: AsyncSession,
|
|
140
135
|
id_value: Any,
|
|
141
136
|
*,
|
|
142
|
-
where:
|
|
137
|
+
where: Sequence[Any] | None = None,
|
|
143
138
|
) -> bool:
|
|
144
139
|
# Fast path: when no extra filters provided, use session.get for simplicity (matches tests)
|
|
145
140
|
if not where:
|
|
@@ -156,9 +151,7 @@ class SqlRepository:
|
|
|
156
151
|
# Check attributes on the instance to support test doubles without class-level fields
|
|
157
152
|
if hasattr(obj, self.soft_delete_field):
|
|
158
153
|
setattr(obj, self.soft_delete_field, func.now())
|
|
159
|
-
if self.soft_delete_flag_field and hasattr(
|
|
160
|
-
obj, self.soft_delete_flag_field
|
|
161
|
-
):
|
|
154
|
+
if self.soft_delete_flag_field and hasattr(obj, self.soft_delete_flag_field):
|
|
162
155
|
setattr(obj, self.soft_delete_flag_field, False)
|
|
163
156
|
await session.flush()
|
|
164
157
|
return True
|
|
@@ -176,8 +169,8 @@ class SqlRepository:
|
|
|
176
169
|
fields: Sequence[str],
|
|
177
170
|
limit: int,
|
|
178
171
|
offset: int,
|
|
179
|
-
order_by:
|
|
180
|
-
where:
|
|
172
|
+
order_by: Sequence[Any] | None = None,
|
|
173
|
+
where: Sequence[Any] | None = None,
|
|
181
174
|
) -> Sequence[Any]:
|
|
182
175
|
ilike = f"%{_escape_ilike(q)}%"
|
|
183
176
|
conditions = []
|
|
@@ -206,7 +199,7 @@ class SqlRepository:
|
|
|
206
199
|
*,
|
|
207
200
|
q: str,
|
|
208
201
|
fields: Sequence[str],
|
|
209
|
-
where:
|
|
202
|
+
where: Sequence[Any] | None = None,
|
|
210
203
|
) -> int:
|
|
211
204
|
ilike = f"%{_escape_ilike(q)}%"
|
|
212
205
|
conditions = []
|
svc_infra/db/sql/resource.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from svc_infra.db.sql.repository import SqlRepository
|
|
7
8
|
|
|
@@ -14,28 +15,28 @@ if TYPE_CHECKING:
|
|
|
14
15
|
class SqlResource:
|
|
15
16
|
model: type[object]
|
|
16
17
|
prefix: str
|
|
17
|
-
tags:
|
|
18
|
+
tags: list[str] | None = None
|
|
18
19
|
|
|
19
20
|
id_attr: str = "id"
|
|
20
21
|
soft_delete: bool = False
|
|
21
|
-
search_fields:
|
|
22
|
-
ordering_default:
|
|
23
|
-
allowed_order_fields:
|
|
22
|
+
search_fields: list[str] | None = None
|
|
23
|
+
ordering_default: str | None = None
|
|
24
|
+
allowed_order_fields: list[str] | None = None
|
|
24
25
|
|
|
25
|
-
read_schema:
|
|
26
|
-
create_schema:
|
|
27
|
-
update_schema:
|
|
26
|
+
read_schema: type | None = None
|
|
27
|
+
create_schema: type | None = None
|
|
28
|
+
update_schema: type | None = None
|
|
28
29
|
|
|
29
|
-
read_name:
|
|
30
|
-
create_name:
|
|
31
|
-
update_name:
|
|
30
|
+
read_name: str | None = None
|
|
31
|
+
create_name: str | None = None
|
|
32
|
+
update_name: str | None = None
|
|
32
33
|
|
|
33
34
|
create_exclude: tuple[str, ...] = ("id",)
|
|
34
35
|
|
|
35
36
|
# Only a type reference; no runtime dependency on FastAPI layer
|
|
36
|
-
service_factory:
|
|
37
|
+
service_factory: Callable[[SqlRepository], SqlService] | None = None
|
|
37
38
|
|
|
38
39
|
# Tenancy
|
|
39
|
-
tenant_field:
|
|
40
|
+
tenant_field: str | None = (
|
|
40
41
|
None # when set, CRUD router will require TenantId and scope by field
|
|
41
42
|
)
|
svc_infra/db/sql/scaffold.py
CHANGED
|
@@ -2,20 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, Literal
|
|
6
6
|
|
|
7
7
|
from svc_infra.db.utils import normalize_dir, pascal, plural_snake, snake
|
|
8
8
|
from svc_infra.utils import ensure_init_py, render_template, write
|
|
9
9
|
|
|
10
10
|
# ---------------- helpers ----------------
|
|
11
11
|
|
|
12
|
-
_INIT_CONTENT_PAIRED =
|
|
13
|
-
'from . import models, schemas\n\n__all__ = ["models", "schemas"]\n'
|
|
14
|
-
)
|
|
12
|
+
_INIT_CONTENT_PAIRED = 'from . import models, schemas\n\n__all__ = ["models", "schemas"]\n'
|
|
15
13
|
_INIT_CONTENT_MINIMAL = "# package marker; add explicit exports here if desired\n"
|
|
16
14
|
|
|
17
15
|
|
|
18
|
-
def _ensure_init_py(dir_path: Path, overwrite: bool, paired: bool) ->
|
|
16
|
+
def _ensure_init_py(dir_path: Path, overwrite: bool, paired: bool) -> dict[str, Any]:
|
|
19
17
|
"""Create __init__.py; paired=True writes models/schemas re-exports, otherwise minimal."""
|
|
20
18
|
content = _INIT_CONTENT_PAIRED if paired else _INIT_CONTENT_MINIMAL
|
|
21
19
|
return ensure_init_py(dir_path, overwrite, paired, content)
|
|
@@ -33,14 +31,14 @@ def scaffold_core(
|
|
|
33
31
|
schemas_dir: Path | str,
|
|
34
32
|
kind: Kind = "entity",
|
|
35
33
|
entity_name: str = "Item",
|
|
36
|
-
table_name:
|
|
34
|
+
table_name: str | None = None,
|
|
37
35
|
include_tenant: bool = True,
|
|
38
36
|
include_soft_delete: bool = False,
|
|
39
37
|
overwrite: bool = False,
|
|
40
38
|
same_dir: bool = False,
|
|
41
|
-
models_filename:
|
|
42
|
-
schemas_filename:
|
|
43
|
-
) ->
|
|
39
|
+
models_filename: str | None = None,
|
|
40
|
+
schemas_filename: str | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
44
42
|
"""
|
|
45
43
|
Create starter model + schema files.
|
|
46
44
|
|
|
@@ -104,9 +102,7 @@ def scaffold_core(
|
|
|
104
102
|
},
|
|
105
103
|
)
|
|
106
104
|
|
|
107
|
-
tenant_schema_field =
|
|
108
|
-
" tenant_id: Optional[str] = None\n" if include_tenant else ""
|
|
109
|
-
)
|
|
105
|
+
tenant_schema_field = " tenant_id: Optional[str] = None\n" if include_tenant else ""
|
|
110
106
|
schemas_txt = render_template(
|
|
111
107
|
tmpl_dir="svc_infra.db.sql.templates.models_schemas.entity",
|
|
112
108
|
name="schemas.py.tmpl",
|
|
@@ -154,12 +150,12 @@ def scaffold_models_core(
|
|
|
154
150
|
dest_dir: Path | str,
|
|
155
151
|
kind: Kind = "entity",
|
|
156
152
|
entity_name: str = "Item",
|
|
157
|
-
table_name:
|
|
153
|
+
table_name: str | None = None,
|
|
158
154
|
include_tenant: bool = True,
|
|
159
155
|
include_soft_delete: bool = False,
|
|
160
156
|
overwrite: bool = False,
|
|
161
|
-
models_filename:
|
|
162
|
-
) ->
|
|
157
|
+
models_filename: str | None = None, # <--- NEW
|
|
158
|
+
) -> dict[str, Any]:
|
|
163
159
|
"""Create only a model file (defaults to <snake(entity)>.py unless models_filename is provided)."""
|
|
164
160
|
dest = normalize_dir(dest_dir)
|
|
165
161
|
|
|
@@ -220,8 +216,8 @@ def scaffold_schemas_core(
|
|
|
220
216
|
entity_name: str = "Item",
|
|
221
217
|
include_tenant: bool = True,
|
|
222
218
|
overwrite: bool = False,
|
|
223
|
-
schemas_filename:
|
|
224
|
-
) ->
|
|
219
|
+
schemas_filename: str | None = None, # <--- NEW
|
|
220
|
+
) -> dict[str, Any]:
|
|
225
221
|
"""Create only a schema file (defaults to <snake(entity)>.py unless schemas_filename is provided)."""
|
|
226
222
|
dest = normalize_dir(dest_dir)
|
|
227
223
|
|
svc_infra/db/sql/service.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from fastapi import HTTPException
|
|
6
7
|
from sqlalchemy.exc import IntegrityError
|
|
@@ -24,12 +25,8 @@ class SqlService:
|
|
|
24
25
|
async def pre_update(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
25
26
|
return data
|
|
26
27
|
|
|
27
|
-
async def list(
|
|
28
|
-
self
|
|
29
|
-
):
|
|
30
|
-
return await self.repo.list(
|
|
31
|
-
session, limit=limit, offset=offset, order_by=order_by
|
|
32
|
-
)
|
|
28
|
+
async def list(self, session: AsyncSession, *, limit: int, offset: int, order_by=None):
|
|
29
|
+
return await self.repo.list(session, limit=limit, offset=offset, order_by=order_by)
|
|
33
30
|
|
|
34
31
|
async def count(self, session: AsyncSession) -> int:
|
|
35
32
|
return await self.repo.count(session)
|
|
@@ -45,13 +42,9 @@ class SqlService:
|
|
|
45
42
|
# unique constraint or not-null -> 409/400 instead of 500
|
|
46
43
|
msg = str(e.orig) if getattr(e, "orig", None) else str(e)
|
|
47
44
|
if "duplicate key value" in msg or "UniqueViolation" in msg:
|
|
48
|
-
raise HTTPException(
|
|
49
|
-
status_code=409, detail="Record already exists."
|
|
50
|
-
) from e
|
|
45
|
+
raise HTTPException(status_code=409, detail="Record already exists.") from e
|
|
51
46
|
if "not-null" in msg or "NotNullViolation" in msg:
|
|
52
|
-
raise HTTPException(
|
|
53
|
-
status_code=400, detail="Missing required field."
|
|
54
|
-
) from e
|
|
47
|
+
raise HTTPException(status_code=400, detail="Missing required field.") from e
|
|
55
48
|
raise # unknown, let your error middleware turn into 500
|
|
56
49
|
|
|
57
50
|
async def update(self, session: AsyncSession, id_value: Any, data: dict[str, Any]):
|
|
@@ -75,9 +68,7 @@ class SqlService:
|
|
|
75
68
|
session, q=q, fields=fields, limit=limit, offset=offset, order_by=order_by
|
|
76
69
|
)
|
|
77
70
|
|
|
78
|
-
async def count_filtered(
|
|
79
|
-
self, session: AsyncSession, *, q: str, fields: Sequence[str]
|
|
80
|
-
) -> int:
|
|
71
|
+
async def count_filtered(self, session: AsyncSession, *, q: str, fields: Sequence[str]) -> int:
|
|
81
72
|
return await self.repo.count_filtered(session, q=q, fields=fields)
|
|
82
73
|
|
|
83
74
|
async def exists(self, session: AsyncSession, *, where):
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
from svc_infra.db.sql.service import SqlService
|
|
4
5
|
|
|
@@ -9,8 +10,8 @@ class SqlServiceWithHooks(SqlService):
|
|
|
9
10
|
def __init__(
|
|
10
11
|
self,
|
|
11
12
|
repo,
|
|
12
|
-
pre_create:
|
|
13
|
-
pre_update:
|
|
13
|
+
pre_create: PreHook | None = None,
|
|
14
|
+
pre_update: PreHook | None = None,
|
|
14
15
|
):
|
|
15
16
|
super().__init__(repo)
|
|
16
17
|
self._pre_create = pre_create
|
svc_infra/db/sql/tenant.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
7
|
|
|
@@ -27,9 +28,7 @@ class TenantSqlService(SqlService):
|
|
|
27
28
|
return []
|
|
28
29
|
return [col == self.tenant_id]
|
|
29
30
|
|
|
30
|
-
async def list(
|
|
31
|
-
self, session: AsyncSession, *, limit: int, offset: int, order_by=None
|
|
32
|
-
):
|
|
31
|
+
async def list(self, session: AsyncSession, *, limit: int, offset: int, order_by=None):
|
|
33
32
|
return await self.repo.list(
|
|
34
33
|
session, limit=limit, offset=offset, order_by=order_by, where=self._where()
|
|
35
34
|
)
|
|
@@ -43,10 +42,7 @@ class TenantSqlService(SqlService):
|
|
|
43
42
|
async def create(self, session: AsyncSession, data: dict[str, Any]):
|
|
44
43
|
data = await self.pre_create(data)
|
|
45
44
|
# inject tenant_id if model supports it and value missing
|
|
46
|
-
if (
|
|
47
|
-
self.tenant_field in self.repo._model_columns()
|
|
48
|
-
and self.tenant_field not in data
|
|
49
|
-
):
|
|
45
|
+
if self.tenant_field in self.repo._model_columns() and self.tenant_field not in data:
|
|
50
46
|
data[self.tenant_field] = self.tenant_id
|
|
51
47
|
return await self.repo.create(session, data)
|
|
52
48
|
|
|
@@ -77,12 +73,8 @@ class TenantSqlService(SqlService):
|
|
|
77
73
|
where=self._where(),
|
|
78
74
|
)
|
|
79
75
|
|
|
80
|
-
async def count_filtered(
|
|
81
|
-
self
|
|
82
|
-
) -> int:
|
|
83
|
-
return await self.repo.count_filtered(
|
|
84
|
-
session, q=q, fields=fields, where=self._where()
|
|
85
|
-
)
|
|
76
|
+
async def count_filtered(self, session: AsyncSession, *, q: str, fields: Sequence[str]) -> int:
|
|
77
|
+
return await self.repo.count_filtered(session, q=q, fields=fields, where=self._where())
|
|
86
78
|
|
|
87
79
|
|
|
88
80
|
__all__ = ["TenantSqlService"]
|