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/api/fastapi/db/http.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Iterable, Sequence
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
4
5
|
|
|
5
6
|
from fastapi import Query
|
|
6
7
|
from pydantic import BaseModel
|
|
@@ -21,39 +22,37 @@ def dep_limit_offset(
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class OrderParams(BaseModel):
|
|
24
|
-
order_by:
|
|
25
|
+
order_by: str | None = None
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def dep_order(
|
|
28
|
-
order_by:
|
|
29
|
-
None, description="Comma-separated fields; '-' for DESC"
|
|
30
|
-
),
|
|
29
|
+
order_by: str | None = Query(None, description="Comma-separated fields; '-' for DESC"),
|
|
31
30
|
) -> OrderParams:
|
|
32
31
|
return OrderParams(order_by=order_by)
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
class SearchParams(BaseModel):
|
|
36
|
-
q:
|
|
37
|
-
fields:
|
|
35
|
+
q: str | None = None
|
|
36
|
+
fields: str | None = None
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
def dep_search(
|
|
41
|
-
q:
|
|
42
|
-
fields:
|
|
40
|
+
q: str | None = Query(None, description="Search query"),
|
|
41
|
+
fields: str | None = Query(None, description="Comma-separated list of fields"),
|
|
43
42
|
) -> SearchParams:
|
|
44
43
|
return SearchParams(q=q, fields=fields)
|
|
45
44
|
|
|
46
45
|
|
|
47
46
|
class Page(BaseModel, Generic[T]):
|
|
48
47
|
total: int
|
|
49
|
-
items:
|
|
48
|
+
items: list[T]
|
|
50
49
|
limit: int
|
|
51
50
|
offset: int
|
|
52
51
|
|
|
53
52
|
@classmethod
|
|
54
53
|
def from_items(
|
|
55
54
|
cls, *, total: int, items: Sequence[T] | Iterable[T], limit: int, offset: int
|
|
56
|
-
) ->
|
|
55
|
+
) -> Page[T]:
|
|
57
56
|
return cls(total=total, items=list(items), limit=limit, offset=offset)
|
|
58
57
|
|
|
59
58
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from contextlib import asynccontextmanager
|
|
5
|
-
from typing import Sequence
|
|
6
6
|
|
|
7
7
|
from bson import ObjectId
|
|
8
8
|
from fastapi import FastAPI
|
|
@@ -30,9 +30,7 @@ def add_mongo_db_with_url(app: FastAPI, url: str, db_name: str) -> None:
|
|
|
30
30
|
expected = get_mongo_dbname_from_env(required=False)
|
|
31
31
|
db = await acquire_db()
|
|
32
32
|
if expected and db.name != expected:
|
|
33
|
-
raise RuntimeError(
|
|
34
|
-
f"Connected to Mongo DB '{db.name}', expected '{expected}'."
|
|
35
|
-
)
|
|
33
|
+
raise RuntimeError(f"Connected to Mongo DB '{db.name}', expected '{expected}'.")
|
|
36
34
|
yield
|
|
37
35
|
finally:
|
|
38
36
|
await close_mongo()
|
|
@@ -49,9 +47,7 @@ def add_mongo_db(app: FastAPI, *, dsn_env: str = "MONGO_URL") -> None:
|
|
|
49
47
|
expected = get_mongo_dbname_from_env(required=False)
|
|
50
48
|
db = await acquire_db()
|
|
51
49
|
if expected and db.name != expected:
|
|
52
|
-
raise RuntimeError(
|
|
53
|
-
f"Connected to Mongo DB '{db.name}', expected '{expected}'."
|
|
54
|
-
)
|
|
50
|
+
raise RuntimeError(f"Connected to Mongo DB '{db.name}', expected '{expected}'.")
|
|
55
51
|
try:
|
|
56
52
|
yield
|
|
57
53
|
finally:
|
|
@@ -65,9 +61,7 @@ def add_mongo_health(
|
|
|
65
61
|
) -> None:
|
|
66
62
|
if include_in_schema is None:
|
|
67
63
|
include_in_schema = CURRENT_ENVIRONMENT == LOCAL_ENV
|
|
68
|
-
app.include_router(
|
|
69
|
-
make_mongo_health_router(prefix=prefix, include_in_schema=include_in_schema)
|
|
70
|
-
)
|
|
64
|
+
app.include_router(make_mongo_health_router(prefix=prefix, include_in_schema=include_in_schema))
|
|
71
65
|
|
|
72
66
|
|
|
73
67
|
def add_mongo_resources(app: FastAPI, resources: Sequence[NoSqlResource]) -> None:
|
|
@@ -79,11 +73,7 @@ def add_mongo_resources(app: FastAPI, resources: Sequence[NoSqlResource]) -> Non
|
|
|
79
73
|
soft_delete_field=resource.soft_delete_field,
|
|
80
74
|
soft_delete_flag_field=resource.soft_delete_flag_field,
|
|
81
75
|
)
|
|
82
|
-
svc = (
|
|
83
|
-
resource.service_factory(repo)
|
|
84
|
-
if resource.service_factory
|
|
85
|
-
else NoSqlService(repo)
|
|
86
|
-
)
|
|
76
|
+
svc = resource.service_factory(repo) if resource.service_factory else NoSqlService(repo)
|
|
87
77
|
|
|
88
78
|
if resource.read_schema and resource.create_schema and resource.update_schema:
|
|
89
79
|
Read, Create, Update = (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Annotated, Any, cast
|
|
2
3
|
|
|
3
4
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
|
4
5
|
|
|
@@ -27,7 +28,7 @@ DBDep = Annotated[AsyncIOMotorDatabase, Depends(acquire_db)]
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def _parse_sort(
|
|
30
|
-
order_spec:
|
|
31
|
+
order_spec: str | None, allowed_order_fields: list[str] | None
|
|
31
32
|
) -> list[tuple[str, int]]:
|
|
32
33
|
if not order_spec:
|
|
33
34
|
return []
|
|
@@ -43,18 +44,18 @@ def _parse_sort(
|
|
|
43
44
|
def make_crud_router_plus_mongo(
|
|
44
45
|
*,
|
|
45
46
|
service: NoSqlService,
|
|
46
|
-
read_schema:
|
|
47
|
-
create_schema:
|
|
48
|
-
update_schema:
|
|
47
|
+
read_schema: type[Any],
|
|
48
|
+
create_schema: type[Any],
|
|
49
|
+
update_schema: type[Any],
|
|
49
50
|
prefix: str,
|
|
50
51
|
tags: list[str] | None = None,
|
|
51
|
-
search_fields:
|
|
52
|
-
default_ordering:
|
|
53
|
-
allowed_order_fields:
|
|
52
|
+
search_fields: Sequence[str] | None = None,
|
|
53
|
+
default_ordering: str | None = None,
|
|
54
|
+
allowed_order_fields: list[str] | None = None,
|
|
54
55
|
mount_under_db_prefix: bool = True,
|
|
55
56
|
) -> APIRouter:
|
|
56
|
-
read_model = cast(Any, read_schema)
|
|
57
|
-
page_model = cast(Any, Page[read_schema]) # type: ignore[valid-type]
|
|
57
|
+
read_model = cast("Any", read_schema)
|
|
58
|
+
page_model = cast("Any", Page[read_schema]) # type: ignore[valid-type]
|
|
58
59
|
|
|
59
60
|
router_prefix = ("/_mongo" + prefix) if mount_under_db_prefix else prefix
|
|
60
61
|
router = public_router(
|
|
@@ -89,9 +90,7 @@ def make_crud_router_plus_mongo(
|
|
|
89
90
|
else:
|
|
90
91
|
items = await service.list(db, limit=lp.limit, offset=lp.offset, sort=sort)
|
|
91
92
|
total = await service.count(db)
|
|
92
|
-
return Page[Any].from_items(
|
|
93
|
-
total=total, items=items, limit=lp.limit, offset=lp.offset
|
|
94
|
-
)
|
|
93
|
+
return Page[Any].from_items(total=total, items=items, limit=lp.limit, offset=lp.offset)
|
|
95
94
|
|
|
96
95
|
# GET by id
|
|
97
96
|
@router.get(
|
|
@@ -113,7 +112,7 @@ def make_crud_router_plus_mongo(
|
|
|
113
112
|
description=f"Create item in {prefix} collection",
|
|
114
113
|
)
|
|
115
114
|
async def create_item(db: DBDep, payload: create_schema = Body(...)): # type: ignore[valid-type]
|
|
116
|
-
data = cast(Any, payload).model_dump(exclude_unset=True)
|
|
115
|
+
data = cast("Any", payload).model_dump(exclude_unset=True)
|
|
117
116
|
return await service.create(db, data)
|
|
118
117
|
|
|
119
118
|
# UPDATE
|
|
@@ -127,7 +126,7 @@ def make_crud_router_plus_mongo(
|
|
|
127
126
|
item_id: Any,
|
|
128
127
|
payload: update_schema = Body(...), # type: ignore[valid-type]
|
|
129
128
|
):
|
|
130
|
-
data = cast(Any, payload).model_dump(exclude_unset=True)
|
|
129
|
+
data = cast("Any", payload).model_dump(exclude_unset=True)
|
|
131
130
|
row = await service.update(db, item_id, data)
|
|
132
131
|
if not row:
|
|
133
132
|
raise HTTPException(404, "Not found")
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from contextlib import asynccontextmanager
|
|
5
|
-
from typing import Optional, Sequence
|
|
6
6
|
|
|
7
7
|
from fastapi import FastAPI
|
|
8
8
|
|
|
@@ -17,9 +17,7 @@ from .session import dispose_session, initialize_session
|
|
|
17
17
|
|
|
18
18
|
def add_sql_resources(app: FastAPI, resources: Sequence[SqlResource]) -> None:
|
|
19
19
|
for r in resources:
|
|
20
|
-
repo = SqlRepository(
|
|
21
|
-
model=r.model, id_attr=r.id_attr, soft_delete=r.soft_delete
|
|
22
|
-
)
|
|
20
|
+
repo = SqlRepository(model=r.model, id_attr=r.id_attr, soft_delete=r.soft_delete)
|
|
23
21
|
|
|
24
22
|
if r.service_factory:
|
|
25
23
|
svc = r.service_factory(repo)
|
|
@@ -73,9 +71,7 @@ def add_sql_resources(app: FastAPI, resources: Sequence[SqlResource]) -> None:
|
|
|
73
71
|
app.include_router(router)
|
|
74
72
|
|
|
75
73
|
|
|
76
|
-
def add_sql_db(
|
|
77
|
-
app: FastAPI, *, url: Optional[str] = None, dsn_env: str = "SQL_URL"
|
|
78
|
-
) -> None:
|
|
74
|
+
def add_sql_db(app: FastAPI, *, url: str | None = None, dsn_env: str = "SQL_URL") -> None:
|
|
79
75
|
"""Configure DB lifecycle for the app (either explicit URL or from env).
|
|
80
76
|
|
|
81
77
|
This preserves any existing lifespan context (like user-defined lifespans)
|
|
@@ -106,9 +102,7 @@ def add_sql_db(
|
|
|
106
102
|
async def lifespan_from_env(_app: FastAPI):
|
|
107
103
|
env_url = os.getenv(dsn_env)
|
|
108
104
|
if not env_url:
|
|
109
|
-
raise RuntimeError(
|
|
110
|
-
f"Missing environment variable {dsn_env} for database URL"
|
|
111
|
-
)
|
|
105
|
+
raise RuntimeError(f"Missing environment variable {dsn_env} for database URL")
|
|
112
106
|
initialize_session(env_url)
|
|
113
107
|
try:
|
|
114
108
|
if existing_lifespan is not None:
|
|
@@ -125,16 +119,14 @@ def add_sql_db(
|
|
|
125
119
|
def add_sql_health(
|
|
126
120
|
app: FastAPI, *, prefix: str = "/_sql/health", include_in_schema: bool = False
|
|
127
121
|
) -> None:
|
|
128
|
-
app.include_router(
|
|
129
|
-
_make_db_health_router(prefix=prefix, include_in_schema=include_in_schema)
|
|
130
|
-
)
|
|
122
|
+
app.include_router(_make_db_health_router(prefix=prefix, include_in_schema=include_in_schema))
|
|
131
123
|
|
|
132
124
|
|
|
133
125
|
def setup_sql(
|
|
134
126
|
app: FastAPI,
|
|
135
127
|
resources: Sequence[SqlResource],
|
|
136
128
|
*,
|
|
137
|
-
url:
|
|
129
|
+
url: str | None = None,
|
|
138
130
|
dsn_env: str = "SQL_URL",
|
|
139
131
|
include_health: bool = True,
|
|
140
132
|
health_prefix: str = "/_sql/health",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
|
+
from typing import Annotated, Any, TypeVar, cast
|
|
2
3
|
|
|
3
4
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
|
4
5
|
from pydantic import BaseModel
|
|
@@ -29,14 +30,14 @@ def make_crud_router_plus_sql(
|
|
|
29
30
|
*,
|
|
30
31
|
model: type[Any],
|
|
31
32
|
service: SqlService,
|
|
32
|
-
read_schema:
|
|
33
|
-
create_schema:
|
|
34
|
-
update_schema:
|
|
33
|
+
read_schema: type[ReadModel],
|
|
34
|
+
create_schema: type[CreateModel],
|
|
35
|
+
update_schema: type[UpdateModel],
|
|
35
36
|
prefix: str,
|
|
36
37
|
tags: list[str] | None = None,
|
|
37
|
-
search_fields:
|
|
38
|
-
default_ordering:
|
|
39
|
-
allowed_order_fields:
|
|
38
|
+
search_fields: Sequence[str] | None = None,
|
|
39
|
+
default_ordering: str | None = None,
|
|
40
|
+
allowed_order_fields: list[str] | None = None,
|
|
40
41
|
mount_under_db_prefix: bool = True,
|
|
41
42
|
) -> APIRouter:
|
|
42
43
|
router_prefix = ("/_sql" + prefix) if mount_under_db_prefix else prefix
|
|
@@ -58,7 +59,7 @@ def make_crud_router_plus_sql(
|
|
|
58
59
|
return v
|
|
59
60
|
return v
|
|
60
61
|
|
|
61
|
-
def _parse_ordering_to_fields(order_spec:
|
|
62
|
+
def _parse_ordering_to_fields(order_spec: str | None) -> list[str]:
|
|
62
63
|
if not order_spec:
|
|
63
64
|
return []
|
|
64
65
|
pieces = [p.strip() for p in order_spec.split(",") if p.strip()]
|
|
@@ -102,13 +103,9 @@ def make_crud_router_plus_sql(
|
|
|
102
103
|
)
|
|
103
104
|
total = await service.count_filtered(session, q=sp.q, fields=fields)
|
|
104
105
|
else:
|
|
105
|
-
items = await service.list(
|
|
106
|
-
session, limit=lp.limit, offset=lp.offset, order_by=order_by
|
|
107
|
-
)
|
|
106
|
+
items = await service.list(session, limit=lp.limit, offset=lp.offset, order_by=order_by)
|
|
108
107
|
total = await service.count(session)
|
|
109
|
-
return Page[Any].from_items(
|
|
110
|
-
total=total, items=items, limit=lp.limit, offset=lp.offset
|
|
111
|
-
)
|
|
108
|
+
return Page[Any].from_items(total=total, items=items, limit=lp.limit, offset=lp.offset)
|
|
112
109
|
|
|
113
110
|
# -------- GET by id --------
|
|
114
111
|
@router.get(
|
|
@@ -134,7 +131,7 @@ def make_crud_router_plus_sql(
|
|
|
134
131
|
payload: create_schema = Body(...), # type: ignore[valid-type]
|
|
135
132
|
):
|
|
136
133
|
if isinstance(payload, BaseModel):
|
|
137
|
-
data = cast(BaseModel, payload).model_dump(exclude_unset=True)
|
|
134
|
+
data = cast("BaseModel", payload).model_dump(exclude_unset=True)
|
|
138
135
|
elif isinstance(payload, dict):
|
|
139
136
|
data = payload
|
|
140
137
|
else:
|
|
@@ -153,7 +150,7 @@ def make_crud_router_plus_sql(
|
|
|
153
150
|
payload: update_schema = Body(...), # type: ignore[valid-type]
|
|
154
151
|
):
|
|
155
152
|
if isinstance(payload, BaseModel):
|
|
156
|
-
data = cast(BaseModel, payload).model_dump(exclude_unset=True)
|
|
153
|
+
data = cast("BaseModel", payload).model_dump(exclude_unset=True)
|
|
157
154
|
elif isinstance(payload, dict):
|
|
158
155
|
data = payload
|
|
159
156
|
else:
|
|
@@ -181,18 +178,16 @@ def make_crud_router_plus_sql(
|
|
|
181
178
|
def make_tenant_crud_router_plus_sql(
|
|
182
179
|
*,
|
|
183
180
|
model: type[Any],
|
|
184
|
-
service_factory: Callable[
|
|
185
|
-
|
|
186
|
-
],
|
|
187
|
-
|
|
188
|
-
create_schema: Type[CreateModel],
|
|
189
|
-
update_schema: Type[UpdateModel],
|
|
181
|
+
service_factory: Callable[[], Any], # factory that returns a SqlService (will be wrapped)
|
|
182
|
+
read_schema: type[ReadModel],
|
|
183
|
+
create_schema: type[CreateModel],
|
|
184
|
+
update_schema: type[UpdateModel],
|
|
190
185
|
prefix: str,
|
|
191
186
|
tenant_field: str = "tenant_id",
|
|
192
187
|
tags: list[str] | None = None,
|
|
193
|
-
search_fields:
|
|
194
|
-
default_ordering:
|
|
195
|
-
allowed_order_fields:
|
|
188
|
+
search_fields: Sequence[str] | None = None,
|
|
189
|
+
default_ordering: str | None = None,
|
|
190
|
+
allowed_order_fields: list[str] | None = None,
|
|
196
191
|
mount_under_db_prefix: bool = True,
|
|
197
192
|
) -> APIRouter:
|
|
198
193
|
"""Like make_crud_router_plus_sql, but requires TenantId and scopes all operations."""
|
|
@@ -206,9 +201,7 @@ def make_tenant_crud_router_plus_sql(
|
|
|
206
201
|
# Evaluate the base service once to preserve in-memory state across requests in tests/local.
|
|
207
202
|
# Consumers may pass either an instance or a zero-arg factory function.
|
|
208
203
|
try:
|
|
209
|
-
_base_instance = (
|
|
210
|
-
service_factory() if callable(service_factory) else service_factory
|
|
211
|
-
)
|
|
204
|
+
_base_instance = service_factory() if callable(service_factory) else service_factory
|
|
212
205
|
except TypeError:
|
|
213
206
|
# If the callable requires args, assume it's already an instance
|
|
214
207
|
_base_instance = service_factory
|
|
@@ -224,7 +217,7 @@ def make_tenant_crud_router_plus_sql(
|
|
|
224
217
|
return v
|
|
225
218
|
return v
|
|
226
219
|
|
|
227
|
-
def _parse_ordering_to_fields(order_spec:
|
|
220
|
+
def _parse_ordering_to_fields(order_spec: str | None) -> list[str]:
|
|
228
221
|
if not order_spec:
|
|
229
222
|
return []
|
|
230
223
|
pieces = [p.strip() for p in order_spec.split(",") if p.strip()]
|
|
@@ -239,9 +232,7 @@ def make_tenant_crud_router_plus_sql(
|
|
|
239
232
|
# create per-request service with tenant scoping
|
|
240
233
|
async def _svc(session: SqlSessionDep, tenant_id: TenantId):
|
|
241
234
|
repo_or_service = getattr(_base_instance, "repo", _base_instance)
|
|
242
|
-
svc: Any = TenantSqlService(
|
|
243
|
-
repo_or_service, tenant_id=tenant_id, tenant_field=tenant_field
|
|
244
|
-
)
|
|
235
|
+
svc: Any = TenantSqlService(repo_or_service, tenant_id=tenant_id, tenant_field=tenant_field)
|
|
245
236
|
return svc
|
|
246
237
|
|
|
247
238
|
@router.get("", response_model=Page[read_schema]) # type: ignore[valid-type]
|
|
@@ -272,13 +263,9 @@ def make_tenant_crud_router_plus_sql(
|
|
|
272
263
|
)
|
|
273
264
|
total = await svc.count_filtered(session, q=sp.q, fields=fields)
|
|
274
265
|
else:
|
|
275
|
-
items = await svc.list(
|
|
276
|
-
session, limit=lp.limit, offset=lp.offset, order_by=order_by
|
|
277
|
-
)
|
|
266
|
+
items = await svc.list(session, limit=lp.limit, offset=lp.offset, order_by=order_by)
|
|
278
267
|
total = await svc.count(session)
|
|
279
|
-
return Page[Any].from_items(
|
|
280
|
-
total=total, items=items, limit=lp.limit, offset=lp.offset
|
|
281
|
-
)
|
|
268
|
+
return Page[Any].from_items(total=total, items=items, limit=lp.limit, offset=lp.offset)
|
|
282
269
|
|
|
283
270
|
@router.get("/{item_id}", response_model=read_schema)
|
|
284
271
|
async def get_item(item_id: Any, session: SqlSessionDep, tenant_id: TenantId):
|
|
@@ -296,7 +283,7 @@ def make_tenant_crud_router_plus_sql(
|
|
|
296
283
|
):
|
|
297
284
|
svc = await _svc(session, tenant_id)
|
|
298
285
|
if isinstance(payload, BaseModel):
|
|
299
|
-
data = cast(BaseModel, payload).model_dump(exclude_unset=True)
|
|
286
|
+
data = cast("BaseModel", payload).model_dump(exclude_unset=True)
|
|
300
287
|
elif isinstance(payload, dict):
|
|
301
288
|
data = payload
|
|
302
289
|
else:
|
|
@@ -312,7 +299,7 @@ def make_tenant_crud_router_plus_sql(
|
|
|
312
299
|
):
|
|
313
300
|
svc = await _svc(session, tenant_id)
|
|
314
301
|
if isinstance(payload, BaseModel):
|
|
315
|
-
data = cast(BaseModel, payload).model_dump(exclude_unset=True)
|
|
302
|
+
data = cast("BaseModel", payload).model_dump(exclude_unset=True)
|
|
316
303
|
elif isinstance(payload, dict):
|
|
317
304
|
data = payload
|
|
318
305
|
else:
|
|
@@ -14,9 +14,7 @@ def _make_db_health_router(
|
|
|
14
14
|
include_in_schema: bool = False,
|
|
15
15
|
) -> APIRouter:
|
|
16
16
|
"""Internal factory for the DB health router."""
|
|
17
|
-
router = public_router(
|
|
18
|
-
prefix=prefix, tags=["health"], include_in_schema=include_in_schema
|
|
19
|
-
)
|
|
17
|
+
router = public_router(prefix=prefix, tags=["health"], include_in_schema=include_in_schema)
|
|
20
18
|
|
|
21
19
|
@router.get("", status_code=status.HTTP_200_OK)
|
|
22
20
|
async def db_health(session: SqlSessionDep) -> Response:
|
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Annotated
|
|
6
7
|
|
|
7
8
|
from fastapi import Depends
|
|
8
9
|
from sqlalchemy import text
|
|
@@ -23,7 +24,7 @@ _SessionLocal: async_sessionmaker[AsyncSession] | None = None
|
|
|
23
24
|
|
|
24
25
|
def _init_engine_and_session(
|
|
25
26
|
url: str,
|
|
26
|
-
) ->
|
|
27
|
+
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
|
|
27
28
|
async_url = _coerce_to_async_url(url)
|
|
28
29
|
if async_url != url:
|
|
29
30
|
logger.info(
|
|
@@ -63,9 +64,7 @@ async def get_session() -> AsyncIterator[AsyncSession]:
|
|
|
63
64
|
if ms > 0:
|
|
64
65
|
try:
|
|
65
66
|
# SET LOCAL applies for the duration of the current transaction only
|
|
66
|
-
await session.execute(
|
|
67
|
-
text("SET LOCAL statement_timeout = :ms"), {"ms": ms}
|
|
68
|
-
)
|
|
67
|
+
await session.execute(text("SET LOCAL statement_timeout = :ms"), {"ms": ms})
|
|
69
68
|
except Exception:
|
|
70
69
|
# Non-PG dialects (e.g., SQLite) will error; ignore silently
|
|
71
70
|
pass
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import AsyncIterator, Callable
|
|
4
|
+
from typing import Any
|
|
4
5
|
from uuid import UUID
|
|
5
6
|
|
|
6
7
|
from fastapi import Depends
|
|
@@ -30,7 +31,7 @@ def get_fastapi_users(
|
|
|
30
31
|
user_schema_update: Any,
|
|
31
32
|
*,
|
|
32
33
|
public_auth_prefix: str = "/auth",
|
|
33
|
-
) ->
|
|
34
|
+
) -> tuple[
|
|
34
35
|
FastAPIUsers,
|
|
35
36
|
AuthenticationBackend,
|
|
36
37
|
DualAPIRouter,
|
|
@@ -51,9 +52,7 @@ def get_fastapi_users(
|
|
|
51
52
|
|
|
52
53
|
async def on_after_register(self, user: Any, request=None):
|
|
53
54
|
st = get_auth_settings()
|
|
54
|
-
if CURRENT_ENVIRONMENT in (DEV_ENV, LOCAL_ENV) and bool(
|
|
55
|
-
st.auto_verify_in_dev
|
|
56
|
-
):
|
|
55
|
+
if CURRENT_ENVIRONMENT in (DEV_ENV, LOCAL_ENV) and bool(st.auto_verify_in_dev):
|
|
57
56
|
await self.user_db.update(user, {"is_verified": True})
|
|
58
57
|
return
|
|
59
58
|
await self.request_verify(user, request)
|
|
@@ -62,10 +61,10 @@ def get_fastapi_users(
|
|
|
62
61
|
verify_url = f"{public_auth_prefix}/verify?token={token}"
|
|
63
62
|
sender = get_sender()
|
|
64
63
|
sender.send(
|
|
65
|
-
to=
|
|
64
|
+
to=user.email,
|
|
66
65
|
subject="Verify your account",
|
|
67
66
|
html_body=f"""
|
|
68
|
-
<p>Hi {getattr(user,
|
|
67
|
+
<p>Hi {getattr(user, "full_name", "") or "there"},</p>
|
|
69
68
|
<p>Click to verify your account:</p>
|
|
70
69
|
<p><a href="{verify_url}">{verify_url}</a></p>
|
|
71
70
|
""",
|
|
@@ -75,7 +74,7 @@ def get_fastapi_users(
|
|
|
75
74
|
reset_url = f"{public_auth_prefix}/reset-password?token={token}"
|
|
76
75
|
sender = get_sender()
|
|
77
76
|
sender.send(
|
|
78
|
-
to=
|
|
77
|
+
to=user.email,
|
|
79
78
|
subject="Reset your password",
|
|
80
79
|
html_body=f"""
|
|
81
80
|
<p>We received a request to reset your password.</p>
|
|
@@ -116,9 +115,7 @@ def get_fastapi_users(
|
|
|
116
115
|
old_secrets=old,
|
|
117
116
|
token_audience=audience,
|
|
118
117
|
)
|
|
119
|
-
return JWTStrategy(
|
|
120
|
-
secret=secret, lifetime_seconds=lifetime, token_audience=audience
|
|
121
|
-
)
|
|
118
|
+
return JWTStrategy(secret=secret, lifetime_seconds=lifetime, token_audience=audience)
|
|
122
119
|
|
|
123
120
|
bearer_transport = BearerTransport(tokenUrl=auth_login_path)
|
|
124
121
|
auth_backend = AuthenticationBackend(
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import time
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Callable
|
|
6
6
|
|
|
7
7
|
from fastapi import HTTPException
|
|
8
8
|
from starlette.requests import Request
|
|
@@ -30,9 +30,7 @@ class RateLimiter:
|
|
|
30
30
|
limit: int,
|
|
31
31
|
window: int = 60,
|
|
32
32
|
key_fn: Callable = lambda r: "global",
|
|
33
|
-
limit_resolver:
|
|
34
|
-
Callable[[Request, Optional[str]], Optional[int]]
|
|
35
|
-
] = None,
|
|
33
|
+
limit_resolver: Callable[[Request, str | None], int | None] | None = None,
|
|
36
34
|
scope_by_tenant: bool = False,
|
|
37
35
|
store: RateLimitStore | None = None,
|
|
38
36
|
):
|
|
@@ -65,7 +63,7 @@ class RateLimiter:
|
|
|
65
63
|
except Exception:
|
|
66
64
|
eff_limit = self.limit
|
|
67
65
|
|
|
68
|
-
count,
|
|
66
|
+
count, _store_limit, reset = self.store.incr(str(key), self.window)
|
|
69
67
|
if count > eff_limit:
|
|
70
68
|
retry = max(0, reset - int(time.time()))
|
|
71
69
|
try:
|
|
@@ -87,7 +85,7 @@ def rate_limiter(
|
|
|
87
85
|
limit: int,
|
|
88
86
|
window: int = 60,
|
|
89
87
|
key_fn: Callable = lambda r: "global",
|
|
90
|
-
limit_resolver:
|
|
88
|
+
limit_resolver: Callable[[Request, str | None], int | None] | None = None,
|
|
91
89
|
scope_by_tenant: bool = False,
|
|
92
90
|
store: RateLimitStore | None = None,
|
|
93
91
|
):
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Callable, Optional
|
|
6
6
|
|
|
7
7
|
from fastapi import FastAPI, Request
|
|
8
8
|
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
|
@@ -18,7 +18,7 @@ def add_docs(
|
|
|
18
18
|
redoc_url: str = "/redoc",
|
|
19
19
|
swagger_url: str = "/docs",
|
|
20
20
|
openapi_url: str = "/openapi.json",
|
|
21
|
-
export_openapi_to:
|
|
21
|
+
export_openapi_to: str | None = None,
|
|
22
22
|
# Landing page options
|
|
23
23
|
landing_url: str = "/",
|
|
24
24
|
include_landing: bool = True,
|
|
@@ -29,15 +29,13 @@ def add_docs(
|
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
# OpenAPI JSON route
|
|
32
|
-
async def openapi_handler() -> JSONResponse:
|
|
32
|
+
async def openapi_handler() -> JSONResponse:
|
|
33
33
|
return JSONResponse(app.openapi())
|
|
34
34
|
|
|
35
|
-
app.add_api_route(
|
|
36
|
-
openapi_url, openapi_handler, methods=["GET"], include_in_schema=False
|
|
37
|
-
)
|
|
35
|
+
app.add_api_route(openapi_url, openapi_handler, methods=["GET"], include_in_schema=False)
|
|
38
36
|
|
|
39
37
|
# Swagger UI route
|
|
40
|
-
async def swagger_ui(request: Request) -> HTMLResponse:
|
|
38
|
+
async def swagger_ui(request: Request) -> HTMLResponse:
|
|
41
39
|
resp = get_swagger_ui_html(openapi_url=openapi_url, title="API Docs")
|
|
42
40
|
theme = request.query_params.get("theme")
|
|
43
41
|
if theme == "dark":
|
|
@@ -47,7 +45,7 @@ def add_docs(
|
|
|
47
45
|
app.add_api_route(swagger_url, swagger_ui, methods=["GET"], include_in_schema=False)
|
|
48
46
|
|
|
49
47
|
# Redoc route
|
|
50
|
-
async def redoc_ui(request: Request) -> HTMLResponse:
|
|
48
|
+
async def redoc_ui(request: Request) -> HTMLResponse:
|
|
51
49
|
resp = get_redoc_html(openapi_url=openapi_url, title="API ReDoc")
|
|
52
50
|
theme = request.query_params.get("theme")
|
|
53
51
|
if theme == "dark":
|
|
@@ -80,15 +78,13 @@ def add_docs(
|
|
|
80
78
|
if landing_path in existing_paths:
|
|
81
79
|
landing_path = "/_docs"
|
|
82
80
|
|
|
83
|
-
async def _landing() -> HTMLResponse:
|
|
81
|
+
async def _landing() -> HTMLResponse:
|
|
84
82
|
cards: list[CardSpec] = []
|
|
85
83
|
# Root docs card using the provided paths
|
|
86
84
|
cards.append(
|
|
87
85
|
CardSpec(
|
|
88
86
|
tag="",
|
|
89
|
-
docs=DocTargets(
|
|
90
|
-
swagger=swagger_url, redoc=redoc_url, openapi_json=openapi_url
|
|
91
|
-
),
|
|
87
|
+
docs=DocTargets(swagger=swagger_url, redoc=redoc_url, openapi_json=openapi_url),
|
|
92
88
|
)
|
|
93
89
|
)
|
|
94
90
|
# Scoped docs (if any were registered via add_prefixed_docs)
|
|
@@ -96,9 +92,7 @@ def add_docs(
|
|
|
96
92
|
cards.append(
|
|
97
93
|
CardSpec(
|
|
98
94
|
tag=scope.strip("/"),
|
|
99
|
-
docs=DocTargets(
|
|
100
|
-
swagger=swagger, redoc=redoc, openapi_json=openapi_json
|
|
101
|
-
),
|
|
95
|
+
docs=DocTargets(swagger=swagger, redoc=redoc, openapi_json=openapi_json),
|
|
102
96
|
)
|
|
103
97
|
)
|
|
104
98
|
html = render_index_html(
|
|
@@ -106,9 +100,7 @@ def add_docs(
|
|
|
106
100
|
)
|
|
107
101
|
return HTMLResponse(html)
|
|
108
102
|
|
|
109
|
-
app.add_api_route(
|
|
110
|
-
landing_path, _landing, methods=["GET"], include_in_schema=False
|
|
111
|
-
)
|
|
103
|
+
app.add_api_route(landing_path, _landing, methods=["GET"], include_in_schema=False)
|
|
112
104
|
|
|
113
105
|
|
|
114
106
|
def _with_dark_mode(resp: HTMLResponse) -> HTMLResponse:
|
|
@@ -130,9 +122,7 @@ def _with_dark_mode(resp: HTMLResponse) -> HTMLResponse:
|
|
|
130
122
|
body = body.replace("</head>", f"<style>\n{css}\n</style></head>", 1)
|
|
131
123
|
# add class to body to allow stronger selectors
|
|
132
124
|
body = body.replace("<body>", '<body class="dark">', 1)
|
|
133
|
-
return HTMLResponse(
|
|
134
|
-
content=body, status_code=resp.status_code, headers=dict(resp.headers)
|
|
135
|
-
)
|
|
125
|
+
return HTMLResponse(content=body, status_code=resp.status_code, headers=dict(resp.headers))
|
|
136
126
|
|
|
137
127
|
|
|
138
128
|
_DARK_CSS = """
|
|
@@ -147,7 +137,7 @@ a { color: #62aef7; }
|
|
|
147
137
|
def add_sdk_generation_stub(
|
|
148
138
|
app: FastAPI,
|
|
149
139
|
*,
|
|
150
|
-
on_generate:
|
|
140
|
+
on_generate: Callable[[], None] | None = None,
|
|
151
141
|
openapi_path: str = "/openapi.json",
|
|
152
142
|
) -> None:
|
|
153
143
|
"""Hook to add an SDK generation stub.
|
|
@@ -163,7 +153,7 @@ def add_sdk_generation_stub(
|
|
|
163
153
|
router = public_router(prefix="/_docs", include_in_schema=False)
|
|
164
154
|
|
|
165
155
|
@router.post("/generate-sdk")
|
|
166
|
-
async def _generate() -> dict:
|
|
156
|
+
async def _generate() -> dict:
|
|
167
157
|
on_generate()
|
|
168
158
|
return {"status": "ok"}
|
|
169
159
|
|