xtr-http-kernel 4.0.4__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.0.4 → xtr_http_kernel-5.0.0}/PKG-INFO +142 -118
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/README.md +134 -103
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/pyproject.toml +12 -20
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/pyproject.toml.orig +12 -24
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/SKILL.md +69 -50
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/rate-limits.md +2 -2
- 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.0.4 → 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.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/http_message_factory.py +82 -74
- {xtr_http_kernel-4.0.4 → 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.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/__init__.py +2 -2
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/_messages.py +40 -22
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/http_kernel_bundle.py +23 -31
- {xtr_http_kernel-4.0.4 → 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.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/exception_event.py +25 -5
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/finish_request_event.py +19 -5
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/request_event.py +19 -5
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/response_event.py +25 -6
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/terminate_event.py +26 -5
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/__init__.py +2 -2
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/error_logging_listener.py +2 -2
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/log_unit_listener.py +2 -2
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/rate_limit_headers_listener.py +9 -9
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/request_id_listener.py +13 -14
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/__init__.py +2 -2
- {xtr_http_kernel-4.0.4 → 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.0.4 → 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.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/setup.py +39 -27
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/routing.md +0 -73
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/_route_mount.py +0 -125
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/bundle/_routing.py +0 -22
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/bundle/routing_resolver_pass.py +0 -102
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/__init__.py +0 -36
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/alias.py +0 -94
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/alias_deprecation.py +0 -21
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/decorator/__init__.py +0 -20
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/decorator/as_route_loader.py +0 -65
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/decorator/as_routes.py +0 -44
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/decorator/route_loader_marker.py +0 -23
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/exception/__init__.py +0 -21
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/exception/route_circular_reference_error.py +0 -32
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/__init__.py +0 -7
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/__init__.py +0 -17
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/_step_interface.py +0 -19
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/alias_configurator.py +0 -46
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/collection_configurator.py +0 -69
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/import_configurator.py +0 -84
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/route_configurator.py +0 -111
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/routing_configurator.py +0 -123
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/routes_file_loader.py +0 -103
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/route.py +0 -66
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/route_collection.py +0 -169
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/route_loader_interface.py +0 -25
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/route_table_parameter.py +0 -12
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/router.py +0 -38
- xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/router_interface.py +0 -23
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/LICENSE +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/configure.md +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/listeners.md +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/__init__.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_kernel_middleware.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_route_contexts.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_state.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/_headers.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/_scope_uri.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/response.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/response_factory.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/starlette_http_factory.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/starlette_http_factory_interface.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/http_kernel_config.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/__init__.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/debug_router_command.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/route_description.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/router_command.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/router_match_command.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/__init__.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/disallow_robots_indexing_listener.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/__init__.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/http_kernel_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_argument_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_cookie_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_middleware_priority_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_rate_limit_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/ranged_response_body_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/response_already_a_message_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/streamed_response_body_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/too_many_requests_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/unknown_rate_limiter_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/unsupported_connection_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/kernel_events.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/middleware_stack.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/middleware_tag.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/py.typed +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/_applied_rate_limit.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/rate_limited.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/recipe/__init__.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/recipe/manifest.toml +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/routing/exception/invalid_argument_error.py +0 -0
- {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/routing/exception/routing_error.py +0 -0
- {xtr_http_kernel-4.0.4 → 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,23 +16,16 @@ 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
|
-
Requires-Dist: xtr-console>=4.0,<5 ; extra == 'console'
|
|
29
|
-
Requires-Dist: xtr-logging>=4.0,<5 ; extra == 'logging'
|
|
30
|
-
Requires-Dist: xtr-clock>=4.0,<5 ; extra == 'rate-limiter'
|
|
31
|
-
Requires-Dist: xtr-rate-limiter[di]>=4.0,<5 ; extra == 'rate-limiter'
|
|
32
28
|
Requires-Python: >=3.11
|
|
33
|
-
Provides-Extra: console
|
|
34
|
-
Provides-Extra: logging
|
|
35
|
-
Provides-Extra: rate-limiter
|
|
36
29
|
Description-Content-Type: text/markdown
|
|
37
30
|
|
|
38
31
|
<div align="center">
|
|
@@ -75,16 +68,17 @@ Listeners are services, so they come from the container with everything else the
|
|
|
75
68
|
## Install
|
|
76
69
|
|
|
77
70
|
```sh
|
|
78
|
-
uv add xtr-http-kernel
|
|
79
|
-
uv add
|
|
80
|
-
uv add
|
|
81
|
-
uv add
|
|
71
|
+
uv add xtr-http-kernel # the lifecycle, its bundle and setup
|
|
72
|
+
uv add xtr-logging # beside it: the listeners that log
|
|
73
|
+
uv add xtr-console # beside it: the commands that report on the router
|
|
74
|
+
uv add xtr-rate-limiter # beside it: rate limits on routes, routers and the app
|
|
82
75
|
```
|
|
83
76
|
|
|
84
77
|
Requires Python 3.11+. The web framework and the container layer come with the package
|
|
85
|
-
itself — this *is* the integration —
|
|
86
|
-
[HTTP message contract](../xtr-http-message-contracts)
|
|
87
|
-
[its implementation](../xtr-http-message): the bridge below is made of them.
|
|
78
|
+
itself — this *is* the integration — and so do the
|
|
79
|
+
[HTTP message contract](../xtr-http-message-contracts) and
|
|
80
|
+
[its implementation](../xtr-http-message): the bridge below is made of them. The three
|
|
81
|
+
packages beside it are optional: each is used when it is installed and left alone when not.
|
|
88
82
|
|
|
89
83
|
## Quick start
|
|
90
84
|
|
|
@@ -169,7 +163,10 @@ surfaces then, rather than a request running without its lifecycle.
|
|
|
169
163
|
## The lifecycle
|
|
170
164
|
|
|
171
165
|
Every request goes through the same events, and a listener joins wherever it has something to
|
|
172
|
-
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:
|
|
173
170
|
|
|
174
171
|
| Event | `KernelEvents` | When | What a listener may do |
|
|
175
172
|
|---|---|---|---|
|
|
@@ -250,12 +247,34 @@ async def issue(
|
|
|
250
247
|
return response.with_status(201).with_header("content-type", "application/json")
|
|
251
248
|
```
|
|
252
249
|
|
|
253
|
-
- **
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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.
|
|
259
278
|
- **One response, two names.** `xtr_http_kernel.bridge.Response` and `ResponseInterface` lead to
|
|
260
279
|
that same build: an endpoint asks for the class and declares it returns it — parameter and
|
|
261
280
|
return annotation the same type, nothing to narrow and nothing to configure — while a library
|
|
@@ -319,7 +338,7 @@ one:
|
|
|
319
338
|
|
|
320
339
|
| Service | Does |
|
|
321
340
|
|---|---|
|
|
322
|
-
| `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 |
|
|
323
342
|
| `StarletteHttpFactoryInterface` | `await create_request(message, streamed=False)` and `await create_response(message, streamed=False)`: a message, as the framework's own |
|
|
324
343
|
|
|
325
344
|
A response that arrived from outside the application — one a worker built with
|
|
@@ -363,11 +382,11 @@ The bundle registers these; which ones depend on what is installed and active:
|
|
|
363
382
|
|
|
364
383
|
| Listener | Events | Active when | Does |
|
|
365
384
|
|---|---|---|---|
|
|
366
|
-
| `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 |
|
|
367
386
|
| `DisallowRobotsIndexingListener` | `ResponseEvent` | always | stamps `X-Robots-Tag: noindex` on every response, when the config turns it on |
|
|
368
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 |
|
|
369
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 |
|
|
370
|
-
| `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 |
|
|
371
390
|
|
|
372
391
|
The two logging listeners join only when the logging bundle is active, and open and close the
|
|
373
392
|
unit of work outside everything else so every record made while handling carries the request's
|
|
@@ -375,9 +394,10 @@ id. The request id settles right after the unit opens, for the same reason.
|
|
|
375
394
|
|
|
376
395
|
## Rate limits
|
|
377
396
|
|
|
378
|
-
With
|
|
379
|
-
limiter configured in
|
|
380
|
-
declaration, written as a decorator or wherever the framework takes a
|
|
397
|
+
With [xtr-rate-limiter](../xtr-rate-limiter) installed beside this package, a route, a router or
|
|
398
|
+
the whole application can be held to a limiter configured in that package's bundle.
|
|
399
|
+
`RateLimited` is one declaration, written as a decorator or wherever the framework takes a
|
|
400
|
+
dependency:
|
|
381
401
|
|
|
382
402
|
```python
|
|
383
403
|
from fastapi import APIRouter, FastAPI, Request
|
|
@@ -454,11 +474,12 @@ Everything adding this package to an application on
|
|
|
454
474
|
[xtr-dependency-injection](../xtr-dependency-injection) takes — and, read backwards, what
|
|
455
475
|
removing it undoes.
|
|
456
476
|
|
|
457
|
-
- **Install** — `uv add
|
|
458
|
-
that write to a log,
|
|
459
|
-
[rate limits](#rate-limits).
|
|
477
|
+
- **Install** — `uv add xtr-http-kernel`; install [xtr-logging](../xtr-logging) beside it for
|
|
478
|
+
the listeners that write to a log, [xtr-console](../xtr-console) for the commands that report
|
|
479
|
+
on the router, [xtr-rate-limiter](../xtr-rate-limiter) for the [rate limits](#rate-limits).
|
|
480
|
+
None is needed to serve requests. The
|
|
460
481
|
[HTTP message contract](../xtr-http-message-contracts) and
|
|
461
|
-
[its implementation](../xtr-http-message) come with the package
|
|
482
|
+
[its implementation](../xtr-http-message) come with the package: the
|
|
462
483
|
[messages](#the-request-and-the-response-as-messages) are part of what it does. So does the
|
|
463
484
|
parser a submitted form with files in it is read with: building the request as a message
|
|
464
485
|
asks the web framework for the form behind every multipart body, and the framework refuses
|
|
@@ -470,10 +491,11 @@ removing it undoes.
|
|
|
470
491
|
from `xtr_http_kernel.bundle`. Then call `setup(app, kernel)` where the application is built.
|
|
471
492
|
- **Brings along** — the [event dispatcher](../xtr-event-dispatcher) bundle always, because the
|
|
472
493
|
lifecycle dispatches through it; the [logging](../xtr-logging), [console](../xtr-console) and
|
|
473
|
-
[rate limiter](../xtr-rate-limiter) bundles whenever those packages are *installed* — they are
|
|
474
|
-
active without being listed, and left out silently when the
|
|
475
|
-
is not what activates them; installing the
|
|
476
|
-
neither message package ships one, and this bundle
|
|
494
|
+
[rate limiter](../xtr-rate-limiter) bundles whenever those packages are *installed* — they are
|
|
495
|
+
required peers, pulled in and made active without being listed, and left out silently when the
|
|
496
|
+
package is not installed. Listing is not what activates them; installing the package is. No
|
|
497
|
+
bundle comes along for the messages: neither message package ships one, and this bundle
|
|
498
|
+
registers their services itself.
|
|
477
499
|
- **Configure** — nothing is required: the zero-config path gives every request a fresh `uuid4`
|
|
478
500
|
id under `X-Request-Id` — an inbound one is not trusted — no robots header, and the `request`
|
|
479
501
|
log channel, and makes the request and the response of a request askable as
|
|
@@ -485,8 +507,8 @@ removing it undoes.
|
|
|
485
507
|
- **Remove** — drop the `setup(app, kernel)` call, drop the `BUNDLES` entry, delete
|
|
486
508
|
`<app>/config/http_kernel.py` if you wrote one, then `uv remove xtr-http-kernel`.
|
|
487
509
|
- **Check** — `debug:bundles` shows `http_kernel` as `listed` and `active`, `event_dispatcher`
|
|
488
|
-
as `required`, and (
|
|
489
|
-
lists the application's routes.
|
|
510
|
+
as `required`, and (when those packages are installed) `logging` and `console` as `required`;
|
|
511
|
+
`debug:router` lists the application's routes.
|
|
490
512
|
|
|
491
513
|
## Configure
|
|
492
514
|
|
|
@@ -572,96 +594,98 @@ offending definition.
|
|
|
572
594
|
|
|
573
595
|
## Routes
|
|
574
596
|
|
|
575
|
-
|
|
576
|
-
|
|
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.
|
|
577
602
|
|
|
578
|
-
###
|
|
603
|
+
### What a package ships
|
|
579
604
|
|
|
580
|
-
A route
|
|
581
|
-
|
|
605
|
+
A route resource is a plain function declared with `@as_route_resource`, filling an
|
|
606
|
+
`APIRouter` with the framework's own routes:
|
|
582
607
|
|
|
583
608
|
```python
|
|
584
|
-
from
|
|
585
|
-
from xtr_http_kernel.routing.decorator import
|
|
609
|
+
from fastapi import APIRouter
|
|
610
|
+
from xtr_http_kernel.routing.decorator import as_route_resource
|
|
586
611
|
|
|
587
612
|
|
|
588
|
-
|
|
589
|
-
|
|
613
|
+
@as_route_resource("books")
|
|
614
|
+
def books_routes(router: APIRouter) -> None:
|
|
615
|
+
"""Fill the router with the books endpoints."""
|
|
590
616
|
|
|
617
|
+
@router.get("/books", name="books_list")
|
|
618
|
+
async def list_books() -> list[str]:
|
|
619
|
+
return ["Dune"]
|
|
591
620
|
|
|
592
|
-
@
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
routes = RouteCollection()
|
|
596
|
-
routes.add("books_list", Route("/books", list_books, methods=("GET",)))
|
|
597
|
-
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}
|
|
598
624
|
```
|
|
599
625
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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.
|
|
613
640
|
|
|
614
641
|
### The application's routes files
|
|
615
642
|
|
|
616
|
-
A routes file is a function declared `@as_routes` in `<app>/config/routes/`, receiving
|
|
617
|
-
|
|
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:
|
|
618
645
|
|
|
619
646
|
```python
|
|
620
647
|
from xtr_http_kernel.routing.decorator import as_routes
|
|
621
|
-
from xtr_http_kernel.routing.loader.configurator import RoutingConfigurator
|
|
622
648
|
|
|
623
649
|
|
|
624
650
|
@as_routes
|
|
625
|
-
def shop(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
async def home() -> dict[str, str]:
|
|
632
|
-
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")
|
|
633
657
|
```
|
|
634
658
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
- `
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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.
|
|
652
678
|
|
|
653
679
|
### Collisions with the application's own routes
|
|
654
680
|
|
|
655
|
-
The application's own routes are never changed, and they
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
The table is also a service: `RouterInterface.get_route_collection()` returns a copy of what was
|
|
664
|
-
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.
|
|
665
689
|
|
|
666
690
|
## Router commands
|
|
667
691
|
|
|
@@ -740,20 +764,20 @@ itself, as well as from `xtr_http_kernel.exception`.
|
|
|
740
764
|
| `UnknownRateLimiterError` | a `RateLimited` names a limiter the application did not configure; also a `LookupError` |
|
|
741
765
|
| `UnsupportedConnectionError` | something asked for the request as a message while a WebSocket was being served |
|
|
742
766
|
|
|
743
|
-
The
|
|
767
|
+
The routing layer's errors live in `xtr_http_kernel.routing`, under `RoutingError` — itself an
|
|
744
768
|
`HttpKernelError`:
|
|
745
769
|
|
|
746
770
|
| Error | Raised when |
|
|
747
771
|
|---|---|
|
|
748
|
-
| `InvalidArgumentError` | a routes file
|
|
749
|
-
| `RouteConflictError` | `setup` finds a contributed route sharing a name with one the application declared,
|
|
750
|
-
| `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 |
|
|
751
774
|
|
|
752
775
|
## Layout
|
|
753
776
|
|
|
754
777
|
```
|
|
755
778
|
xtr_http_kernel/
|
|
756
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
|
|
757
781
|
├── testing.py override_services, for a served application under test
|
|
758
782
|
├── bridge/ the request and the response as HTTP messages, both ways
|
|
759
783
|
├── event/ the five lifecycle events, one class per file
|
|
@@ -762,8 +786,8 @@ xtr_http_kernel/
|
|
|
762
786
|
├── request_lifecycle_middleware.py the middleware that dispatches the events
|
|
763
787
|
├── middleware_stack.py the ordered chain a bundle contributes to
|
|
764
788
|
├── middleware_tag.py MIDDLEWARE_TAG, the tag bundles agree on
|
|
765
|
-
├── rate_limiter/ RateLimited,
|
|
766
|
-
├── routing/
|
|
789
|
+
├── rate_limiter/ RateLimited, when xtr-rate-limiter is installed
|
|
790
|
+
├── routing/ as_route_resource, as_routes, the registry setup stages from
|
|
767
791
|
├── command/ debug:router and router:match
|
|
768
792
|
├── exception/ HttpKernelError, the root of everything this library raises
|
|
769
793
|
└── bundle/ HttpKernelBundle and HttpKernelConfig
|
|
@@ -776,7 +800,7 @@ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, und
|
|
|
776
800
|
repository is a read-only copy, so send issues and pull requests to the monorepo.
|
|
777
801
|
|
|
778
802
|
```sh
|
|
779
|
-
uv sync
|
|
803
|
+
uv sync
|
|
780
804
|
uv run ruff check
|
|
781
805
|
uv run ruff format --check
|
|
782
806
|
uv run basedpyright
|