xtr-http-kernel 4.1.0__tar.gz → 5.0.0__tar.gz
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.
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/PKG-INFO +116 -89
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/README.md +108 -81
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/pyproject.toml +12 -12
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/pyproject.toml.orig +12 -12
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/SKILL.md +61 -42
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/routing.md +59 -0
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/_staging.py +175 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/__init__.py +8 -3
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/bridge/_media_types.py +38 -0
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/bridge/_server_params.py +39 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/http_message_factory.py +82 -74
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/http_message_factory_interface.py +23 -0
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/bridge/served_connection.py +279 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/__init__.py +2 -2
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/_messages.py +40 -22
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/http_kernel_bundle.py +19 -28
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/request_lifecycle_middleware_factory.py +19 -6
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/bundle/route_registry_pass.py +59 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/exception_event.py +25 -5
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/finish_request_event.py +19 -5
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/request_event.py +19 -5
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/response_event.py +25 -6
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/terminate_event.py +26 -5
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/error_logging_listener.py +2 -2
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/rate_limit_headers_listener.py +9 -9
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/request_id_listener.py +9 -10
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/request_lifecycle_middleware.py +93 -24
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/__init__.py +22 -0
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/decorator/__init__.py +19 -0
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/decorator/as_route_resource.py +65 -0
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/decorator/as_routes.py +58 -0
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/exception/__init__.py +14 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/routing/exception/route_conflict_error.py +6 -5
- xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/route_registry.py +34 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/setup.py +39 -27
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/routing.md +0 -73
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/_route_mount.py +0 -125
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/bundle/_routing.py +0 -22
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/bundle/routing_resolver_pass.py +0 -102
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/__init__.py +0 -36
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/alias.py +0 -94
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/alias_deprecation.py +0 -21
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/decorator/__init__.py +0 -20
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/decorator/as_route_loader.py +0 -65
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/decorator/as_routes.py +0 -44
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/decorator/route_loader_marker.py +0 -23
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/exception/__init__.py +0 -21
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/exception/route_circular_reference_error.py +0 -32
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/__init__.py +0 -7
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/configurator/__init__.py +0 -17
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/configurator/_step_interface.py +0 -19
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/configurator/alias_configurator.py +0 -46
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/configurator/collection_configurator.py +0 -69
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/configurator/import_configurator.py +0 -84
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/configurator/route_configurator.py +0 -111
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/configurator/routing_configurator.py +0 -123
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/loader/routes_file_loader.py +0 -103
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/route.py +0 -66
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/route_collection.py +0 -169
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/route_loader_interface.py +0 -25
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/route_table_parameter.py +0 -12
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/router.py +0 -38
- xtr_http_kernel-4.1.0/src/xtr_http_kernel/routing/router_interface.py +0 -23
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/LICENSE +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/configure.md +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/listeners.md +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/rate-limits.md +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/__init__.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_kernel_middleware.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_route_contexts.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_state.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/_headers.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/_scope_uri.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/response.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/response_factory.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/starlette_http_factory.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/starlette_http_factory_interface.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/http_kernel_config.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/__init__.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/debug_router_command.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/route_description.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/router_command.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/router_match_command.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/__init__.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/__init__.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/disallow_robots_indexing_listener.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/log_unit_listener.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/__init__.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/http_kernel_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_argument_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_cookie_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_middleware_priority_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_rate_limit_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/ranged_response_body_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/response_already_a_message_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/streamed_response_body_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/too_many_requests_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/unknown_rate_limiter_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/unsupported_connection_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/kernel_events.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/middleware_stack.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/middleware_tag.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/py.typed +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/__init__.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/_applied_rate_limit.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/rate_limited.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/recipe/__init__.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/recipe/manifest.toml +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/routing/exception/invalid_argument_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/routing/exception/routing_error.py +0 -0
- {xtr_http_kernel-4.1.0 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/testing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xtr-http-kernel
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: Requests turned into responses through events: a request lifecycle for FastAPI applications on the xtr kernel.
|
|
5
5
|
Keywords: http,kernel,request,lifecycle,events,middleware,fastapi
|
|
6
6
|
Author: Xterr
|
|
@@ -16,13 +16,13 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
16
16
|
Classifier: Typing :: Typed
|
|
17
17
|
Requires-Dist: fastapi>=0.121.3
|
|
18
18
|
Requires-Dist: starlette>=0.40
|
|
19
|
-
Requires-Dist: xtr-dependency-injection>=
|
|
20
|
-
Requires-Dist: xtr-event-dispatcher>=
|
|
21
|
-
Requires-Dist: xtr-event-dispatcher-contracts>=
|
|
22
|
-
Requires-Dist: xtr-http-message>=
|
|
23
|
-
Requires-Dist: xtr-http-message-contracts>=
|
|
24
|
-
Requires-Dist: xtr-logging-contracts>=
|
|
25
|
-
Requires-Dist: xtr-service-contracts>=
|
|
19
|
+
Requires-Dist: xtr-dependency-injection>=5.0,<6
|
|
20
|
+
Requires-Dist: xtr-event-dispatcher>=5.0,<6
|
|
21
|
+
Requires-Dist: xtr-event-dispatcher-contracts>=5.0,<6
|
|
22
|
+
Requires-Dist: xtr-http-message>=5.0,<6
|
|
23
|
+
Requires-Dist: xtr-http-message-contracts>=5.0,<6
|
|
24
|
+
Requires-Dist: xtr-logging-contracts>=5.0,<6
|
|
25
|
+
Requires-Dist: xtr-service-contracts>=5.0,<6
|
|
26
26
|
Requires-Dist: typing-extensions>=4.4
|
|
27
27
|
Requires-Dist: python-multipart>=0.0.18
|
|
28
28
|
Requires-Python: >=3.11
|
|
@@ -163,7 +163,10 @@ surfaces then, rather than a request running without its lifecycle.
|
|
|
163
163
|
## The lifecycle
|
|
164
164
|
|
|
165
165
|
Every request goes through the same events, and a listener joins wherever it has something to
|
|
166
|
-
contribute. Each is dispatched at most once, however the request went
|
|
166
|
+
contribute. Each is dispatched at most once, however the request went, and each carries the
|
|
167
|
+
request as a message — the served request below — plus `state`, the per-request state bag the
|
|
168
|
+
framework's own `request.state` wraps: what a listener puts there, the framework and every other
|
|
169
|
+
listener read:
|
|
167
170
|
|
|
168
171
|
| Event | `KernelEvents` | When | What a listener may do |
|
|
169
172
|
|---|---|---|---|
|
|
@@ -244,12 +247,34 @@ async def issue(
|
|
|
244
247
|
return response.with_status(201).with_header("content-type", "application/json")
|
|
245
248
|
```
|
|
246
249
|
|
|
247
|
-
- **
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
- **The served request, read through the connection.** The request the events carry is the
|
|
251
|
+
message package's own `ServerRequest`, born connected over the bridge's `ServedConnection`:
|
|
252
|
+
its head — method, URI, headers, cookies, query parameters, environment parameters, protocol
|
|
253
|
+
version — is read straight off the connection, and its body is one lazy stream. Nothing is
|
|
254
|
+
read off the wire until a listener or an endpoint reads that stream; the first read
|
|
255
|
+
materialises the whole body once, parsing it per its content type on the way, and every later
|
|
256
|
+
read is served out of the buffer, seekable and rewindable. One instance is parked on the
|
|
257
|
+
connection's scope, so the events, the scoped `server_request` service and a route's injected
|
|
258
|
+
message are all the same object.
|
|
259
|
+
- **A listener that reads the body drains the wire** — so the application is handed a `receive`
|
|
260
|
+
that replays the buffered bytes, and its routes read the body the caller sent as if nobody
|
|
261
|
+
had. A request no listener reads passes its original channel through untouched: nothing is
|
|
262
|
+
buffered unless something asks to read it.
|
|
263
|
+
- **The parsed body answers by the time an endpoint asks.** A body that has a parsed side — a
|
|
264
|
+
form, JSON — is materialised before the container hands the message to a service or an
|
|
265
|
+
endpoint, so `get_parsed_body()` and `get_uploaded_files()` answer what the message contract
|
|
266
|
+
promises, unconditionally, for every consumer written against it. A body with no parsed side
|
|
267
|
+
is left unread. The events carry the message as it stands: a listener that asks for fields
|
|
268
|
+
reads the body stream first, and the reading parses — the one place the contract's wording
|
|
269
|
+
still needs that order, and a listener is the one consumer that can await.
|
|
270
|
+
- **The attributes are live.** `get_attributes()` reads the matched route's path parameters as
|
|
271
|
+
they are now — routing fills them after the message exists. The `with_*` members return plain
|
|
272
|
+
copies: immutable snapshots of the message as it stands, so a change a listener makes never
|
|
273
|
+
reaches the connection.
|
|
274
|
+
- **Once per request.** Both the request and the response are `lifetime="scoped"`, so every
|
|
275
|
+
endpoint, dependency and service of one request sees the same request message and the same
|
|
276
|
+
response. `RequestInterface` and `MessageInterface` lead to the same build, so a parameter
|
|
277
|
+
typed by any of the three gets it.
|
|
253
278
|
- **One response, two names.** `xtr_http_kernel.bridge.Response` and `ResponseInterface` lead to
|
|
254
279
|
that same build: an endpoint asks for the class and declares it returns it — parameter and
|
|
255
280
|
return annotation the same type, nothing to narrow and nothing to configure — while a library
|
|
@@ -313,7 +338,7 @@ one:
|
|
|
313
338
|
|
|
314
339
|
| Service | Does |
|
|
315
340
|
|---|---|
|
|
316
|
-
| `HttpMessageFactoryInterface` | `await create_request(request)` and `create_response(response)`: the framework's own, as messages |
|
|
341
|
+
| `HttpMessageFactoryInterface` | `serve(request)`: the framework's own request, as a served request — a read-through view nothing has read the body of; `await create_request(request)` and `create_response(response)`: the framework's own, as messages read whole |
|
|
317
342
|
| `StarletteHttpFactoryInterface` | `await create_request(message, streamed=False)` and `await create_response(message, streamed=False)`: a message, as the framework's own |
|
|
318
343
|
|
|
319
344
|
A response that arrived from outside the application — one a worker built with
|
|
@@ -357,11 +382,11 @@ The bundle registers these; which ones depend on what is installed and active:
|
|
|
357
382
|
|
|
358
383
|
| Listener | Events | Active when | Does |
|
|
359
384
|
|---|---|---|---|
|
|
360
|
-
| `RequestIdListener` | `RequestEvent`, `ResponseEvent` | always | mints a `uuid4().hex` — or keeps a well-formed incoming id, where `trust_request_id` says to — puts it on
|
|
385
|
+
| `RequestIdListener` | `RequestEvent`, `ResponseEvent` | always | mints a `uuid4().hex` — or keeps a well-formed incoming id, where `trust_request_id` says to — puts it on the request's state bag, binds it to the log context when logging is around, and echoes it on the response |
|
|
361
386
|
| `DisallowRobotsIndexingListener` | `ResponseEvent` | always | stamps `X-Robots-Tag: noindex` on every response, when the config turns it on |
|
|
362
387
|
| `LogUnitListener` | `RequestEvent`, `TerminateEvent` | logging bundle active | opens a [logging unit of work](../xtr-logging#units-of-work) per request and closes it once all was sent |
|
|
363
388
|
| `ErrorLoggingListener` | `ExceptionEvent` | logging bundle active | writes every uncaught exception to the request channel — `info` for a cancellation, an interrupt or an exit, `error` below a 500 status, `critical` otherwise — leaving the response to whoever answers it |
|
|
364
|
-
| `RateLimitHeadersListener` | `ResponseEvent` | rate limiter bundle active | writes the `X-RateLimit-*` headers of the [rate limit](#rate-limits) that speaks for the response
|
|
389
|
+
| `RateLimitHeadersListener` | `ResponseEvent` | rate limiter bundle active | writes the `X-RateLimit-*` headers of the [rate limit](#rate-limits) that speaks for the response — the one a route's limit left on the request's state bag — and makes it private |
|
|
365
390
|
|
|
366
391
|
The two logging listeners join only when the logging bundle is active, and open and close the
|
|
367
392
|
unit of work outside everything else so every record made while handling carries the request's
|
|
@@ -569,96 +594,98 @@ offending definition.
|
|
|
569
594
|
|
|
570
595
|
## Routes
|
|
571
596
|
|
|
572
|
-
|
|
573
|
-
|
|
597
|
+
Routes are native framework routes, end to end. A package ships them as *route resources* —
|
|
598
|
+
named routers its registration functions fill — and an application registers them in its
|
|
599
|
+
routes files. **Routing is never imported automatically:** a resource no routes file includes
|
|
600
|
+
is not served. The file a package's recipe writes into `<app>/config/routes/` is what
|
|
601
|
+
registers its endpoints; deleting the file uninstalls them.
|
|
574
602
|
|
|
575
|
-
###
|
|
603
|
+
### What a package ships
|
|
576
604
|
|
|
577
|
-
A route
|
|
578
|
-
|
|
605
|
+
A route resource is a plain function declared with `@as_route_resource`, filling an
|
|
606
|
+
`APIRouter` with the framework's own routes:
|
|
579
607
|
|
|
580
608
|
```python
|
|
581
|
-
from
|
|
582
|
-
from xtr_http_kernel.routing.decorator import
|
|
609
|
+
from fastapi import APIRouter
|
|
610
|
+
from xtr_http_kernel.routing.decorator import as_route_resource
|
|
583
611
|
|
|
584
612
|
|
|
585
|
-
|
|
586
|
-
|
|
613
|
+
@as_route_resource("books")
|
|
614
|
+
def books_routes(router: APIRouter) -> None:
|
|
615
|
+
"""Fill the router with the books endpoints."""
|
|
587
616
|
|
|
617
|
+
@router.get("/books", name="books_list")
|
|
618
|
+
async def list_books() -> list[str]:
|
|
619
|
+
return ["Dune"]
|
|
588
620
|
|
|
589
|
-
@
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
routes = RouteCollection()
|
|
593
|
-
routes.add("books_list", Route("/books", list_books, methods=("GET",)))
|
|
594
|
-
return routes
|
|
621
|
+
@router.get("/books/{book_id}", name="books_show")
|
|
622
|
+
async def show_book(book_id: str) -> dict[str, str]:
|
|
623
|
+
return {"book": book_id}
|
|
595
624
|
```
|
|
596
625
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
626
|
+
Everything the framework's own decorators take — `dependencies=`, `response_class`,
|
|
627
|
+
`openapi_extra`, websockets — a resource takes: there is no layer in between. An endpoint
|
|
628
|
+
needing a service declares it `Injected[...]` as anywhere else; a controller is a service
|
|
629
|
+
like any other, and an application replaces one the way it replaces any service — its own
|
|
630
|
+
definition of the type outranks a bundle's.
|
|
631
|
+
|
|
632
|
+
The function is found like any other declaration — in the application's scan, or in a module
|
|
633
|
+
a bundle scans with `services.load(...)` — and `setup` calls it once per resource with a
|
|
634
|
+
fresh router, no services and no side effects: what a package contributes is fixed by the
|
|
635
|
+
code that ships it. Several functions may fill one resource; two routes of a resource
|
|
636
|
+
claiming one name are refused while staging. A route keeps the name it was declared with —
|
|
637
|
+
`url_path_for`, and anything building a URL a stranger keeps, builds it by name — and a
|
|
638
|
+
route a deployment turns off keeps answering: its endpoint refuses, and an application that
|
|
639
|
+
wants it gone filters it out of the staged router.
|
|
610
640
|
|
|
611
641
|
### The application's routes files
|
|
612
642
|
|
|
613
|
-
A routes file is a function declared `@as_routes` in `<app>/config/routes/`, receiving
|
|
614
|
-
|
|
643
|
+
A routes file is a function declared `@as_routes` in `<app>/config/routes/`, receiving the
|
|
644
|
+
application and every staged resource as a native router:
|
|
615
645
|
|
|
616
646
|
```python
|
|
617
647
|
from xtr_http_kernel.routing.decorator import as_routes
|
|
618
|
-
from xtr_http_kernel.routing.loader.configurator import RoutingConfigurator
|
|
619
648
|
|
|
620
649
|
|
|
621
650
|
@as_routes
|
|
622
|
-
def shop(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
async def home() -> dict[str, str]:
|
|
629
|
-
return {"page": "home"}
|
|
651
|
+
def shop(app: FastAPI, resources: Mapping[str, APIRouter]) -> None:
|
|
652
|
+
"""Serve the books under /shop, under renamed names."""
|
|
653
|
+
books = resources["books"]
|
|
654
|
+
for route in books.routes:
|
|
655
|
+
route.name = f"shop_{route.name}"
|
|
656
|
+
app.include_router(books, prefix="/shop")
|
|
630
657
|
```
|
|
631
658
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
- `
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
659
|
+
The arrangement is native calls on a staged router — before it is included, so nothing
|
|
660
|
+
mounted is ever rebuilt:
|
|
661
|
+
|
|
662
|
+
- **Serve** — `app.include_router(resources["books"])`.
|
|
663
|
+
- **Move** — `app.include_router(resources["books"], prefix="/shop")`.
|
|
664
|
+
- **Skip** — do not include it; omission is the skip.
|
|
665
|
+
- **Leave one route out** — `books.routes = [r for r in books.routes if r.name != "books_show"]`
|
|
666
|
+
before including.
|
|
667
|
+
- **Rename the group** — the loop above sets `route.name` (and `route.operation_id`, when the
|
|
668
|
+
description matters); the framework has no name-prefixed include.
|
|
669
|
+
- **Replace a route** — declare the application's own before `setup` — the application's
|
|
670
|
+
routes come first — and filter the contributed one out of its resource.
|
|
671
|
+
|
|
672
|
+
The files run in scan order, and each sees the resources as staged. A resource is included by
|
|
673
|
+
the first file including it; a second include of one is refused, as every name of it would
|
|
674
|
+
land twice. A mistake is refused at `setup` with an `InvalidArgumentError` naming it: a file
|
|
675
|
+
asking for a resource nothing installed contributes, two routes of one resource claiming one
|
|
676
|
+
name. The kernel scans only packages, so `config/routes/` needs an `__init__.py`; xtr-recipes
|
|
677
|
+
creates it with the first routes file a recipe writes there.
|
|
649
678
|
|
|
650
679
|
### Collisions with the application's own routes
|
|
651
680
|
|
|
652
|
-
The application's own routes are never changed, and they
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
The table is also a service: `RouterInterface.get_route_collection()` returns a copy of what was
|
|
661
|
-
mounted, the same on every start of the application, for code that reads a route by name.
|
|
681
|
+
The application's own routes — the ones declared before `setup` — are never changed, and they
|
|
682
|
+
come first: a catch-all or a mount of the application's shadowing a contributed route is the
|
|
683
|
+
application's own doing, and `router:match` answers who serves what. What `setup` refuses with
|
|
684
|
+
a `RouteConflictError` naming both is the silent kind: a contributed route sharing a name with
|
|
685
|
+
one the application declared, so `url_path_for` would guess; a contributed route answering a
|
|
686
|
+
method at the path shape of one the application declared — parameters told apart by shape, not
|
|
687
|
+
by name; and two contributed routes answering one method and path, the second of which no
|
|
688
|
+
request could ever reach.
|
|
662
689
|
|
|
663
690
|
## Router commands
|
|
664
691
|
|
|
@@ -737,20 +764,20 @@ itself, as well as from `xtr_http_kernel.exception`.
|
|
|
737
764
|
| `UnknownRateLimiterError` | a `RateLimited` names a limiter the application did not configure; also a `LookupError` |
|
|
738
765
|
| `UnsupportedConnectionError` | something asked for the request as a message while a WebSocket was being served |
|
|
739
766
|
|
|
740
|
-
The
|
|
767
|
+
The routing layer's errors live in `xtr_http_kernel.routing`, under `RoutingError` — itself an
|
|
741
768
|
`HttpKernelError`:
|
|
742
769
|
|
|
743
770
|
| Error | Raised when |
|
|
744
771
|
|---|---|
|
|
745
|
-
| `InvalidArgumentError` | a routes file
|
|
746
|
-
| `RouteConflictError` | `setup` finds a contributed route sharing a name with one the application declared,
|
|
747
|
-
| `RouteCircularReferenceError` | aliases lead back to a name they already passed |
|
|
772
|
+
| `InvalidArgumentError` | a routes file asks for a resource nothing installed contributes, or is `async`; two routes of one resource claim one name; `@as_route_resource` decorates anything but a plain function, or names no resource. Also the library's own `InvalidArgumentError`, so a `ValueError` |
|
|
773
|
+
| `RouteConflictError` | `setup` finds a contributed route sharing a name with one the application declared, answering a method at the path shape of one the application declared, or answering a method and path shape another contributed route answers; or a resource included twice |
|
|
748
774
|
|
|
749
775
|
## Layout
|
|
750
776
|
|
|
751
777
|
```
|
|
752
778
|
xtr_http_kernel/
|
|
753
779
|
├── setup.py setup(app, kernel), the one call an application makes
|
|
780
|
+
├── _staging.py stages the resources, runs the routes files, refuses the collisions
|
|
754
781
|
├── testing.py override_services, for a served application under test
|
|
755
782
|
├── bridge/ the request and the response as HTTP messages, both ways
|
|
756
783
|
├── event/ the five lifecycle events, one class per file
|
|
@@ -760,7 +787,7 @@ xtr_http_kernel/
|
|
|
760
787
|
├── middleware_stack.py the ordered chain a bundle contributes to
|
|
761
788
|
├── middleware_tag.py MIDDLEWARE_TAG, the tag bundles agree on
|
|
762
789
|
├── rate_limiter/ RateLimited, when xtr-rate-limiter is installed
|
|
763
|
-
├── routing/
|
|
790
|
+
├── routing/ as_route_resource, as_routes, the registry setup stages from
|
|
764
791
|
├── command/ debug:router and router:match
|
|
765
792
|
├── exception/ HttpKernelError, the root of everything this library raises
|
|
766
793
|
└── bundle/ HttpKernelBundle and HttpKernelConfig
|
|
@@ -133,7 +133,10 @@ surfaces then, rather than a request running without its lifecycle.
|
|
|
133
133
|
## The lifecycle
|
|
134
134
|
|
|
135
135
|
Every request goes through the same events, and a listener joins wherever it has something to
|
|
136
|
-
contribute. Each is dispatched at most once, however the request went
|
|
136
|
+
contribute. Each is dispatched at most once, however the request went, and each carries the
|
|
137
|
+
request as a message — the served request below — plus `state`, the per-request state bag the
|
|
138
|
+
framework's own `request.state` wraps: what a listener puts there, the framework and every other
|
|
139
|
+
listener read:
|
|
137
140
|
|
|
138
141
|
| Event | `KernelEvents` | When | What a listener may do |
|
|
139
142
|
|---|---|---|---|
|
|
@@ -214,12 +217,34 @@ async def issue(
|
|
|
214
217
|
return response.with_status(201).with_header("content-type", "application/json")
|
|
215
218
|
```
|
|
216
219
|
|
|
217
|
-
- **
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
- **The served request, read through the connection.** The request the events carry is the
|
|
221
|
+
message package's own `ServerRequest`, born connected over the bridge's `ServedConnection`:
|
|
222
|
+
its head — method, URI, headers, cookies, query parameters, environment parameters, protocol
|
|
223
|
+
version — is read straight off the connection, and its body is one lazy stream. Nothing is
|
|
224
|
+
read off the wire until a listener or an endpoint reads that stream; the first read
|
|
225
|
+
materialises the whole body once, parsing it per its content type on the way, and every later
|
|
226
|
+
read is served out of the buffer, seekable and rewindable. One instance is parked on the
|
|
227
|
+
connection's scope, so the events, the scoped `server_request` service and a route's injected
|
|
228
|
+
message are all the same object.
|
|
229
|
+
- **A listener that reads the body drains the wire** — so the application is handed a `receive`
|
|
230
|
+
that replays the buffered bytes, and its routes read the body the caller sent as if nobody
|
|
231
|
+
had. A request no listener reads passes its original channel through untouched: nothing is
|
|
232
|
+
buffered unless something asks to read it.
|
|
233
|
+
- **The parsed body answers by the time an endpoint asks.** A body that has a parsed side — a
|
|
234
|
+
form, JSON — is materialised before the container hands the message to a service or an
|
|
235
|
+
endpoint, so `get_parsed_body()` and `get_uploaded_files()` answer what the message contract
|
|
236
|
+
promises, unconditionally, for every consumer written against it. A body with no parsed side
|
|
237
|
+
is left unread. The events carry the message as it stands: a listener that asks for fields
|
|
238
|
+
reads the body stream first, and the reading parses — the one place the contract's wording
|
|
239
|
+
still needs that order, and a listener is the one consumer that can await.
|
|
240
|
+
- **The attributes are live.** `get_attributes()` reads the matched route's path parameters as
|
|
241
|
+
they are now — routing fills them after the message exists. The `with_*` members return plain
|
|
242
|
+
copies: immutable snapshots of the message as it stands, so a change a listener makes never
|
|
243
|
+
reaches the connection.
|
|
244
|
+
- **Once per request.** Both the request and the response are `lifetime="scoped"`, so every
|
|
245
|
+
endpoint, dependency and service of one request sees the same request message and the same
|
|
246
|
+
response. `RequestInterface` and `MessageInterface` lead to the same build, so a parameter
|
|
247
|
+
typed by any of the three gets it.
|
|
223
248
|
- **One response, two names.** `xtr_http_kernel.bridge.Response` and `ResponseInterface` lead to
|
|
224
249
|
that same build: an endpoint asks for the class and declares it returns it — parameter and
|
|
225
250
|
return annotation the same type, nothing to narrow and nothing to configure — while a library
|
|
@@ -283,7 +308,7 @@ one:
|
|
|
283
308
|
|
|
284
309
|
| Service | Does |
|
|
285
310
|
|---|---|
|
|
286
|
-
| `HttpMessageFactoryInterface` | `await create_request(request)` and `create_response(response)`: the framework's own, as messages |
|
|
311
|
+
| `HttpMessageFactoryInterface` | `serve(request)`: the framework's own request, as a served request — a read-through view nothing has read the body of; `await create_request(request)` and `create_response(response)`: the framework's own, as messages read whole |
|
|
287
312
|
| `StarletteHttpFactoryInterface` | `await create_request(message, streamed=False)` and `await create_response(message, streamed=False)`: a message, as the framework's own |
|
|
288
313
|
|
|
289
314
|
A response that arrived from outside the application — one a worker built with
|
|
@@ -327,11 +352,11 @@ The bundle registers these; which ones depend on what is installed and active:
|
|
|
327
352
|
|
|
328
353
|
| Listener | Events | Active when | Does |
|
|
329
354
|
|---|---|---|---|
|
|
330
|
-
| `RequestIdListener` | `RequestEvent`, `ResponseEvent` | always | mints a `uuid4().hex` — or keeps a well-formed incoming id, where `trust_request_id` says to — puts it on
|
|
355
|
+
| `RequestIdListener` | `RequestEvent`, `ResponseEvent` | always | mints a `uuid4().hex` — or keeps a well-formed incoming id, where `trust_request_id` says to — puts it on the request's state bag, binds it to the log context when logging is around, and echoes it on the response |
|
|
331
356
|
| `DisallowRobotsIndexingListener` | `ResponseEvent` | always | stamps `X-Robots-Tag: noindex` on every response, when the config turns it on |
|
|
332
357
|
| `LogUnitListener` | `RequestEvent`, `TerminateEvent` | logging bundle active | opens a [logging unit of work](../xtr-logging#units-of-work) per request and closes it once all was sent |
|
|
333
358
|
| `ErrorLoggingListener` | `ExceptionEvent` | logging bundle active | writes every uncaught exception to the request channel — `info` for a cancellation, an interrupt or an exit, `error` below a 500 status, `critical` otherwise — leaving the response to whoever answers it |
|
|
334
|
-
| `RateLimitHeadersListener` | `ResponseEvent` | rate limiter bundle active | writes the `X-RateLimit-*` headers of the [rate limit](#rate-limits) that speaks for the response
|
|
359
|
+
| `RateLimitHeadersListener` | `ResponseEvent` | rate limiter bundle active | writes the `X-RateLimit-*` headers of the [rate limit](#rate-limits) that speaks for the response — the one a route's limit left on the request's state bag — and makes it private |
|
|
335
360
|
|
|
336
361
|
The two logging listeners join only when the logging bundle is active, and open and close the
|
|
337
362
|
unit of work outside everything else so every record made while handling carries the request's
|
|
@@ -539,96 +564,98 @@ offending definition.
|
|
|
539
564
|
|
|
540
565
|
## Routes
|
|
541
566
|
|
|
542
|
-
|
|
543
|
-
|
|
567
|
+
Routes are native framework routes, end to end. A package ships them as *route resources* —
|
|
568
|
+
named routers its registration functions fill — and an application registers them in its
|
|
569
|
+
routes files. **Routing is never imported automatically:** a resource no routes file includes
|
|
570
|
+
is not served. The file a package's recipe writes into `<app>/config/routes/` is what
|
|
571
|
+
registers its endpoints; deleting the file uninstalls them.
|
|
544
572
|
|
|
545
|
-
###
|
|
573
|
+
### What a package ships
|
|
546
574
|
|
|
547
|
-
A route
|
|
548
|
-
|
|
575
|
+
A route resource is a plain function declared with `@as_route_resource`, filling an
|
|
576
|
+
`APIRouter` with the framework's own routes:
|
|
549
577
|
|
|
550
578
|
```python
|
|
551
|
-
from
|
|
552
|
-
from xtr_http_kernel.routing.decorator import
|
|
579
|
+
from fastapi import APIRouter
|
|
580
|
+
from xtr_http_kernel.routing.decorator import as_route_resource
|
|
553
581
|
|
|
554
582
|
|
|
555
|
-
|
|
556
|
-
|
|
583
|
+
@as_route_resource("books")
|
|
584
|
+
def books_routes(router: APIRouter) -> None:
|
|
585
|
+
"""Fill the router with the books endpoints."""
|
|
557
586
|
|
|
587
|
+
@router.get("/books", name="books_list")
|
|
588
|
+
async def list_books() -> list[str]:
|
|
589
|
+
return ["Dune"]
|
|
558
590
|
|
|
559
|
-
@
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
routes = RouteCollection()
|
|
563
|
-
routes.add("books_list", Route("/books", list_books, methods=("GET",)))
|
|
564
|
-
return routes
|
|
591
|
+
@router.get("/books/{book_id}", name="books_show")
|
|
592
|
+
async def show_book(book_id: str) -> dict[str, str]:
|
|
593
|
+
return {"book": book_id}
|
|
565
594
|
```
|
|
566
595
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
596
|
+
Everything the framework's own decorators take — `dependencies=`, `response_class`,
|
|
597
|
+
`openapi_extra`, websockets — a resource takes: there is no layer in between. An endpoint
|
|
598
|
+
needing a service declares it `Injected[...]` as anywhere else; a controller is a service
|
|
599
|
+
like any other, and an application replaces one the way it replaces any service — its own
|
|
600
|
+
definition of the type outranks a bundle's.
|
|
601
|
+
|
|
602
|
+
The function is found like any other declaration — in the application's scan, or in a module
|
|
603
|
+
a bundle scans with `services.load(...)` — and `setup` calls it once per resource with a
|
|
604
|
+
fresh router, no services and no side effects: what a package contributes is fixed by the
|
|
605
|
+
code that ships it. Several functions may fill one resource; two routes of a resource
|
|
606
|
+
claiming one name are refused while staging. A route keeps the name it was declared with —
|
|
607
|
+
`url_path_for`, and anything building a URL a stranger keeps, builds it by name — and a
|
|
608
|
+
route a deployment turns off keeps answering: its endpoint refuses, and an application that
|
|
609
|
+
wants it gone filters it out of the staged router.
|
|
580
610
|
|
|
581
611
|
### The application's routes files
|
|
582
612
|
|
|
583
|
-
A routes file is a function declared `@as_routes` in `<app>/config/routes/`, receiving
|
|
584
|
-
|
|
613
|
+
A routes file is a function declared `@as_routes` in `<app>/config/routes/`, receiving the
|
|
614
|
+
application and every staged resource as a native router:
|
|
585
615
|
|
|
586
616
|
```python
|
|
587
617
|
from xtr_http_kernel.routing.decorator import as_routes
|
|
588
|
-
from xtr_http_kernel.routing.loader.configurator import RoutingConfigurator
|
|
589
618
|
|
|
590
619
|
|
|
591
620
|
@as_routes
|
|
592
|
-
def shop(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
async def home() -> dict[str, str]:
|
|
599
|
-
return {"page": "home"}
|
|
621
|
+
def shop(app: FastAPI, resources: Mapping[str, APIRouter]) -> None:
|
|
622
|
+
"""Serve the books under /shop, under renamed names."""
|
|
623
|
+
books = resources["books"]
|
|
624
|
+
for route in books.routes:
|
|
625
|
+
route.name = f"shop_{route.name}"
|
|
626
|
+
app.include_router(books, prefix="/shop")
|
|
600
627
|
```
|
|
601
628
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
- `
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
629
|
+
The arrangement is native calls on a staged router — before it is included, so nothing
|
|
630
|
+
mounted is ever rebuilt:
|
|
631
|
+
|
|
632
|
+
- **Serve** — `app.include_router(resources["books"])`.
|
|
633
|
+
- **Move** — `app.include_router(resources["books"], prefix="/shop")`.
|
|
634
|
+
- **Skip** — do not include it; omission is the skip.
|
|
635
|
+
- **Leave one route out** — `books.routes = [r for r in books.routes if r.name != "books_show"]`
|
|
636
|
+
before including.
|
|
637
|
+
- **Rename the group** — the loop above sets `route.name` (and `route.operation_id`, when the
|
|
638
|
+
description matters); the framework has no name-prefixed include.
|
|
639
|
+
- **Replace a route** — declare the application's own before `setup` — the application's
|
|
640
|
+
routes come first — and filter the contributed one out of its resource.
|
|
641
|
+
|
|
642
|
+
The files run in scan order, and each sees the resources as staged. A resource is included by
|
|
643
|
+
the first file including it; a second include of one is refused, as every name of it would
|
|
644
|
+
land twice. A mistake is refused at `setup` with an `InvalidArgumentError` naming it: a file
|
|
645
|
+
asking for a resource nothing installed contributes, two routes of one resource claiming one
|
|
646
|
+
name. The kernel scans only packages, so `config/routes/` needs an `__init__.py`; xtr-recipes
|
|
647
|
+
creates it with the first routes file a recipe writes there.
|
|
619
648
|
|
|
620
649
|
### Collisions with the application's own routes
|
|
621
650
|
|
|
622
|
-
The application's own routes are never changed, and they
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
The table is also a service: `RouterInterface.get_route_collection()` returns a copy of what was
|
|
631
|
-
mounted, the same on every start of the application, for code that reads a route by name.
|
|
651
|
+
The application's own routes — the ones declared before `setup` — are never changed, and they
|
|
652
|
+
come first: a catch-all or a mount of the application's shadowing a contributed route is the
|
|
653
|
+
application's own doing, and `router:match` answers who serves what. What `setup` refuses with
|
|
654
|
+
a `RouteConflictError` naming both is the silent kind: a contributed route sharing a name with
|
|
655
|
+
one the application declared, so `url_path_for` would guess; a contributed route answering a
|
|
656
|
+
method at the path shape of one the application declared — parameters told apart by shape, not
|
|
657
|
+
by name; and two contributed routes answering one method and path, the second of which no
|
|
658
|
+
request could ever reach.
|
|
632
659
|
|
|
633
660
|
## Router commands
|
|
634
661
|
|
|
@@ -707,20 +734,20 @@ itself, as well as from `xtr_http_kernel.exception`.
|
|
|
707
734
|
| `UnknownRateLimiterError` | a `RateLimited` names a limiter the application did not configure; also a `LookupError` |
|
|
708
735
|
| `UnsupportedConnectionError` | something asked for the request as a message while a WebSocket was being served |
|
|
709
736
|
|
|
710
|
-
The
|
|
737
|
+
The routing layer's errors live in `xtr_http_kernel.routing`, under `RoutingError` — itself an
|
|
711
738
|
`HttpKernelError`:
|
|
712
739
|
|
|
713
740
|
| Error | Raised when |
|
|
714
741
|
|---|---|
|
|
715
|
-
| `InvalidArgumentError` | a routes file
|
|
716
|
-
| `RouteConflictError` | `setup` finds a contributed route sharing a name with one the application declared,
|
|
717
|
-
| `RouteCircularReferenceError` | aliases lead back to a name they already passed |
|
|
742
|
+
| `InvalidArgumentError` | a routes file asks for a resource nothing installed contributes, or is `async`; two routes of one resource claim one name; `@as_route_resource` decorates anything but a plain function, or names no resource. Also the library's own `InvalidArgumentError`, so a `ValueError` |
|
|
743
|
+
| `RouteConflictError` | `setup` finds a contributed route sharing a name with one the application declared, answering a method at the path shape of one the application declared, or answering a method and path shape another contributed route answers; or a resource included twice |
|
|
718
744
|
|
|
719
745
|
## Layout
|
|
720
746
|
|
|
721
747
|
```
|
|
722
748
|
xtr_http_kernel/
|
|
723
749
|
├── setup.py setup(app, kernel), the one call an application makes
|
|
750
|
+
├── _staging.py stages the resources, runs the routes files, refuses the collisions
|
|
724
751
|
├── testing.py override_services, for a served application under test
|
|
725
752
|
├── bridge/ the request and the response as HTTP messages, both ways
|
|
726
753
|
├── event/ the five lifecycle events, one class per file
|
|
@@ -730,7 +757,7 @@ xtr_http_kernel/
|
|
|
730
757
|
├── middleware_stack.py the ordered chain a bundle contributes to
|
|
731
758
|
├── middleware_tag.py MIDDLEWARE_TAG, the tag bundles agree on
|
|
732
759
|
├── rate_limiter/ RateLimited, when xtr-rate-limiter is installed
|
|
733
|
-
├── routing/
|
|
760
|
+
├── routing/ as_route_resource, as_routes, the registry setup stages from
|
|
734
761
|
├── command/ debug:router and router:match
|
|
735
762
|
├── exception/ HttpKernelError, the root of everything this library raises
|
|
736
763
|
└── bundle/ HttpKernelBundle and HttpKernelConfig
|