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/db/nosql/repository.py
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import builtins
|
|
4
|
+
from collections.abc import Iterable, Sequence
|
|
5
|
+
from datetime import UTC
|
|
6
|
+
from typing import Any, cast
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
try:
|
|
9
|
+
from bson import ObjectId
|
|
10
|
+
|
|
11
|
+
_HAS_BSON = True
|
|
12
|
+
except ModuleNotFoundError:
|
|
13
|
+
# `bson` is provided by the optional `pymongo` dependency.
|
|
14
|
+
# Keep imports working for non-mongo users/tests; runtime Mongo usage still
|
|
15
|
+
# requires installing pymongo.
|
|
16
|
+
_HAS_BSON = False
|
|
17
|
+
|
|
18
|
+
class ObjectId: # type: ignore[no-redef]
|
|
19
|
+
pass
|
|
6
20
|
|
|
7
21
|
|
|
8
22
|
class NoSqlRepository:
|
|
@@ -24,7 +38,7 @@ class NoSqlRepository:
|
|
|
24
38
|
soft_delete: bool = False,
|
|
25
39
|
soft_delete_field: str = "deleted_at",
|
|
26
40
|
soft_delete_flag_field: str | None = None,
|
|
27
|
-
immutable_fields:
|
|
41
|
+
immutable_fields: set[str] | None = None,
|
|
28
42
|
):
|
|
29
43
|
self.collection_name = collection_name
|
|
30
44
|
self.id_field = id_field
|
|
@@ -35,7 +49,7 @@ class NoSqlRepository:
|
|
|
35
49
|
immutable_fields or {self.id_field, "created_at", "updated_at"}
|
|
36
50
|
)
|
|
37
51
|
|
|
38
|
-
def _alive_filter(self) ->
|
|
52
|
+
def _alive_filter(self) -> dict[str, Any]:
|
|
39
53
|
"""
|
|
40
54
|
Build a filter that returns 'alive' docs when soft_delete is enabled.
|
|
41
55
|
- deleted_at is either null or absent
|
|
@@ -73,16 +87,18 @@ class NoSqlRepository:
|
|
|
73
87
|
|
|
74
88
|
return clauses[0] if len(clauses) == 1 else {"$and": clauses}
|
|
75
89
|
|
|
76
|
-
def _merge_and(self, *filters:
|
|
90
|
+
def _merge_and(self, *filters: dict[str, Any] | None) -> dict[str, Any]:
|
|
77
91
|
parts = [f for f in filters if f]
|
|
78
92
|
if not parts:
|
|
79
93
|
return {}
|
|
80
94
|
if len(parts) == 1:
|
|
81
|
-
return parts[0]
|
|
95
|
+
return parts[0]
|
|
82
96
|
return {"$and": parts}
|
|
83
97
|
|
|
84
98
|
def _normalize_id_value(self, val: Any) -> Any:
|
|
85
99
|
"""If we use Mongo’s _id and a string is passed, coerce to ObjectId when possible."""
|
|
100
|
+
if not _HAS_BSON:
|
|
101
|
+
return val
|
|
86
102
|
if self.id_field == "_id" and isinstance(val, str):
|
|
87
103
|
try:
|
|
88
104
|
return ObjectId(val)
|
|
@@ -91,13 +107,14 @@ class NoSqlRepository:
|
|
|
91
107
|
return val
|
|
92
108
|
|
|
93
109
|
@staticmethod
|
|
94
|
-
def _public_doc(doc:
|
|
95
|
-
if not doc:
|
|
96
|
-
return doc
|
|
110
|
+
def _public_doc(doc: dict[str, Any]) -> dict[str, Any]:
|
|
97
111
|
d = dict(doc)
|
|
98
112
|
if "_id" in d and "id" not in d:
|
|
99
113
|
_id = d.pop("_id", None)
|
|
100
|
-
|
|
114
|
+
if _HAS_BSON and isinstance(_id, ObjectId):
|
|
115
|
+
d["id"] = str(_id)
|
|
116
|
+
else:
|
|
117
|
+
d["id"] = _id
|
|
101
118
|
return d
|
|
102
119
|
|
|
103
120
|
async def list(
|
|
@@ -106,26 +123,28 @@ class NoSqlRepository:
|
|
|
106
123
|
*,
|
|
107
124
|
limit: int,
|
|
108
125
|
offset: int,
|
|
109
|
-
sort:
|
|
110
|
-
filter:
|
|
111
|
-
) ->
|
|
126
|
+
sort: builtins.list[tuple[str, int]] | None = None,
|
|
127
|
+
filter: dict[str, Any] | None = None,
|
|
128
|
+
) -> builtins.list[dict[str, Any]]:
|
|
112
129
|
filt = self._merge_and(self._alive_filter(), filter)
|
|
113
130
|
cursor = db[self.collection_name].find(filt).skip(offset).limit(limit)
|
|
114
131
|
if sort:
|
|
115
132
|
cursor = cursor.sort(sort)
|
|
116
133
|
return [self._public_doc(doc) async for doc in cursor]
|
|
117
134
|
|
|
118
|
-
async def count(self, db, *, filter:
|
|
135
|
+
async def count(self, db, *, filter: dict[str, Any] | None = None) -> int:
|
|
119
136
|
filt = self._merge_and(self._alive_filter(), filter)
|
|
120
|
-
return await db[self.collection_name].count_documents(filt or {})
|
|
137
|
+
return cast("int", await db[self.collection_name].count_documents(filt or {}))
|
|
121
138
|
|
|
122
|
-
async def get(self, db, id_value: Any) ->
|
|
139
|
+
async def get(self, db, id_value: Any) -> dict | None:
|
|
123
140
|
id_value = self._normalize_id_value(id_value)
|
|
124
141
|
filt = self._merge_and(self._alive_filter(), {self.id_field: id_value})
|
|
125
142
|
doc = await db[self.collection_name].find_one(filt)
|
|
143
|
+
if doc is None:
|
|
144
|
+
return None
|
|
126
145
|
return self._public_doc(doc)
|
|
127
146
|
|
|
128
|
-
async def create(self, db, data:
|
|
147
|
+
async def create(self, db, data: dict[str, Any]) -> dict[str, Any]:
|
|
129
148
|
# don't let clients supply soft-delete artifacts on create
|
|
130
149
|
if self.soft_delete:
|
|
131
150
|
data.pop(self.soft_delete_field, None)
|
|
@@ -134,7 +153,7 @@ class NoSqlRepository:
|
|
|
134
153
|
res = await db[self.collection_name].insert_one(data)
|
|
135
154
|
return self._public_doc({**data, "_id": res.inserted_id})
|
|
136
155
|
|
|
137
|
-
async def update(self, db, id_value: Any, data:
|
|
156
|
+
async def update(self, db, id_value: Any, data: dict[str, Any]) -> dict | None:
|
|
138
157
|
for k in list(data.keys()):
|
|
139
158
|
if k in self.immutable_fields:
|
|
140
159
|
data.pop(k, None)
|
|
@@ -146,19 +165,19 @@ class NoSqlRepository:
|
|
|
146
165
|
async def delete(self, db, id_value: Any) -> bool:
|
|
147
166
|
id_value = self._normalize_id_value(id_value)
|
|
148
167
|
if self.soft_delete:
|
|
149
|
-
set_ops:
|
|
168
|
+
set_ops: dict[str, Any] = {}
|
|
150
169
|
if self.soft_delete_flag_field:
|
|
151
170
|
set_ops[self.soft_delete_flag_field] = False
|
|
152
|
-
from datetime import datetime
|
|
171
|
+
from datetime import datetime
|
|
153
172
|
|
|
154
|
-
set_ops[self.soft_delete_field] = datetime.now(
|
|
173
|
+
set_ops[self.soft_delete_field] = datetime.now(UTC)
|
|
155
174
|
res = await db[self.collection_name].update_one(
|
|
156
175
|
{self.id_field: id_value}, {"$set": set_ops}
|
|
157
176
|
)
|
|
158
|
-
return res.modified_count > 0
|
|
177
|
+
return cast("int", res.modified_count) > 0
|
|
159
178
|
|
|
160
179
|
res = await db[self.collection_name].delete_one({self.id_field: id_value})
|
|
161
|
-
return res.deleted_count > 0
|
|
180
|
+
return cast("int", res.deleted_count) > 0
|
|
162
181
|
|
|
163
182
|
async def search(
|
|
164
183
|
self,
|
|
@@ -168,8 +187,8 @@ class NoSqlRepository:
|
|
|
168
187
|
fields: Sequence[str],
|
|
169
188
|
limit: int,
|
|
170
189
|
offset: int,
|
|
171
|
-
sort:
|
|
172
|
-
) ->
|
|
190
|
+
sort: builtins.list[tuple[str, int]] | None = None,
|
|
191
|
+
) -> builtins.list[dict[str, Any]]:
|
|
173
192
|
regex = {"$regex": q, "$options": "i"}
|
|
174
193
|
or_filter = [{"$or": [{f: regex} for f in fields]}] if fields else []
|
|
175
194
|
filt = (
|
|
@@ -184,9 +203,9 @@ class NoSqlRepository:
|
|
|
184
203
|
regex = {"$regex": q, "$options": "i"}
|
|
185
204
|
or_filter = {"$or": [{f: regex} for f in fields]} if fields else {}
|
|
186
205
|
filt = self._merge_and(self._alive_filter(), or_filter)
|
|
187
|
-
return await db[self.collection_name].count_documents(filt or {})
|
|
206
|
+
return cast("int", await db[self.collection_name].count_documents(filt or {}))
|
|
188
207
|
|
|
189
|
-
async def exists(self, db, *, where: Iterable[
|
|
208
|
+
async def exists(self, db, *, where: Iterable[dict[str, Any]]) -> bool:
|
|
190
209
|
filt = self._merge_and(self._alive_filter(), *list(where))
|
|
191
210
|
doc = await db[self.collection_name].find_one(filt, projection={self.id_field: 1})
|
|
192
211
|
return doc is not None
|
svc_infra/db/nosql/resource.py
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
from typing import (
|
|
6
|
+
TYPE_CHECKING,
|
|
7
|
+
Any,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pymongo import IndexModel
|
|
12
|
+
else:
|
|
13
|
+
try:
|
|
14
|
+
from pymongo import IndexModel
|
|
15
|
+
except ModuleNotFoundError:
|
|
16
|
+
# Minimal runtime stub so importing svc_infra works without optional Mongo deps.
|
|
17
|
+
class IndexModel: # type: ignore[no-redef]
|
|
18
|
+
pass
|
|
7
19
|
|
|
8
20
|
|
|
9
21
|
def _snake(name: str) -> str:
|
|
@@ -39,36 +51,36 @@ class NoSqlResource:
|
|
|
39
51
|
"""
|
|
40
52
|
|
|
41
53
|
# API mounting
|
|
42
|
-
collection:
|
|
54
|
+
collection: str | None = None
|
|
43
55
|
prefix: str = ""
|
|
44
|
-
document_model:
|
|
56
|
+
document_model: type[Any] | None = None
|
|
45
57
|
|
|
46
58
|
# optional Pydantic schemas (auto-derived if omitted)
|
|
47
|
-
read_schema:
|
|
48
|
-
create_schema:
|
|
49
|
-
update_schema:
|
|
59
|
+
read_schema: type[Any] | None = None
|
|
60
|
+
create_schema: type[Any] | None = None
|
|
61
|
+
update_schema: type[Any] | None = None
|
|
50
62
|
|
|
51
63
|
# behavior
|
|
52
|
-
search_fields:
|
|
53
|
-
tags:
|
|
64
|
+
search_fields: Sequence[str] | None = None
|
|
65
|
+
tags: list[str] | None = None
|
|
54
66
|
id_field: str = "_id"
|
|
55
67
|
soft_delete: bool = False
|
|
56
68
|
soft_delete_field: str = "deleted_at"
|
|
57
|
-
soft_delete_flag_field:
|
|
69
|
+
soft_delete_flag_field: str | None = None
|
|
58
70
|
|
|
59
71
|
# custom wiring
|
|
60
|
-
service_factory:
|
|
72
|
+
service_factory: Callable[[Any], Any] | None = None
|
|
61
73
|
|
|
62
74
|
# generated schema naming and exclusions
|
|
63
|
-
read_name:
|
|
64
|
-
create_name:
|
|
65
|
-
update_name:
|
|
75
|
+
read_name: str | None = None
|
|
76
|
+
create_name: str | None = None
|
|
77
|
+
update_name: str | None = None
|
|
66
78
|
create_exclude: tuple[str, ...] = ("_id",)
|
|
67
79
|
read_exclude: tuple[str, ...] = ()
|
|
68
80
|
update_exclude: tuple[str, ...] = ()
|
|
69
81
|
|
|
70
82
|
# NEW: indexes defined per collection (normalized to IndexModel at prepare time)
|
|
71
|
-
indexes:
|
|
83
|
+
indexes: Iterable[IndexModel | IndexAlias] | None = None
|
|
72
84
|
|
|
73
85
|
def __post_init__(self):
|
|
74
86
|
if not self.collection and self.document_model:
|
svc_infra/db/nosql/scaffold.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Literal
|
|
5
5
|
|
|
6
6
|
from svc_infra.db.utils import normalize_dir, pascal, plural_snake, snake
|
|
7
7
|
from svc_infra.utils import ensure_init_py, render_template, write
|
|
@@ -10,7 +10,7 @@ _INIT_CONTENT_PAIRED = 'from . import documents, schemas\n\n__all__ = ["document
|
|
|
10
10
|
_INIT_CONTENT_MINIMAL = "# package marker; add explicit exports here if desired\n"
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def _ensure_init_py(dir_path: Path, overwrite: bool, paired: bool) ->
|
|
13
|
+
def _ensure_init_py(dir_path: Path, overwrite: bool, paired: bool) -> dict[str, Any]:
|
|
14
14
|
content = _INIT_CONTENT_PAIRED if paired else _INIT_CONTENT_MINIMAL
|
|
15
15
|
return ensure_init_py(dir_path, overwrite, paired, content)
|
|
16
16
|
|
|
@@ -27,9 +27,9 @@ def scaffold_core(
|
|
|
27
27
|
entity_name: str = "Item",
|
|
28
28
|
overwrite: bool = False,
|
|
29
29
|
same_dir: bool = False,
|
|
30
|
-
documents_filename:
|
|
31
|
-
schemas_filename:
|
|
32
|
-
) ->
|
|
30
|
+
documents_filename: str | None = None,
|
|
31
|
+
schemas_filename: str | None = None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
33
|
"""Create starter Mongo document model + CRUD schemas."""
|
|
34
34
|
|
|
35
35
|
documents_dir = normalize_dir(documents_dir)
|
|
@@ -76,8 +76,8 @@ def scaffold_documents_core(
|
|
|
76
76
|
dest_dir: Path | str,
|
|
77
77
|
entity_name: str = "Item",
|
|
78
78
|
overwrite: bool = False,
|
|
79
|
-
documents_filename:
|
|
80
|
-
) ->
|
|
79
|
+
documents_filename: str | None = None,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
81
|
dest = normalize_dir(dest_dir)
|
|
82
82
|
ent = pascal(entity_name)
|
|
83
83
|
coll = plural_snake(ent)
|
|
@@ -98,12 +98,14 @@ def scaffold_schemas_core(
|
|
|
98
98
|
dest_dir: Path | str,
|
|
99
99
|
entity_name: str = "Item",
|
|
100
100
|
overwrite: bool = False,
|
|
101
|
-
schemas_filename:
|
|
102
|
-
) ->
|
|
101
|
+
schemas_filename: str | None = None,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
103
|
dest = normalize_dir(dest_dir)
|
|
104
104
|
ent = pascal(entity_name)
|
|
105
105
|
txt = render_template(
|
|
106
|
-
tmpl_dir="svc_infra.db.nosql.mongo.templates",
|
|
106
|
+
tmpl_dir="svc_infra.db.nosql.mongo.templates",
|
|
107
|
+
name="schemas.py.tmpl",
|
|
108
|
+
subs={"Entity": ent},
|
|
107
109
|
)
|
|
108
110
|
filename = schemas_filename or f"{snake(entity_name)}.py"
|
|
109
111
|
res = write(dest / filename, txt, overwrite)
|
|
@@ -115,9 +117,9 @@ def scaffold_resources_core(
|
|
|
115
117
|
*,
|
|
116
118
|
dest_dir: Path | str,
|
|
117
119
|
entity_name: str = "Item",
|
|
118
|
-
filename:
|
|
120
|
+
filename: str | None = None, # defaults to "resources.py"
|
|
119
121
|
overwrite: bool = False,
|
|
120
|
-
) ->
|
|
122
|
+
) -> dict[str, Any]:
|
|
121
123
|
"""
|
|
122
124
|
Create a starter resources.py with:
|
|
123
125
|
- empty RESOURCES list
|
svc_infra/db/nosql/service.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from .service import NoSqlService
|
|
6
7
|
|
|
@@ -11,8 +12,8 @@ class NoSqlServiceWithHooks(NoSqlService):
|
|
|
11
12
|
def __init__(
|
|
12
13
|
self,
|
|
13
14
|
repo,
|
|
14
|
-
pre_create:
|
|
15
|
-
pre_update:
|
|
15
|
+
pre_create: PreHook | None = None,
|
|
16
|
+
pre_update: PreHook | None = None,
|
|
16
17
|
):
|
|
17
18
|
super().__init__(repo)
|
|
18
19
|
self._pre_create = pre_create
|
svc_infra/db/nosql/utils.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Optional, Sequence
|
|
6
6
|
|
|
7
7
|
from dotenv import load_dotenv
|
|
8
8
|
|
|
@@ -27,7 +27,7 @@ def prepare_process_env(project_root: Path | str) -> Path:
|
|
|
27
27
|
return root
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def _read_secret_from_file(path: str) ->
|
|
30
|
+
def _read_secret_from_file(path: str) -> str | None:
|
|
31
31
|
try:
|
|
32
32
|
p = Path(path)
|
|
33
33
|
if p.exists():
|
|
@@ -40,7 +40,7 @@ def _read_secret_from_file(path: str) -> Optional[str]:
|
|
|
40
40
|
def get_mongo_url_from_env(
|
|
41
41
|
required: bool = True,
|
|
42
42
|
env_vars: Sequence[str] = DEFAULT_MONGO_ENV_VARS,
|
|
43
|
-
) ->
|
|
43
|
+
) -> str | None:
|
|
44
44
|
"""
|
|
45
45
|
Resolve the Mongo connection string with support for:
|
|
46
46
|
- Primary env vars (DEFAULT_MONGO_ENV_VARS).
|
|
@@ -99,7 +99,7 @@ def get_mongo_dbname_from_env(
|
|
|
99
99
|
required: bool = False,
|
|
100
100
|
env_vars: Sequence[str] = DEFAULT_MONGO_DB_ENV_VARS,
|
|
101
101
|
default: str = "app",
|
|
102
|
-
) ->
|
|
102
|
+
) -> str | None:
|
|
103
103
|
"""Return a database name from env; optional (Motor can connect without it)."""
|
|
104
104
|
load_dotenv(override=False)
|
|
105
105
|
for key in env_vars:
|