tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__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.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +72 -0
- tigrbl/api/api_spec.py +30 -0
- tigrbl/api/mro_collect.py +43 -0
- tigrbl/api/shortcuts.py +56 -0
- tigrbl/api/tigrbl_api.py +286 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +61 -0
- tigrbl/app/app_spec.py +42 -0
- tigrbl/app/mro_collect.py +67 -0
- tigrbl/app/shortcuts.py +65 -0
- tigrbl/app/tigrbl_app.py +314 -0
- tigrbl/bindings/__init__.py +73 -0
- tigrbl/bindings/api/__init__.py +12 -0
- tigrbl/bindings/api/common.py +109 -0
- tigrbl/bindings/api/include.py +256 -0
- tigrbl/bindings/api/resource_proxy.py +149 -0
- tigrbl/bindings/api/rpc.py +111 -0
- tigrbl/bindings/columns.py +49 -0
- tigrbl/bindings/handlers/__init__.py +11 -0
- tigrbl/bindings/handlers/builder.py +119 -0
- tigrbl/bindings/handlers/ctx.py +74 -0
- tigrbl/bindings/handlers/identifiers.py +228 -0
- tigrbl/bindings/handlers/namespaces.py +51 -0
- tigrbl/bindings/handlers/steps.py +276 -0
- tigrbl/bindings/hooks.py +311 -0
- tigrbl/bindings/model.py +194 -0
- tigrbl/bindings/model_helpers.py +139 -0
- tigrbl/bindings/model_registry.py +77 -0
- tigrbl/bindings/rest/__init__.py +7 -0
- tigrbl/bindings/rest/attach.py +34 -0
- tigrbl/bindings/rest/collection.py +265 -0
- tigrbl/bindings/rest/common.py +116 -0
- tigrbl/bindings/rest/fastapi.py +76 -0
- tigrbl/bindings/rest/helpers.py +119 -0
- tigrbl/bindings/rest/io.py +317 -0
- tigrbl/bindings/rest/member.py +367 -0
- tigrbl/bindings/rest/router.py +292 -0
- tigrbl/bindings/rest/routing.py +133 -0
- tigrbl/bindings/rpc.py +364 -0
- tigrbl/bindings/schemas/__init__.py +11 -0
- tigrbl/bindings/schemas/builder.py +348 -0
- tigrbl/bindings/schemas/defaults.py +260 -0
- tigrbl/bindings/schemas/utils.py +193 -0
- tigrbl/column/README.md +62 -0
- tigrbl/column/__init__.py +72 -0
- tigrbl/column/_column.py +96 -0
- tigrbl/column/column_spec.py +40 -0
- tigrbl/column/field_spec.py +31 -0
- tigrbl/column/infer/__init__.py +25 -0
- tigrbl/column/infer/core.py +92 -0
- tigrbl/column/infer/jsonhints.py +44 -0
- tigrbl/column/infer/planning.py +133 -0
- tigrbl/column/infer/types.py +102 -0
- tigrbl/column/infer/utils.py +59 -0
- tigrbl/column/io_spec.py +133 -0
- tigrbl/column/mro_collect.py +59 -0
- tigrbl/column/shortcuts.py +89 -0
- tigrbl/column/storage_spec.py +65 -0
- tigrbl/config/__init__.py +19 -0
- tigrbl/config/constants.py +224 -0
- tigrbl/config/defaults.py +29 -0
- tigrbl/config/resolver.py +295 -0
- tigrbl/core/__init__.py +47 -0
- tigrbl/core/crud/__init__.py +36 -0
- tigrbl/core/crud/bulk.py +168 -0
- tigrbl/core/crud/helpers/__init__.py +76 -0
- tigrbl/core/crud/helpers/db.py +92 -0
- tigrbl/core/crud/helpers/enum.py +86 -0
- tigrbl/core/crud/helpers/filters.py +162 -0
- tigrbl/core/crud/helpers/model.py +123 -0
- tigrbl/core/crud/helpers/normalize.py +99 -0
- tigrbl/core/crud/ops.py +235 -0
- tigrbl/ddl/__init__.py +344 -0
- tigrbl/decorators.py +17 -0
- tigrbl/deps/__init__.py +20 -0
- tigrbl/deps/fastapi.py +45 -0
- tigrbl/deps/favicon.svg +4 -0
- tigrbl/deps/jinja.py +27 -0
- tigrbl/deps/pydantic.py +10 -0
- tigrbl/deps/sqlalchemy.py +94 -0
- tigrbl/deps/starlette.py +36 -0
- tigrbl/engine/__init__.py +26 -0
- tigrbl/engine/_engine.py +130 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +108 -0
- tigrbl/engine/engine_spec.py +261 -0
- tigrbl/engine/resolver.py +224 -0
- tigrbl/engine/shortcuts.py +216 -0
- tigrbl/hook/__init__.py +21 -0
- tigrbl/hook/_hook.py +22 -0
- tigrbl/hook/decorators.py +28 -0
- tigrbl/hook/hook_spec.py +24 -0
- tigrbl/hook/mro_collect.py +98 -0
- tigrbl/hook/shortcuts.py +44 -0
- tigrbl/hook/types.py +76 -0
- tigrbl/op/__init__.py +50 -0
- tigrbl/op/_op.py +31 -0
- tigrbl/op/canonical.py +31 -0
- tigrbl/op/collect.py +11 -0
- tigrbl/op/decorators.py +238 -0
- tigrbl/op/model_registry.py +301 -0
- tigrbl/op/mro_collect.py +99 -0
- tigrbl/op/resolver.py +216 -0
- tigrbl/op/types.py +136 -0
- tigrbl/orm/__init__.py +1 -0
- tigrbl/orm/mixins/_RowBound.py +83 -0
- tigrbl/orm/mixins/__init__.py +95 -0
- tigrbl/orm/mixins/bootstrappable.py +113 -0
- tigrbl/orm/mixins/bound.py +47 -0
- tigrbl/orm/mixins/edges.py +40 -0
- tigrbl/orm/mixins/fields.py +165 -0
- tigrbl/orm/mixins/hierarchy.py +54 -0
- tigrbl/orm/mixins/key_digest.py +44 -0
- tigrbl/orm/mixins/lifecycle.py +115 -0
- tigrbl/orm/mixins/locks.py +51 -0
- tigrbl/orm/mixins/markers.py +16 -0
- tigrbl/orm/mixins/operations.py +57 -0
- tigrbl/orm/mixins/ownable.py +337 -0
- tigrbl/orm/mixins/principals.py +98 -0
- tigrbl/orm/mixins/tenant_bound.py +301 -0
- tigrbl/orm/mixins/upsertable.py +111 -0
- tigrbl/orm/mixins/utils.py +49 -0
- tigrbl/orm/tables/__init__.py +72 -0
- tigrbl/orm/tables/_base.py +8 -0
- tigrbl/orm/tables/audit.py +56 -0
- tigrbl/orm/tables/client.py +25 -0
- tigrbl/orm/tables/group.py +29 -0
- tigrbl/orm/tables/org.py +30 -0
- tigrbl/orm/tables/rbac.py +76 -0
- tigrbl/orm/tables/status.py +106 -0
- tigrbl/orm/tables/tenant.py +22 -0
- tigrbl/orm/tables/user.py +39 -0
- tigrbl/response/README.md +34 -0
- tigrbl/response/__init__.py +33 -0
- tigrbl/response/bind.py +12 -0
- tigrbl/response/decorators.py +37 -0
- tigrbl/response/resolver.py +83 -0
- tigrbl/response/shortcuts.py +144 -0
- tigrbl/response/types.py +49 -0
- tigrbl/rest/__init__.py +27 -0
- tigrbl/runtime/README.md +129 -0
- tigrbl/runtime/__init__.py +20 -0
- tigrbl/runtime/atoms/__init__.py +102 -0
- tigrbl/runtime/atoms/emit/__init__.py +42 -0
- tigrbl/runtime/atoms/emit/paired_post.py +158 -0
- tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
- tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
- tigrbl/runtime/atoms/out/__init__.py +38 -0
- tigrbl/runtime/atoms/out/masking.py +135 -0
- tigrbl/runtime/atoms/refresh/__init__.py +38 -0
- tigrbl/runtime/atoms/refresh/demand.py +130 -0
- tigrbl/runtime/atoms/resolve/__init__.py +40 -0
- tigrbl/runtime/atoms/resolve/assemble.py +167 -0
- tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
- tigrbl/runtime/atoms/response/__init__.py +17 -0
- tigrbl/runtime/atoms/response/negotiate.py +30 -0
- tigrbl/runtime/atoms/response/negotiation.py +43 -0
- tigrbl/runtime/atoms/response/render.py +36 -0
- tigrbl/runtime/atoms/response/renderer.py +116 -0
- tigrbl/runtime/atoms/response/template.py +44 -0
- tigrbl/runtime/atoms/response/templates.py +88 -0
- tigrbl/runtime/atoms/schema/__init__.py +40 -0
- tigrbl/runtime/atoms/schema/collect_in.py +21 -0
- tigrbl/runtime/atoms/schema/collect_out.py +21 -0
- tigrbl/runtime/atoms/storage/__init__.py +38 -0
- tigrbl/runtime/atoms/storage/to_stored.py +167 -0
- tigrbl/runtime/atoms/wire/__init__.py +45 -0
- tigrbl/runtime/atoms/wire/build_in.py +166 -0
- tigrbl/runtime/atoms/wire/build_out.py +87 -0
- tigrbl/runtime/atoms/wire/dump.py +206 -0
- tigrbl/runtime/atoms/wire/validate_in.py +227 -0
- tigrbl/runtime/context.py +206 -0
- tigrbl/runtime/errors/__init__.py +61 -0
- tigrbl/runtime/errors/converters.py +214 -0
- tigrbl/runtime/errors/exceptions.py +124 -0
- tigrbl/runtime/errors/mappings.py +71 -0
- tigrbl/runtime/errors/utils.py +150 -0
- tigrbl/runtime/events.py +209 -0
- tigrbl/runtime/executor/__init__.py +6 -0
- tigrbl/runtime/executor/guards.py +132 -0
- tigrbl/runtime/executor/helpers.py +88 -0
- tigrbl/runtime/executor/invoke.py +150 -0
- tigrbl/runtime/executor/types.py +84 -0
- tigrbl/runtime/kernel.py +628 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +87 -0
- tigrbl/runtime/ordering.py +256 -0
- tigrbl/runtime/system.py +279 -0
- tigrbl/runtime/trace.py +330 -0
- tigrbl/schema/__init__.py +38 -0
- tigrbl/schema/_schema.py +27 -0
- tigrbl/schema/builder/__init__.py +17 -0
- tigrbl/schema/builder/build_schema.py +209 -0
- tigrbl/schema/builder/cache.py +24 -0
- tigrbl/schema/builder/compat.py +16 -0
- tigrbl/schema/builder/extras.py +85 -0
- tigrbl/schema/builder/helpers.py +51 -0
- tigrbl/schema/builder/list_params.py +117 -0
- tigrbl/schema/builder/strip_parent_fields.py +70 -0
- tigrbl/schema/collect.py +55 -0
- tigrbl/schema/decorators.py +68 -0
- tigrbl/schema/get_schema.py +86 -0
- tigrbl/schema/schema_spec.py +20 -0
- tigrbl/schema/shortcuts.py +42 -0
- tigrbl/schema/types.py +34 -0
- tigrbl/schema/utils.py +143 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +12 -0
- tigrbl/system/diagnostics/__init__.py +24 -0
- tigrbl/system/diagnostics/compat.py +31 -0
- tigrbl/system/diagnostics/healthz.py +41 -0
- tigrbl/system/diagnostics/hookz.py +51 -0
- tigrbl/system/diagnostics/kernelz.py +20 -0
- tigrbl/system/diagnostics/methodz.py +43 -0
- tigrbl/system/diagnostics/router.py +73 -0
- tigrbl/system/diagnostics/utils.py +43 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +237 -0
- tigrbl/table/_table.py +54 -0
- tigrbl/table/mro_collect.py +69 -0
- tigrbl/table/shortcuts.py +57 -0
- tigrbl/table/table_spec.py +28 -0
- tigrbl/transport/__init__.py +74 -0
- tigrbl/transport/jsonrpc/__init__.py +19 -0
- tigrbl/transport/jsonrpc/dispatcher.py +352 -0
- tigrbl/transport/jsonrpc/helpers.py +115 -0
- tigrbl/transport/jsonrpc/models.py +41 -0
- tigrbl/transport/rest/__init__.py +25 -0
- tigrbl/transport/rest/aggregator.py +132 -0
- tigrbl/types/__init__.py +174 -0
- tigrbl/types/allow_anon_provider.py +19 -0
- tigrbl/types/authn_abc.py +30 -0
- tigrbl/types/nested_path_provider.py +22 -0
- tigrbl/types/op.py +35 -0
- tigrbl/types/op_config_provider.py +17 -0
- tigrbl/types/op_verb_alias_provider.py +33 -0
- tigrbl/types/request_extras_provider.py +22 -0
- tigrbl/types/response_extras_provider.py +22 -0
- tigrbl/types/table_config_provider.py +13 -0
- tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
- tigrbl/ExampleAgent.py +0 -1
- tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
- tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# tigrbl/v3/transport/jsonrpc/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Tigrbl v3 – JSON-RPC transport.
|
|
4
|
+
|
|
5
|
+
Public helper:
|
|
6
|
+
- build_jsonrpc_router(
|
|
7
|
+
api, *, get_db=None, tags=("rpc",)
|
|
8
|
+
) -> Router
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from tigrbl.transport.jsonrpc import build_jsonrpc_router
|
|
12
|
+
app.include_router(build_jsonrpc_router(api), prefix="/rpc")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .dispatcher import build_jsonrpc_router
|
|
18
|
+
|
|
19
|
+
__all__ = ["build_jsonrpc_router"]
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# tigrbl/v3/transport/jsonrpc/dispatcher.py
|
|
2
|
+
"""
|
|
3
|
+
JSON-RPC 2.0 dispatcher for Tigrbl v3.
|
|
4
|
+
|
|
5
|
+
This module exposes a single helper:
|
|
6
|
+
|
|
7
|
+
build_jsonrpc_router(api, *, get_db=None) -> Router
|
|
8
|
+
|
|
9
|
+
- It mounts a POST endpoint at "/" that accepts either a single JSON-RPC request
|
|
10
|
+
object or a batch (array) of request objects.
|
|
11
|
+
- Each JSON-RPC `method` must be of the form "Model.alias". The dispatcher will
|
|
12
|
+
look up `api.models["Model"]`, then call the bound coroutine at
|
|
13
|
+
`Model.rpc.<alias>(params, *, db, request, ctx)`.
|
|
14
|
+
- Input validation and output shaping are handled by the per-op RPC wrappers
|
|
15
|
+
built in `tigrbl.bindings.rpc`.
|
|
16
|
+
- Errors are converted to JSON-RPC error objects using the v3 runtime error
|
|
17
|
+
mappers (HTTP → RPC codes).
|
|
18
|
+
|
|
19
|
+
You would usually mount the returned router at `/rpc`, e.g.:
|
|
20
|
+
|
|
21
|
+
app.include_router(build_jsonrpc_router(api), prefix="/rpc")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from typing import (
|
|
28
|
+
Any,
|
|
29
|
+
Callable,
|
|
30
|
+
Dict,
|
|
31
|
+
List,
|
|
32
|
+
Mapping,
|
|
33
|
+
Optional,
|
|
34
|
+
Sequence,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from ...types import Router, Request, Body, Depends, HTTPException, Response
|
|
39
|
+
except Exception: # pragma: no cover
|
|
40
|
+
# Minimal shims to keep this importable without FastAPI (for typing/tools)
|
|
41
|
+
class Router: # type: ignore
|
|
42
|
+
def __init__(self, *a, **kw):
|
|
43
|
+
self.routes = []
|
|
44
|
+
self.dependencies = kw.get("dependencies", []) # for parity
|
|
45
|
+
|
|
46
|
+
def add_api_route(
|
|
47
|
+
self, path: str, endpoint: Callable, methods: Sequence[str], **opts
|
|
48
|
+
):
|
|
49
|
+
self.routes.append((path, methods, endpoint, opts))
|
|
50
|
+
|
|
51
|
+
class Request: # type: ignore
|
|
52
|
+
def __init__(self, scope=None):
|
|
53
|
+
self.scope = scope or {}
|
|
54
|
+
self.state = type("S", (), {})()
|
|
55
|
+
self.query_params = {}
|
|
56
|
+
|
|
57
|
+
async def json(self) -> Any:
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
def Body(default=None, **kw): # type: ignore
|
|
61
|
+
return default
|
|
62
|
+
|
|
63
|
+
def Depends(fn): # type: ignore
|
|
64
|
+
return fn
|
|
65
|
+
|
|
66
|
+
class Response: # type: ignore
|
|
67
|
+
def __init__(self, status_code: int = 200, content: Any = None):
|
|
68
|
+
self.status_code = status_code
|
|
69
|
+
self.body = content
|
|
70
|
+
|
|
71
|
+
class HTTPException(Exception): # type: ignore
|
|
72
|
+
def __init__(self, status_code: int, detail: Any = None):
|
|
73
|
+
super().__init__(detail)
|
|
74
|
+
self.status_code = status_code
|
|
75
|
+
self.detail = detail
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
from ...runtime.errors import ERROR_MESSAGES, http_exc_to_rpc
|
|
79
|
+
from ...config.constants import TIGRBL_AUTH_CONTEXT_ATTR
|
|
80
|
+
from .models import RPCRequest, RPCResponse
|
|
81
|
+
from .helpers import (
|
|
82
|
+
_authorize,
|
|
83
|
+
_err,
|
|
84
|
+
_model_for,
|
|
85
|
+
_normalize_deps,
|
|
86
|
+
_normalize_params,
|
|
87
|
+
_ok,
|
|
88
|
+
_select_auth_dep,
|
|
89
|
+
_user_from_request,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
logger = logging.getLogger(__name__)
|
|
93
|
+
|
|
94
|
+
Json = Mapping[str, Any]
|
|
95
|
+
Batch = Sequence[Mapping[str, Any]]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _dispatch_one(
|
|
99
|
+
*,
|
|
100
|
+
api: Any,
|
|
101
|
+
request: Request,
|
|
102
|
+
db: Any,
|
|
103
|
+
obj: Mapping[str, Any],
|
|
104
|
+
) -> Optional[Dict[str, Any]]:
|
|
105
|
+
"""
|
|
106
|
+
Handle a single JSON-RPC request object and return a response dict,
|
|
107
|
+
or None if it's a "notification" (no id field).
|
|
108
|
+
"""
|
|
109
|
+
rid = obj.get("id", 1)
|
|
110
|
+
try:
|
|
111
|
+
# Basic JSON-RPC validation
|
|
112
|
+
if not isinstance(obj, Mapping):
|
|
113
|
+
return _err(-32600, "Invalid Request", rid) # not an object
|
|
114
|
+
# Be lenient: default to 2.0 when "jsonrpc" is omitted
|
|
115
|
+
if obj.get("jsonrpc", "2.0") != "2.0":
|
|
116
|
+
return _err(-32600, "Invalid Request", rid)
|
|
117
|
+
method = obj.get("method")
|
|
118
|
+
if not isinstance(method, str) or "." not in method:
|
|
119
|
+
return _err(-32601, "Method not found", rid)
|
|
120
|
+
|
|
121
|
+
model_name, alias = method.split(".", 1)
|
|
122
|
+
model = _model_for(api, model_name)
|
|
123
|
+
if model is None:
|
|
124
|
+
return _err(-32601, f"Unknown model '{model_name}'", rid)
|
|
125
|
+
|
|
126
|
+
# Locate RPC callable built by bindings.rpc
|
|
127
|
+
rpc_ns = getattr(model, "rpc", None)
|
|
128
|
+
rpc_call = getattr(rpc_ns, alias, None)
|
|
129
|
+
if rpc_call is None:
|
|
130
|
+
return _err(-32601, f"Method not found: {model_name}.{alias}", rid)
|
|
131
|
+
|
|
132
|
+
# Params
|
|
133
|
+
try:
|
|
134
|
+
params = _normalize_params(obj.get("params"))
|
|
135
|
+
except HTTPException as exc:
|
|
136
|
+
code, msg, data = http_exc_to_rpc(exc)
|
|
137
|
+
return _err(code, msg, rid, data)
|
|
138
|
+
|
|
139
|
+
# Enforce auth when required
|
|
140
|
+
if getattr(api, "_authn", None):
|
|
141
|
+
method_id = f"{model.__name__}.{alias}"
|
|
142
|
+
allow = getattr(api, "_allow_anon_ops", set())
|
|
143
|
+
user = _user_from_request(request)
|
|
144
|
+
if method_id not in allow and user is None:
|
|
145
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
146
|
+
|
|
147
|
+
# Compose a context; allow middlewares to seed request.state.ctx
|
|
148
|
+
base_ctx: Dict[str, Any] = {}
|
|
149
|
+
extra_ctx = getattr(request.state, "ctx", None)
|
|
150
|
+
if isinstance(extra_ctx, Mapping):
|
|
151
|
+
base_ctx.update(extra_ctx)
|
|
152
|
+
base_ctx.setdefault("rpc_id", rid)
|
|
153
|
+
ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
|
|
154
|
+
if ac is not None:
|
|
155
|
+
base_ctx["auth_context"] = ac
|
|
156
|
+
|
|
157
|
+
# Authorize (auth dep may already have raised; user may be on request.state)
|
|
158
|
+
_authorize(api, request, model, alias, params, _user_from_request(request))
|
|
159
|
+
|
|
160
|
+
# Execute
|
|
161
|
+
result = await rpc_call(params, db=db, request=request, ctx=base_ctx)
|
|
162
|
+
|
|
163
|
+
return _ok(result, rid)
|
|
164
|
+
|
|
165
|
+
except HTTPException as exc:
|
|
166
|
+
code, msg, data = http_exc_to_rpc(exc)
|
|
167
|
+
return _err(code, msg, rid, data)
|
|
168
|
+
except Exception:
|
|
169
|
+
logger.exception("jsonrpc dispatch failed")
|
|
170
|
+
# Internal error (per JSON-RPC); do not leak details
|
|
171
|
+
return _err(-32603, ERROR_MESSAGES.get(-32603, "Internal error"), rid)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --------------------------------------------------------------------------- #
|
|
175
|
+
# Public router factory
|
|
176
|
+
# --------------------------------------------------------------------------- #
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def build_jsonrpc_router(
|
|
180
|
+
api: Any,
|
|
181
|
+
*,
|
|
182
|
+
get_db: Optional[Callable[..., Any]] = None,
|
|
183
|
+
tags: Sequence[str] | None = ("rpc",),
|
|
184
|
+
) -> Router:
|
|
185
|
+
"""
|
|
186
|
+
Build and return a Router that serves a single POST endpoint at "/".
|
|
187
|
+
Mount it at your preferred prefix (e.g., "/rpc").
|
|
188
|
+
|
|
189
|
+
If `get_db` is provided, it will be used as a FastAPI
|
|
190
|
+
dependency for obtaining a DB session/connection. If not provided,
|
|
191
|
+
the dispatcher will try to use `request.state.db` (or pass `db=None`).
|
|
192
|
+
|
|
193
|
+
Security:
|
|
194
|
+
• If `api._authn` (or `api._optional_authn_dep`) is set, we inject it as a dependency
|
|
195
|
+
so it runs before dispatch. It may set `request.state.user` and/or raise 401.
|
|
196
|
+
• If `api._authorize` is set, we call it before executing the op; False/exception → 403.
|
|
197
|
+
• Additional router-level dependencies can be provided via `api.rpc_dependencies`.
|
|
198
|
+
|
|
199
|
+
The generated endpoint is tagged as "rpc" by default. Supply a custom
|
|
200
|
+
sequence via ``tags`` to override or set ``None`` to omit tags.
|
|
201
|
+
"""
|
|
202
|
+
# Extra router-level deps (e.g., tracing, IP allowlist)
|
|
203
|
+
extra_router_deps = _normalize_deps(getattr(api, "rpc_dependencies", None))
|
|
204
|
+
router = Router(dependencies=extra_router_deps or None)
|
|
205
|
+
|
|
206
|
+
dep = get_db
|
|
207
|
+
auth_dep = _select_auth_dep(api)
|
|
208
|
+
|
|
209
|
+
if dep is not None and auth_dep is not None:
|
|
210
|
+
# Inject both DB and user via Depends
|
|
211
|
+
async def _endpoint(
|
|
212
|
+
request: Request,
|
|
213
|
+
body: RPCRequest | list[RPCRequest] = Body(...),
|
|
214
|
+
db: Any = Depends(dep),
|
|
215
|
+
user: Any = Depends(auth_dep),
|
|
216
|
+
):
|
|
217
|
+
# set state for downstream handlers if dep returned user
|
|
218
|
+
try:
|
|
219
|
+
if user is not None and not hasattr(request.state, "user"):
|
|
220
|
+
setattr(request.state, "user", user)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
if isinstance(body, list):
|
|
225
|
+
responses: List[Dict[str, Any]] = []
|
|
226
|
+
for item in body:
|
|
227
|
+
resp = await _dispatch_one(
|
|
228
|
+
api=api, request=request, db=db, obj=item.model_dump()
|
|
229
|
+
)
|
|
230
|
+
if resp is not None:
|
|
231
|
+
responses.append(resp)
|
|
232
|
+
return responses
|
|
233
|
+
elif isinstance(body, RPCRequest):
|
|
234
|
+
resp = await _dispatch_one(
|
|
235
|
+
api=api, request=request, db=db, obj=body.model_dump()
|
|
236
|
+
)
|
|
237
|
+
if resp is None:
|
|
238
|
+
return Response(status_code=204)
|
|
239
|
+
return resp
|
|
240
|
+
else:
|
|
241
|
+
return _err(-32600, "Invalid Request", None)
|
|
242
|
+
|
|
243
|
+
elif dep is not None:
|
|
244
|
+
# Only DB dependency
|
|
245
|
+
async def _endpoint(
|
|
246
|
+
request: Request,
|
|
247
|
+
body: RPCRequest | list[RPCRequest] = Body(...),
|
|
248
|
+
db: Any = Depends(dep),
|
|
249
|
+
):
|
|
250
|
+
if isinstance(body, list):
|
|
251
|
+
responses: List[Dict[str, Any]] = []
|
|
252
|
+
for item in body:
|
|
253
|
+
resp = await _dispatch_one(
|
|
254
|
+
api=api, request=request, db=db, obj=item.model_dump()
|
|
255
|
+
)
|
|
256
|
+
if resp is not None:
|
|
257
|
+
responses.append(resp)
|
|
258
|
+
return responses
|
|
259
|
+
elif isinstance(body, RPCRequest):
|
|
260
|
+
resp = await _dispatch_one(
|
|
261
|
+
api=api, request=request, db=db, obj=body.model_dump()
|
|
262
|
+
)
|
|
263
|
+
if resp is None:
|
|
264
|
+
return Response(status_code=204)
|
|
265
|
+
return resp
|
|
266
|
+
else:
|
|
267
|
+
return _err(-32600, "Invalid Request", None)
|
|
268
|
+
|
|
269
|
+
elif auth_dep is not None:
|
|
270
|
+
# Only auth dependency; DB will come from request.state.db
|
|
271
|
+
async def _endpoint(
|
|
272
|
+
request: Request,
|
|
273
|
+
body: RPCRequest | list[RPCRequest] = Body(...),
|
|
274
|
+
user: Any = Depends(auth_dep),
|
|
275
|
+
):
|
|
276
|
+
try:
|
|
277
|
+
if user is not None and not hasattr(request.state, "user"):
|
|
278
|
+
setattr(request.state, "user", user)
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
db = getattr(request.state, "db", None)
|
|
283
|
+
if isinstance(body, list):
|
|
284
|
+
responses: List[Dict[str, Any]] = []
|
|
285
|
+
for item in body:
|
|
286
|
+
resp = await _dispatch_one(
|
|
287
|
+
api=api, request=request, db=db, obj=item.model_dump()
|
|
288
|
+
)
|
|
289
|
+
if resp is not None:
|
|
290
|
+
responses.append(resp)
|
|
291
|
+
return responses
|
|
292
|
+
elif isinstance(body, RPCRequest):
|
|
293
|
+
resp = await _dispatch_one(
|
|
294
|
+
api=api, request=request, db=db, obj=body.model_dump()
|
|
295
|
+
)
|
|
296
|
+
if resp is None:
|
|
297
|
+
return Response(status_code=204)
|
|
298
|
+
return resp
|
|
299
|
+
else:
|
|
300
|
+
return _err(-32600, "Invalid Request", None)
|
|
301
|
+
|
|
302
|
+
else:
|
|
303
|
+
# No dependencies; attempt to read db (and user) from request.state
|
|
304
|
+
async def _endpoint(
|
|
305
|
+
request: Request, body: RPCRequest | list[RPCRequest] = Body(...)
|
|
306
|
+
):
|
|
307
|
+
db = getattr(request.state, "db", None)
|
|
308
|
+
if isinstance(body, list):
|
|
309
|
+
responses: List[Dict[str, Any]] = []
|
|
310
|
+
for item in body:
|
|
311
|
+
resp = await _dispatch_one(
|
|
312
|
+
api=api, request=request, db=db, obj=item.model_dump()
|
|
313
|
+
)
|
|
314
|
+
if resp is not None:
|
|
315
|
+
responses.append(resp)
|
|
316
|
+
return responses
|
|
317
|
+
elif isinstance(body, RPCRequest):
|
|
318
|
+
resp = await _dispatch_one(
|
|
319
|
+
api=api, request=request, db=db, obj=body.model_dump()
|
|
320
|
+
)
|
|
321
|
+
if resp is None:
|
|
322
|
+
return Response(status_code=204)
|
|
323
|
+
return resp
|
|
324
|
+
else:
|
|
325
|
+
return _err(-32600, "Invalid Request", None)
|
|
326
|
+
|
|
327
|
+
# Attach routes for both "/rpc" and "/rpc/"
|
|
328
|
+
router.add_api_route(
|
|
329
|
+
path="",
|
|
330
|
+
endpoint=_endpoint,
|
|
331
|
+
methods=["POST"],
|
|
332
|
+
name="jsonrpc",
|
|
333
|
+
tags=list(tags) if tags else None,
|
|
334
|
+
summary="JSONRPC",
|
|
335
|
+
description="JSON-RPC 2.0 endpoint.",
|
|
336
|
+
response_model=RPCResponse | list[RPCResponse],
|
|
337
|
+
# extra router deps already applied via Router(dependencies=...)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Compatibility: serve same endpoint without trailing slash
|
|
341
|
+
router.add_api_route(
|
|
342
|
+
path="/",
|
|
343
|
+
endpoint=_endpoint,
|
|
344
|
+
methods=["POST"],
|
|
345
|
+
name="jsonrpc_alt",
|
|
346
|
+
include_in_schema=False,
|
|
347
|
+
response_model=RPCResponse | list[RPCResponse],
|
|
348
|
+
)
|
|
349
|
+
return router
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
__all__ = ["build_jsonrpc_router"]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping, Optional, Sequence
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from ...types import Depends, HTTPException
|
|
7
|
+
except Exception: # pragma: no cover
|
|
8
|
+
|
|
9
|
+
def Depends(fn): # type: ignore
|
|
10
|
+
return fn
|
|
11
|
+
|
|
12
|
+
class HTTPException(Exception): # type: ignore
|
|
13
|
+
def __init__(self, status_code: int, detail: Any = None):
|
|
14
|
+
super().__init__(detail)
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
self.detail = detail
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ok(result: Any, id_: Any) -> Dict[str, Any]:
|
|
20
|
+
return {"jsonrpc": "2.0", "result": result, "id": id_}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _err(code: int, msg: str, id_: Any, data: Any | None = None) -> Dict[str, Any]:
|
|
24
|
+
e: Dict[str, Any] = {
|
|
25
|
+
"jsonrpc": "2.0",
|
|
26
|
+
"error": {"code": code, "message": msg},
|
|
27
|
+
"id": id_,
|
|
28
|
+
}
|
|
29
|
+
if data is not None:
|
|
30
|
+
e["error"]["data"] = data
|
|
31
|
+
return e
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_params(params: Any) -> Any:
|
|
35
|
+
if params is None:
|
|
36
|
+
return {}
|
|
37
|
+
if isinstance(params, Mapping):
|
|
38
|
+
return dict(params)
|
|
39
|
+
if isinstance(params, Sequence) and not isinstance(params, (str, bytes)):
|
|
40
|
+
return list(params)
|
|
41
|
+
raise HTTPException(
|
|
42
|
+
status_code=400, detail="Invalid params: expected object or array"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _model_for(api: Any, name: str) -> Optional[type]:
|
|
47
|
+
models: Dict[str, type] = getattr(api, "models", {}) or {}
|
|
48
|
+
mdl = models.get(name)
|
|
49
|
+
if mdl is not None:
|
|
50
|
+
return mdl
|
|
51
|
+
lower = name.lower()
|
|
52
|
+
for k, v in models.items():
|
|
53
|
+
if k.lower() == lower:
|
|
54
|
+
return v
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _user_from_request(request: Any) -> Any | None:
|
|
59
|
+
return getattr(request.state, "user", None)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _select_auth_dep(api: Any):
|
|
63
|
+
if getattr(api, "_optional_authn_dep", None):
|
|
64
|
+
return api._optional_authn_dep
|
|
65
|
+
if getattr(api, "_allow_anon", True) is False and getattr(api, "_authn", None):
|
|
66
|
+
return api._authn
|
|
67
|
+
if getattr(api, "_authn", None):
|
|
68
|
+
return api._authn
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _normalize_deps(deps: Optional[Sequence[Any]]) -> list:
|
|
73
|
+
out = []
|
|
74
|
+
for d in deps or ():
|
|
75
|
+
try:
|
|
76
|
+
is_dep_obj = hasattr(d, "dependency")
|
|
77
|
+
except Exception:
|
|
78
|
+
is_dep_obj = False
|
|
79
|
+
out.append(d if is_dep_obj else Depends(d))
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _authorize(
|
|
84
|
+
api: Any,
|
|
85
|
+
request: Any,
|
|
86
|
+
model: type,
|
|
87
|
+
alias: str,
|
|
88
|
+
payload: Mapping[str, Any],
|
|
89
|
+
user: Any | None,
|
|
90
|
+
):
|
|
91
|
+
fn = getattr(api, "_authorize", None) or getattr(
|
|
92
|
+
model, "__tigrbl_authorize__", None
|
|
93
|
+
)
|
|
94
|
+
if not fn:
|
|
95
|
+
return
|
|
96
|
+
try:
|
|
97
|
+
rv = fn(request=request, model=model, alias=alias, payload=payload, user=user)
|
|
98
|
+
if rv is False:
|
|
99
|
+
raise HTTPException(status_code=403, detail="Forbidden")
|
|
100
|
+
except HTTPException:
|
|
101
|
+
raise
|
|
102
|
+
except Exception:
|
|
103
|
+
raise HTTPException(status_code=403, detail="Forbidden")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = [
|
|
107
|
+
"_ok",
|
|
108
|
+
"_err",
|
|
109
|
+
"_normalize_params",
|
|
110
|
+
"_model_for",
|
|
111
|
+
"_user_from_request",
|
|
112
|
+
"_select_auth_dep",
|
|
113
|
+
"_normalize_deps",
|
|
114
|
+
"_authorize",
|
|
115
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
from uuid import UUID, uuid4
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _uuid_examples(schema: dict[str, Any]) -> None:
|
|
10
|
+
"""Populate schema examples with a random UUID."""
|
|
11
|
+
schema["examples"] = [str(uuid4())]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RPCRequest(BaseModel):
|
|
15
|
+
"""JSON-RPC 2.0 request envelope."""
|
|
16
|
+
|
|
17
|
+
jsonrpc: Literal["2.0"] = "2.0"
|
|
18
|
+
method: str
|
|
19
|
+
params: dict[str, Any] | list[Any] = Field(default_factory=dict)
|
|
20
|
+
id: UUID | str | int | None = Field(
|
|
21
|
+
default_factory=uuid4,
|
|
22
|
+
json_schema_extra=_uuid_examples,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RPCError(BaseModel):
|
|
27
|
+
code: int
|
|
28
|
+
message: str
|
|
29
|
+
data: Any | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RPCResponse(BaseModel):
|
|
33
|
+
"""JSON-RPC 2.0 response envelope."""
|
|
34
|
+
|
|
35
|
+
jsonrpc: Literal["2.0"] = "2.0"
|
|
36
|
+
result: Any | None = None
|
|
37
|
+
error: RPCError | None = None
|
|
38
|
+
id: UUID | str | int | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
json_schema_extra=_uuid_examples,
|
|
41
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# tigrbl/v3/transport/rest/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Tigrbl v3 – REST transport wrapper.
|
|
4
|
+
|
|
5
|
+
Use this when you prefer to mount a single top-level router that aggregates all
|
|
6
|
+
model routers (instead of mounting each one inside include_model).
|
|
7
|
+
|
|
8
|
+
Typical usage:
|
|
9
|
+
from tigrbl.transport.rest import build_rest_router, mount_rest
|
|
10
|
+
|
|
11
|
+
# When including models, skip mounting per-model:
|
|
12
|
+
api.include_model(User, mount_router=False)
|
|
13
|
+
api.include_model(Team, mount_router=False)
|
|
14
|
+
|
|
15
|
+
# Then aggregate & mount once:
|
|
16
|
+
app.include_router(build_rest_router(api, base_prefix="/api"))
|
|
17
|
+
# or:
|
|
18
|
+
mount_rest(api, app, base_prefix="/api")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .aggregator import build_rest_router, mount_rest
|
|
24
|
+
|
|
25
|
+
__all__ = ["build_rest_router", "mount_rest"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# tigrbl/v3/transport/rest/aggregator.py
|
|
2
|
+
"""
|
|
3
|
+
Aggregates per-model REST routers into a single Router.
|
|
4
|
+
|
|
5
|
+
This does not build endpoints by itself — it simply collects the routers that
|
|
6
|
+
`tigrbl.bindings.rest` attached to each model at `model.rest.router`.
|
|
7
|
+
|
|
8
|
+
Recommended workflow:
|
|
9
|
+
1) Include models with `mount_router=False` so you don't double-mount:
|
|
10
|
+
api.include_model(User, mount_router=False)
|
|
11
|
+
api.include_model(Team, mount_router=False)
|
|
12
|
+
2) Aggregate and mount once:
|
|
13
|
+
app.include_router(build_rest_router(api, base_prefix="/api"))
|
|
14
|
+
or:
|
|
15
|
+
mount_rest(api, app, base_prefix="/api")
|
|
16
|
+
|
|
17
|
+
Notes:
|
|
18
|
+
• Router paths already include `/{resource}`; we only add `base_prefix`.
|
|
19
|
+
• Model-level auth/db deps and extra REST deps are already attached to each
|
|
20
|
+
model router by `bindings.rest`; this wrapper can add *additional* top-level
|
|
21
|
+
dependencies if you pass them in.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from ...types import Router, Depends
|
|
30
|
+
except Exception: # pragma: no cover
|
|
31
|
+
# Minimal shim to keep importable without FastAPI
|
|
32
|
+
class Router: # type: ignore
|
|
33
|
+
def __init__(self, *a, dependencies: Optional[Sequence[Any]] = None, **kw):
|
|
34
|
+
self.routes = []
|
|
35
|
+
self.includes = []
|
|
36
|
+
self.dependencies = list(dependencies or [])
|
|
37
|
+
|
|
38
|
+
def add_api_route(self, path: str, endpoint, methods: Sequence[str], **opts):
|
|
39
|
+
self.routes.append((path, methods, endpoint, opts))
|
|
40
|
+
|
|
41
|
+
def include_router(self, router: "Router", *, prefix: str = "", **opts):
|
|
42
|
+
self.includes.append((router, prefix, opts))
|
|
43
|
+
|
|
44
|
+
def Depends(fn): # type: ignore
|
|
45
|
+
return fn
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _norm_prefix(p: Optional[str]) -> str:
|
|
49
|
+
if not p:
|
|
50
|
+
return ""
|
|
51
|
+
if not p.startswith("/"):
|
|
52
|
+
p = "/" + p
|
|
53
|
+
# Avoid double trailing slashes; FastAPI is lenient but keep it clean
|
|
54
|
+
return p.rstrip("/")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _normalize_deps(deps: Optional[Sequence[Any]]) -> list:
|
|
58
|
+
"""Accept either Depends(...) objects or plain callables."""
|
|
59
|
+
out = []
|
|
60
|
+
for d in deps or ():
|
|
61
|
+
try:
|
|
62
|
+
is_dep_obj = hasattr(d, "dependency")
|
|
63
|
+
except Exception:
|
|
64
|
+
is_dep_obj = False
|
|
65
|
+
out.append(d if is_dep_obj else Depends(d))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _iter_models(api: Any, only: Optional[Sequence[type]] = None) -> Sequence[type]:
|
|
70
|
+
if only:
|
|
71
|
+
return list(only)
|
|
72
|
+
models: Mapping[str, type] = getattr(api, "models", {}) or {}
|
|
73
|
+
# deterministic iteration
|
|
74
|
+
return [models[k] for k in sorted(models.keys())]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_rest_router(
|
|
78
|
+
api: Any,
|
|
79
|
+
*,
|
|
80
|
+
models: Optional[Sequence[type]] = None,
|
|
81
|
+
base_prefix: str = "",
|
|
82
|
+
dependencies: Optional[Sequence[Any]] = None,
|
|
83
|
+
) -> Router:
|
|
84
|
+
"""
|
|
85
|
+
Build a top-level Router that includes each model's router under `base_prefix`.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
api: your Tigrbl facade (or any object with `.models` dict).
|
|
89
|
+
models: optional subset of models to include; defaults to all bound models.
|
|
90
|
+
base_prefix: prefix applied once for all included routers (e.g., "/api").
|
|
91
|
+
dependencies: additional router-level dependencies (Depends(...) or callables).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Router ready to be mounted on your FastAPI app.
|
|
95
|
+
"""
|
|
96
|
+
root = Router(dependencies=_normalize_deps(dependencies))
|
|
97
|
+
prefix = _norm_prefix(base_prefix)
|
|
98
|
+
|
|
99
|
+
for model in _iter_models(api, models):
|
|
100
|
+
rest_ns = getattr(model, "rest", None)
|
|
101
|
+
router = getattr(rest_ns, "router", None) if rest_ns is not None else None
|
|
102
|
+
if router is None:
|
|
103
|
+
# Nothing to include for this model (not bound or no routes)
|
|
104
|
+
continue
|
|
105
|
+
# Include with only the base prefix; the model router already has /{resource} in its paths
|
|
106
|
+
root.include_router(router, prefix=prefix or "")
|
|
107
|
+
return root
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def mount_rest(
|
|
111
|
+
api: Any,
|
|
112
|
+
app: Any,
|
|
113
|
+
*,
|
|
114
|
+
models: Optional[Sequence[type]] = None,
|
|
115
|
+
base_prefix: str = "",
|
|
116
|
+
dependencies: Optional[Sequence[Any]] = None,
|
|
117
|
+
) -> Router:
|
|
118
|
+
"""
|
|
119
|
+
Convenience helper: build the aggregated router and include it on `app`.
|
|
120
|
+
|
|
121
|
+
Returns the created router so you can keep a reference if desired.
|
|
122
|
+
"""
|
|
123
|
+
router = build_rest_router(
|
|
124
|
+
api, models=models, base_prefix=base_prefix, dependencies=dependencies
|
|
125
|
+
)
|
|
126
|
+
include = getattr(app, "include_router", None)
|
|
127
|
+
if callable(include):
|
|
128
|
+
include(router)
|
|
129
|
+
return router
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["build_rest_router", "mount_rest"]
|