tba-app-backend-core 0.4.0.dev2__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.
Files changed (30) hide show
  1. tba_app_backend_core/__init__.py +15 -0
  2. tba_app_backend_core/app.py +146 -0
  3. tba_app_backend_core/plugin.py +36 -0
  4. tba_app_backend_core/product.py +55 -0
  5. tba_app_backend_core/py.typed +0 -0
  6. tba_app_backend_core/surfaces/__init__.py +3 -0
  7. tba_app_backend_core/surfaces/admin_routes.py +353 -0
  8. tba_app_backend_core/surfaces/advanced_protocols.py +170 -0
  9. tba_app_backend_core/surfaces/auth_flows.py +16 -0
  10. tba_app_backend_core/surfaces/authorization_server_metadata_surface.py +37 -0
  11. tba_app_backend_core/surfaces/authorization_surface.py +15 -0
  12. tba_app_backend_core/surfaces/authz_surface.py +14 -0
  13. tba_app_backend_core/surfaces/client_registration_surface.py +366 -0
  14. tba_app_backend_core/surfaces/device_authorization_surface.py +44 -0
  15. tba_app_backend_core/surfaces/introspection_surface.py +247 -0
  16. tba_app_backend_core/surfaces/login_surface.py +25 -0
  17. tba_app_backend_core/surfaces/logout_surface.py +38 -0
  18. tba_app_backend_core/surfaces/oidc_discovery_surface.py +79 -0
  19. tba_app_backend_core/surfaces/par_surface.py +394 -0
  20. tba_app_backend_core/surfaces/protected_resource_metadata_surface.py +34 -0
  21. tba_app_backend_core/surfaces/resource_validation_metadata_surface.py +33 -0
  22. tba_app_backend_core/surfaces/revocation_surface.py +39 -0
  23. tba_app_backend_core/surfaces/standards_manifest.py +24 -0
  24. tba_app_backend_core/surfaces/surface_api.py +284 -0
  25. tba_app_backend_core/surfaces/token_exchange_surface.py +34 -0
  26. tba_app_backend_core/surfaces/token_surface.py +12 -0
  27. tba_app_backend_core/surfaces/userinfo_surface.py +34 -0
  28. tba_app_backend_core-0.4.0.dev2.dist-info/METADATA +60 -0
  29. tba_app_backend_core-0.4.0.dev2.dist-info/RECORD +30 -0
  30. tba_app_backend_core-0.4.0.dev2.dist-info/WHEEL +4 -0
@@ -0,0 +1,15 @@
1
+ """Shared backend-application composition APIs."""
2
+
3
+ from .app import app, build_app, build_application_runtime_plan
4
+ from .plugin import TbaBackendAppPlugin, install
5
+ from .product import build_product_app, validate_backend_app_contract
6
+
7
+ __all__ = [
8
+ "app",
9
+ "build_app",
10
+ "build_application_runtime_plan",
11
+ "TbaBackendAppPlugin",
12
+ "build_product_app",
13
+ "install",
14
+ "validate_backend_app_contract",
15
+ ]
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ from importlib.metadata import (
5
+ PackageNotFoundError,
6
+ version as installed_package_version,
7
+ )
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING
10
+
11
+ from tba_runtime_identity.deployment import ResolvedDeployment, resolve_deployment
12
+ from tba_operator_identity.repo_truth import (
13
+ package_version as repository_package_version,
14
+ )
15
+ from tba_runtime_identity import LazyASGIApplication
16
+ from tba_server_identity.composition.app import (
17
+ _load_default_settings,
18
+ build_application_runtime_plan as _build_application_runtime_plan,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from tigrbl import TigrblApp
23
+ from tba_server_identity.composition.lifecycle import AssemblyFactory
24
+
25
+
26
+ def _runtime_package_version() -> str:
27
+ try:
28
+ return installed_package_version("tba-app-backend-core")
29
+ except PackageNotFoundError:
30
+ return repository_package_version(Path(__file__).resolve().parents[2])
31
+
32
+
33
+ def _path_has_prefix(path: str, prefix: str) -> bool:
34
+ return path == prefix or path.startswith(f"{prefix}/")
35
+
36
+
37
+ def _hide_disabled_control_plane_docs(
38
+ app: object,
39
+ deployment: ResolvedDeployment,
40
+ *,
41
+ diagnostics_prefix: str,
42
+ ) -> None:
43
+ routes = getattr(app, "_routes", None)
44
+ if not isinstance(routes, list):
45
+ return
46
+
47
+ hidden = []
48
+ for route in routes:
49
+ path = str(getattr(route, "path_template", None) or getattr(route, "path", ""))
50
+ hide = not deployment.flag_enabled(
51
+ "surface_diagnostics_enabled"
52
+ ) and _path_has_prefix(path, diagnostics_prefix)
53
+ if hide and hasattr(route, "include_in_schema"):
54
+ hidden.append(replace(route, include_in_schema=False))
55
+ else:
56
+ hidden.append(route)
57
+ routes[:] = hidden
58
+
59
+
60
+ def build_app(
61
+ settings_obj: object | None = None,
62
+ *,
63
+ deployment: ResolvedDeployment | None = None,
64
+ runtime_assembly_factory: AssemblyFactory | None = None,
65
+ ) -> "TigrblApp":
66
+ resolved_settings = (
67
+ settings_obj if settings_obj is not None else _load_default_settings()
68
+ )
69
+ resolved_deployment = deployment or resolve_deployment(resolved_settings)
70
+ if runtime_assembly_factory is None:
71
+ from tba_server_identity.composition.runtime_assembly import (
72
+ build_server_runtime_assembly_async,
73
+ )
74
+
75
+ async def default_runtime_assembly_factory():
76
+ return await build_server_runtime_assembly_async(resolved_settings)
77
+
78
+ runtime_assembly_factory = default_runtime_assembly_factory
79
+
80
+ from tba_contracts_identity.protocol_configuration import (
81
+ ProtocolSettingsOverlay,
82
+ bind_protocol_settings,
83
+ )
84
+
85
+ protocol_settings = ProtocolSettingsOverlay(
86
+ resolved_settings, _load_default_settings()
87
+ )
88
+ bind_protocol_settings(protocol_settings)
89
+
90
+ from tigrbl import TigrblApp
91
+
92
+ from tba_server_identity.composition.lifecycle import register_lifecycle
93
+ from tba_app_backend_core.surfaces import (
94
+ admin_resource_path_prefixes,
95
+ attach_runtime_surfaces,
96
+ )
97
+ from tba_router_administration_gate import AdminGate
98
+ from tba_runtime_identity_storage.engine import dsn
99
+
100
+ app = TigrblApp(
101
+ title="tba_auth",
102
+ version=_runtime_package_version(),
103
+ openapi_url="/openapi.json",
104
+ docs_url="/docs",
105
+ engine=dsn,
106
+ )
107
+ state = getattr(app, "state", None)
108
+ if state is not None:
109
+ state.tba_auth_deployment = resolved_deployment
110
+ state.tba_auth_settings = resolved_settings
111
+ surface_router = attach_runtime_surfaces(
112
+ app,
113
+ resolved_settings,
114
+ deployment=resolved_deployment,
115
+ diagnostics_prefix="/system",
116
+ )
117
+ _hide_disabled_control_plane_docs(
118
+ app,
119
+ resolved_deployment,
120
+ diagnostics_prefix="/system",
121
+ )
122
+ register_lifecycle(
123
+ app,
124
+ assembly_factory=runtime_assembly_factory,
125
+ surface_initializer=surface_router.initialize,
126
+ )
127
+ return AdminGate(
128
+ app,
129
+ deployment=resolved_deployment,
130
+ settings_obj=resolved_settings,
131
+ admin_path_prefixes=admin_resource_path_prefixes(resolved_deployment),
132
+ diagnostics_prefix="/system",
133
+ )
134
+
135
+
136
+ def build_application_runtime_plan(*args: object, **kwargs: object):
137
+ kwargs.setdefault(
138
+ "app_factory",
139
+ "tba_app_backend_core.app.build_app",
140
+ )
141
+ return _build_application_runtime_plan(*args, **kwargs)
142
+
143
+
144
+ app = LazyASGIApplication(build_app)
145
+
146
+ __all__ = ["app", "build_app", "build_application_runtime_plan"]
@@ -0,0 +1,36 @@
1
+ """Layer-90 host-application plugin with HTTP surface ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from tba_plugin_tigrbl import TbaPlugin as RuntimePlugin
8
+
9
+ from .surfaces import attach_runtime_surfaces
10
+
11
+ if TYPE_CHECKING:
12
+ from tigrbl import TigrblApp
13
+
14
+
15
+ class TbaBackendAppPlugin(RuntimePlugin):
16
+ def __init__(
17
+ self,
18
+ *,
19
+ diagnostics_prefix: str = "/system",
20
+ settings: object | None = None,
21
+ ) -> None:
22
+ super().__init__(
23
+ surface_attacher=attach_runtime_surfaces,
24
+ diagnostics_prefix=diagnostics_prefix,
25
+ settings=settings,
26
+ )
27
+
28
+
29
+ def install(app: "TigrblApp", settings: object | None = None) -> "TigrblApp":
30
+ return TbaBackendAppPlugin(settings=settings).install(
31
+ app,
32
+ settings=settings,
33
+ )
34
+
35
+
36
+ __all__ = ["TbaBackendAppPlugin", "install"]
@@ -0,0 +1,55 @@
1
+ """Product-specific backend application assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from tba_runtime_identity.deployment import (
8
+ PRODUCT_SURFACE_REGISTRY,
9
+ resolve_deployment,
10
+ )
11
+
12
+ from .app import build_app
13
+
14
+ if TYPE_CHECKING:
15
+ from tigrbl import TigrblApp
16
+
17
+
18
+ def validate_backend_app_contract(contract: object, deployment: object) -> None:
19
+ expected_surface = getattr(contract, "product_surface", None)
20
+ actual_surface = getattr(deployment, "product_surface", None)
21
+ if expected_surface != actual_surface:
22
+ raise ValueError(
23
+ "backend-app contract product surface does not match deployment: "
24
+ f"{expected_surface!r} != {actual_surface!r}"
25
+ )
26
+
27
+ declared_routers = tuple(getattr(contract, "mounted_router_packages", ()))
28
+ resolved_routers = tuple(getattr(deployment, "router_packages", ()))
29
+ if declared_routers != resolved_routers:
30
+ raise ValueError(
31
+ "backend-app contract router closure does not match deployment: "
32
+ f"{declared_routers!r} != {resolved_routers!r}"
33
+ )
34
+
35
+
36
+ def build_product_app(
37
+ product_surface: str,
38
+ settings_obj: object | None = None,
39
+ *,
40
+ contract: object | None = None,
41
+ ) -> "TigrblApp":
42
+ """Build the shared ASGI app constrained to one declared product surface."""
43
+ if product_surface not in PRODUCT_SURFACE_REGISTRY:
44
+ raise ValueError(f"unknown tba_auth product surface: {product_surface}")
45
+ deployment = resolve_deployment(
46
+ settings_obj,
47
+ product_surface=product_surface,
48
+ runtime_style="standalone",
49
+ )
50
+ if contract is not None:
51
+ validate_backend_app_contract(contract, deployment)
52
+ return build_app(settings_obj, deployment=deployment)
53
+
54
+
55
+ __all__ = ["build_product_app", "validate_backend_app_contract"]
File without changes
@@ -0,0 +1,3 @@
1
+ """HTTP surface and router composition owned by layer 90."""
2
+
3
+ from .surface_api import *
@@ -0,0 +1,353 @@
1
+ """Executable release-path surface composition for Tigrbl-auth.
2
+
3
+ The capability identifiers used here must stay synchronized with
4
+ ``tba_auth.config.surfaces.PUBLIC_CAPABILITIES`` so deployment resolution,
5
+ runtime mounting, contract generation, and verification all observe the same
6
+ installed surface shape.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import replace
12
+ import re
13
+ from typing import Any, Final
14
+
15
+ from tigrbl import TigrblRouter
16
+ from tba_server_identity.admin_auth import admin_api as admin_auth_api
17
+ from tba_runtime_identity.deployment import ResolvedDeployment, resolve_deployment
18
+ from tba_router_administration_gate import ADMIN_OPENAPI_SECURITY_DEPENDENCIES
19
+ from tba_storage_core import ensure_identity_storage_importable
20
+ from tba_storage_core.tables import (
21
+ AuditEvent,
22
+ AuthCode,
23
+ AuthSession,
24
+ Client,
25
+ ClientRegistration,
26
+ Consent,
27
+ CredentialApiKey,
28
+ CredentialServiceKey,
29
+ CryptoKey,
30
+ CryptoKeyVersion,
31
+ KeyAttestationEvidence,
32
+ KeyEnvelope,
33
+ KeyRotationEvent,
34
+ LogoutState,
35
+ PrincipalKeyBinding,
36
+ PushedAuthorizationRequest,
37
+ Realm,
38
+ RevokedToken,
39
+ ServiceIdentity,
40
+ Tenant,
41
+ TokenRecord,
42
+ User,
43
+ )
44
+ from tba_runtime_identity_storage.engine import dsn
45
+
46
+ ensure_identity_storage_importable()
47
+
48
+ TABLE_RESOURCES = [
49
+ Realm,
50
+ Tenant,
51
+ User,
52
+ Client,
53
+ ClientRegistration,
54
+ CredentialApiKey,
55
+ ServiceIdentity,
56
+ CredentialServiceKey,
57
+ CryptoKey,
58
+ CryptoKeyVersion,
59
+ PrincipalKeyBinding,
60
+ KeyEnvelope,
61
+ KeyAttestationEvidence,
62
+ AuthSession,
63
+ AuthCode,
64
+ TokenRecord,
65
+ RevokedToken,
66
+ Consent,
67
+ LogoutState,
68
+ KeyRotationEvent,
69
+ AuditEvent,
70
+ PushedAuthorizationRequest,
71
+ ]
72
+
73
+ ADMIN_ROUTER_BINDINGS: Final[tuple[dict[str, Any], ...]] = (
74
+ {"mount_group": "admin_auth", "router": admin_auth_api},
75
+ )
76
+
77
+
78
+ class TableInitializationScopeError(ValueError):
79
+ """Raised when a product surface selects table resources outside its contract."""
80
+
81
+
82
+ def _admin_table_resources(
83
+ deployment: ResolvedDeployment | None = None,
84
+ ) -> tuple[type[Any], ...]:
85
+ if deployment is None or getattr(deployment, "product_surface", None) is None:
86
+ return tuple(TABLE_RESOURCES)
87
+ required = getattr(deployment, "required_table_resources", ())
88
+ if required:
89
+ resources_by_name = {
90
+ resource.__name__: resource for resource in TABLE_RESOURCES
91
+ }
92
+ return tuple(
93
+ resources_by_name[name]
94
+ for name in tuple(str(item) for item in required)
95
+ if name in resources_by_name
96
+ )
97
+ return tuple(
98
+ resource
99
+ for resource in TABLE_RESOURCES
100
+ if deployment.admin_resource_enabled(resource.__name__)
101
+ )
102
+
103
+
104
+ def _resource_name(resource: type[Any] | str) -> str:
105
+ return resource if isinstance(resource, str) else resource.__name__
106
+
107
+
108
+ def required_table_resource_names(
109
+ deployment: ResolvedDeployment | None = None,
110
+ ) -> tuple[str, ...]:
111
+ return tuple(resource.__name__ for resource in _admin_table_resources(deployment))
112
+
113
+
114
+ def table_initialization_manifest(
115
+ deployment: ResolvedDeployment | None = None,
116
+ ) -> dict[str, Any]:
117
+ return {
118
+ "product_surface": getattr(deployment, "product_surface", None),
119
+ "required_table_resources": list(required_table_resource_names(deployment)),
120
+ }
121
+
122
+
123
+ def assert_table_initialization_scope(
124
+ deployment: ResolvedDeployment,
125
+ selected_resources: tuple[type[Any] | str, ...],
126
+ ) -> None:
127
+ if deployment.product_surface is None:
128
+ return
129
+ expected = set(required_table_resource_names(deployment))
130
+ selected = set(_resource_name(resource) for resource in selected_resources)
131
+ unexpected = sorted(selected - expected)
132
+ missing = sorted(expected - selected)
133
+ if unexpected or missing:
134
+ raise TableInitializationScopeError(
135
+ "product surface table initialization mismatch: "
136
+ f"unexpected={unexpected}, missing={missing}"
137
+ )
138
+
139
+
140
+ def _admin_resource_path(
141
+ resource: type[Any], deployment: ResolvedDeployment | None = None
142
+ ) -> str:
143
+ if getattr(deployment, "product_surface", None) == "platform-admin-app":
144
+ if resource.__name__ == "Realm":
145
+ return "/admin/realm"
146
+ if resource.__name__ == "Tenant":
147
+ return "/admin/tenant"
148
+ if resource.__name__ == "User":
149
+ return "/admin/identity"
150
+ return f"/{resource.__name__.lower()}"
151
+
152
+
153
+ def admin_resource_path_prefixes(
154
+ deployment: ResolvedDeployment | None = None,
155
+ ) -> tuple[str, ...]:
156
+ return tuple(
157
+ _admin_resource_path(resource, deployment)
158
+ for resource in _admin_table_resources(deployment)
159
+ )
160
+
161
+
162
+ def _path_has_prefix(path: str, prefix: str) -> bool:
163
+ return path == prefix or path.startswith(f"{prefix}/")
164
+
165
+
166
+ def _requires_admin_security_metadata(
167
+ path: str,
168
+ deployment: ResolvedDeployment,
169
+ *,
170
+ diagnostics_prefix: str,
171
+ ) -> bool:
172
+ if deployment.flag_enabled("surface_admin_enabled") and any(
173
+ _path_has_prefix(path, prefix)
174
+ for prefix in admin_resource_path_prefixes(deployment)
175
+ ):
176
+ return True
177
+ if deployment.flag_enabled("surface_diagnostics_enabled") and _path_has_prefix(
178
+ path, diagnostics_prefix
179
+ ):
180
+ return True
181
+ return False
182
+
183
+
184
+ def _attach_admin_security_metadata(
185
+ router: TigrblRouter,
186
+ deployment: ResolvedDeployment,
187
+ *,
188
+ diagnostics_prefix: str,
189
+ ) -> None:
190
+ secured_routes = []
191
+ for route in getattr(router, "_routes", []):
192
+ route_path = getattr(route, "path_template", None) or getattr(route, "path", "")
193
+ if not _requires_admin_security_metadata(
194
+ str(route_path),
195
+ deployment,
196
+ diagnostics_prefix=diagnostics_prefix,
197
+ ):
198
+ secured_routes.append(route)
199
+ continue
200
+
201
+ existing = list(getattr(route, "dependencies", None) or [])
202
+ secured_routes.append(
203
+ replace(
204
+ route,
205
+ dependencies=existing + list(ADMIN_OPENAPI_SECURITY_DEPENDENCIES),
206
+ )
207
+ )
208
+ router._routes[:] = secured_routes
209
+
210
+
211
+ def _route_pattern(path_template: str, param_names: tuple[str, ...]) -> re.Pattern[str]:
212
+ pattern = re.escape(path_template)
213
+ for name in param_names:
214
+ pattern = pattern.replace(r"\{" + name + r"\}", f"(?P<{name}>[^/]+)")
215
+ return re.compile(f"^{pattern}/?$")
216
+
217
+
218
+ def _rewrite_model_rest_bindings(
219
+ model: type[Any],
220
+ *,
221
+ old_prefix: str,
222
+ new_prefix: str,
223
+ ) -> None:
224
+ ops = getattr(model, "ops", None)
225
+ by_alias = getattr(ops, "by_alias", None)
226
+ if not isinstance(by_alias, dict):
227
+ return
228
+
229
+ for alias, specs in list(by_alias.items()):
230
+ rewritten_specs = []
231
+ changed = False
232
+ for spec in tuple(specs or ()):
233
+ next_bindings = []
234
+ spec_changed = False
235
+ for binding in tuple(getattr(spec, "bindings", ()) or ()):
236
+ path = getattr(binding, "path", None)
237
+ if isinstance(path, str) and path.startswith(old_prefix):
238
+ next_path = f"{new_prefix}{path[len(old_prefix) :]}"
239
+ next_bindings.append(replace(binding, path=next_path))
240
+ spec_changed = True
241
+ else:
242
+ next_bindings.append(binding)
243
+ if spec_changed:
244
+ rewritten_specs.append(replace(spec, bindings=tuple(next_bindings)))
245
+ changed = True
246
+ else:
247
+ rewritten_specs.append(spec)
248
+ if changed:
249
+ try:
250
+ by_alias[alias] = type(specs)(tuple(rewritten_specs))
251
+ except TypeError:
252
+ by_alias[alias] = tuple(rewritten_specs)
253
+
254
+
255
+ def _rewrite_admin_table_routes(
256
+ router: TigrblRouter,
257
+ deployment: ResolvedDeployment,
258
+ ) -> None:
259
+ if deployment.product_surface != "platform-admin-app":
260
+ return
261
+ rewrites = {
262
+ "Tenant": ("/tenant", "/admin/tenant"),
263
+ "User": ("/user", "/admin/identity"),
264
+ }
265
+ for resource in _admin_table_resources(deployment):
266
+ rewrite = rewrites.get(resource.__name__)
267
+ if rewrite is not None:
268
+ old_prefix, new_prefix = rewrite
269
+ _rewrite_model_rest_bindings(
270
+ resource,
271
+ old_prefix=old_prefix,
272
+ new_prefix=new_prefix,
273
+ )
274
+ rewritten = []
275
+ for route in getattr(router, "_routes", []):
276
+ model = getattr(route, "tigrbl_model", None)
277
+ resource_name = getattr(model, "__name__", "")
278
+ rewrite = rewrites.get(resource_name)
279
+ path_template = str(getattr(route, "path_template", "") or "")
280
+ if rewrite is None or not path_template.startswith(rewrite[0]):
281
+ rewritten.append(route)
282
+ continue
283
+ old_prefix, new_prefix = rewrite
284
+ next_path = f"{new_prefix}{path_template[len(old_prefix) :]}"
285
+ binding = getattr(route, "tigrbl_binding", None)
286
+ if binding is not None and hasattr(binding, "path"):
287
+ binding = replace(binding, path=next_path)
288
+ _rewrite_model_rest_bindings(
289
+ model,
290
+ old_prefix=old_prefix,
291
+ new_prefix=new_prefix,
292
+ )
293
+ rewritten.append(
294
+ replace(
295
+ route,
296
+ path_template=next_path,
297
+ pattern=_route_pattern(
298
+ next_path, tuple(getattr(route, "param_names", ()) or ())
299
+ ),
300
+ tigrbl_binding=binding,
301
+ )
302
+ )
303
+ router._routes[:] = rewritten
304
+
305
+
306
+ def _as_deployment(
307
+ settings_obj: object | None = None,
308
+ *,
309
+ deployment: ResolvedDeployment | None = None,
310
+ ) -> ResolvedDeployment:
311
+ return deployment or resolve_deployment(settings_obj)
312
+
313
+
314
+ def include_admin_routes(
315
+ router: TigrblRouter,
316
+ settings_obj: object | None = None,
317
+ *,
318
+ deployment: ResolvedDeployment | None = None,
319
+ ) -> tuple[type[Any], ...]:
320
+ deployment = _as_deployment(settings_obj, deployment=deployment)
321
+ selected_table_resources: tuple[type[Any], ...] = ()
322
+ if deployment.flag_enabled("surface_admin_enabled"):
323
+ selected_table_resources = _admin_table_resources(deployment)
324
+ if selected_table_resources:
325
+ if deployment.product_surface == "platform-admin-app":
326
+ for resource in selected_table_resources:
327
+ if resource.__name__ in {"Realm", "Tenant"}:
328
+ continue
329
+ router.include_table(resource)
330
+ else:
331
+ router.include_tables(selected_table_resources)
332
+ _rewrite_admin_table_routes(router, deployment)
333
+ for entry in ADMIN_ROUTER_BINDINGS:
334
+ if deployment.admin_rest_group_enabled(str(entry["mount_group"])):
335
+ router.include_router(entry["router"])
336
+ return selected_table_resources
337
+
338
+
339
+ def build_admin_router(
340
+ settings_obj: object | None = None,
341
+ *,
342
+ deployment: ResolvedDeployment | None = None,
343
+ ) -> TigrblRouter:
344
+ deployment = _as_deployment(settings_obj, deployment=deployment)
345
+ router = TigrblRouter(engine=dsn)
346
+ selected_table_resources = include_admin_routes(router, deployment=deployment)
347
+ assert_table_initialization_scope(deployment, selected_table_resources)
348
+ return router
349
+
350
+
351
+ AdminRouter = build_admin_router(
352
+ deployment=resolve_deployment(plugin_mode="admin-only")
353
+ )