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,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Iterable, List, Optional
|
|
5
5
|
|
|
6
6
|
FAVICON_DATA_URI = (
|
|
7
7
|
"data:image/svg+xml,"
|
|
@@ -14,9 +14,9 @@ FAVICON_DATA_URI = (
|
|
|
14
14
|
|
|
15
15
|
@dataclass(frozen=True)
|
|
16
16
|
class DocTargets:
|
|
17
|
-
swagger:
|
|
18
|
-
redoc:
|
|
19
|
-
openapi_json:
|
|
17
|
+
swagger: str | None = None
|
|
18
|
+
redoc: str | None = None
|
|
19
|
+
openapi_json: str | None = None
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@dataclass(frozen=True)
|
|
@@ -31,7 +31,7 @@ def _btn(label: str, href: str) -> str:
|
|
|
31
31
|
|
|
32
32
|
def _card(spec: CardSpec) -> str:
|
|
33
33
|
tag = "/" if spec.tag.strip("/") == "" else f"/{spec.tag.strip('/')}"
|
|
34
|
-
links:
|
|
34
|
+
links: list[str] = []
|
|
35
35
|
if spec.docs.swagger:
|
|
36
36
|
links.append(_btn("Swagger", spec.docs.swagger))
|
|
37
37
|
if spec.docs.redoc:
|
|
@@ -50,9 +50,7 @@ def _card(spec: CardSpec) -> str:
|
|
|
50
50
|
""".strip()
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def render_index_html(
|
|
54
|
-
*, service_name: str, release: str, cards: Iterable[CardSpec]
|
|
55
|
-
) -> str:
|
|
53
|
+
def render_index_html(*, service_name: str, release: str, cards: Iterable[CardSpec]) -> str:
|
|
56
54
|
grid = "\n".join(_card(c) for c in cards)
|
|
57
55
|
return f"""
|
|
58
56
|
<!doctype html>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Iterable
|
|
5
5
|
|
|
6
6
|
from fastapi import FastAPI
|
|
7
7
|
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
|
@@ -10,15 +10,15 @@ from fastapi.responses import HTMLResponse
|
|
|
10
10
|
from svc_infra.app.env import CURRENT_ENVIRONMENT, DEV_ENV, LOCAL_ENV, Environment
|
|
11
11
|
|
|
12
12
|
# (prefix, swagger_path, redoc_path, openapi_path, title)
|
|
13
|
-
DOC_SCOPES:
|
|
13
|
+
DOC_SCOPES: list[tuple[str, str, str, str, str]] = []
|
|
14
14
|
|
|
15
15
|
_HTTP_METHODS = {"get", "put", "post", "delete", "patch", "options", "head", "trace"}
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def _path_included(
|
|
19
19
|
path: str,
|
|
20
|
-
include_prefixes:
|
|
21
|
-
exclude_prefixes:
|
|
20
|
+
include_prefixes: Iterable[str] | None = None,
|
|
21
|
+
exclude_prefixes: Iterable[str] | None = None,
|
|
22
22
|
) -> bool:
|
|
23
23
|
def _match(pfx: str) -> bool:
|
|
24
24
|
pfx = pfx.rstrip("/") or "/"
|
|
@@ -31,7 +31,7 @@ def _path_included(
|
|
|
31
31
|
return True
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def _collect_refs(obj, refset:
|
|
34
|
+
def _collect_refs(obj, refset: set[tuple[str, str]]):
|
|
35
35
|
if isinstance(obj, dict):
|
|
36
36
|
for k, v in obj.items():
|
|
37
37
|
if k == "$ref" and isinstance(v, str) and v.startswith("#/components/"):
|
|
@@ -46,8 +46,8 @@ def _collect_refs(obj, refset: Set[Tuple[str, str]]):
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def _close_over_component_refs(
|
|
49
|
-
full_components:
|
|
50
|
-
) ->
|
|
49
|
+
full_components: dict, initial: set[tuple[str, str]]
|
|
50
|
+
) -> set[tuple[str, str]]:
|
|
51
51
|
to_visit = list(initial)
|
|
52
52
|
seen = set(initial)
|
|
53
53
|
while to_visit:
|
|
@@ -55,7 +55,7 @@ def _close_over_component_refs(
|
|
|
55
55
|
comp = (full_components or {}).get(section, {}).get(name)
|
|
56
56
|
if not isinstance(comp, dict):
|
|
57
57
|
continue
|
|
58
|
-
nested:
|
|
58
|
+
nested: set[tuple[str, str]] = set()
|
|
59
59
|
_collect_refs(comp, nested)
|
|
60
60
|
for ref in nested:
|
|
61
61
|
if ref not in seen:
|
|
@@ -65,11 +65,11 @@ def _close_over_component_refs(
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def _prune_to_paths(
|
|
68
|
-
full_schema:
|
|
69
|
-
keep_paths:
|
|
70
|
-
title_suffix:
|
|
71
|
-
server_prefix:
|
|
72
|
-
) ->
|
|
68
|
+
full_schema: dict,
|
|
69
|
+
keep_paths: dict[str, dict],
|
|
70
|
+
title_suffix: str | None,
|
|
71
|
+
server_prefix: str | None = None,
|
|
72
|
+
) -> dict:
|
|
73
73
|
schema = copy.deepcopy(full_schema)
|
|
74
74
|
schema["paths"] = keep_paths
|
|
75
75
|
|
|
@@ -77,9 +77,9 @@ def _prune_to_paths(
|
|
|
77
77
|
if server_prefix is not None:
|
|
78
78
|
schema["servers"] = [{"url": server_prefix}]
|
|
79
79
|
|
|
80
|
-
used_tags:
|
|
81
|
-
direct_refs:
|
|
82
|
-
used_security_schemes:
|
|
80
|
+
used_tags: set[str] = set()
|
|
81
|
+
direct_refs: set[tuple[str, str]] = set()
|
|
82
|
+
used_security_schemes: set[str] = set()
|
|
83
83
|
|
|
84
84
|
for path_item in keep_paths.values():
|
|
85
85
|
for method, op in path_item.items():
|
|
@@ -95,7 +95,7 @@ def _prune_to_paths(
|
|
|
95
95
|
comps = schema.get("components") or {}
|
|
96
96
|
all_refs = _close_over_component_refs(comps, direct_refs)
|
|
97
97
|
|
|
98
|
-
pruned_components:
|
|
98
|
+
pruned_components: dict[str, dict] = {}
|
|
99
99
|
if isinstance(comps, dict):
|
|
100
100
|
for section, items in comps.items():
|
|
101
101
|
keep_names = {name for (sec, name) in all_refs if sec == section}
|
|
@@ -110,9 +110,7 @@ def _prune_to_paths(
|
|
|
110
110
|
|
|
111
111
|
if "tags" in schema and isinstance(schema["tags"], list):
|
|
112
112
|
schema["tags"] = [
|
|
113
|
-
t
|
|
114
|
-
for t in schema["tags"]
|
|
115
|
-
if isinstance(t, dict) and t.get("name") in used_tags
|
|
113
|
+
t for t in schema["tags"] if isinstance(t, dict) and t.get("name") in used_tags
|
|
116
114
|
]
|
|
117
115
|
|
|
118
116
|
info = dict(schema.get("info") or {})
|
|
@@ -123,17 +121,15 @@ def _prune_to_paths(
|
|
|
123
121
|
|
|
124
122
|
|
|
125
123
|
def _build_filtered_schema(
|
|
126
|
-
full_schema:
|
|
124
|
+
full_schema: dict,
|
|
127
125
|
*,
|
|
128
|
-
include_prefixes:
|
|
129
|
-
exclude_prefixes:
|
|
130
|
-
title_suffix:
|
|
131
|
-
) ->
|
|
126
|
+
include_prefixes: list[str] | None = None,
|
|
127
|
+
exclude_prefixes: list[str] | None = None,
|
|
128
|
+
title_suffix: str | None = None,
|
|
129
|
+
) -> dict:
|
|
132
130
|
paths = full_schema.get("paths", {}) or {}
|
|
133
131
|
keep_paths = {
|
|
134
|
-
p: v
|
|
135
|
-
for p, v in paths.items()
|
|
136
|
-
if _path_included(p, include_prefixes, exclude_prefixes)
|
|
132
|
+
p: v for p, v in paths.items() if _path_included(p, include_prefixes, exclude_prefixes)
|
|
137
133
|
}
|
|
138
134
|
|
|
139
135
|
# Determine the server prefix for scoped docs
|
|
@@ -154,9 +150,7 @@ def _build_filtered_schema(
|
|
|
154
150
|
stripped_paths[path] = spec
|
|
155
151
|
keep_paths = stripped_paths
|
|
156
152
|
|
|
157
|
-
return _prune_to_paths(
|
|
158
|
-
full_schema, keep_paths, title_suffix, server_prefix=server_prefix
|
|
159
|
-
)
|
|
153
|
+
return _prune_to_paths(full_schema, keep_paths, title_suffix, server_prefix=server_prefix)
|
|
160
154
|
|
|
161
155
|
|
|
162
156
|
def _ensure_original_openapi_saved(app: FastAPI) -> None:
|
|
@@ -164,12 +158,12 @@ def _ensure_original_openapi_saved(app: FastAPI) -> None:
|
|
|
164
158
|
app.state._scoped_original_openapi = app.openapi
|
|
165
159
|
|
|
166
160
|
|
|
167
|
-
def _get_full_schema_from_original(app: FastAPI) ->
|
|
161
|
+
def _get_full_schema_from_original(app: FastAPI) -> dict:
|
|
168
162
|
_ensure_original_openapi_saved(app)
|
|
169
163
|
return copy.deepcopy(app.state._scoped_original_openapi())
|
|
170
164
|
|
|
171
165
|
|
|
172
|
-
def _install_root_filter(app: FastAPI, exclude_prefixes:
|
|
166
|
+
def _install_root_filter(app: FastAPI, exclude_prefixes: list[str]) -> None:
|
|
173
167
|
_ensure_original_openapi_saved(app)
|
|
174
168
|
app.state._scoped_root_exclusions = sorted(set(exclude_prefixes))
|
|
175
169
|
|
|
@@ -179,10 +173,10 @@ def _install_root_filter(app: FastAPI, exclude_prefixes: List[str]) -> None:
|
|
|
179
173
|
full_schema, exclude_prefixes=app.state._scoped_root_exclusions
|
|
180
174
|
)
|
|
181
175
|
|
|
182
|
-
|
|
176
|
+
app.openapi = root_filtered_openapi # type: ignore[method-assign]
|
|
183
177
|
|
|
184
178
|
|
|
185
|
-
def _current_registered_scopes() ->
|
|
179
|
+
def _current_registered_scopes() -> list[str]:
|
|
186
180
|
return [scope for (scope, *_rest) in DOC_SCOPES]
|
|
187
181
|
|
|
188
182
|
|
|
@@ -193,8 +187,8 @@ def _ensure_root_excludes_registered_scopes(app: FastAPI) -> None:
|
|
|
193
187
|
|
|
194
188
|
|
|
195
189
|
def _normalize_envs(
|
|
196
|
-
envs:
|
|
197
|
-
) ->
|
|
190
|
+
envs: Iterable[Environment | str] | None,
|
|
191
|
+
) -> set[Environment] | None:
|
|
198
192
|
if envs is None:
|
|
199
193
|
return None
|
|
200
194
|
out: set[Environment] = set()
|
|
@@ -209,7 +203,7 @@ def add_prefixed_docs(
|
|
|
209
203
|
prefix: str,
|
|
210
204
|
title: str,
|
|
211
205
|
auto_exclude_from_root: bool = True,
|
|
212
|
-
visible_envs:
|
|
206
|
+
visible_envs: Iterable[Environment | str] | None = (LOCAL_ENV, DEV_ENV),
|
|
213
207
|
) -> None:
|
|
214
208
|
scope = prefix.rstrip("/") or "/"
|
|
215
209
|
|
|
@@ -233,7 +227,7 @@ def add_prefixed_docs(
|
|
|
233
227
|
redoc_path = f"{scope}/redoc"
|
|
234
228
|
|
|
235
229
|
_ensure_original_openapi_saved(app)
|
|
236
|
-
_scope_cache:
|
|
230
|
+
_scope_cache: dict | None = None
|
|
237
231
|
|
|
238
232
|
def _scoped_schema():
|
|
239
233
|
nonlocal _scope_cache
|
|
@@ -260,7 +254,5 @@ def add_prefixed_docs(
|
|
|
260
254
|
DOC_SCOPES.append((scope, swagger_path, redoc_path, openapi_path, title))
|
|
261
255
|
|
|
262
256
|
|
|
263
|
-
def replace_root_openapi_with_exclusions(
|
|
264
|
-
app: FastAPI, *, exclude_prefixes: List[str]
|
|
265
|
-
) -> None:
|
|
257
|
+
def replace_root_openapi_with_exclusions(app: FastAPI, *, exclude_prefixes: list[str]) -> None:
|
|
266
258
|
_install_root_filter(app, exclude_prefixes)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from ..auth.security import (
|
|
6
7
|
AllowIdentity,
|
|
@@ -21,7 +22,7 @@ from ..openapi.responses import (
|
|
|
21
22
|
from .router import DualAPIRouter
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
def _merge(base:
|
|
25
|
+
def _merge(base: Sequence[Any] | None, extra: Sequence[Any] | None) -> list[Any]:
|
|
25
26
|
out: list[Any] = []
|
|
26
27
|
if base:
|
|
27
28
|
out.extend(base)
|
|
@@ -32,7 +33,7 @@ def _merge(base: Optional[Sequence[Any]], extra: Optional[Sequence[Any]]) -> lis
|
|
|
32
33
|
|
|
33
34
|
# PUBLIC (but attach OptionalIdentity for convenience)
|
|
34
35
|
def optional_identity_router(
|
|
35
|
-
*, dependencies:
|
|
36
|
+
*, dependencies: Sequence[Any] | None = None, **kwargs: Any
|
|
36
37
|
) -> DualAPIRouter:
|
|
37
38
|
r = DualAPIRouter(dependencies=_merge([AllowIdentity], dependencies), **kwargs)
|
|
38
39
|
apply_default_security(r, default_security=[]) # public looking in docs
|
|
@@ -41,9 +42,7 @@ def optional_identity_router(
|
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
# PROTECTED: any auth (JWT/cookie OR API key)
|
|
44
|
-
def protected_router(
|
|
45
|
-
*, dependencies: Optional[Sequence[Any]] = None, **kwargs: Any
|
|
46
|
-
) -> DualAPIRouter:
|
|
45
|
+
def protected_router(*, dependencies: Sequence[Any] | None = None, **kwargs: Any) -> DualAPIRouter:
|
|
47
46
|
r = DualAPIRouter(dependencies=_merge([RequireIdentity], dependencies), **kwargs)
|
|
48
47
|
apply_default_security(
|
|
49
48
|
r,
|
|
@@ -58,9 +57,7 @@ def protected_router(
|
|
|
58
57
|
|
|
59
58
|
|
|
60
59
|
# USER-ONLY (no API-key-only access)
|
|
61
|
-
def user_router(
|
|
62
|
-
*, dependencies: Optional[Sequence[Any]] = None, **kwargs: Any
|
|
63
|
-
) -> DualAPIRouter:
|
|
60
|
+
def user_router(*, dependencies: Sequence[Any] | None = None, **kwargs: Any) -> DualAPIRouter:
|
|
64
61
|
r = DualAPIRouter(dependencies=_merge([RequireUser()], dependencies), **kwargs)
|
|
65
62
|
apply_default_security(
|
|
66
63
|
r, default_security=[{"OAuth2PasswordBearer": []}, {"SessionCookie": []}]
|
|
@@ -70,9 +67,7 @@ def user_router(
|
|
|
70
67
|
|
|
71
68
|
|
|
72
69
|
# SERVICE-ONLY (API key required)
|
|
73
|
-
def service_router(
|
|
74
|
-
*, dependencies: Optional[Sequence[Any]] = None, **kwargs: Any
|
|
75
|
-
) -> DualAPIRouter:
|
|
70
|
+
def service_router(*, dependencies: Sequence[Any] | None = None, **kwargs: Any) -> DualAPIRouter:
|
|
76
71
|
r = DualAPIRouter(dependencies=_merge([RequireService()], dependencies), **kwargs)
|
|
77
72
|
apply_default_security(r, default_security=[{"APIKeyHeader": []}])
|
|
78
73
|
apply_default_responses(r, DEFAULT_SERVICE)
|
|
@@ -111,7 +106,7 @@ def roles_router(*roles: str, role_resolver=None, **kwargs):
|
|
|
111
106
|
|
|
112
107
|
|
|
113
108
|
def ws_protected_router(
|
|
114
|
-
*, dependencies:
|
|
109
|
+
*, dependencies: Sequence[Any] | None = None, **kwargs: Any
|
|
115
110
|
) -> DualAPIRouter:
|
|
116
111
|
"""
|
|
117
112
|
Protected WebSocket router - requires valid JWT token.
|
|
@@ -141,7 +136,7 @@ def ws_protected_router(
|
|
|
141
136
|
|
|
142
137
|
|
|
143
138
|
def ws_optional_router(
|
|
144
|
-
*, dependencies:
|
|
139
|
+
*, dependencies: Sequence[Any] | None = None, **kwargs: Any
|
|
145
140
|
) -> DualAPIRouter:
|
|
146
141
|
"""
|
|
147
142
|
Optional auth WebSocket router - allows anonymous connections.
|
|
@@ -163,9 +158,7 @@ def ws_optional_router(
|
|
|
163
158
|
return r
|
|
164
159
|
|
|
165
160
|
|
|
166
|
-
def ws_user_router(
|
|
167
|
-
*, dependencies: Optional[Sequence[Any]] = None, **kwargs: Any
|
|
168
|
-
) -> DualAPIRouter:
|
|
161
|
+
def ws_user_router(*, dependencies: Sequence[Any] | None = None, **kwargs: Any) -> DualAPIRouter:
|
|
169
162
|
"""
|
|
170
163
|
User-only WebSocket router - requires valid user JWT (no API key).
|
|
171
164
|
|
|
@@ -189,7 +182,7 @@ def ws_user_router(
|
|
|
189
182
|
|
|
190
183
|
|
|
191
184
|
def ws_scopes_router(
|
|
192
|
-
*scopes: str, dependencies:
|
|
185
|
+
*scopes: str, dependencies: Sequence[Any] | None = None, **kwargs: Any
|
|
193
186
|
) -> DualAPIRouter:
|
|
194
187
|
"""
|
|
195
188
|
Scope-gated WebSocket router - requires valid JWT with specific scopes.
|
|
@@ -207,9 +200,7 @@ def ws_scopes_router(
|
|
|
207
200
|
...
|
|
208
201
|
"""
|
|
209
202
|
r = DualAPIRouter(
|
|
210
|
-
dependencies=_merge(
|
|
211
|
-
[RequireWSIdentity, RequireWSScopes(*scopes)], dependencies
|
|
212
|
-
),
|
|
203
|
+
dependencies=_merge([RequireWSIdentity, RequireWSScopes(*scopes)], dependencies),
|
|
213
204
|
**kwargs,
|
|
214
205
|
)
|
|
215
206
|
apply_default_security(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from fastapi import APIRouter
|
|
6
7
|
from fastapi.params import Depends
|
|
@@ -44,7 +45,7 @@ class DualAPIRouter(APIRouter):
|
|
|
44
45
|
**kwargs,
|
|
45
46
|
)
|
|
46
47
|
# only add the "/" twin for *safe* methods
|
|
47
|
-
if
|
|
48
|
+
if {m.upper() for m in methods} <= safe_methods:
|
|
48
49
|
self.add_api_route(
|
|
49
50
|
"/", func, methods=methods, include_in_schema=False, **kwargs
|
|
50
51
|
)
|
|
@@ -59,15 +60,13 @@ class DualAPIRouter(APIRouter):
|
|
|
59
60
|
**kwargs,
|
|
60
61
|
)
|
|
61
62
|
if alt != primary:
|
|
62
|
-
self.add_api_route(
|
|
63
|
-
alt, func, methods=methods, include_in_schema=False, **kwargs
|
|
64
|
-
)
|
|
63
|
+
self.add_api_route(alt, func, methods=methods, include_in_schema=False, **kwargs)
|
|
65
64
|
return func
|
|
66
65
|
|
|
67
66
|
return decorator
|
|
68
67
|
|
|
69
68
|
def add_api_route(self, path, endpoint, **kwargs):
|
|
70
|
-
methods = set(
|
|
69
|
+
methods = set(kwargs.get("methods") or [])
|
|
71
70
|
for r in self.routes:
|
|
72
71
|
if getattr(r, "path", None) == path and methods & (
|
|
73
72
|
getattr(r, "methods", set()) or set()
|
|
@@ -78,45 +77,31 @@ class DualAPIRouter(APIRouter):
|
|
|
78
77
|
# ---------- HTTP method shorthands ----------
|
|
79
78
|
|
|
80
79
|
def get(self, path: str, *_, show_in_schema: bool = True, **kwargs: Any):
|
|
81
|
-
return self._dual_decorator(
|
|
82
|
-
path, ["GET"], show_in_schema=show_in_schema, **kwargs
|
|
83
|
-
)
|
|
80
|
+
return self._dual_decorator(path, ["GET"], show_in_schema=show_in_schema, **kwargs)
|
|
84
81
|
|
|
85
82
|
def post(self, path: str, *_, show_in_schema: bool = True, **kwargs: Any):
|
|
86
|
-
return self._dual_decorator(
|
|
87
|
-
path, ["POST"], show_in_schema=show_in_schema, **kwargs
|
|
88
|
-
)
|
|
83
|
+
return self._dual_decorator(path, ["POST"], show_in_schema=show_in_schema, **kwargs)
|
|
89
84
|
|
|
90
85
|
def patch(self, path: str, *_, show_in_schema: bool = True, **kwargs: Any):
|
|
91
|
-
return self._dual_decorator(
|
|
92
|
-
path, ["PATCH"], show_in_schema=show_in_schema, **kwargs
|
|
93
|
-
)
|
|
86
|
+
return self._dual_decorator(path, ["PATCH"], show_in_schema=show_in_schema, **kwargs)
|
|
94
87
|
|
|
95
88
|
def delete(self, path: str, *_, show_in_schema: bool = True, **kwargs: Any):
|
|
96
|
-
return self._dual_decorator(
|
|
97
|
-
path, ["DELETE"], show_in_schema=show_in_schema, **kwargs
|
|
98
|
-
)
|
|
89
|
+
return self._dual_decorator(path, ["DELETE"], show_in_schema=show_in_schema, **kwargs)
|
|
99
90
|
|
|
100
91
|
def put(self, path: str, *_, show_in_schema: bool = True, **kwargs: Any):
|
|
101
|
-
return self._dual_decorator(
|
|
102
|
-
path, ["PUT"], show_in_schema=show_in_schema, **kwargs
|
|
103
|
-
)
|
|
92
|
+
return self._dual_decorator(path, ["PUT"], show_in_schema=show_in_schema, **kwargs)
|
|
104
93
|
|
|
105
94
|
def options(self, path: str, *_, show_in_schema: bool = True, **kwargs: Any):
|
|
106
|
-
return self._dual_decorator(
|
|
107
|
-
path, ["OPTIONS"], show_in_schema=show_in_schema, **kwargs
|
|
108
|
-
)
|
|
95
|
+
return self._dual_decorator(path, ["OPTIONS"], show_in_schema=show_in_schema, **kwargs)
|
|
109
96
|
|
|
110
97
|
def head(self, path: str, *_, show_in_schema: bool = True, **kwargs: Any):
|
|
111
|
-
return self._dual_decorator(
|
|
112
|
-
path, ["HEAD"], show_in_schema=show_in_schema, **kwargs
|
|
113
|
-
)
|
|
98
|
+
return self._dual_decorator(path, ["HEAD"], show_in_schema=show_in_schema, **kwargs)
|
|
114
99
|
|
|
115
100
|
def list(
|
|
116
101
|
self,
|
|
117
102
|
path: str,
|
|
118
103
|
*,
|
|
119
|
-
model:
|
|
104
|
+
model: type[BaseModel],
|
|
120
105
|
envelope: bool = False,
|
|
121
106
|
cursor: bool = True,
|
|
122
107
|
page: bool = True,
|
|
@@ -155,9 +140,7 @@ class DualAPIRouter(APIRouter):
|
|
|
155
140
|
kwargs["response_model"] = kwargs.get("response_model") or response_model
|
|
156
141
|
|
|
157
142
|
# we still want the dual-registration behavior
|
|
158
|
-
return self._dual_decorator(
|
|
159
|
-
path, ["GET"], show_in_schema=show_in_schema, **kwargs
|
|
160
|
-
)
|
|
143
|
+
return self._dual_decorator(path, ["GET"], show_in_schema=show_in_schema, **kwargs)
|
|
161
144
|
|
|
162
145
|
# ---------- WebSocket ----------
|
|
163
146
|
|
svc_infra/api/fastapi/ease.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 Iterable, Sequence
|
|
5
5
|
|
|
6
6
|
from fastapi import FastAPI
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
@@ -64,7 +64,7 @@ class EasyAppOptions(BaseModel):
|
|
|
64
64
|
observability: ObservabilityOptions = ObservabilityOptions()
|
|
65
65
|
|
|
66
66
|
@classmethod
|
|
67
|
-
def from_env(cls) ->
|
|
67
|
+
def from_env(cls) -> EasyAppOptions:
|
|
68
68
|
"""
|
|
69
69
|
Build options from environment variables:
|
|
70
70
|
|
|
@@ -88,7 +88,7 @@ class EasyAppOptions(BaseModel):
|
|
|
88
88
|
),
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
-
def merged_with(self, override:
|
|
91
|
+
def merged_with(self, override: EasyAppOptions | None) -> EasyAppOptions:
|
|
92
92
|
"""
|
|
93
93
|
Merge two option sets. Non-None fields in `override` win.
|
|
94
94
|
(For iterables, if override provides a non-None value, it wins entirely.)
|
|
@@ -104,13 +104,9 @@ class EasyAppOptions(BaseModel):
|
|
|
104
104
|
else self.logging.enable
|
|
105
105
|
),
|
|
106
106
|
level=(
|
|
107
|
-
override.logging.level
|
|
108
|
-
if override.logging.level is not None
|
|
109
|
-
else self.logging.level
|
|
107
|
+
override.logging.level if override.logging.level is not None else self.logging.level
|
|
110
108
|
),
|
|
111
|
-
fmt=override.logging.fmt
|
|
112
|
-
if override.logging.fmt is not None
|
|
113
|
-
else self.logging.fmt,
|
|
109
|
+
fmt=override.logging.fmt if override.logging.fmt is not None else self.logging.fmt,
|
|
114
110
|
)
|
|
115
111
|
|
|
116
112
|
# observability
|
|
@@ -152,8 +148,33 @@ def easy_service_api(
|
|
|
152
148
|
public_cors_origins: list[str] | str | None = None,
|
|
153
149
|
root_public_base_url: str | None = None,
|
|
154
150
|
root_include_api_key: bool | None = None,
|
|
151
|
+
skip_paths: list[str] | None = None,
|
|
155
152
|
**fastapi_kwargs, # Forward all other FastAPI kwargs
|
|
156
153
|
) -> FastAPI:
|
|
154
|
+
"""
|
|
155
|
+
Create a FastAPI application with standard service configuration.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
name: Service name for OpenAPI docs and logging.
|
|
159
|
+
release: Version string for the service.
|
|
160
|
+
versions: List of (tag, routers_package, public_base_url) tuples for API versioning.
|
|
161
|
+
root_routers: Router module(s) to mount at root level.
|
|
162
|
+
public_cors_origins: Origins to allow for CORS.
|
|
163
|
+
root_public_base_url: Public base URL for root-level routes.
|
|
164
|
+
root_include_api_key: Whether to include API key auth for root routes.
|
|
165
|
+
skip_paths: Path prefixes to exclude from timeout/rate-limit middleware.
|
|
166
|
+
Uses prefix matching: "/v1/chat" matches "/v1/chat" and "/v1/chat/stream"
|
|
167
|
+
but not "/api/v1/chat". Falls back to SKIP_MIDDLEWARE_PATHS env var.
|
|
168
|
+
**fastapi_kwargs: Additional kwargs passed to FastAPI constructor.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Configured FastAPI application.
|
|
172
|
+
"""
|
|
173
|
+
# Env fallback for skip_paths
|
|
174
|
+
effective_skip = (
|
|
175
|
+
skip_paths if skip_paths is not None else _env_csv_paths("SKIP_MIDDLEWARE_PATHS")
|
|
176
|
+
)
|
|
177
|
+
|
|
157
178
|
service = ServiceInfo(name=name, release=release)
|
|
158
179
|
specs = [
|
|
159
180
|
APIVersionSpec(tag=str(tag), routers_package=pkg, public_base_url=base)
|
|
@@ -166,6 +187,7 @@ def easy_service_api(
|
|
|
166
187
|
public_cors_origins=public_cors_origins,
|
|
167
188
|
root_public_base_url=root_public_base_url,
|
|
168
189
|
root_include_api_key=root_include_api_key,
|
|
190
|
+
skip_paths=effective_skip,
|
|
169
191
|
**fastapi_kwargs, # Forward to setup_service_api
|
|
170
192
|
)
|
|
171
193
|
|
|
@@ -179,26 +201,47 @@ def easy_service_app(
|
|
|
179
201
|
public_cors_origins: list[str] | str | None = None,
|
|
180
202
|
root_public_base_url: str | None = None,
|
|
181
203
|
root_include_api_key: bool | None = None,
|
|
204
|
+
skip_paths: list[str] | None = None,
|
|
182
205
|
options: EasyAppOptions | None = None,
|
|
183
206
|
enable_logging: bool | None = None,
|
|
184
207
|
enable_observability: bool | None = None,
|
|
185
208
|
**fastapi_kwargs, # Forward all other FastAPI kwargs (lifespan, etc.)
|
|
186
209
|
) -> FastAPI:
|
|
187
210
|
"""
|
|
188
|
-
One-call bootstrap with env + options + flags
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
One-call bootstrap with env + options + flags.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
name: Service name for OpenAPI docs and logging.
|
|
215
|
+
release: Version string for the service.
|
|
216
|
+
versions: List of (tag, routers_package, public_base_url) tuples for API versioning.
|
|
217
|
+
root_routers: Router module(s) to mount at root level.
|
|
218
|
+
public_cors_origins: Origins to allow for CORS.
|
|
219
|
+
root_public_base_url: Public base URL for root-level routes.
|
|
220
|
+
root_include_api_key: Whether to include API key auth for root routes.
|
|
221
|
+
skip_paths: Path prefixes to exclude from timeout/rate-limit middleware.
|
|
222
|
+
Uses prefix matching: "/v1/chat" matches "/v1/chat" and "/v1/chat/stream"
|
|
223
|
+
but not "/api/v1/chat". Falls back to SKIP_MIDDLEWARE_PATHS env var.
|
|
224
|
+
options: EasyAppOptions for logging/observability configuration.
|
|
225
|
+
enable_logging: Override to enable/disable logging.
|
|
226
|
+
enable_observability: Override to enable/disable observability.
|
|
227
|
+
**fastapi_kwargs: Additional kwargs passed to FastAPI constructor.
|
|
228
|
+
|
|
229
|
+
Precedence (strongest → weakest):
|
|
191
230
|
1) enable_logging / enable_observability args
|
|
192
231
|
2) `options=` object (per-field)
|
|
193
232
|
3) `EasyAppOptions.from_env()`
|
|
194
233
|
|
|
195
|
-
|
|
234
|
+
Env recognized:
|
|
196
235
|
ENABLE_LOGGING=true|false
|
|
197
236
|
ENABLE_OBS=true|false
|
|
198
237
|
LOG_LEVEL=DEBUG|INFO|...
|
|
199
238
|
LOG_FORMAT=json|plain
|
|
200
239
|
METRICS_PATH=/metrics
|
|
201
240
|
OBS_SKIP_PATHS=/metrics,/health,/internal
|
|
241
|
+
SKIP_MIDDLEWARE_PATHS=/v1/chat,/v1/stream (for timeout/rate-limit skip)
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Configured FastAPI application with logging and observability.
|
|
202
245
|
"""
|
|
203
246
|
# 0) Start from env
|
|
204
247
|
env_opts = EasyAppOptions.from_env()
|
|
@@ -233,6 +276,7 @@ def easy_service_app(
|
|
|
233
276
|
public_cors_origins=public_cors_origins,
|
|
234
277
|
root_public_base_url=root_public_base_url,
|
|
235
278
|
root_include_api_key=root_include_api_key,
|
|
279
|
+
skip_paths=skip_paths,
|
|
236
280
|
**fastapi_kwargs, # Forward FastAPI kwargs (lifespan, etc.)
|
|
237
281
|
)
|
|
238
282
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
2
|
from email.utils import format_datetime, parsedate_to_datetime
|
|
3
3
|
from hashlib import sha256
|
|
4
4
|
|
|
@@ -16,13 +16,11 @@ def set_conditional_headers(
|
|
|
16
16
|
resp.headers["ETag"] = etag
|
|
17
17
|
if last_modified:
|
|
18
18
|
if last_modified.tzinfo is None:
|
|
19
|
-
last_modified = last_modified.replace(tzinfo=
|
|
19
|
+
last_modified = last_modified.replace(tzinfo=UTC)
|
|
20
20
|
resp.headers["Last-Modified"] = format_datetime(last_modified)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def maybe_not_modified(
|
|
24
|
-
request: Request, etag: str | None, last_modified: datetime | None
|
|
25
|
-
) -> bool:
|
|
23
|
+
def maybe_not_modified(request: Request, etag: str | None, last_modified: datetime | None) -> bool:
|
|
26
24
|
inm = request.headers.get("If-None-Match")
|
|
27
25
|
ims = request.headers.get("If-Modified-Since")
|
|
28
26
|
etag_ok = etag and inm and etag in [t.strip() for t in inm.split(",")]
|
|
@@ -31,9 +31,7 @@ class CatchAllExceptionMiddleware:
|
|
|
31
31
|
|
|
32
32
|
if response_started:
|
|
33
33
|
try:
|
|
34
|
-
await send(
|
|
35
|
-
{"type": "http.response.body", "body": b"", "more_body": False}
|
|
36
|
-
)
|
|
34
|
+
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
|
37
35
|
except Exception:
|
|
38
36
|
pass
|
|
39
37
|
else:
|
|
@@ -54,6 +52,4 @@ class CatchAllExceptionMiddleware:
|
|
|
54
52
|
"headers": [(b"content-type", PROBLEM_MT.encode("ascii"))],
|
|
55
53
|
}
|
|
56
54
|
)
|
|
57
|
-
await send(
|
|
58
|
-
{"type": "http.response.body", "body": body, "more_body": False}
|
|
59
|
-
)
|
|
55
|
+
await send({"type": "http.response.body", "body": body, "more_body": False})
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
class FastApiException(Exception):
|
|
5
2
|
"""
|
|
6
3
|
Application error that should be rendered as Problem Details.
|
|
@@ -9,7 +6,7 @@ class FastApiException(Exception):
|
|
|
9
6
|
def __init__(
|
|
10
7
|
self,
|
|
11
8
|
title: str,
|
|
12
|
-
detail:
|
|
9
|
+
detail: str | None = None,
|
|
13
10
|
status_code: int = 400,
|
|
14
11
|
*,
|
|
15
12
|
code: str | None = None,
|