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/billing/models.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from sqlalchemy import JSON, DateTime, Index, Numeric, String, UniqueConstraint, text
|
|
7
6
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
@@ -15,9 +14,7 @@ class UsageEvent(ModelBase):
|
|
|
15
14
|
__tablename__ = "billing_usage_events"
|
|
16
15
|
|
|
17
16
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
18
|
-
tenant_id: Mapped[str] = mapped_column(
|
|
19
|
-
String(TENANT_ID_LEN), index=True, nullable=False
|
|
20
|
-
)
|
|
17
|
+
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
21
18
|
metric: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
22
19
|
amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
23
20
|
at_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
@@ -30,9 +27,7 @@ class UsageEvent(ModelBase):
|
|
|
30
27
|
)
|
|
31
28
|
|
|
32
29
|
__table_args__ = (
|
|
33
|
-
UniqueConstraint(
|
|
34
|
-
"tenant_id", "metric", "idempotency_key", name="uq_usage_idem"
|
|
35
|
-
),
|
|
30
|
+
UniqueConstraint("tenant_id", "metric", "idempotency_key", name="uq_usage_idem"),
|
|
36
31
|
Index("ix_usage_tenant_metric_ts", "tenant_id", "metric", "at_ts"),
|
|
37
32
|
)
|
|
38
33
|
|
|
@@ -41,16 +36,10 @@ class UsageAggregate(ModelBase):
|
|
|
41
36
|
__tablename__ = "billing_usage_aggregates"
|
|
42
37
|
|
|
43
38
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
44
|
-
tenant_id: Mapped[str] = mapped_column(
|
|
45
|
-
String(TENANT_ID_LEN), index=True, nullable=False
|
|
46
|
-
)
|
|
39
|
+
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
47
40
|
metric: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
48
|
-
period_start: Mapped[datetime] = mapped_column(
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
granularity: Mapped[str] = mapped_column(
|
|
52
|
-
String(8), nullable=False
|
|
53
|
-
) # hour|day|month
|
|
41
|
+
period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
42
|
+
granularity: Mapped[str] = mapped_column(String(8), nullable=False) # hour|day|month
|
|
54
43
|
total: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
55
44
|
updated_at: Mapped[datetime] = mapped_column(
|
|
56
45
|
DateTime(timezone=True),
|
|
@@ -59,9 +48,7 @@ class UsageAggregate(ModelBase):
|
|
|
59
48
|
)
|
|
60
49
|
|
|
61
50
|
__table_args__ = (
|
|
62
|
-
UniqueConstraint(
|
|
63
|
-
"tenant_id", "metric", "period_start", "granularity", name="uq_usage_agg"
|
|
64
|
-
),
|
|
51
|
+
UniqueConstraint("tenant_id", "metric", "period_start", "granularity", name="uq_usage_agg"),
|
|
65
52
|
)
|
|
66
53
|
|
|
67
54
|
|
|
@@ -69,11 +56,9 @@ class Plan(ModelBase):
|
|
|
69
56
|
__tablename__ = "billing_plans"
|
|
70
57
|
|
|
71
58
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
72
|
-
key: Mapped[str] = mapped_column(
|
|
73
|
-
String(64), unique=True, index=True, nullable=False
|
|
74
|
-
)
|
|
59
|
+
key: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
75
60
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
76
|
-
description: Mapped[
|
|
61
|
+
description: Mapped[str | None] = mapped_column(String(255))
|
|
77
62
|
created_at: Mapped[datetime] = mapped_column(
|
|
78
63
|
DateTime(timezone=True),
|
|
79
64
|
server_default=text("CURRENT_TIMESTAMP"),
|
|
@@ -100,14 +85,10 @@ class Subscription(ModelBase):
|
|
|
100
85
|
__tablename__ = "billing_subscriptions"
|
|
101
86
|
|
|
102
87
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
103
|
-
tenant_id: Mapped[str] = mapped_column(
|
|
104
|
-
String(TENANT_ID_LEN), index=True, nullable=False
|
|
105
|
-
)
|
|
88
|
+
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
106
89
|
plan_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
107
|
-
effective_at: Mapped[datetime] = mapped_column(
|
|
108
|
-
|
|
109
|
-
)
|
|
110
|
-
ended_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
90
|
+
effective_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
91
|
+
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
111
92
|
created_at: Mapped[datetime] = mapped_column(
|
|
112
93
|
DateTime(timezone=True),
|
|
113
94
|
server_default=text("CURRENT_TIMESTAMP"),
|
|
@@ -119,17 +100,11 @@ class Price(ModelBase):
|
|
|
119
100
|
__tablename__ = "billing_prices"
|
|
120
101
|
|
|
121
102
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
122
|
-
key: Mapped[str] = mapped_column(
|
|
123
|
-
String(64), unique=True, index=True, nullable=False
|
|
124
|
-
)
|
|
103
|
+
key: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
125
104
|
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
126
|
-
unit_amount: Mapped[int] = mapped_column(
|
|
127
|
-
|
|
128
|
-
) #
|
|
129
|
-
metric: Mapped[Optional[str]] = mapped_column(
|
|
130
|
-
String(64)
|
|
131
|
-
) # null for fixed recurring
|
|
132
|
-
recurring_interval: Mapped[Optional[str]] = mapped_column(String(8)) # month|year
|
|
105
|
+
unit_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False) # minor units
|
|
106
|
+
metric: Mapped[str | None] = mapped_column(String(64)) # null for fixed recurring
|
|
107
|
+
recurring_interval: Mapped[str | None] = mapped_column(String(8)) # month|year
|
|
133
108
|
created_at: Mapped[datetime] = mapped_column(
|
|
134
109
|
DateTime(timezone=True),
|
|
135
110
|
server_default=text("CURRENT_TIMESTAMP"),
|
|
@@ -141,19 +116,13 @@ class Invoice(ModelBase):
|
|
|
141
116
|
__tablename__ = "billing_invoices"
|
|
142
117
|
|
|
143
118
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
144
|
-
tenant_id: Mapped[str] = mapped_column(
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
period_start: Mapped[datetime] = mapped_column(
|
|
148
|
-
DateTime(timezone=True), nullable=False
|
|
149
|
-
)
|
|
150
|
-
period_end: Mapped[datetime] = mapped_column(
|
|
151
|
-
DateTime(timezone=True), nullable=False
|
|
152
|
-
)
|
|
119
|
+
tenant_id: Mapped[str] = mapped_column(String(TENANT_ID_LEN), index=True, nullable=False)
|
|
120
|
+
period_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
121
|
+
period_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
153
122
|
status: Mapped[str] = mapped_column(String(16), index=True, nullable=False)
|
|
154
123
|
total_amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
155
124
|
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
156
|
-
provider_invoice_id: Mapped[
|
|
125
|
+
provider_invoice_id: Mapped[str | None] = mapped_column(String(128), index=True)
|
|
157
126
|
created_at: Mapped[datetime] = mapped_column(
|
|
158
127
|
DateTime(timezone=True),
|
|
159
128
|
server_default=text("CURRENT_TIMESTAMP"),
|
|
@@ -166,8 +135,8 @@ class InvoiceLine(ModelBase):
|
|
|
166
135
|
|
|
167
136
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
168
137
|
invoice_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
169
|
-
price_id: Mapped[
|
|
170
|
-
metric: Mapped[
|
|
138
|
+
price_id: Mapped[str | None] = mapped_column(String(64), index=True)
|
|
139
|
+
metric: Mapped[str | None] = mapped_column(String(64))
|
|
171
140
|
quantity: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
172
141
|
amount: Mapped[int] = mapped_column(Numeric(18, 0), nullable=False)
|
|
173
142
|
created_at: Mapped[datetime] = mapped_column(
|
svc_infra/billing/quotas.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from datetime import
|
|
4
|
-
from typing import Annotated
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
from fastapi import Depends, HTTPException, status
|
|
7
7
|
from sqlalchemy import select
|
|
@@ -13,10 +13,8 @@ from svc_infra.api.fastapi.tenancy.context import TenantId
|
|
|
13
13
|
from .models import PlanEntitlement, Subscription, UsageAggregate
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
async def _current_subscription(
|
|
17
|
-
|
|
18
|
-
) -> Optional[Subscription]:
|
|
19
|
-
now = datetime.now(tz=timezone.utc)
|
|
16
|
+
async def _current_subscription(session: AsyncSession, tenant_id: str) -> Subscription | None:
|
|
17
|
+
now = datetime.now(tz=UTC)
|
|
20
18
|
row = (
|
|
21
19
|
(
|
|
22
20
|
await session.execute(
|
|
@@ -59,7 +57,7 @@ def require_quota(metric: str, *, window: str = "day", soft: bool = True):
|
|
|
59
57
|
# no entitlement → unlimited
|
|
60
58
|
return
|
|
61
59
|
# compute current window start
|
|
62
|
-
now = datetime.now(tz=
|
|
60
|
+
now = datetime.now(tz=UTC)
|
|
63
61
|
if window == "day":
|
|
64
62
|
period_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
65
63
|
granularity = "day"
|
svc_infra/billing/schemas.py
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import Annotated
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class UsageIn(BaseModel):
|
|
10
10
|
metric: str = Field(..., min_length=1, max_length=64)
|
|
11
|
-
amount: Annotated[
|
|
12
|
-
|
|
13
|
-
]
|
|
14
|
-
at: Optional[datetime] = Field(
|
|
11
|
+
amount: Annotated[int, Field(ge=0, description="Non-negative amount for the metric")]
|
|
12
|
+
at: datetime | None = Field(
|
|
15
13
|
default=None,
|
|
16
14
|
description="Event timestamp (UTC). Defaults to server time if omitted.",
|
|
17
15
|
)
|
|
@@ -33,4 +31,4 @@ class UsageAggregateRow(BaseModel):
|
|
|
33
31
|
|
|
34
32
|
class UsageAggregatesOut(BaseModel):
|
|
35
33
|
items: list[UsageAggregateRow] = Field(default_factory=list)
|
|
36
|
-
next_cursor:
|
|
34
|
+
next_cursor: str | None = None
|
svc_infra/cache/__init__.py
CHANGED
|
@@ -11,16 +11,23 @@ from .add import add_cache
|
|
|
11
11
|
from .backend import get_cache
|
|
12
12
|
|
|
13
13
|
# Core decorators - main public API
|
|
14
|
-
from .decorators import
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
from .decorators import (
|
|
15
|
+
cache_read,
|
|
16
|
+
cache_write,
|
|
17
|
+
cached, # alias for cache_read
|
|
18
|
+
init_cache,
|
|
19
|
+
init_cache_async,
|
|
20
|
+
mutates, # alias for cache_write
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
# Recaching functionality for advanced use cases
|
|
19
24
|
from .recache import RecachePlan, recache
|
|
20
25
|
|
|
21
26
|
# Resource management for entity-based caching
|
|
22
|
-
from .resources import
|
|
23
|
-
|
|
27
|
+
from .resources import (
|
|
28
|
+
entity, # legacy alias
|
|
29
|
+
resource,
|
|
30
|
+
)
|
|
24
31
|
|
|
25
32
|
__all__ = [
|
|
26
33
|
# Primary decorators developers use
|
svc_infra/cache/add.py
CHANGED
|
@@ -14,7 +14,8 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
|
-
from
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from typing import Any
|
|
18
19
|
|
|
19
20
|
from svc_infra.cache.backend import DEFAULT_READINESS_TIMEOUT
|
|
20
21
|
from svc_infra.cache.backend import get_cache as _get_cache
|
|
@@ -35,7 +36,7 @@ def _instance() -> Any:
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def _derive_settings(
|
|
38
|
-
url:
|
|
39
|
+
url: str | None, prefix: str | None, version: str | None
|
|
39
40
|
) -> tuple[str, str, str]:
|
|
40
41
|
"""Derive cache settings from parameters or environment variables.
|
|
41
42
|
|
|
@@ -111,9 +112,7 @@ def add_cache(
|
|
|
111
112
|
try:
|
|
112
113
|
setattr(app.state, state_key, _instance())
|
|
113
114
|
except Exception:
|
|
114
|
-
logger.debug(
|
|
115
|
-
"Unable to expose cache instance on app.state", exc_info=True
|
|
116
|
-
)
|
|
115
|
+
logger.debug("Unable to expose cache instance on app.state", exc_info=True)
|
|
117
116
|
|
|
118
117
|
async def _shutdown():
|
|
119
118
|
try:
|
|
@@ -146,16 +145,14 @@ def add_cache(
|
|
|
146
145
|
# Mark wired and expose state immediately if desired
|
|
147
146
|
if hasattr(app, "state"):
|
|
148
147
|
try:
|
|
149
|
-
|
|
148
|
+
app.state._svc_cache_wired = True
|
|
150
149
|
if expose_state and not hasattr(app.state, state_key):
|
|
151
150
|
setattr(app.state, state_key, _instance())
|
|
152
151
|
except Exception:
|
|
153
152
|
pass
|
|
154
153
|
|
|
155
154
|
if register_ok:
|
|
156
|
-
logger.info(
|
|
157
|
-
"Cache wired: url=%s namespace=%s", eff_url, f"{eff_prefix}:{eff_version}"
|
|
158
|
-
)
|
|
155
|
+
logger.info("Cache wired: url=%s namespace=%s", eff_url, f"{eff_prefix}:{eff_version}")
|
|
159
156
|
else:
|
|
160
157
|
# If we cannot register handlers, at least initialize now
|
|
161
158
|
try:
|
svc_infra/cache/backend.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from cashews import cache as _cache
|
|
7
6
|
|
|
@@ -42,10 +41,10 @@ def _full_prefix() -> str:
|
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
def setup_cache(
|
|
45
|
-
url:
|
|
44
|
+
url: str | None = None,
|
|
46
45
|
*,
|
|
47
|
-
prefix:
|
|
48
|
-
version:
|
|
46
|
+
prefix: str | None = None,
|
|
47
|
+
version: str | None = None,
|
|
49
48
|
):
|
|
50
49
|
"""
|
|
51
50
|
Configure Cashews and set a global key prefix for namespacing.
|
|
@@ -118,7 +117,9 @@ async def wait_ready(timeout: float = DEFAULT_READINESS_TIMEOUT) -> None:
|
|
|
118
117
|
retrieved_value = await _cache.get(probe_key)
|
|
119
118
|
|
|
120
119
|
if retrieved_value != PROBE_VALUE:
|
|
121
|
-
error_msg =
|
|
120
|
+
error_msg = (
|
|
121
|
+
f"Cache readiness probe failed. Expected '{PROBE_VALUE}', got '{retrieved_value}'"
|
|
122
|
+
)
|
|
122
123
|
logger.error(error_msg)
|
|
123
124
|
raise RuntimeError(error_msg)
|
|
124
125
|
|
svc_infra/cache/decorators.py
CHANGED
|
@@ -9,7 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import inspect
|
|
11
11
|
import logging
|
|
12
|
-
from
|
|
12
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
13
|
+
from typing import Any
|
|
13
14
|
|
|
14
15
|
from cashews import cache as _cache
|
|
15
16
|
|
|
@@ -64,11 +65,11 @@ async def init_cache_async(
|
|
|
64
65
|
|
|
65
66
|
def cache_read(
|
|
66
67
|
*,
|
|
67
|
-
key:
|
|
68
|
-
ttl:
|
|
69
|
-
tags:
|
|
70
|
-
early_ttl:
|
|
71
|
-
refresh:
|
|
68
|
+
key: str | tuple[str, ...],
|
|
69
|
+
ttl: int | None = None,
|
|
70
|
+
tags: Iterable[str] | Callable[..., Iterable[str]] | None = None,
|
|
71
|
+
early_ttl: int | None = None,
|
|
72
|
+
refresh: bool | None = None,
|
|
72
73
|
):
|
|
73
74
|
"""
|
|
74
75
|
Cache decorator for read operations with version-resilient key handling.
|
|
@@ -126,9 +127,7 @@ def cache_read(
|
|
|
126
127
|
# Attempt 1: With prefix parameter (preferred)
|
|
127
128
|
if namespace:
|
|
128
129
|
try:
|
|
129
|
-
wrapped = _cache.cache(
|
|
130
|
-
ttl_val, template, prefix=namespace, **cache_kwargs
|
|
131
|
-
)(func)
|
|
130
|
+
wrapped = _cache.cache(ttl_val, template, prefix=namespace, **cache_kwargs)(func)
|
|
132
131
|
except TypeError as e:
|
|
133
132
|
error_msgs.append(f"prefix parameter: {e}")
|
|
134
133
|
|
|
@@ -140,28 +139,22 @@ def cache_read(
|
|
|
140
139
|
if namespace and not template.startswith(f"{namespace}:")
|
|
141
140
|
else template
|
|
142
141
|
)
|
|
143
|
-
wrapped = _cache.cache(ttl_val, key_with_namespace, **cache_kwargs)(
|
|
144
|
-
func
|
|
145
|
-
)
|
|
142
|
+
wrapped = _cache.cache(ttl_val, key_with_namespace, **cache_kwargs)(func)
|
|
146
143
|
except TypeError as e:
|
|
147
144
|
error_msgs.append(f"embedded namespace: {e}")
|
|
148
145
|
|
|
149
146
|
# Attempt 3: Minimal fallback
|
|
150
147
|
if wrapped is None:
|
|
151
148
|
try:
|
|
152
|
-
key_with_namespace =
|
|
153
|
-
f"{namespace}:{template}" if namespace else template
|
|
154
|
-
)
|
|
149
|
+
key_with_namespace = f"{namespace}:{template}" if namespace else template
|
|
155
150
|
wrapped = _cache.cache(ttl_val, key_with_namespace)(func)
|
|
156
151
|
except Exception as e:
|
|
157
152
|
error_msgs.append(f"minimal fallback: {e}")
|
|
158
153
|
logger.error(f"All cache decorator attempts failed: {error_msgs}")
|
|
159
|
-
raise RuntimeError(
|
|
160
|
-
f"Failed to apply cache decorator: {error_msgs[-1]}"
|
|
161
|
-
) from e
|
|
154
|
+
raise RuntimeError(f"Failed to apply cache decorator: {error_msgs[-1]}") from e
|
|
162
155
|
|
|
163
156
|
# Attach key variants renderer for cache writers
|
|
164
|
-
|
|
157
|
+
wrapped.__svc_key_variants__ = build_key_variants_renderer(template) # type: ignore[attr-defined]
|
|
165
158
|
|
|
166
159
|
# If tags were provided as a callable, populate cashews tag sets manually.
|
|
167
160
|
# This is best-effort and only affects invalidation-by-tag behavior.
|
|
@@ -191,19 +184,15 @@ def cache_read(
|
|
|
191
184
|
except Exception:
|
|
192
185
|
pass
|
|
193
186
|
if tag_val:
|
|
194
|
-
await _cache.set_add(
|
|
195
|
-
tag_key_prefix + tag_val, full_key, expire=ttl_val
|
|
196
|
-
)
|
|
187
|
+
await _cache.set_add(tag_key_prefix + tag_val, full_key, expire=ttl_val)
|
|
197
188
|
except Exception:
|
|
198
189
|
# Don't let best-effort tag mapping break cache reads.
|
|
199
190
|
pass
|
|
200
191
|
|
|
201
192
|
return result
|
|
202
193
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"__svc_key_variants__",
|
|
206
|
-
getattr(wrapped, "__svc_key_variants__", None),
|
|
194
|
+
_wrapped_with_dynamic_tags.__svc_key_variants__ = getattr( # type: ignore[attr-defined]
|
|
195
|
+
wrapped, "__svc_key_variants__", None
|
|
207
196
|
)
|
|
208
197
|
return _wrapped_with_dynamic_tags
|
|
209
198
|
|
|
@@ -219,8 +208,8 @@ cached = cache_read
|
|
|
219
208
|
|
|
220
209
|
def cache_write(
|
|
221
210
|
*,
|
|
222
|
-
tags:
|
|
223
|
-
recache:
|
|
211
|
+
tags: Iterable[str] | Callable[..., Iterable[str]],
|
|
212
|
+
recache: Iterable[RecacheSpec] | None = None,
|
|
224
213
|
recache_max_concurrency: int = 5,
|
|
225
214
|
):
|
|
226
215
|
"""
|
svc_infra/cache/keys.py
CHANGED
|
@@ -6,7 +6,7 @@ with version-resilient handling and namespace support.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from
|
|
9
|
+
from collections.abc import Callable
|
|
10
10
|
|
|
11
11
|
from svc_infra.cache.backend import alias as _alias
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ from .utils import validate_cache_key
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def build_key_template(key:
|
|
18
|
+
def build_key_template(key: str | tuple[str, ...]) -> str:
|
|
19
19
|
"""Convert key to template string."""
|
|
20
20
|
if isinstance(key, tuple):
|
|
21
21
|
parts = [part for part in key if part]
|
svc_infra/cache/recache.py
CHANGED
|
@@ -7,9 +7,10 @@ including recache plans and execution strategies.
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import logging
|
|
10
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
10
11
|
from dataclasses import dataclass
|
|
11
12
|
from inspect import Parameter, signature
|
|
12
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
13
14
|
|
|
14
15
|
from cashews import cache as _cache
|
|
15
16
|
|
|
@@ -34,19 +35,19 @@ class RecachePlan:
|
|
|
34
35
|
"""
|
|
35
36
|
|
|
36
37
|
getter: Callable[..., Awaitable[Any]]
|
|
37
|
-
include:
|
|
38
|
-
rename:
|
|
39
|
-
extra:
|
|
40
|
-
key:
|
|
38
|
+
include: Iterable[str] | None = None
|
|
39
|
+
rename: dict[str, str] | None = None
|
|
40
|
+
extra: dict[str, Any] | None = None
|
|
41
|
+
key: str | tuple[str, ...] | None = None
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def recache(
|
|
44
45
|
getter: Callable[..., Awaitable[Any]],
|
|
45
46
|
*,
|
|
46
|
-
include:
|
|
47
|
-
rename:
|
|
48
|
-
extra:
|
|
49
|
-
key:
|
|
47
|
+
include: Iterable[str] | None = None,
|
|
48
|
+
rename: dict[str, str] | None = None,
|
|
49
|
+
extra: dict[str, Any] | None = None,
|
|
50
|
+
key: str | tuple[str, ...] | None = None,
|
|
50
51
|
) -> RecachePlan:
|
|
51
52
|
"""
|
|
52
53
|
Create a recache plan for cache warming after invalidation.
|
|
@@ -61,21 +62,17 @@ def recache(
|
|
|
61
62
|
Returns:
|
|
62
63
|
RecachePlan instance
|
|
63
64
|
"""
|
|
64
|
-
return RecachePlan(
|
|
65
|
-
getter=getter, include=include, rename=rename, extra=extra, key=key
|
|
66
|
-
)
|
|
65
|
+
return RecachePlan(getter=getter, include=include, rename=rename, extra=extra, key=key)
|
|
67
66
|
|
|
68
67
|
|
|
69
|
-
RecacheSpec =
|
|
70
|
-
Callable[..., Awaitable[Any]]
|
|
71
|
-
RecachePlan
|
|
72
|
-
tuple[Callable[..., Awaitable[Any]], Any]
|
|
73
|
-
|
|
68
|
+
RecacheSpec = (
|
|
69
|
+
Callable[..., Awaitable[Any]]
|
|
70
|
+
| RecachePlan
|
|
71
|
+
| tuple[Callable[..., Awaitable[Any]], Any] # Legacy format
|
|
72
|
+
)
|
|
74
73
|
|
|
75
74
|
|
|
76
|
-
def generate_key_variants(
|
|
77
|
-
template: Union[str, tuple[str, ...]], params: dict[str, Any]
|
|
78
|
-
) -> list[str]:
|
|
75
|
+
def generate_key_variants(template: str | tuple[str, ...], params: dict[str, Any]) -> list[str]:
|
|
79
76
|
"""
|
|
80
77
|
Generate all possible cache key variants for deletion.
|
|
81
78
|
|
|
@@ -184,24 +181,18 @@ def build_getter_kwargs(
|
|
|
184
181
|
continue
|
|
185
182
|
try:
|
|
186
183
|
if callable(source):
|
|
187
|
-
legacy_call_kwargs[getter_param] = source(
|
|
188
|
-
*mut_args, **mut_kwargs
|
|
189
|
-
)
|
|
184
|
+
legacy_call_kwargs[getter_param] = source(*mut_args, **mut_kwargs)
|
|
190
185
|
elif isinstance(source, str) and source in mut_kwargs:
|
|
191
186
|
legacy_call_kwargs[getter_param] = mut_kwargs[source]
|
|
192
187
|
except Exception as e:
|
|
193
|
-
logger.warning(
|
|
194
|
-
f"Recache parameter mapping failed for {getter_param}: {e}"
|
|
195
|
-
)
|
|
188
|
+
logger.warning(f"Recache parameter mapping failed for {getter_param}: {e}")
|
|
196
189
|
|
|
197
190
|
# Add direct parameter matches
|
|
198
191
|
for param_name in getter_params.keys():
|
|
199
192
|
if param_name not in legacy_call_kwargs and param_name in mut_kwargs:
|
|
200
193
|
legacy_call_kwargs[param_name] = mut_kwargs[param_name]
|
|
201
194
|
|
|
202
|
-
legacy_call_kwargs = {
|
|
203
|
-
k: v for k, v in legacy_call_kwargs.items() if k in getter_params
|
|
204
|
-
}
|
|
195
|
+
legacy_call_kwargs = {k: v for k, v in legacy_call_kwargs.items() if k in getter_params}
|
|
205
196
|
return getter, legacy_call_kwargs
|
|
206
197
|
|
|
207
198
|
# Handle simple getter function
|
|
@@ -232,9 +223,7 @@ async def execute_recache(
|
|
|
232
223
|
try:
|
|
233
224
|
await _cache.delete(key_variant)
|
|
234
225
|
except Exception as e:
|
|
235
|
-
logger.debug(
|
|
236
|
-
f"Failed to delete cache key {key_variant}: {e}"
|
|
237
|
-
)
|
|
226
|
+
logger.debug(f"Failed to delete cache key {key_variant}: {e}")
|
|
238
227
|
|
|
239
228
|
# Execute the getter to warm the cache
|
|
240
229
|
await getter(**call_kwargs)
|
|
@@ -243,6 +232,4 @@ async def execute_recache(
|
|
|
243
232
|
logger.error(f"Recache operation failed: {e}")
|
|
244
233
|
|
|
245
234
|
# Execute all recache operations concurrently
|
|
246
|
-
await asyncio.gather(
|
|
247
|
-
*[_run_single_recache(spec) for spec in specs], return_exceptions=True
|
|
248
|
-
)
|
|
235
|
+
await asyncio.gather(*[_run_single_recache(spec) for spec in specs], return_exceptions=True)
|
svc_infra/cache/resources.py
CHANGED
|
@@ -8,7 +8,7 @@ with standardized key patterns and tag management.
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import inspect
|
|
10
10
|
import logging
|
|
11
|
-
from
|
|
11
|
+
from collections.abc import Callable
|
|
12
12
|
|
|
13
13
|
from cashews import cache as _cache
|
|
14
14
|
|
|
@@ -41,8 +41,8 @@ class Resource:
|
|
|
41
41
|
*,
|
|
42
42
|
suffix: str,
|
|
43
43
|
ttl: int,
|
|
44
|
-
key_template:
|
|
45
|
-
tags_template:
|
|
44
|
+
key_template: str | None = None,
|
|
45
|
+
tags_template: tuple[str, ...] | None = None,
|
|
46
46
|
lock: bool = True,
|
|
47
47
|
):
|
|
48
48
|
"""
|
|
@@ -63,9 +63,7 @@ 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)(
|
|
67
|
-
func
|
|
68
|
-
)
|
|
66
|
+
return _cache(ttl=ttl, key=key_template, tags=tags_template, lock=lock)(func)
|
|
69
67
|
except TypeError:
|
|
70
68
|
# Fallback for older cashews versions
|
|
71
69
|
return _cache(ttl=ttl, key=key_template, tags=tags_template)(func)
|
|
@@ -75,7 +73,7 @@ class Resource:
|
|
|
75
73
|
def cache_write(
|
|
76
74
|
self,
|
|
77
75
|
*,
|
|
78
|
-
recache:
|
|
76
|
+
recache: list[tuple[Callable, Callable]] | None = None,
|
|
79
77
|
recache_max_concurrency: int = 5,
|
|
80
78
|
):
|
|
81
79
|
"""
|
|
@@ -99,9 +97,7 @@ class Resource:
|
|
|
99
97
|
"""Delete all cache keys for a specific entity."""
|
|
100
98
|
namespace = _alias() or ""
|
|
101
99
|
namespace_prefix = (
|
|
102
|
-
f"{namespace}:"
|
|
103
|
-
if namespace and not namespace.endswith(":")
|
|
104
|
-
else namespace
|
|
100
|
+
f"{namespace}:" if namespace and not namespace.endswith(":") else namespace
|
|
105
101
|
)
|
|
106
102
|
|
|
107
103
|
# Generate candidate keys to delete
|
|
@@ -137,9 +133,7 @@ class Resource:
|
|
|
137
133
|
# Namespaced wildcard
|
|
138
134
|
if namespace_prefix:
|
|
139
135
|
await _maybe_await(
|
|
140
|
-
delete_match(
|
|
141
|
-
f"{namespace_prefix}{entity_name}:*:{entity_id}*"
|
|
142
|
-
)
|
|
136
|
+
delete_match(f"{namespace_prefix}{entity_name}:*:{entity_id}*")
|
|
143
137
|
)
|
|
144
138
|
# Non-namespaced wildcard
|
|
145
139
|
await _maybe_await(delete_match(f"{entity_name}:*:{entity_id}*"))
|
|
@@ -178,9 +172,7 @@ class Resource:
|
|
|
178
172
|
# Tag invalidation
|
|
179
173
|
invalidate_func = getattr(_cache, "invalidate", None)
|
|
180
174
|
if callable(invalidate_func):
|
|
181
|
-
await _maybe_await(
|
|
182
|
-
invalidate_func(f"{self.name}:{entity_id}")
|
|
183
|
-
)
|
|
175
|
+
await _maybe_await(invalidate_func(f"{self.name}:{entity_id}"))
|
|
184
176
|
|
|
185
177
|
# Precise key deletion
|
|
186
178
|
await _delete_entity_keys(self.name, str(entity_id))
|
svc_infra/cache/ttl.py
CHANGED
|
@@ -6,7 +6,6 @@ via environment variables with sensible defaults.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
|
-
from typing import Optional
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
def _get_env_int(key: str, default: int) -> int:
|
|
@@ -36,7 +35,7 @@ TTL_SHORT: int = _get_env_int("CACHE_TTL_SHORT", 30) # 30 seconds
|
|
|
36
35
|
TTL_LONG: int = _get_env_int("CACHE_TTL_LONG", 3600) # 1 hour
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
def get_ttl(duration_type: str) ->
|
|
38
|
+
def get_ttl(duration_type: str) -> int | None:
|
|
40
39
|
"""
|
|
41
40
|
Get TTL value by duration type name.
|
|
42
41
|
|
|
@@ -60,7 +59,7 @@ def get_ttl(duration_type: str) -> Optional[int]:
|
|
|
60
59
|
return ttl_map.get(duration_type.lower())
|
|
61
60
|
|
|
62
61
|
|
|
63
|
-
def validate_ttl(ttl:
|
|
62
|
+
def validate_ttl(ttl: int | None) -> int:
|
|
64
63
|
"""
|
|
65
64
|
Validate and normalize a TTL value.
|
|
66
65
|
|