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
|
@@ -1,13 +1,12 @@
|
|
|
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 fastapi import APIRouter
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
def apply_default_security(
|
|
9
|
-
router: APIRouter, *, default_security: list[dict] | None
|
|
10
|
-
) -> None:
|
|
9
|
+
def apply_default_security(router: APIRouter, *, default_security: list[dict] | None) -> None:
|
|
11
10
|
if default_security is None:
|
|
12
11
|
return
|
|
13
12
|
original_add = router.add_api_route
|
|
@@ -19,7 +18,7 @@ def apply_default_security(
|
|
|
19
18
|
kwargs["openapi_extra"] = ox
|
|
20
19
|
return original_add(path, endpoint, **kwargs)
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
router.add_api_route = _wrapped_add_api_route # type: ignore[method-assign]
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def apply_default_responses(router: APIRouter, defaults: dict[int, dict]) -> None:
|
|
@@ -40,4 +39,4 @@ def apply_default_responses(router: APIRouter, defaults: dict[int, dict]) -> Non
|
|
|
40
39
|
kwargs["responses"] = responses
|
|
41
40
|
return original_add(path, endpoint, **kwargs)
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
router.add_api_route = _wrapped_add_api_route # type: ignore[method-assign]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
from fastapi import FastAPI
|
|
6
6
|
|
|
7
7
|
from .mutators import conventions_mutator
|
|
8
8
|
from .pipeline import apply_mutators
|
|
9
9
|
|
|
10
|
-
PROBLEM_SCHEMA:
|
|
10
|
+
PROBLEM_SCHEMA: dict[str, Any] = {
|
|
11
11
|
"type": "object",
|
|
12
12
|
"properties": {
|
|
13
13
|
"type": {
|
|
@@ -49,7 +49,7 @@ PROBLEM_SCHEMA: Dict[str, Any] = {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def _problem_example(**kw: Any) ->
|
|
52
|
+
def _problem_example(**kw: Any) -> dict[str, Any]:
|
|
53
53
|
base = {
|
|
54
54
|
"type": "about:blank",
|
|
55
55
|
"title": "Internal Server Error",
|
|
@@ -63,7 +63,7 @@ def _problem_example(**kw: Any) -> Dict[str, Any]:
|
|
|
63
63
|
return base
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
STANDARD_RESPONSES:
|
|
66
|
+
STANDARD_RESPONSES: dict[str, dict[str, Any]] = {
|
|
67
67
|
"BadRequest": {
|
|
68
68
|
"description": "The request is malformed or missing required fields",
|
|
69
69
|
"content": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
5
5
|
|
|
6
6
|
from ..auth.security import auth_login_path
|
|
7
7
|
from .models import APIVersionSpec, ServiceInfo, VersionInfo
|
|
@@ -9,7 +9,7 @@ from .models import APIVersionSpec, ServiceInfo, VersionInfo
|
|
|
9
9
|
_HTTP_METHODS = ("get", "put", "post", "delete", "options", "head", "patch", "trace")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def _iter_ops(schema: dict) -> Iterator[
|
|
12
|
+
def _iter_ops(schema: dict) -> Iterator[tuple[str, str, dict]]:
|
|
13
13
|
"""Yield (path, method, op) for each operation object."""
|
|
14
14
|
paths = schema.get("paths") or {}
|
|
15
15
|
for path, methods in paths.items():
|
|
@@ -273,9 +273,7 @@ def normalize_problem_and_examples_mutator():
|
|
|
273
273
|
if not isinstance(val, dict):
|
|
274
274
|
return
|
|
275
275
|
inst = val.get("instance")
|
|
276
|
-
if isinstance(inst, str) and (
|
|
277
|
-
inst.startswith("/") or inst.startswith("about:")
|
|
278
|
-
):
|
|
276
|
+
if isinstance(inst, str) and (inst.startswith("/") or inst.startswith("about:")):
|
|
279
277
|
# make absolute to satisfy format: uri
|
|
280
278
|
val["instance"] = ABSOLUTE_INSTANCE
|
|
281
279
|
|
|
@@ -481,7 +479,7 @@ def ensure_global_tags_mutator(default_desc: str = "Operations related to {tag}.
|
|
|
481
479
|
|
|
482
480
|
# map existing tags by name and preserve their fields
|
|
483
481
|
existing_list = schema.get("tags") or []
|
|
484
|
-
existing_map:
|
|
482
|
+
existing_map: dict[str, dict] = {}
|
|
485
483
|
for item in existing_list:
|
|
486
484
|
if isinstance(item, dict) and "name" in item:
|
|
487
485
|
existing_map[item["name"]] = dict(item)
|
|
@@ -498,9 +496,7 @@ def ensure_global_tags_mutator(default_desc: str = "Operations related to {tag}.
|
|
|
498
496
|
existing_map[name]["description"] = default_desc.format(tag=name)
|
|
499
497
|
|
|
500
498
|
if existing_map:
|
|
501
|
-
schema["tags"] = sorted(
|
|
502
|
-
existing_map.values(), key=lambda x: x.get("name", "")
|
|
503
|
-
)
|
|
499
|
+
schema["tags"] = sorted(existing_map.values(), key=lambda x: x.get("name", ""))
|
|
504
500
|
|
|
505
501
|
return schema
|
|
506
502
|
|
|
@@ -508,8 +504,8 @@ def ensure_global_tags_mutator(default_desc: str = "Operations related to {tag}.
|
|
|
508
504
|
|
|
509
505
|
|
|
510
506
|
def attach_standard_responses_mutator(
|
|
511
|
-
codes:
|
|
512
|
-
per_method:
|
|
507
|
+
codes: dict[int, str] | None = None,
|
|
508
|
+
per_method: dict[str, Iterable[int]] | None = None,
|
|
513
509
|
exclude_tags: set[str] | None = None,
|
|
514
510
|
op_flag_disable: str = "x_disable_standard_responses",
|
|
515
511
|
):
|
|
@@ -657,9 +653,7 @@ def ensure_request_body_descriptions_mutator(
|
|
|
657
653
|
if isinstance(rb, dict):
|
|
658
654
|
desc = rb.get("description")
|
|
659
655
|
if not isinstance(desc, str) or not desc.strip():
|
|
660
|
-
rb["description"] = default_template.format(
|
|
661
|
-
method=method.upper(), path=path
|
|
662
|
-
)
|
|
656
|
+
rb["description"] = default_template.format(method=method.upper(), path=path)
|
|
663
657
|
return schema
|
|
664
658
|
|
|
665
659
|
return m
|
|
@@ -847,9 +841,7 @@ def inject_safe_examples_mutator():
|
|
|
847
841
|
"""
|
|
848
842
|
|
|
849
843
|
def _has_examples(mt_obj: dict) -> bool:
|
|
850
|
-
return isinstance(mt_obj, dict) and (
|
|
851
|
-
"example" in mt_obj or "examples" in mt_obj
|
|
852
|
-
)
|
|
844
|
+
return isinstance(mt_obj, dict) and ("example" in mt_obj or "examples" in mt_obj)
|
|
853
845
|
|
|
854
846
|
def m(schema: dict) -> dict:
|
|
855
847
|
schema = dict(schema)
|
|
@@ -1095,11 +1087,7 @@ def ensure_success_examples_mutator():
|
|
|
1095
1087
|
if not (200 <= ic < 300) or ic == 204:
|
|
1096
1088
|
continue
|
|
1097
1089
|
mt_obj = (resp.get("content") or {}).get("application/json")
|
|
1098
|
-
if (
|
|
1099
|
-
not isinstance(mt_obj, dict)
|
|
1100
|
-
or "example" in mt_obj
|
|
1101
|
-
or "examples" in mt_obj
|
|
1102
|
-
):
|
|
1090
|
+
if not isinstance(mt_obj, dict) or "example" in mt_obj or "examples" in mt_obj:
|
|
1103
1091
|
continue
|
|
1104
1092
|
sch = mt_obj.get("schema") or {}
|
|
1105
1093
|
|
|
@@ -1199,9 +1187,7 @@ def ensure_problem_examples_mutator():
|
|
|
1199
1187
|
if mt_obj is None:
|
|
1200
1188
|
# Create a basic media type referencing Problem schema when appropriate
|
|
1201
1189
|
if mt == "application/problem+json":
|
|
1202
|
-
mt_obj = {
|
|
1203
|
-
"schema": {"$ref": "#/components/schemas/Problem"}
|
|
1204
|
-
}
|
|
1190
|
+
mt_obj = {"schema": {"$ref": "#/components/schemas/Problem"}}
|
|
1205
1191
|
content[mt] = mt_obj
|
|
1206
1192
|
else:
|
|
1207
1193
|
continue
|
|
@@ -1495,12 +1481,8 @@ def attach_header_params_mutator():
|
|
|
1495
1481
|
if 200 <= ic < 300:
|
|
1496
1482
|
hdrs = resp.setdefault("headers", {})
|
|
1497
1483
|
hdrs.setdefault("ETag", {"$ref": "#/components/headers/ETag"})
|
|
1498
|
-
hdrs.setdefault(
|
|
1499
|
-
|
|
1500
|
-
)
|
|
1501
|
-
hdrs.setdefault(
|
|
1502
|
-
"X-Request-Id", {"$ref": "#/components/headers/XRequestId"}
|
|
1503
|
-
)
|
|
1484
|
+
hdrs.setdefault("Last-Modified", {"$ref": "#/components/headers/LastModified"})
|
|
1485
|
+
hdrs.setdefault("X-Request-Id", {"$ref": "#/components/headers/XRequestId"})
|
|
1504
1486
|
hdrs.setdefault(
|
|
1505
1487
|
"X-RateLimit-Limit",
|
|
1506
1488
|
{"$ref": "#/components/headers/XRateLimitLimit"},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
4
|
|
|
5
5
|
from fastapi import FastAPI
|
|
6
6
|
from fastapi.openapi.utils import get_openapi
|
|
@@ -23,4 +23,4 @@ def apply_mutators(app: FastAPI, *mutators):
|
|
|
23
23
|
app.openapi_schema = schema
|
|
24
24
|
return schema
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
app.openapi = patched # type: ignore[method-assign]
|
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Dict
|
|
4
|
-
|
|
5
3
|
|
|
6
4
|
def ref(name: str) -> dict:
|
|
7
5
|
return {"$ref": f"#/components/responses/{name}"}
|
|
8
6
|
|
|
9
7
|
|
|
10
|
-
DEFAULT_PUBLIC:
|
|
8
|
+
DEFAULT_PUBLIC: dict[int, dict] = {
|
|
11
9
|
400: ref("BadRequest"),
|
|
12
10
|
422: ref("ValidationError"),
|
|
13
11
|
500: ref("ServerError"),
|
|
14
12
|
}
|
|
15
|
-
DEFAULT_USER:
|
|
13
|
+
DEFAULT_USER: dict[int, dict] = {
|
|
16
14
|
400: ref("BadRequest"),
|
|
17
15
|
401: ref("Unauthorized"),
|
|
18
16
|
403: ref("Forbidden"),
|
|
19
17
|
422: ref("ValidationError"),
|
|
20
18
|
500: ref("ServerError"),
|
|
21
19
|
}
|
|
22
|
-
DEFAULT_SERVICE:
|
|
20
|
+
DEFAULT_SERVICE: dict[int, dict] = {
|
|
23
21
|
400: ref("BadRequest"),
|
|
24
22
|
401: ref("Unauthorized"),
|
|
25
23
|
403: ref("Forbidden"),
|
|
26
24
|
429: ref("TooManyRequests"),
|
|
27
25
|
500: ref("ServerError"),
|
|
28
26
|
}
|
|
29
|
-
DEFAULT_PROTECTED:
|
|
27
|
+
DEFAULT_PROTECTED: dict[int, dict] = {
|
|
30
28
|
400: ref("BadRequest"),
|
|
31
29
|
401: ref("Unauthorized"),
|
|
32
30
|
403: ref("Forbidden"),
|
|
@@ -6,9 +6,7 @@ from .mutators import auth_mutator
|
|
|
6
6
|
from .pipeline import apply_mutators
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def _normalize_security_list(
|
|
10
|
-
sec: list | None, *, drop_schemes: set[str] | None = None
|
|
11
|
-
) -> list:
|
|
9
|
+
def _normalize_security_list(sec: list | None, *, drop_schemes: set[str] | None = None) -> list:
|
|
12
10
|
if not sec:
|
|
13
11
|
return []
|
|
14
12
|
drop_schemes = drop_schemes or set()
|
svc_infra/api/fastapi/ops/add.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Callable
|
|
5
5
|
|
|
6
6
|
from fastapi import FastAPI, HTTPException, Request
|
|
7
7
|
from starlette.responses import JSONResponse
|
|
@@ -16,21 +16,19 @@ def add_probes(
|
|
|
16
16
|
"""Mount basic liveness/readiness/startup probes under prefix."""
|
|
17
17
|
from svc_infra.api.fastapi.dual.public import public_router
|
|
18
18
|
|
|
19
|
-
router = public_router(
|
|
20
|
-
prefix=prefix, tags=["ops"], include_in_schema=include_in_schema
|
|
21
|
-
)
|
|
19
|
+
router = public_router(prefix=prefix, tags=["ops"], include_in_schema=include_in_schema)
|
|
22
20
|
|
|
23
21
|
@router.get("/live")
|
|
24
|
-
async def live() -> JSONResponse:
|
|
22
|
+
async def live() -> JSONResponse:
|
|
25
23
|
return JSONResponse({"status": "ok"})
|
|
26
24
|
|
|
27
25
|
@router.get("/ready")
|
|
28
|
-
async def ready() -> JSONResponse:
|
|
26
|
+
async def ready() -> JSONResponse:
|
|
29
27
|
# In the future, add checks (DB ping, cache ping) via DI hooks.
|
|
30
28
|
return JSONResponse({"status": "ok"})
|
|
31
29
|
|
|
32
30
|
@router.get("/startup")
|
|
33
|
-
async def startup_probe() -> JSONResponse:
|
|
31
|
+
async def startup_probe() -> JSONResponse:
|
|
34
32
|
return JSONResponse({"status": "ok"})
|
|
35
33
|
|
|
36
34
|
app.include_router(router)
|
|
@@ -48,7 +46,7 @@ def add_maintenance_mode(
|
|
|
48
46
|
"""
|
|
49
47
|
|
|
50
48
|
@app.middleware("http")
|
|
51
|
-
async def _maintenance_gate(request: Request, call_next):
|
|
49
|
+
async def _maintenance_gate(request: Request, call_next):
|
|
52
50
|
flag = str(os.getenv(env_var, "")).lower() in {"1", "true", "yes", "on"}
|
|
53
51
|
if flag and request.method not in {"GET", "HEAD", "OPTIONS"}:
|
|
54
52
|
path = request.scope.get("path", "")
|
|
@@ -65,7 +63,7 @@ def circuit_breaker_dependency(limit: int = 100, window_seconds: int = 60) -> Ca
|
|
|
65
63
|
breaker. Here, we read an env var to simulate an open breaker.
|
|
66
64
|
"""
|
|
67
65
|
|
|
68
|
-
async def _dep(_: Request) -> None:
|
|
66
|
+
async def _dep(_: Request) -> None:
|
|
69
67
|
if str(os.getenv("CIRCUIT_OPEN", "")).lower() in {"1", "true", "yes", "on"}:
|
|
70
68
|
raise HTTPException(status_code=503, detail="circuit open")
|
|
71
69
|
|
|
@@ -4,14 +4,10 @@ import base64
|
|
|
4
4
|
import contextvars
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
7
8
|
from typing import (
|
|
8
9
|
Any,
|
|
9
|
-
Callable,
|
|
10
10
|
Generic,
|
|
11
|
-
Iterable,
|
|
12
|
-
List,
|
|
13
|
-
Optional,
|
|
14
|
-
Sequence,
|
|
15
11
|
TypeVar,
|
|
16
12
|
cast,
|
|
17
13
|
)
|
|
@@ -26,7 +22,7 @@ T = TypeVar("T")
|
|
|
26
22
|
|
|
27
23
|
# ---------- Core query models ----------
|
|
28
24
|
class CursorParams(BaseModel):
|
|
29
|
-
cursor:
|
|
25
|
+
cursor: str | None = None
|
|
30
26
|
limit: int = 50
|
|
31
27
|
|
|
32
28
|
|
|
@@ -36,19 +32,19 @@ class PageParams(BaseModel):
|
|
|
36
32
|
|
|
37
33
|
|
|
38
34
|
class FilterParams(BaseModel):
|
|
39
|
-
q:
|
|
40
|
-
sort:
|
|
41
|
-
created_after:
|
|
42
|
-
created_before:
|
|
43
|
-
updated_after:
|
|
44
|
-
updated_before:
|
|
35
|
+
q: str | None = None
|
|
36
|
+
sort: str | None = None
|
|
37
|
+
created_after: str | None = None
|
|
38
|
+
created_before: str | None = None
|
|
39
|
+
updated_after: str | None = None
|
|
40
|
+
updated_before: str | None = None
|
|
45
41
|
|
|
46
42
|
|
|
47
43
|
# ---------- Envelope model ----------
|
|
48
44
|
class Paginated(BaseModel, Generic[T]):
|
|
49
|
-
items:
|
|
50
|
-
next_cursor:
|
|
51
|
-
total:
|
|
45
|
+
items: list[T]
|
|
46
|
+
next_cursor: str | None = Field(None, description="Opaque cursor for next page")
|
|
47
|
+
total: int | None = Field(None, description="Total items (optional)")
|
|
52
48
|
|
|
53
49
|
|
|
54
50
|
# ---------- Cursor helpers ----------
|
|
@@ -57,13 +53,13 @@ def _encode_cursor(payload: dict) -> str:
|
|
|
57
53
|
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
|
58
54
|
|
|
59
55
|
|
|
60
|
-
def decode_cursor(token:
|
|
56
|
+
def decode_cursor(token: str | None) -> dict[Any, Any]:
|
|
61
57
|
"""Public: decode an incoming cursor token for debugging/ops."""
|
|
62
58
|
if not token:
|
|
63
59
|
return {}
|
|
64
60
|
s = token + "=" * (-len(token) % 4)
|
|
65
61
|
raw = base64.urlsafe_b64decode(s.encode("ascii")).decode("utf-8")
|
|
66
|
-
return cast(dict[Any, Any], json.loads(raw))
|
|
62
|
+
return cast("dict[Any, Any]", json.loads(raw))
|
|
67
63
|
|
|
68
64
|
|
|
69
65
|
# ---------- Context ----------
|
|
@@ -97,10 +93,8 @@ class PaginationContext(Generic[T]):
|
|
|
97
93
|
self.limit_override = limit_override
|
|
98
94
|
|
|
99
95
|
@property
|
|
100
|
-
def cursor(self) ->
|
|
101
|
-
return (
|
|
102
|
-
(self.cursor_params or CursorParams()).cursor if self.allow_cursor else None
|
|
103
|
-
)
|
|
96
|
+
def cursor(self) -> str | None:
|
|
97
|
+
return (self.cursor_params or CursorParams()).cursor if self.allow_cursor else None
|
|
104
98
|
|
|
105
99
|
@property
|
|
106
100
|
def limit(self) -> int:
|
|
@@ -113,16 +107,12 @@ class PaginationContext(Generic[T]):
|
|
|
113
107
|
return 50
|
|
114
108
|
|
|
115
109
|
@property
|
|
116
|
-
def page(self) ->
|
|
110
|
+
def page(self) -> int | None:
|
|
117
111
|
return self.page_params.page if (self.allow_page and self.page_params) else None
|
|
118
112
|
|
|
119
113
|
@property
|
|
120
|
-
def page_size(self) ->
|
|
121
|
-
return (
|
|
122
|
-
self.page_params.page_size
|
|
123
|
-
if (self.allow_page and self.page_params)
|
|
124
|
-
else None
|
|
125
|
-
)
|
|
114
|
+
def page_size(self) -> int | None:
|
|
115
|
+
return self.page_params.page_size if (self.allow_page and self.page_params) else None
|
|
126
116
|
|
|
127
117
|
@property
|
|
128
118
|
def offset(self) -> int:
|
|
@@ -134,8 +124,8 @@ class PaginationContext(Generic[T]):
|
|
|
134
124
|
self,
|
|
135
125
|
items: list[T],
|
|
136
126
|
*,
|
|
137
|
-
next_cursor:
|
|
138
|
-
total:
|
|
127
|
+
next_cursor: str | None = None,
|
|
128
|
+
total: int | None = None,
|
|
139
129
|
):
|
|
140
130
|
if self.envelope:
|
|
141
131
|
return Paginated[T](items=items, next_cursor=next_cursor, total=total)
|
|
@@ -143,15 +133,15 @@ class PaginationContext(Generic[T]):
|
|
|
143
133
|
|
|
144
134
|
def next_cursor_from_last(
|
|
145
135
|
self, items: Sequence[T], *, key: Callable[[T], str | int]
|
|
146
|
-
) ->
|
|
136
|
+
) -> str | None:
|
|
147
137
|
if not items:
|
|
148
138
|
return None
|
|
149
139
|
last_key = key(items[-1])
|
|
150
140
|
return _encode_cursor({"after": last_key})
|
|
151
141
|
|
|
152
142
|
|
|
153
|
-
_pagination_ctx: contextvars.ContextVar[PaginationContext | None] = (
|
|
154
|
-
|
|
143
|
+
_pagination_ctx: contextvars.ContextVar[PaginationContext | None] = contextvars.ContextVar(
|
|
144
|
+
"pagination_ctx", default=None
|
|
155
145
|
)
|
|
156
146
|
|
|
157
147
|
|
|
@@ -171,9 +161,7 @@ def use_pagination() -> PaginationContext:
|
|
|
171
161
|
|
|
172
162
|
|
|
173
163
|
# ---------- Utilities ----------
|
|
174
|
-
def text_filter(
|
|
175
|
-
items: Iterable[T], q: Optional[str], *getters: Callable[[T], str]
|
|
176
|
-
) -> list[T]:
|
|
164
|
+
def text_filter(items: Iterable[T], q: str | None, *getters: Callable[[T], str]) -> list[T]:
|
|
177
165
|
if not q:
|
|
178
166
|
return list(items)
|
|
179
167
|
ql = q.lower()
|
|
@@ -195,7 +183,7 @@ def sort_by(
|
|
|
195
183
|
key: Callable[[T], Any],
|
|
196
184
|
desc: bool = False,
|
|
197
185
|
) -> list[T]:
|
|
198
|
-
return sorted(
|
|
186
|
+
return sorted(items, key=key, reverse=desc)
|
|
199
187
|
|
|
200
188
|
|
|
201
189
|
def cursor_window(items, *, cursor, limit, key, descending: bool, offset: int = 0):
|
|
@@ -4,7 +4,7 @@ import importlib
|
|
|
4
4
|
import logging
|
|
5
5
|
import pkgutil
|
|
6
6
|
from types import ModuleType
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
from fastapi import FastAPI
|
|
10
10
|
from fastapi.routing import APIRoute
|
|
@@ -55,9 +55,7 @@ def _validate_base_package(base_package: str) -> ModuleType:
|
|
|
55
55
|
try:
|
|
56
56
|
package_module: ModuleType = importlib.import_module(base_package)
|
|
57
57
|
except Exception as exc:
|
|
58
|
-
raise RuntimeError(
|
|
59
|
-
f"Could not import base_package '{base_package}': {exc}"
|
|
60
|
-
) from exc
|
|
58
|
+
raise RuntimeError(f"Could not import base_package '{base_package}': {exc}") from exc
|
|
61
59
|
|
|
62
60
|
if not hasattr(package_module, "__path__"):
|
|
63
61
|
raise RuntimeError(
|
|
@@ -67,21 +65,17 @@ def _validate_base_package(base_package: str) -> ModuleType:
|
|
|
67
65
|
return package_module
|
|
68
66
|
|
|
69
67
|
|
|
70
|
-
def _normalize_environment(environment:
|
|
68
|
+
def _normalize_environment(environment: Environment | str | None) -> Environment:
|
|
71
69
|
"""Normalize the environment parameter."""
|
|
72
70
|
return (
|
|
73
71
|
CURRENT_ENVIRONMENT
|
|
74
72
|
if environment is None
|
|
75
|
-
else (
|
|
76
|
-
Environment(environment)
|
|
77
|
-
if not isinstance(environment, Environment)
|
|
78
|
-
else environment
|
|
79
|
-
)
|
|
73
|
+
else (Environment(environment) if not isinstance(environment, Environment) else environment)
|
|
80
74
|
)
|
|
81
75
|
|
|
82
76
|
|
|
83
77
|
def _should_force_include_in_schema(
|
|
84
|
-
environment: Environment, force_include_in_schema:
|
|
78
|
+
environment: Environment, force_include_in_schema: bool | None
|
|
85
79
|
) -> bool:
|
|
86
80
|
"""Determine if routers should be forced to include in schema."""
|
|
87
81
|
if force_include_in_schema is None:
|
|
@@ -99,12 +93,9 @@ def _is_router_excluded_by_environment(
|
|
|
99
93
|
|
|
100
94
|
# Support ALL_ENVIRONMENTS as a special value
|
|
101
95
|
if router_excluded_envs is ALL_ENVIRONMENTS or (
|
|
102
|
-
isinstance(router_excluded_envs, set)
|
|
103
|
-
and router_excluded_envs == ALL_ENVIRONMENTS
|
|
96
|
+
isinstance(router_excluded_envs, set) and router_excluded_envs == ALL_ENVIRONMENTS
|
|
104
97
|
):
|
|
105
|
-
logger.debug(
|
|
106
|
-
f"Skipping router module {module_name} due to ALL_ENVIRONMENTS exclusion."
|
|
107
|
-
)
|
|
98
|
+
logger.debug(f"Skipping router module {module_name} due to ALL_ENVIRONMENTS exclusion.")
|
|
108
99
|
return True
|
|
109
100
|
|
|
110
101
|
# Normalize to set of Environment or str
|
|
@@ -117,16 +108,11 @@ def _is_router_excluded_by_environment(
|
|
|
117
108
|
normalized_excluded_envs: set[Environment | str] = set()
|
|
118
109
|
for e in router_excluded_envs:
|
|
119
110
|
try:
|
|
120
|
-
normalized_excluded_envs.add(
|
|
121
|
-
Environment(e) if not isinstance(e, Environment) else e
|
|
122
|
-
)
|
|
111
|
+
normalized_excluded_envs.add(Environment(e) if not isinstance(e, Environment) else e)
|
|
123
112
|
except Exception:
|
|
124
113
|
normalized_excluded_envs.add(str(e))
|
|
125
114
|
|
|
126
|
-
if (
|
|
127
|
-
environment in normalized_excluded_envs
|
|
128
|
-
or str(environment) in normalized_excluded_envs
|
|
129
|
-
):
|
|
115
|
+
if environment in normalized_excluded_envs or str(environment) in normalized_excluded_envs:
|
|
130
116
|
logger.debug(
|
|
131
117
|
f"Skipping router module {module_name} due to ROUTER_EXCLUDED_ENVIRONMENTS restriction: {router_excluded_envs}"
|
|
132
118
|
)
|
|
@@ -225,10 +211,10 @@ def _process_router_module(
|
|
|
225
211
|
def register_all_routers(
|
|
226
212
|
app: FastAPI,
|
|
227
213
|
*,
|
|
228
|
-
base_package:
|
|
214
|
+
base_package: str | None = None,
|
|
229
215
|
prefix: str = "",
|
|
230
|
-
environment:
|
|
231
|
-
force_include_in_schema:
|
|
216
|
+
environment: Environment | str | None = None,
|
|
217
|
+
force_include_in_schema: bool | None = None,
|
|
232
218
|
) -> None:
|
|
233
219
|
"""
|
|
234
220
|
Recursively discover and register all FastAPI routers under a routers package.
|
|
@@ -250,24 +236,18 @@ def register_all_routers(
|
|
|
250
236
|
"""
|
|
251
237
|
if base_package is None:
|
|
252
238
|
if __package__ is None:
|
|
253
|
-
raise RuntimeError(
|
|
254
|
-
"Cannot derive base_package; please pass base_package explicitly."
|
|
255
|
-
)
|
|
239
|
+
raise RuntimeError("Cannot derive base_package; please pass base_package explicitly.")
|
|
256
240
|
base_package = __package__
|
|
257
241
|
|
|
258
242
|
package_module = _validate_base_package(base_package)
|
|
259
243
|
environment = _normalize_environment(environment)
|
|
260
|
-
force_include = _should_force_include_in_schema(
|
|
261
|
-
environment, force_include_in_schema
|
|
262
|
-
)
|
|
244
|
+
force_include = _should_force_include_in_schema(environment, force_include_in_schema)
|
|
263
245
|
|
|
264
246
|
for _, module_name, _ in pkgutil.walk_packages(
|
|
265
247
|
package_module.__path__, prefix=f"{base_package}."
|
|
266
248
|
):
|
|
267
249
|
if _should_skip_module(module_name):
|
|
268
|
-
logger.debug(
|
|
269
|
-
"Skipping router module due to exclusion/private: %s", module_name
|
|
270
|
-
)
|
|
250
|
+
logger.debug("Skipping router module due to exclusion/private: %s", module_name)
|
|
271
251
|
continue
|
|
272
252
|
|
|
273
253
|
try:
|
|
@@ -276,6 +256,4 @@ def register_all_routers(
|
|
|
276
256
|
logger.exception("Failed to import router module %s: %s", module_name, exc)
|
|
277
257
|
continue
|
|
278
258
|
|
|
279
|
-
_process_router_module(
|
|
280
|
-
app, module, module_name, prefix, environment, force_include
|
|
281
|
-
)
|
|
259
|
+
_process_router_module(app, module, module_name, prefix, environment, force_include)
|