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.
Files changed (111) hide show
  1. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/PKG-INFO +142 -118
  2. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/README.md +134 -103
  3. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/pyproject.toml +12 -20
  4. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/pyproject.toml.orig +12 -24
  5. {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
  6. {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
  7. xtr_http_kernel-5.0.0/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/routing.md +59 -0
  8. xtr_http_kernel-5.0.0/src/xtr_http_kernel/_staging.py +175 -0
  9. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/__init__.py +8 -3
  10. xtr_http_kernel-5.0.0/src/xtr_http_kernel/bridge/_media_types.py +38 -0
  11. xtr_http_kernel-5.0.0/src/xtr_http_kernel/bridge/_server_params.py +39 -0
  12. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/http_message_factory.py +82 -74
  13. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/http_message_factory_interface.py +23 -0
  14. xtr_http_kernel-5.0.0/src/xtr_http_kernel/bridge/served_connection.py +279 -0
  15. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/__init__.py +2 -2
  16. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/_messages.py +40 -22
  17. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/http_kernel_bundle.py +23 -31
  18. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/request_lifecycle_middleware_factory.py +19 -6
  19. xtr_http_kernel-5.0.0/src/xtr_http_kernel/bundle/route_registry_pass.py +59 -0
  20. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/exception_event.py +25 -5
  21. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/finish_request_event.py +19 -5
  22. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/request_event.py +19 -5
  23. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/response_event.py +25 -6
  24. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/terminate_event.py +26 -5
  25. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/__init__.py +2 -2
  26. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/error_logging_listener.py +2 -2
  27. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/log_unit_listener.py +2 -2
  28. {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
  29. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event_listener/request_id_listener.py +13 -14
  30. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/__init__.py +2 -2
  31. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/request_lifecycle_middleware.py +93 -24
  32. xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/__init__.py +22 -0
  33. xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/decorator/__init__.py +19 -0
  34. xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/decorator/as_route_resource.py +65 -0
  35. xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/decorator/as_routes.py +58 -0
  36. xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/exception/__init__.py +14 -0
  37. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/routing/exception/route_conflict_error.py +6 -5
  38. xtr_http_kernel-5.0.0/src/xtr_http_kernel/routing/route_registry.py +34 -0
  39. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/setup.py +39 -27
  40. xtr_http_kernel-4.0.4/src/xtr_http_kernel/.agents/skills/xtr-http-kernel/references/routing.md +0 -73
  41. xtr_http_kernel-4.0.4/src/xtr_http_kernel/_route_mount.py +0 -125
  42. xtr_http_kernel-4.0.4/src/xtr_http_kernel/bundle/_routing.py +0 -22
  43. xtr_http_kernel-4.0.4/src/xtr_http_kernel/bundle/routing_resolver_pass.py +0 -102
  44. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/__init__.py +0 -36
  45. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/alias.py +0 -94
  46. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/alias_deprecation.py +0 -21
  47. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/decorator/__init__.py +0 -20
  48. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/decorator/as_route_loader.py +0 -65
  49. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/decorator/as_routes.py +0 -44
  50. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/decorator/route_loader_marker.py +0 -23
  51. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/exception/__init__.py +0 -21
  52. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/exception/route_circular_reference_error.py +0 -32
  53. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/__init__.py +0 -7
  54. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/__init__.py +0 -17
  55. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/_step_interface.py +0 -19
  56. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/alias_configurator.py +0 -46
  57. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/collection_configurator.py +0 -69
  58. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/import_configurator.py +0 -84
  59. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/route_configurator.py +0 -111
  60. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/configurator/routing_configurator.py +0 -123
  61. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/loader/routes_file_loader.py +0 -103
  62. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/route.py +0 -66
  63. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/route_collection.py +0 -169
  64. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/route_loader_interface.py +0 -25
  65. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/route_table_parameter.py +0 -12
  66. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/router.py +0 -38
  67. xtr_http_kernel-4.0.4/src/xtr_http_kernel/routing/router_interface.py +0 -23
  68. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/LICENSE +0 -0
  69. {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
  70. {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
  71. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/__init__.py +0 -0
  72. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_kernel_middleware.py +0 -0
  73. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_route_contexts.py +0 -0
  74. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/_state.py +0 -0
  75. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/_headers.py +0 -0
  76. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/_scope_uri.py +0 -0
  77. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/response.py +0 -0
  78. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/response_factory.py +0 -0
  79. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/starlette_http_factory.py +0 -0
  80. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bridge/starlette_http_factory_interface.py +0 -0
  81. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/bundle/http_kernel_config.py +0 -0
  82. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/__init__.py +0 -0
  83. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/debug_router_command.py +0 -0
  84. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/route_description.py +0 -0
  85. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/router_command.py +0 -0
  86. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/command/router_match_command.py +0 -0
  87. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/event/__init__.py +0 -0
  88. {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
  89. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/__init__.py +0 -0
  90. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/http_kernel_error.py +0 -0
  91. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_argument_error.py +0 -0
  92. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_cookie_error.py +0 -0
  93. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_middleware_priority_error.py +0 -0
  94. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/invalid_rate_limit_error.py +0 -0
  95. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/ranged_response_body_error.py +0 -0
  96. {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
  97. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/streamed_response_body_error.py +0 -0
  98. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/too_many_requests_error.py +0 -0
  99. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/unknown_rate_limiter_error.py +0 -0
  100. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/exception/unsupported_connection_error.py +0 -0
  101. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/kernel_events.py +0 -0
  102. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/middleware_stack.py +0 -0
  103. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/middleware_tag.py +0 -0
  104. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/py.typed +0 -0
  105. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/_applied_rate_limit.py +0 -0
  106. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/rate_limiter/rate_limited.py +0 -0
  107. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/recipe/__init__.py +0 -0
  108. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/recipe/manifest.toml +0 -0
  109. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/routing/exception/invalid_argument_error.py +0 -0
  110. {xtr_http_kernel-4.0.4 → xtr_http_kernel-5.0.0}/src/xtr_http_kernel/routing/exception/routing_error.py +0 -0
  111. {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: 4.0.4
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[fastapi]>=4.0,<5
20
- Requires-Dist: xtr-event-dispatcher>=4.0,<5
21
- Requires-Dist: xtr-event-dispatcher-contracts>=4.0,<5
22
- Requires-Dist: xtr-http-message>=4.0,<5
23
- Requires-Dist: xtr-http-message-contracts>=4.0,<5
24
- Requires-Dist: xtr-logging-contracts>=4.0,<5
25
- Requires-Dist: xtr-service-contracts>=4.0,<5
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 # the lifecycle, its bundle and setup
79
- uv add "xtr-http-kernel[logging]" # + the listeners that log
80
- uv add "xtr-http-kernel[console]" # + the commands that report on the router
81
- uv add "xtr-http-kernel[rate-limiter]" # + rate limits on routes, routers and the app
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 — so neither is an extra, and neither is the
86
- [HTTP message contract](../xtr-http-message-contracts) or
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
- - **Built only when asked.** The request is built the first time something asks for it, from
254
- FastAPI's own request object — the body it already read, the form it already parsed. A route
255
- that does not ask pays nothing, and nothing is buffered anywhere.
256
- - **Once per request.** Both are `lifetime="scoped"`, so every endpoint, dependency and service
257
- of one request sees the same request message and the same response. `RequestInterface` and
258
- `MessageInterface` lead to the same build, so a parameter typed by any of the three gets it.
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 `request.state.request_id`, binds it to the log context when logging is around, and echoes it on the response |
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, and makes it private |
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 the `rate-limiter` extra, a route, a router or the whole application can be held to a
379
- limiter configured in [xtr-rate-limiter](../xtr-rate-limiter)'s bundle. `RateLimited` is one
380
- declaration, written as a decorator or wherever the framework takes a dependency:
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 "xtr-http-kernel[logging,console]"`; `logging` brings the listeners
458
- that write to a log, `console` the commands that report on the router, `rate-limiter` the
459
- [rate limits](#rate-limits). None is needed to serve requests. The
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, not behind an extra: the
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 required peers, pulled in and made
474
- active without being listed, and left out silently when the package is not installed. Listing
475
- is not what activates them; installing the extra is. No bundle comes along for the messages:
476
- neither message package ships one, and this bundle registers their services itself.
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 (with the extras) `logging` and `console` as `required`; `debug:router`
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
- A bundle ships its routes, and an application gets them served with no code in its entry point.
576
- The application arranges them in its routes files, and `setup` adds the result.
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
- ### A bundle's routes
603
+ ### What a package ships
579
604
 
580
- A route loader is a class with `load_routes()`, declared with `@as_route_loader`. It names the
581
- *resource* an application imports its routes by:
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 xtr_http_kernel.routing import Route, RouteCollection
585
- from xtr_http_kernel.routing.decorator import as_route_loader
609
+ from fastapi import APIRouter
610
+ from xtr_http_kernel.routing.decorator import as_route_resource
586
611
 
587
612
 
588
- async def list_books() -> list[str]:
589
- return ["Dune"]
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
- @as_route_loader("books")
593
- class BooksRouteLoader:
594
- def load_routes(self) -> RouteCollection:
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
- The loader is built with no arguments while the kernel compiles, before any service exists, so
601
- what it contributes is fixed by the code that ships it. A route a deployment turns off keeps
602
- answering — its endpoint refuses — and an application that wants it gone leaves it out in a
603
- routes file. Pass `prefixable=False` for routes a standard puts at a fixed place, such as a
604
- `/.well-known/` document: no routes file may move them under a prefix. A loader is found like any
605
- other declaration: in the application's scan, or in a module a bundle scans with
606
- `services.load(...)`.
607
-
608
- A `Route` carries a path, the endpoint the framework calls — its `Injected[...]` parameters
609
- untouched — and the options handed to the framework as they are: `methods`,
610
- `include_in_schema`, `tags`, `summary`, `status_code`, `response_model`. Its name is the key it
611
- has in its `RouteCollection`. A mounted route validates, documents and is reached by
612
- `url_path_for` exactly like the same function declared on a router.
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 a
617
- `RoutingConfigurator`:
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(routes: RoutingConfigurator) -> None:
626
- routes.import_("books").prefix("/shop").name_prefix("shop_")
627
- routes.add("shop_books_list", "/shop/catalogue") # move one route by its name
628
- routes.add("home", "/").endpoint(home).methods("GET")
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
- - `import_(resource)` brings in a resource's routes, arranged with `prefix`, `name_prefix` and
636
- `exclude(*names)`; `skip(resource)` serves none of them. A resource no routes file imports or
637
- skips is served at its own paths and names.
638
- - `add(name, path)` defines a route. Under a name already in the table it replaces that route:
639
- without `.endpoint(...)` it moves the route and keeps the rest, with one it redefines it. Its
640
- options — `methods`, `priority`, `tags`, `summary`, `status_code`, `include_in_schema`,
641
- `response_model` — read like the `Route` fields.
642
- - `collection(name_prefix)` groups routes under one path prefix and name prefix;
643
- `alias(name, target)` adds another name for a route, optionally `.deprecate(...)`d.
644
- - Nothing reaches the table while the file runs: every call is applied when it returns, in the
645
- order written, so a route defined after an import replaces the imported one of that name.
646
- Each file starts from the resources as their loaders returned them.
647
-
648
- A mistake is refused while the kernel compiles — so at `setup` — with an `InvalidArgumentError`
649
- naming it: an unknown resource or route, a prefix on fixed routes, a new route with no endpoint,
650
- two loaders claiming one resource. The kernel scans only packages, so `config/routes/` needs an
651
- `__init__.py`; xtr-recipes creates it with the first routes file a recipe writes there.
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 come first. A contributed route sharing
656
- a name with one the application declared, or one the application's routes would answer first —
657
- the same method and path under any parameter names, a catch-all, a mount, or a router the
658
- application included — fails `setup` with a `RouteConflictError` naming both; so do two
659
- contributed routes answering the same method and path. To change a contributed route on purpose,
660
- redefine it by its name in a routes file.
661
-
662
- An alias is mounted under its own name, out of the API description, so `url_path_for` reaches it.
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 route table's errors live in `xtr_http_kernel.routing`, under `RoutingError` — itself an
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 names an unknown resource or route, prefixes fixed routes, defines a route with no endpoint, or is `async`; one file skips a resource another imports; two loaders claim one resource, or two resources no file arranges contribute one name; a loader needs arguments or has an `async` `load_routes`; an alias is refused. Also the library's own `InvalidArgumentError`, so a `ValueError` |
749
- | `RouteConflictError` | `setup` finds a contributed route sharing a name with one the application declared, or answered first by one of the application's routes, or answering the same method and path as another contributed route |
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, with the rate-limiter extra
766
- ├── routing/ Route, RouteCollection, Router, the loaders and configurators
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 --all-extras
803
+ uv sync
780
804
  uv run ruff check
781
805
  uv run ruff format --check
782
806
  uv run basedpyright