svc-infra 0.1.589__py3-none-any.whl → 0.1.706__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of svc-infra might be problematic. Click here for more details.
- svc_infra/__init__.py +58 -2
- svc_infra/apf_payments/README.md +732 -0
- svc_infra/apf_payments/models.py +133 -42
- svc_infra/apf_payments/provider/__init__.py +4 -0
- svc_infra/apf_payments/provider/aiydan.py +871 -0
- svc_infra/apf_payments/provider/base.py +30 -9
- svc_infra/apf_payments/provider/stripe.py +156 -62
- svc_infra/apf_payments/schemas.py +19 -10
- svc_infra/apf_payments/service.py +211 -68
- svc_infra/apf_payments/settings.py +27 -3
- svc_infra/api/__init__.py +61 -0
- svc_infra/api/fastapi/__init__.py +15 -0
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +245 -0
- svc_infra/api/fastapi/apf_payments/router.py +145 -46
- svc_infra/api/fastapi/apf_payments/setup.py +26 -8
- svc_infra/api/fastapi/auth/__init__.py +65 -0
- svc_infra/api/fastapi/auth/_cookies.py +6 -2
- svc_infra/api/fastapi/auth/add.py +27 -14
- svc_infra/api/fastapi/auth/gaurd.py +104 -13
- svc_infra/api/fastapi/auth/mfa/models.py +3 -1
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +10 -6
- svc_infra/api/fastapi/auth/mfa/router.py +15 -8
- svc_infra/api/fastapi/auth/mfa/security.py +1 -2
- svc_infra/api/fastapi/auth/mfa/utils.py +2 -1
- svc_infra/api/fastapi/auth/mfa/verify.py +9 -2
- svc_infra/api/fastapi/auth/policy.py +0 -1
- svc_infra/api/fastapi/auth/providers.py +3 -1
- svc_infra/api/fastapi/auth/routers/apikey_router.py +6 -6
- svc_infra/api/fastapi/auth/routers/oauth_router.py +214 -75
- svc_infra/api/fastapi/auth/routers/session_router.py +67 -0
- svc_infra/api/fastapi/auth/security.py +31 -10
- svc_infra/api/fastapi/auth/sender.py +8 -1
- svc_infra/api/fastapi/auth/settings.py +2 -0
- svc_infra/api/fastapi/auth/state.py +3 -1
- svc_infra/api/fastapi/auth/ws_security.py +275 -0
- svc_infra/api/fastapi/billing/router.py +73 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/cache/add.py +9 -5
- svc_infra/api/fastapi/db/__init__.py +5 -1
- svc_infra/api/fastapi/db/http.py +3 -1
- svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
- svc_infra/api/fastapi/db/nosql/mongo/add.py +47 -32
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +30 -11
- svc_infra/api/fastapi/db/sql/__init__.py +5 -1
- svc_infra/api/fastapi/db/sql/add.py +71 -26
- svc_infra/api/fastapi/db/sql/crud_router.py +210 -22
- svc_infra/api/fastapi/db/sql/health.py +3 -1
- svc_infra/api/fastapi/db/sql/session.py +18 -0
- svc_infra/api/fastapi/db/sql/users.py +29 -5
- svc_infra/api/fastapi/dependencies/ratelimit.py +130 -0
- svc_infra/api/fastapi/docs/add.py +173 -0
- svc_infra/api/fastapi/docs/landing.py +4 -2
- svc_infra/api/fastapi/docs/scoped.py +62 -15
- svc_infra/api/fastapi/dual/__init__.py +12 -2
- svc_infra/api/fastapi/dual/dualize.py +1 -1
- svc_infra/api/fastapi/dual/protected.py +126 -4
- svc_infra/api/fastapi/dual/public.py +25 -0
- svc_infra/api/fastapi/dual/router.py +40 -13
- svc_infra/api/fastapi/dx.py +33 -2
- svc_infra/api/fastapi/ease.py +10 -2
- svc_infra/api/fastapi/http/concurrency.py +2 -1
- svc_infra/api/fastapi/http/conditional.py +3 -1
- svc_infra/api/fastapi/middleware/debug.py +4 -1
- svc_infra/api/fastapi/middleware/errors/catchall.py +6 -2
- svc_infra/api/fastapi/middleware/errors/exceptions.py +1 -1
- svc_infra/api/fastapi/middleware/errors/handlers.py +54 -8
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +104 -0
- svc_infra/api/fastapi/middleware/idempotency.py +197 -70
- svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
- svc_infra/api/fastapi/middleware/optimistic_lock.py +42 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +143 -31
- svc_infra/api/fastapi/middleware/ratelimit_store.py +111 -0
- svc_infra/api/fastapi/middleware/request_id.py +27 -11
- svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
- svc_infra/api/fastapi/middleware/timeout.py +177 -0
- svc_infra/api/fastapi/openapi/apply.py +5 -3
- svc_infra/api/fastapi/openapi/conventions.py +9 -2
- svc_infra/api/fastapi/openapi/mutators.py +165 -20
- svc_infra/api/fastapi/openapi/pipeline.py +1 -1
- svc_infra/api/fastapi/openapi/security.py +3 -1
- svc_infra/api/fastapi/ops/add.py +75 -0
- svc_infra/api/fastapi/pagination.py +47 -20
- svc_infra/api/fastapi/routers/__init__.py +43 -15
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +188 -56
- svc_infra/api/fastapi/tenancy/add.py +19 -0
- svc_infra/api/fastapi/tenancy/context.py +112 -0
- svc_infra/api/fastapi/versioned.py +101 -0
- svc_infra/app/README.md +5 -5
- svc_infra/app/__init__.py +3 -1
- svc_infra/app/env.py +69 -1
- svc_infra/app/logging/add.py +9 -2
- svc_infra/app/logging/formats.py +12 -5
- svc_infra/billing/__init__.py +23 -0
- svc_infra/billing/async_service.py +147 -0
- svc_infra/billing/jobs.py +241 -0
- svc_infra/billing/models.py +177 -0
- svc_infra/billing/quotas.py +103 -0
- svc_infra/billing/schemas.py +36 -0
- svc_infra/billing/service.py +123 -0
- svc_infra/bundled_docs/README.md +5 -0
- svc_infra/bundled_docs/__init__.py +1 -0
- svc_infra/bundled_docs/getting-started.md +6 -0
- svc_infra/cache/__init__.py +9 -0
- svc_infra/cache/add.py +170 -0
- svc_infra/cache/backend.py +7 -6
- svc_infra/cache/decorators.py +81 -15
- svc_infra/cache/demo.py +2 -2
- svc_infra/cache/keys.py +24 -4
- svc_infra/cache/recache.py +26 -14
- svc_infra/cache/resources.py +14 -5
- svc_infra/cache/tags.py +19 -44
- svc_infra/cache/utils.py +3 -1
- svc_infra/cli/__init__.py +52 -8
- svc_infra/cli/__main__.py +4 -0
- svc_infra/cli/cmds/__init__.py +39 -2
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +7 -4
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +7 -5
- svc_infra/cli/cmds/db/ops_cmds.py +270 -0
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +103 -18
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +88 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +3 -3
- svc_infra/cli/cmds/docs/docs_cmds.py +142 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +116 -0
- svc_infra/cli/cmds/health/__init__.py +179 -0
- svc_infra/cli/cmds/health/health_cmds.py +8 -0
- svc_infra/cli/cmds/help.py +4 -0
- svc_infra/cli/cmds/jobs/__init__.py +1 -0
- svc_infra/cli/cmds/jobs/jobs_cmds.py +47 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +36 -15
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +112 -0
- svc_infra/cli/foundation/runner.py +6 -2
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +58 -0
- svc_infra/data/erasure.py +45 -0
- svc_infra/data/fixtures.py +42 -0
- svc_infra/data/retention.py +61 -0
- svc_infra/db/__init__.py +15 -0
- svc_infra/db/crud_schema.py +9 -9
- svc_infra/db/inbox.py +67 -0
- svc_infra/db/nosql/__init__.py +3 -0
- svc_infra/db/nosql/core.py +30 -9
- svc_infra/db/nosql/indexes.py +3 -1
- svc_infra/db/nosql/management.py +1 -1
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/nosql/mongo/client.py +19 -2
- svc_infra/db/nosql/mongo/settings.py +6 -2
- svc_infra/db/nosql/repository.py +35 -15
- svc_infra/db/nosql/resource.py +20 -3
- svc_infra/db/nosql/scaffold.py +9 -3
- svc_infra/db/nosql/service.py +3 -1
- svc_infra/db/nosql/types.py +6 -2
- svc_infra/db/ops.py +384 -0
- svc_infra/db/outbox.py +108 -0
- svc_infra/db/sql/apikey.py +37 -9
- svc_infra/db/sql/authref.py +9 -3
- svc_infra/db/sql/constants.py +12 -8
- svc_infra/db/sql/core.py +2 -2
- svc_infra/db/sql/management.py +11 -8
- svc_infra/db/sql/repository.py +99 -26
- svc_infra/db/sql/resource.py +5 -0
- svc_infra/db/sql/scaffold.py +6 -2
- svc_infra/db/sql/service.py +15 -5
- svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
- svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
- svc_infra/db/sql/tenant.py +88 -0
- svc_infra/db/sql/uniq_hooks.py +9 -3
- svc_infra/db/sql/utils.py +138 -51
- svc_infra/db/sql/versioning.py +14 -0
- svc_infra/deploy/__init__.py +538 -0
- svc_infra/documents/__init__.py +100 -0
- svc_infra/documents/add.py +264 -0
- svc_infra/documents/ease.py +233 -0
- svc_infra/documents/models.py +114 -0
- svc_infra/documents/storage.py +264 -0
- svc_infra/dx/add.py +65 -0
- svc_infra/dx/changelog.py +74 -0
- svc_infra/dx/checks.py +68 -0
- svc_infra/exceptions.py +141 -0
- svc_infra/health/__init__.py +864 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +105 -0
- svc_infra/jobs/builtins/outbox_processor.py +40 -0
- svc_infra/jobs/builtins/webhook_delivery.py +95 -0
- svc_infra/jobs/easy.py +33 -0
- svc_infra/jobs/loader.py +50 -0
- svc_infra/jobs/queue.py +116 -0
- svc_infra/jobs/redis_queue.py +256 -0
- svc_infra/jobs/runner.py +79 -0
- svc_infra/jobs/scheduler.py +53 -0
- svc_infra/jobs/worker.py +40 -0
- svc_infra/loaders/__init__.py +186 -0
- svc_infra/loaders/base.py +142 -0
- svc_infra/loaders/github.py +311 -0
- svc_infra/loaders/models.py +147 -0
- svc_infra/loaders/url.py +235 -0
- svc_infra/logging/__init__.py +374 -0
- svc_infra/mcp/svc_infra_mcp.py +91 -33
- svc_infra/obs/README.md +2 -0
- svc_infra/obs/add.py +65 -9
- svc_infra/obs/cloud_dash.py +2 -1
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/obs/metrics/__init__.py +52 -0
- svc_infra/obs/metrics/asgi.py +13 -7
- svc_infra/obs/metrics/http.py +9 -5
- svc_infra/obs/metrics/sqlalchemy.py +13 -9
- svc_infra/obs/metrics.py +53 -0
- svc_infra/obs/settings.py +6 -2
- svc_infra/security/add.py +217 -0
- svc_infra/security/audit.py +212 -0
- svc_infra/security/audit_service.py +74 -0
- svc_infra/security/headers.py +52 -0
- svc_infra/security/hibp.py +101 -0
- svc_infra/security/jwt_rotation.py +105 -0
- svc_infra/security/lockout.py +102 -0
- svc_infra/security/models.py +287 -0
- svc_infra/security/oauth_models.py +73 -0
- svc_infra/security/org_invites.py +130 -0
- svc_infra/security/passwords.py +79 -0
- svc_infra/security/permissions.py +171 -0
- svc_infra/security/session.py +98 -0
- svc_infra/security/signed_cookies.py +100 -0
- svc_infra/storage/__init__.py +93 -0
- svc_infra/storage/add.py +253 -0
- svc_infra/storage/backends/__init__.py +11 -0
- svc_infra/storage/backends/local.py +339 -0
- svc_infra/storage/backends/memory.py +216 -0
- svc_infra/storage/backends/s3.py +353 -0
- svc_infra/storage/base.py +239 -0
- svc_infra/storage/easy.py +185 -0
- svc_infra/storage/settings.py +195 -0
- svc_infra/testing/__init__.py +685 -0
- svc_infra/utils.py +7 -3
- svc_infra/webhooks/__init__.py +69 -0
- svc_infra/webhooks/add.py +339 -0
- svc_infra/webhooks/encryption.py +115 -0
- svc_infra/webhooks/fastapi.py +39 -0
- svc_infra/webhooks/router.py +55 -0
- svc_infra/webhooks/service.py +70 -0
- svc_infra/webhooks/signing.py +34 -0
- svc_infra/websocket/__init__.py +79 -0
- svc_infra/websocket/add.py +140 -0
- svc_infra/websocket/client.py +282 -0
- svc_infra/websocket/config.py +69 -0
- svc_infra/websocket/easy.py +76 -0
- svc_infra/websocket/exceptions.py +61 -0
- svc_infra/websocket/manager.py +344 -0
- svc_infra/websocket/models.py +49 -0
- svc_infra-0.1.706.dist-info/LICENSE +21 -0
- svc_infra-0.1.706.dist-info/METADATA +356 -0
- svc_infra-0.1.706.dist-info/RECORD +357 -0
- svc_infra-0.1.589.dist-info/METADATA +0 -79
- svc_infra-0.1.589.dist-info/RECORD +0 -234
- {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.589.dist-info → svc_infra-0.1.706.dist-info}/entry_points.txt +0 -0
svc_infra/cache/decorators.py
CHANGED
|
@@ -7,6 +7,7 @@ invalidating cache on write operations, and managing cache recaching strategies.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import inspect
|
|
10
11
|
import logging
|
|
11
12
|
from typing import Any, Awaitable, Callable, Iterable, Optional, Union
|
|
12
13
|
|
|
@@ -16,16 +17,12 @@ from svc_infra.cache.backend import alias as _alias
|
|
|
16
17
|
from svc_infra.cache.backend import setup_cache as _setup_cache
|
|
17
18
|
from svc_infra.cache.backend import wait_ready as _wait_ready
|
|
18
19
|
|
|
19
|
-
from .keys import
|
|
20
|
-
build_key_template,
|
|
21
|
-
build_key_variants_renderer,
|
|
22
|
-
create_tags_function,
|
|
23
|
-
resolve_tags,
|
|
24
|
-
)
|
|
20
|
+
from .keys import build_key_template, build_key_variants_renderer, resolve_tags
|
|
25
21
|
from .recache import RecachePlan, RecacheSpec, execute_recache, recache
|
|
26
22
|
from .resources import Resource, entity, resource
|
|
27
23
|
from .tags import invalidate_tags
|
|
28
24
|
from .ttl import validate_ttl
|
|
25
|
+
from .utils import validate_cache_key
|
|
29
26
|
|
|
30
27
|
logger = logging.getLogger(__name__)
|
|
31
28
|
|
|
@@ -98,23 +95,40 @@ def cache_read(
|
|
|
98
95
|
ttl_val = validate_ttl(ttl)
|
|
99
96
|
template = build_key_template(key)
|
|
100
97
|
namespace = _alias() or ""
|
|
101
|
-
|
|
98
|
+
# Cashews expects `tags` to be an iterable of (template) strings.
|
|
99
|
+
# If no explicit tags are provided, default to tagging by the key template.
|
|
100
|
+
# This enables the common pattern:
|
|
101
|
+
# @cache_read(key="thing:{id}")
|
|
102
|
+
# @cache_write(tags=["thing:{id}"])
|
|
103
|
+
# where writes invalidate reads without requiring tags on every read.
|
|
104
|
+
dynamic_tags_func: Callable[..., Iterable[str]] | None = None
|
|
105
|
+
if tags is None:
|
|
106
|
+
tags_param: Iterable[str] = (template,)
|
|
107
|
+
elif callable(tags):
|
|
108
|
+
# Preserve API surface area, but cashews doesn't accept callables here.
|
|
109
|
+
# We'll attach tag mappings manually after each call.
|
|
110
|
+
dynamic_tags_func = tags
|
|
111
|
+
tags_param = ()
|
|
112
|
+
else:
|
|
113
|
+
tags_param = tags
|
|
102
114
|
|
|
103
115
|
def _decorator(func: Callable[..., Awaitable[Any]]):
|
|
104
116
|
# Try different cashews cache decorator signatures for compatibility
|
|
105
|
-
cache_kwargs = {"tags":
|
|
117
|
+
cache_kwargs: dict[str, Any] = {"tags": tuple(tags_param)}
|
|
106
118
|
if early_ttl is not None:
|
|
107
119
|
cache_kwargs["early_ttl"] = early_ttl
|
|
108
120
|
if refresh is not None:
|
|
109
121
|
cache_kwargs["refresh"] = refresh
|
|
110
122
|
|
|
111
123
|
wrapped = None
|
|
112
|
-
error_msgs = []
|
|
124
|
+
error_msgs: list[str] = []
|
|
113
125
|
|
|
114
126
|
# Attempt 1: With prefix parameter (preferred)
|
|
115
127
|
if namespace:
|
|
116
128
|
try:
|
|
117
|
-
wrapped = _cache.cache(
|
|
129
|
+
wrapped = _cache.cache(
|
|
130
|
+
ttl_val, template, prefix=namespace, **cache_kwargs
|
|
131
|
+
)(func)
|
|
118
132
|
except TypeError as e:
|
|
119
133
|
error_msgs.append(f"prefix parameter: {e}")
|
|
120
134
|
|
|
@@ -126,23 +140,72 @@ def cache_read(
|
|
|
126
140
|
if namespace and not template.startswith(f"{namespace}:")
|
|
127
141
|
else template
|
|
128
142
|
)
|
|
129
|
-
wrapped = _cache.cache(ttl_val, key_with_namespace, **cache_kwargs)(
|
|
143
|
+
wrapped = _cache.cache(ttl_val, key_with_namespace, **cache_kwargs)(
|
|
144
|
+
func
|
|
145
|
+
)
|
|
130
146
|
except TypeError as e:
|
|
131
147
|
error_msgs.append(f"embedded namespace: {e}")
|
|
132
148
|
|
|
133
149
|
# Attempt 3: Minimal fallback
|
|
134
150
|
if wrapped is None:
|
|
135
151
|
try:
|
|
136
|
-
key_with_namespace =
|
|
152
|
+
key_with_namespace = (
|
|
153
|
+
f"{namespace}:{template}" if namespace else template
|
|
154
|
+
)
|
|
137
155
|
wrapped = _cache.cache(ttl_val, key_with_namespace)(func)
|
|
138
156
|
except Exception as e:
|
|
139
157
|
error_msgs.append(f"minimal fallback: {e}")
|
|
140
158
|
logger.error(f"All cache decorator attempts failed: {error_msgs}")
|
|
141
|
-
raise RuntimeError(
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
f"Failed to apply cache decorator: {error_msgs[-1]}"
|
|
161
|
+
) from e
|
|
142
162
|
|
|
143
163
|
# Attach key variants renderer for cache writers
|
|
144
164
|
setattr(wrapped, "__svc_key_variants__", build_key_variants_renderer(template))
|
|
145
|
-
|
|
165
|
+
|
|
166
|
+
# If tags were provided as a callable, populate cashews tag sets manually.
|
|
167
|
+
# This is best-effort and only affects invalidation-by-tag behavior.
|
|
168
|
+
if dynamic_tags_func is None:
|
|
169
|
+
return wrapped
|
|
170
|
+
|
|
171
|
+
sig = inspect.signature(func)
|
|
172
|
+
tag_key_prefix = getattr(_cache, "_tags_key_prefix", "_tag:")
|
|
173
|
+
|
|
174
|
+
async def _wrapped_with_dynamic_tags(*args, **kwargs):
|
|
175
|
+
result = await wrapped(*args, **kwargs)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
bound = sig.bind_partial(*args, **kwargs)
|
|
179
|
+
bound.apply_defaults()
|
|
180
|
+
ctx = dict(bound.arguments)
|
|
181
|
+
|
|
182
|
+
rendered_key = validate_cache_key(template.format(**ctx))
|
|
183
|
+
full_key = f"{namespace}:{rendered_key}" if namespace else rendered_key
|
|
184
|
+
|
|
185
|
+
raw_tags = dynamic_tags_func(*args, **kwargs)
|
|
186
|
+
for t in list(raw_tags) if raw_tags is not None else []:
|
|
187
|
+
tag_val = str(t)
|
|
188
|
+
if "{" in tag_val and "}" in tag_val:
|
|
189
|
+
try:
|
|
190
|
+
tag_val = tag_val.format(**ctx)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
if tag_val:
|
|
194
|
+
await _cache.set_add(
|
|
195
|
+
tag_key_prefix + tag_val, full_key, expire=ttl_val
|
|
196
|
+
)
|
|
197
|
+
except Exception:
|
|
198
|
+
# Don't let best-effort tag mapping break cache reads.
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
setattr(
|
|
204
|
+
_wrapped_with_dynamic_tags,
|
|
205
|
+
"__svc_key_variants__",
|
|
206
|
+
getattr(wrapped, "__svc_key_variants__", None),
|
|
207
|
+
)
|
|
208
|
+
return _wrapped_with_dynamic_tags
|
|
146
209
|
|
|
147
210
|
return _decorator
|
|
148
211
|
|
|
@@ -203,7 +266,10 @@ def cache_write(
|
|
|
203
266
|
if recache:
|
|
204
267
|
try:
|
|
205
268
|
await execute_recache(
|
|
206
|
-
recache,
|
|
269
|
+
recache,
|
|
270
|
+
*args,
|
|
271
|
+
max_concurrency=recache_max_concurrency,
|
|
272
|
+
**kwargs,
|
|
207
273
|
)
|
|
208
274
|
except Exception as e:
|
|
209
275
|
logger.error(f"Cache recaching failed: {e}")
|
svc_infra/cache/demo.py
CHANGED
|
@@ -83,10 +83,10 @@ async def main():
|
|
|
83
83
|
# Try to fetch deleted user - should hit DB and get KeyError
|
|
84
84
|
try:
|
|
85
85
|
p4 = await get_user_profile(user_id=uid)
|
|
86
|
-
print("
|
|
86
|
+
print("[ERROR] Fetched profile after delete:", p4)
|
|
87
87
|
print("This shouldn't happen - user should be deleted!")
|
|
88
88
|
except KeyError as e:
|
|
89
|
-
print(f"
|
|
89
|
+
print(f"[OK] User successfully deleted - {e}")
|
|
90
90
|
print("Cache invalidation and deletion worked perfectly!")
|
|
91
91
|
|
|
92
92
|
|
svc_infra/cache/keys.py
CHANGED
|
@@ -88,12 +88,32 @@ def build_key_variants_renderer(template: str) -> Callable[..., list[str]]:
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def resolve_tags(tags, *args, **kwargs) -> list[str]:
|
|
91
|
-
"""Resolve tags from static list or callable.
|
|
91
|
+
"""Resolve tags from static list or callable and render templates with kwargs.
|
|
92
|
+
|
|
93
|
+
Supports entries like "thing:{id}" which will be formatted using provided kwargs.
|
|
94
|
+
Non-string items are passed through as str(). Missing keys are skipped with a warning.
|
|
95
|
+
"""
|
|
92
96
|
try:
|
|
97
|
+
# 1) Obtain raw tags list
|
|
93
98
|
if callable(tags):
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
raw = tags(*args, **kwargs)
|
|
100
|
+
raw_list = list(raw) if raw is not None else []
|
|
101
|
+
else:
|
|
102
|
+
raw_list = list(tags)
|
|
103
|
+
|
|
104
|
+
# 2) Render any templates using kwargs
|
|
105
|
+
rendered: list[str] = []
|
|
106
|
+
for t in raw_list:
|
|
107
|
+
try:
|
|
108
|
+
if isinstance(t, str) and ("{" in t and "}" in t):
|
|
109
|
+
rendered.append(t.format(**kwargs))
|
|
110
|
+
else:
|
|
111
|
+
rendered.append(str(t))
|
|
112
|
+
except KeyError as e:
|
|
113
|
+
logger.warning(f"Tag template missing key {e} in '{t}'")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.warning(f"Failed to render tag '{t}': {e}")
|
|
116
|
+
return [r for r in rendered if r]
|
|
97
117
|
except Exception as e:
|
|
98
118
|
logger.error(f"Failed to resolve cache tags: {e}")
|
|
99
119
|
return []
|
svc_infra/cache/recache.py
CHANGED
|
@@ -61,7 +61,9 @@ def recache(
|
|
|
61
61
|
Returns:
|
|
62
62
|
RecachePlan instance
|
|
63
63
|
"""
|
|
64
|
-
return RecachePlan(
|
|
64
|
+
return RecachePlan(
|
|
65
|
+
getter=getter, include=include, rename=rename, extra=extra, key=key
|
|
66
|
+
)
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
RecacheSpec = Union[
|
|
@@ -165,7 +167,7 @@ def build_getter_kwargs(
|
|
|
165
167
|
if isinstance(spec, tuple):
|
|
166
168
|
getter, mapping_or_builder = spec
|
|
167
169
|
getter_params = signature(getter).parameters
|
|
168
|
-
|
|
170
|
+
legacy_call_kwargs: dict[str, Any] = {}
|
|
169
171
|
|
|
170
172
|
if callable(mapping_or_builder):
|
|
171
173
|
try:
|
|
@@ -173,7 +175,7 @@ def build_getter_kwargs(
|
|
|
173
175
|
if isinstance(produced, dict):
|
|
174
176
|
for param_name, value in produced.items():
|
|
175
177
|
if param_name in getter_params:
|
|
176
|
-
|
|
178
|
+
legacy_call_kwargs[param_name] = value
|
|
177
179
|
except Exception as e:
|
|
178
180
|
logger.warning(f"Recache mapping function failed: {e}")
|
|
179
181
|
elif isinstance(mapping_or_builder, dict):
|
|
@@ -182,25 +184,31 @@ def build_getter_kwargs(
|
|
|
182
184
|
continue
|
|
183
185
|
try:
|
|
184
186
|
if callable(source):
|
|
185
|
-
|
|
187
|
+
legacy_call_kwargs[getter_param] = source(
|
|
188
|
+
*mut_args, **mut_kwargs
|
|
189
|
+
)
|
|
186
190
|
elif isinstance(source, str) and source in mut_kwargs:
|
|
187
|
-
|
|
191
|
+
legacy_call_kwargs[getter_param] = mut_kwargs[source]
|
|
188
192
|
except Exception as e:
|
|
189
|
-
logger.warning(
|
|
193
|
+
logger.warning(
|
|
194
|
+
f"Recache parameter mapping failed for {getter_param}: {e}"
|
|
195
|
+
)
|
|
190
196
|
|
|
191
197
|
# Add direct parameter matches
|
|
192
198
|
for param_name in getter_params.keys():
|
|
193
|
-
if param_name not in
|
|
194
|
-
|
|
199
|
+
if param_name not in legacy_call_kwargs and param_name in mut_kwargs:
|
|
200
|
+
legacy_call_kwargs[param_name] = mut_kwargs[param_name]
|
|
195
201
|
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
legacy_call_kwargs = {
|
|
203
|
+
k: v for k, v in legacy_call_kwargs.items() if k in getter_params
|
|
204
|
+
}
|
|
205
|
+
return getter, legacy_call_kwargs
|
|
198
206
|
|
|
199
207
|
# Handle simple getter function
|
|
200
208
|
getter = spec
|
|
201
209
|
getter_params = signature(getter).parameters
|
|
202
|
-
|
|
203
|
-
return getter,
|
|
210
|
+
simple_call_kwargs = {k: v for k, v in mut_kwargs.items() if k in getter_params}
|
|
211
|
+
return getter, simple_call_kwargs
|
|
204
212
|
|
|
205
213
|
|
|
206
214
|
async def execute_recache(
|
|
@@ -224,7 +232,9 @@ async def execute_recache(
|
|
|
224
232
|
try:
|
|
225
233
|
await _cache.delete(key_variant)
|
|
226
234
|
except Exception as e:
|
|
227
|
-
logger.debug(
|
|
235
|
+
logger.debug(
|
|
236
|
+
f"Failed to delete cache key {key_variant}: {e}"
|
|
237
|
+
)
|
|
228
238
|
|
|
229
239
|
# Execute the getter to warm the cache
|
|
230
240
|
await getter(**call_kwargs)
|
|
@@ -233,4 +243,6 @@ async def execute_recache(
|
|
|
233
243
|
logger.error(f"Recache operation failed: {e}")
|
|
234
244
|
|
|
235
245
|
# Execute all recache operations concurrently
|
|
236
|
-
await asyncio.gather(
|
|
246
|
+
await asyncio.gather(
|
|
247
|
+
*[_run_single_recache(spec) for spec in specs], return_exceptions=True
|
|
248
|
+
)
|
svc_infra/cache/resources.py
CHANGED
|
@@ -63,7 +63,9 @@ class Resource:
|
|
|
63
63
|
|
|
64
64
|
def _decorator(func: Callable):
|
|
65
65
|
try:
|
|
66
|
-
return _cache(ttl=ttl, key=key_template, tags=tags_template, lock=lock)(
|
|
66
|
+
return _cache(ttl=ttl, key=key_template, tags=tags_template, lock=lock)(
|
|
67
|
+
func
|
|
68
|
+
)
|
|
67
69
|
except TypeError:
|
|
68
70
|
# Fallback for older cashews versions
|
|
69
71
|
return _cache(ttl=ttl, key=key_template, tags=tags_template)(func)
|
|
@@ -97,7 +99,9 @@ class Resource:
|
|
|
97
99
|
"""Delete all cache keys for a specific entity."""
|
|
98
100
|
namespace = _alias() or ""
|
|
99
101
|
namespace_prefix = (
|
|
100
|
-
f"{namespace}:"
|
|
102
|
+
f"{namespace}:"
|
|
103
|
+
if namespace and not namespace.endswith(":")
|
|
104
|
+
else namespace
|
|
101
105
|
)
|
|
102
106
|
|
|
103
107
|
# Generate candidate keys to delete
|
|
@@ -133,7 +137,9 @@ class Resource:
|
|
|
133
137
|
# Namespaced wildcard
|
|
134
138
|
if namespace_prefix:
|
|
135
139
|
await _maybe_await(
|
|
136
|
-
delete_match(
|
|
140
|
+
delete_match(
|
|
141
|
+
f"{namespace_prefix}{entity_name}:*:{entity_id}*"
|
|
142
|
+
)
|
|
137
143
|
)
|
|
138
144
|
# Non-namespaced wildcard
|
|
139
145
|
await _maybe_await(delete_match(f"{entity_name}:*:{entity_id}*"))
|
|
@@ -157,7 +163,8 @@ class Resource:
|
|
|
157
163
|
logger.error(f"Resource recache failed: {e}")
|
|
158
164
|
|
|
159
165
|
await asyncio.gather(
|
|
160
|
-
*[_run_single_resource_recache(spec) for spec in specs],
|
|
166
|
+
*[_run_single_resource_recache(spec) for spec in specs],
|
|
167
|
+
return_exceptions=True,
|
|
161
168
|
)
|
|
162
169
|
|
|
163
170
|
def _decorator(mutator: Callable):
|
|
@@ -171,7 +178,9 @@ class Resource:
|
|
|
171
178
|
# Tag invalidation
|
|
172
179
|
invalidate_func = getattr(_cache, "invalidate", None)
|
|
173
180
|
if callable(invalidate_func):
|
|
174
|
-
await _maybe_await(
|
|
181
|
+
await _maybe_await(
|
|
182
|
+
invalidate_func(f"{self.name}:{entity_id}")
|
|
183
|
+
)
|
|
175
184
|
|
|
176
185
|
# Precise key deletion
|
|
177
186
|
await _delete_entity_keys(self.name, str(entity_id))
|
svc_infra/cache/tags.py
CHANGED
|
@@ -28,50 +28,25 @@ async def invalidate_tags(*tags: str) -> int:
|
|
|
28
28
|
if not tags:
|
|
29
29
|
return 0
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# Preserve order while de-duplicating.
|
|
32
|
+
tags_to_delete = list(dict.fromkeys(tags))
|
|
32
33
|
|
|
33
|
-
#
|
|
34
|
+
# Cashews supports explicit tag deletion via delete_tags().
|
|
34
35
|
try:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
pass
|
|
36
|
+
if hasattr(_cache, "delete_tags"):
|
|
37
|
+
await _cache.delete_tags(*tags_to_delete)
|
|
38
|
+
return len(tags_to_delete)
|
|
39
39
|
except Exception as e:
|
|
40
|
-
logger.warning(f"
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for method_name in ("delete_tag", "invalidate_tag", "tag_invalidate"):
|
|
54
|
-
if hasattr(_cache, method_name):
|
|
55
|
-
try:
|
|
56
|
-
method = getattr(_cache, method_name)
|
|
57
|
-
result = await method(tag)
|
|
58
|
-
count += int(result) if isinstance(result, int) else 1
|
|
59
|
-
break
|
|
60
|
-
except Exception as e:
|
|
61
|
-
logger.debug(f"Tag method {method_name} failed for tag {tag}: {e}")
|
|
62
|
-
continue
|
|
63
|
-
else:
|
|
64
|
-
# Strategy 4: Pattern matching fallback
|
|
65
|
-
for method_name in ("delete_match", "invalidate_match", "invalidate"):
|
|
66
|
-
if hasattr(_cache, method_name):
|
|
67
|
-
try:
|
|
68
|
-
method = getattr(_cache, method_name)
|
|
69
|
-
pattern = f"*{tag}*"
|
|
70
|
-
result = await method(pattern)
|
|
71
|
-
count += int(result) if isinstance(result, int) else 1
|
|
72
|
-
break
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.debug(f"Pattern method {method_name} failed for tag {tag}: {e}")
|
|
75
|
-
continue
|
|
76
|
-
|
|
77
|
-
return count
|
|
40
|
+
logger.warning(f"Cache tag invalidation failed: {e}")
|
|
41
|
+
|
|
42
|
+
# Fallback: attempt private per-tag deletion when available.
|
|
43
|
+
deleted = 0
|
|
44
|
+
for tag in tags_to_delete:
|
|
45
|
+
try:
|
|
46
|
+
if hasattr(_cache, "_delete_tag"):
|
|
47
|
+
await _cache._delete_tag(tag)
|
|
48
|
+
deleted += 1
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.debug(f"Tag deletion failed for tag {tag}: {e}")
|
|
51
|
+
|
|
52
|
+
return deleted
|
svc_infra/cache/utils.py
CHANGED
|
@@ -33,7 +33,9 @@ def stable_hash(*args: Any, **kwargs: Any) -> str:
|
|
|
33
33
|
"""
|
|
34
34
|
try:
|
|
35
35
|
# Use JSON serialization for stable, deterministic output
|
|
36
|
-
raw = json.dumps(
|
|
36
|
+
raw = json.dumps(
|
|
37
|
+
[args, kwargs], default=str, sort_keys=True, separators=(",", ":")
|
|
38
|
+
)
|
|
37
39
|
except (TypeError, ValueError) as e:
|
|
38
40
|
# Fallback to repr if JSON serialization fails
|
|
39
41
|
logger.warning(f"JSON serialization failed for hash input, using repr: {e}")
|
svc_infra/cli/__init__.py
CHANGED
|
@@ -4,10 +4,17 @@ import typer
|
|
|
4
4
|
|
|
5
5
|
from svc_infra.cli.cmds import (
|
|
6
6
|
_HELP,
|
|
7
|
+
jobs_app,
|
|
7
8
|
register_alembic,
|
|
9
|
+
register_db_ops,
|
|
10
|
+
register_docs,
|
|
11
|
+
register_dx,
|
|
12
|
+
register_health,
|
|
8
13
|
register_mongo,
|
|
9
14
|
register_mongo_scaffold,
|
|
10
15
|
register_obs,
|
|
16
|
+
register_sdk,
|
|
17
|
+
register_sql_export,
|
|
11
18
|
register_sql_scaffold,
|
|
12
19
|
)
|
|
13
20
|
from svc_infra.cli.foundation.typer_bootstrap import pre_cli
|
|
@@ -15,16 +22,53 @@ from svc_infra.cli.foundation.typer_bootstrap import pre_cli
|
|
|
15
22
|
app = typer.Typer(no_args_is_help=True, add_completion=False, help=_HELP)
|
|
16
23
|
pre_cli(app)
|
|
17
24
|
|
|
18
|
-
# ---
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
# --- db ops group ---
|
|
26
|
+
db_app = typer.Typer(
|
|
27
|
+
no_args_is_help=True, add_completion=False, help="Database operations"
|
|
28
|
+
)
|
|
29
|
+
register_db_ops(db_app)
|
|
30
|
+
app.add_typer(db_app, name="db")
|
|
31
|
+
|
|
32
|
+
# --- sql group ---
|
|
33
|
+
sql_app = typer.Typer(no_args_is_help=True, add_completion=False, help="SQL commands")
|
|
34
|
+
register_alembic(sql_app)
|
|
35
|
+
register_sql_scaffold(sql_app)
|
|
36
|
+
register_sql_export(sql_app)
|
|
37
|
+
app.add_typer(sql_app, name="sql")
|
|
38
|
+
|
|
39
|
+
# --- mongo group ---
|
|
40
|
+
mongo_app = typer.Typer(
|
|
41
|
+
no_args_is_help=True, add_completion=False, help="MongoDB commands"
|
|
42
|
+
)
|
|
43
|
+
register_mongo(mongo_app)
|
|
44
|
+
register_mongo_scaffold(mongo_app)
|
|
45
|
+
app.add_typer(mongo_app, name="mongo")
|
|
46
|
+
|
|
47
|
+
# --- health group ---
|
|
48
|
+
health_app = typer.Typer(
|
|
49
|
+
no_args_is_help=True, add_completion=False, help="Health checks"
|
|
50
|
+
)
|
|
51
|
+
register_health(health_app)
|
|
52
|
+
app.add_typer(health_app, name="health")
|
|
53
|
+
|
|
54
|
+
# -- obs group ---
|
|
55
|
+
obs_app = typer.Typer(
|
|
56
|
+
no_args_is_help=True, add_completion=False, help="Observability commands"
|
|
57
|
+
)
|
|
58
|
+
register_obs(obs_app)
|
|
59
|
+
app.add_typer(obs_app, name="obs")
|
|
60
|
+
|
|
61
|
+
# -- dx commands ---
|
|
62
|
+
register_dx(app)
|
|
63
|
+
|
|
64
|
+
# -- jobs commands ---
|
|
65
|
+
app.add_typer(jobs_app, name="jobs")
|
|
21
66
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
register_mongo_scaffold(app)
|
|
67
|
+
# -- sdk commands ---
|
|
68
|
+
register_sdk(app)
|
|
25
69
|
|
|
26
|
-
# --
|
|
27
|
-
|
|
70
|
+
# -- docs commands ---
|
|
71
|
+
register_docs(app)
|
|
28
72
|
|
|
29
73
|
|
|
30
74
|
def main():
|
svc_infra/cli/cmds/__init__.py
CHANGED
|
@@ -1,18 +1,55 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from svc_infra.cli.cmds.db.nosql.mongo.mongo_cmds import register as register_mongo
|
|
9
|
+
except ModuleNotFoundError as exc:
|
|
10
|
+
_mongo_import_error = exc
|
|
11
|
+
|
|
12
|
+
def register_mongo(app: typer.Typer) -> None: # type: ignore[no-redef]
|
|
13
|
+
def _unavailable() -> Any:
|
|
14
|
+
raise ModuleNotFoundError(
|
|
15
|
+
"MongoDB CLI commands require optional dependencies. Install pymongo and motor "
|
|
16
|
+
"to enable `svc-infra mongo ...` commands."
|
|
17
|
+
) from _mongo_import_error
|
|
18
|
+
|
|
19
|
+
# Provide a single helpful command instead of failing CLI import.
|
|
20
|
+
app.command("unavailable")(_unavailable)
|
|
21
|
+
|
|
22
|
+
|
|
2
23
|
from svc_infra.cli.cmds.db.nosql.mongo.mongo_scaffold_cmds import (
|
|
3
24
|
register as register_mongo_scaffold,
|
|
4
25
|
)
|
|
26
|
+
from svc_infra.cli.cmds.db.ops_cmds import register as register_db_ops
|
|
5
27
|
from svc_infra.cli.cmds.db.sql.alembic_cmds import register as register_alembic
|
|
6
|
-
from svc_infra.cli.cmds.db.sql.
|
|
28
|
+
from svc_infra.cli.cmds.db.sql.sql_export_cmds import register as register_sql_export
|
|
29
|
+
from svc_infra.cli.cmds.db.sql.sql_scaffold_cmds import (
|
|
30
|
+
register as register_sql_scaffold,
|
|
31
|
+
)
|
|
32
|
+
from svc_infra.cli.cmds.docs.docs_cmds import register as register_docs
|
|
33
|
+
from svc_infra.cli.cmds.dx import register_dx
|
|
34
|
+
from svc_infra.cli.cmds.health.health_cmds import register as register_health
|
|
35
|
+
from svc_infra.cli.cmds.jobs.jobs_cmds import app as jobs_app
|
|
7
36
|
from svc_infra.cli.cmds.obs.obs_cmds import register as register_obs
|
|
37
|
+
from svc_infra.cli.cmds.sdk.sdk_cmds import register as register_sdk
|
|
8
38
|
|
|
9
39
|
from .help import _HELP
|
|
10
40
|
|
|
11
41
|
__all__ = [
|
|
12
42
|
"register_alembic",
|
|
13
43
|
"register_sql_scaffold",
|
|
44
|
+
"register_sql_export",
|
|
14
45
|
"register_mongo",
|
|
15
46
|
"register_mongo_scaffold",
|
|
47
|
+
"register_db_ops",
|
|
16
48
|
"register_obs",
|
|
49
|
+
"jobs_app",
|
|
50
|
+
"register_sdk",
|
|
51
|
+
"register_dx",
|
|
52
|
+
"register_docs",
|
|
53
|
+
"register_health",
|
|
17
54
|
"_HELP",
|
|
18
55
|
]
|
|
@@ -172,7 +172,9 @@ def cmd_ping(
|
|
|
172
172
|
|
|
173
173
|
import asyncio
|
|
174
174
|
|
|
175
|
-
from svc_infra.db.nosql.mongo.client import
|
|
175
|
+
from svc_infra.db.nosql.mongo.client import (
|
|
176
|
+
acquire_db,
|
|
177
|
+
) # local import to avoid side effects
|
|
176
178
|
|
|
177
179
|
async def _run():
|
|
178
180
|
await init_mongo()
|
|
@@ -188,6 +190,7 @@ def cmd_ping(
|
|
|
188
190
|
|
|
189
191
|
|
|
190
192
|
def register(app: typer.Typer) -> None:
|
|
191
|
-
app
|
|
192
|
-
app.command("
|
|
193
|
-
app.command("
|
|
193
|
+
# Attach to 'mongo' group app
|
|
194
|
+
app.command("prepare")(cmd_prepare)
|
|
195
|
+
app.command("setup-and-prepare")(cmd_setup_and_prepare)
|
|
196
|
+
app.command("ping")(cmd_ping)
|
|
@@ -17,7 +17,9 @@ def cmd_scaffold(
|
|
|
17
17
|
entity_name: str = typer.Option(
|
|
18
18
|
"Item", help="Entity class name (e.g., User, Member, Product)."
|
|
19
19
|
),
|
|
20
|
-
documents_dir: Path = typer.Option(
|
|
20
|
+
documents_dir: Path = typer.Option(
|
|
21
|
+
..., help="Directory for Mongo document models."
|
|
22
|
+
),
|
|
21
23
|
schemas_dir: Path = typer.Option(..., help="Directory for Pydantic CRUD schemas."),
|
|
22
24
|
overwrite: bool = typer.Option(False, help="Overwrite existing files."),
|
|
23
25
|
same_dir: bool = typer.Option(
|
|
@@ -127,7 +129,7 @@ def register(app: typer.Typer) -> None:
|
|
|
127
129
|
• mongo-scaffold-schemas
|
|
128
130
|
• mongo-scaffold-resources
|
|
129
131
|
"""
|
|
130
|
-
app.command("
|
|
131
|
-
app.command("
|
|
132
|
-
app.command("
|
|
133
|
-
app.command("
|
|
132
|
+
app.command("scaffold")(cmd_scaffold)
|
|
133
|
+
app.command("scaffold-documents")(cmd_scaffold_documents)
|
|
134
|
+
app.command("scaffold-schemas")(cmd_scaffold_schemas)
|
|
135
|
+
app.command("scaffold-resources")(cmd_scaffold_resources)
|