xtr-http-message 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_message-4.0.4 → xtr_http_message-5.0.0}/PKG-INFO +68 -5
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/README.md +66 -3
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/pyproject.toml +2 -2
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/pyproject.toml.orig +2 -2
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/.agents/skills/xtr-http-message/SKILL.md +53 -4
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/__init__.py +10 -1
- xtr_http_message-5.0.0/src/xtr_http_message/_connected_trait.py +115 -0
- xtr_http_message-5.0.0/src/xtr_http_message/lazy_stream.py +157 -0
- xtr_http_message-5.0.0/src/xtr_http_message/request_source.py +71 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/server_request.py +145 -7
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/LICENSE +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/_ascii.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/_message_trait.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/_request_trait.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/exception/__init__.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/exception/http_message_error.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/exception/invalid_argument_error.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/exception/stream_error.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/exception/uploaded_file_error.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/factory/__init__.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/factory/http_factory.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/py.typed +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/request.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/response.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/stream.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/uploaded_file.py +0 -0
- {xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/uri.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xtr-http-message
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: A strict, framework-free implementation of the HTTP message contract: requests, responses, streams, URIs, uploaded files and one factory for all six.
|
|
5
5
|
Keywords: http,http-message,request,response,stream,uri,uploaded-file
|
|
6
6
|
Author: Xterr
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
|
-
Requires-Dist: xtr-http-message-contracts>=
|
|
18
|
+
Requires-Dist: xtr-http-message-contracts>=5.0,<6
|
|
19
19
|
Requires-Dist: typing-extensions>=4.4
|
|
20
20
|
Requires-Dist: uritools>=6.1,<7
|
|
21
21
|
Requires-Python: >=3.11
|
|
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
# xtr-http-message
|
|
27
27
|
|
|
28
|
-
**The HTTP message contract, implemented — six small classes, one factory, and no web framework anywhere in sight.**
|
|
28
|
+
**The HTTP message contract, implemented — six small classes, one factory, a request that can be born connected, and no web framework anywhere in sight.**
|
|
29
29
|
|
|
30
30
|
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
31
31
|
<img alt="core dependencies: 3" src="https://img.shields.io/badge/core%20deps-3-3FB950">
|
|
@@ -47,9 +47,12 @@ besides.
|
|
|
47
47
|
header taken from the URI and sent in front of every other header.
|
|
48
48
|
- 📥 **`ServerRequest`** — a request that arrived: the environment parameters, the cookies, the
|
|
49
49
|
query string parsed, the files that came with it, the parsed body, and the attributes a
|
|
50
|
-
handler annotates it with.
|
|
50
|
+
handler annotates it with. Born complete — or born connected over a `RequestSource`, its body
|
|
51
|
+
side arriving as it is read.
|
|
51
52
|
- 📤 **`Response`** — a status code, the reason phrase recommended for it, headers and a body.
|
|
52
53
|
- 🌊 **`Stream`** — a body, as a cursor over bytes behind an open binary file object.
|
|
54
|
+
- 🌊 **`LazyStream`** — a body that stands in unread: the whole of it is read once, on the first
|
|
55
|
+
read, and buffered after.
|
|
53
56
|
- 🔗 **`Uri`** — a URI taken apart into components and put back together as text, read as
|
|
54
57
|
RFC 3986 defines one.
|
|
55
58
|
- 📎 **`UploadedFile`** — one file that came in, movable once.
|
|
@@ -108,12 +111,15 @@ application that is [xtr-http-kernel](../xtr-http-kernel)'s
|
|
|
108
111
|
| `ServerRequest` | `ServerRequestInterface` |
|
|
109
112
|
| `Response` | `ResponseInterface` |
|
|
110
113
|
| `Stream` | `StreamInterface` |
|
|
114
|
+
| `LazyStream` | `StreamInterface` |
|
|
111
115
|
| `Uri` | `UriInterface` |
|
|
112
116
|
| `UploadedFile` | `UploadedFileInterface` |
|
|
113
117
|
| `HttpFactory` | all six factory interfaces |
|
|
114
118
|
|
|
115
119
|
Each one names its interface as its last base class, so `isinstance` and a container
|
|
116
|
-
registration both work without a plugin registry in between.
|
|
120
|
+
registration both work without a plugin registry in between. `RequestSource` is the one
|
|
121
|
+
protocol among them: not a message, but where a connected request reads its late answers
|
|
122
|
+
from.
|
|
117
123
|
|
|
118
124
|
### The rules that hold across all of them
|
|
119
125
|
|
|
@@ -158,6 +164,63 @@ the stream itself is collected, so a file object you still hold is yours to clos
|
|
|
158
164
|
`get_metadata()` reports three entries — `mode`, `seekable` and `uri`, the last being the path
|
|
159
165
|
the file was opened on when it has one.
|
|
160
166
|
|
|
167
|
+
`LazyStream` is a stream that stands in for a body nobody has read: built with an async
|
|
168
|
+
callable that returns the bytes, it holds nothing until the first `read` or `get_contents`
|
|
169
|
+
calls it, then buffers what came back and serves every later read out of the buffer —
|
|
170
|
+
seekable, rewindable, and closing it before the first read is a no-op rather than a read.
|
|
171
|
+
|
|
172
|
+
### A request born connected
|
|
173
|
+
|
|
174
|
+
A request that arrived is not always known in full when it becomes a message: a connection
|
|
175
|
+
hands over its head at once, but its body is still on the wire. `ServerRequest.over()` builds
|
|
176
|
+
the one class both ways — born complete through the constructor, born connected through a
|
|
177
|
+
`RequestSource`:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from collections.abc import Mapping
|
|
181
|
+
|
|
182
|
+
from xtr_http_message import RequestSource, ServerRequest
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Connection(RequestSource):
|
|
186
|
+
"""Reads the body off the wire, parses it, answers the late side."""
|
|
187
|
+
|
|
188
|
+
async def materialise(self) -> bytes: ... # runs at most once, memoised
|
|
189
|
+
|
|
190
|
+
def parsed_body(self) -> object | None: ... # a sync peek; None until materialised
|
|
191
|
+
def uploaded_files(self) -> Mapping[str, object]: ... # a sync peek; {} until then
|
|
192
|
+
def attributes(self) -> Mapping[str, object]: ... # live; routing fills it late
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
served = ServerRequest.over(
|
|
196
|
+
"POST",
|
|
197
|
+
"https://example.com/orders",
|
|
198
|
+
source=Connection(),
|
|
199
|
+
headers={"host": "example.com"},
|
|
200
|
+
cookies={"session": "abc"},
|
|
201
|
+
query={"page": "2"},
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The head is passed eagerly — it costs no input and never changes mid-request — and the body
|
|
206
|
+
side comes from the source:
|
|
207
|
+
|
|
208
|
+
- **The body is one lazy stream** (`LazyStream` over `source.materialise`), handed out however
|
|
209
|
+
many times it is asked for; the first read materialises the whole body once, and every later
|
|
210
|
+
read is served out of the buffer.
|
|
211
|
+
- **The parsed body and the uploaded files answer what materialisation has yielded so far** —
|
|
212
|
+
`None` and an empty mapping until the body has been read. `await served.ensure_parsed()`
|
|
213
|
+
materialises it at the one seam that can still await, before the message is handed on.
|
|
214
|
+
- **The attributes are live**, read off the source each time: the named parameters a matched
|
|
215
|
+
route fills in are there as soon as routing has filled them.
|
|
216
|
+
- **The `with_*` members detach.** The copy is a plain eager request — the attributes frozen to
|
|
217
|
+
the current snapshot, the parsed body and files to the current peek, the body the one shared
|
|
218
|
+
lazy stream — so a change a handler makes never reaches the connection, and the copy never
|
|
219
|
+
reads the source again.
|
|
220
|
+
|
|
221
|
+
`body=` is refused alongside `source=` — one owner of the body. A header the message grammar
|
|
222
|
+
refuses is left out rather than refusing the request: a connection let it through.
|
|
223
|
+
|
|
161
224
|
### URIs
|
|
162
225
|
|
|
163
226
|
A URI is a value, read as RFC 3986 defines a URI reference — the splitting is `uritools`' work.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# xtr-http-message
|
|
4
4
|
|
|
5
|
-
**The HTTP message contract, implemented — six small classes, one factory, and no web framework anywhere in sight.**
|
|
5
|
+
**The HTTP message contract, implemented — six small classes, one factory, a request that can be born connected, and no web framework anywhere in sight.**
|
|
6
6
|
|
|
7
7
|
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
8
8
|
<img alt="core dependencies: 3" src="https://img.shields.io/badge/core%20deps-3-3FB950">
|
|
@@ -24,9 +24,12 @@ besides.
|
|
|
24
24
|
header taken from the URI and sent in front of every other header.
|
|
25
25
|
- 📥 **`ServerRequest`** — a request that arrived: the environment parameters, the cookies, the
|
|
26
26
|
query string parsed, the files that came with it, the parsed body, and the attributes a
|
|
27
|
-
handler annotates it with.
|
|
27
|
+
handler annotates it with. Born complete — or born connected over a `RequestSource`, its body
|
|
28
|
+
side arriving as it is read.
|
|
28
29
|
- 📤 **`Response`** — a status code, the reason phrase recommended for it, headers and a body.
|
|
29
30
|
- 🌊 **`Stream`** — a body, as a cursor over bytes behind an open binary file object.
|
|
31
|
+
- 🌊 **`LazyStream`** — a body that stands in unread: the whole of it is read once, on the first
|
|
32
|
+
read, and buffered after.
|
|
30
33
|
- 🔗 **`Uri`** — a URI taken apart into components and put back together as text, read as
|
|
31
34
|
RFC 3986 defines one.
|
|
32
35
|
- 📎 **`UploadedFile`** — one file that came in, movable once.
|
|
@@ -85,12 +88,15 @@ application that is [xtr-http-kernel](../xtr-http-kernel)'s
|
|
|
85
88
|
| `ServerRequest` | `ServerRequestInterface` |
|
|
86
89
|
| `Response` | `ResponseInterface` |
|
|
87
90
|
| `Stream` | `StreamInterface` |
|
|
91
|
+
| `LazyStream` | `StreamInterface` |
|
|
88
92
|
| `Uri` | `UriInterface` |
|
|
89
93
|
| `UploadedFile` | `UploadedFileInterface` |
|
|
90
94
|
| `HttpFactory` | all six factory interfaces |
|
|
91
95
|
|
|
92
96
|
Each one names its interface as its last base class, so `isinstance` and a container
|
|
93
|
-
registration both work without a plugin registry in between.
|
|
97
|
+
registration both work without a plugin registry in between. `RequestSource` is the one
|
|
98
|
+
protocol among them: not a message, but where a connected request reads its late answers
|
|
99
|
+
from.
|
|
94
100
|
|
|
95
101
|
### The rules that hold across all of them
|
|
96
102
|
|
|
@@ -135,6 +141,63 @@ the stream itself is collected, so a file object you still hold is yours to clos
|
|
|
135
141
|
`get_metadata()` reports three entries — `mode`, `seekable` and `uri`, the last being the path
|
|
136
142
|
the file was opened on when it has one.
|
|
137
143
|
|
|
144
|
+
`LazyStream` is a stream that stands in for a body nobody has read: built with an async
|
|
145
|
+
callable that returns the bytes, it holds nothing until the first `read` or `get_contents`
|
|
146
|
+
calls it, then buffers what came back and serves every later read out of the buffer —
|
|
147
|
+
seekable, rewindable, and closing it before the first read is a no-op rather than a read.
|
|
148
|
+
|
|
149
|
+
### A request born connected
|
|
150
|
+
|
|
151
|
+
A request that arrived is not always known in full when it becomes a message: a connection
|
|
152
|
+
hands over its head at once, but its body is still on the wire. `ServerRequest.over()` builds
|
|
153
|
+
the one class both ways — born complete through the constructor, born connected through a
|
|
154
|
+
`RequestSource`:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from collections.abc import Mapping
|
|
158
|
+
|
|
159
|
+
from xtr_http_message import RequestSource, ServerRequest
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Connection(RequestSource):
|
|
163
|
+
"""Reads the body off the wire, parses it, answers the late side."""
|
|
164
|
+
|
|
165
|
+
async def materialise(self) -> bytes: ... # runs at most once, memoised
|
|
166
|
+
|
|
167
|
+
def parsed_body(self) -> object | None: ... # a sync peek; None until materialised
|
|
168
|
+
def uploaded_files(self) -> Mapping[str, object]: ... # a sync peek; {} until then
|
|
169
|
+
def attributes(self) -> Mapping[str, object]: ... # live; routing fills it late
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
served = ServerRequest.over(
|
|
173
|
+
"POST",
|
|
174
|
+
"https://example.com/orders",
|
|
175
|
+
source=Connection(),
|
|
176
|
+
headers={"host": "example.com"},
|
|
177
|
+
cookies={"session": "abc"},
|
|
178
|
+
query={"page": "2"},
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The head is passed eagerly — it costs no input and never changes mid-request — and the body
|
|
183
|
+
side comes from the source:
|
|
184
|
+
|
|
185
|
+
- **The body is one lazy stream** (`LazyStream` over `source.materialise`), handed out however
|
|
186
|
+
many times it is asked for; the first read materialises the whole body once, and every later
|
|
187
|
+
read is served out of the buffer.
|
|
188
|
+
- **The parsed body and the uploaded files answer what materialisation has yielded so far** —
|
|
189
|
+
`None` and an empty mapping until the body has been read. `await served.ensure_parsed()`
|
|
190
|
+
materialises it at the one seam that can still await, before the message is handed on.
|
|
191
|
+
- **The attributes are live**, read off the source each time: the named parameters a matched
|
|
192
|
+
route fills in are there as soon as routing has filled them.
|
|
193
|
+
- **The `with_*` members detach.** The copy is a plain eager request — the attributes frozen to
|
|
194
|
+
the current snapshot, the parsed body and files to the current peek, the body the one shared
|
|
195
|
+
lazy stream — so a change a handler makes never reaches the connection, and the copy never
|
|
196
|
+
reads the source again.
|
|
197
|
+
|
|
198
|
+
`body=` is refused alongside `source=` — one owner of the body. A header the message grammar
|
|
199
|
+
refuses is left out rather than refusing the request: a connection let it through.
|
|
200
|
+
|
|
138
201
|
### URIs
|
|
139
202
|
|
|
140
203
|
A URI is a value, read as RFC 3986 defines a URI reference — the splitting is `uritools`' work.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-http-message"
|
|
3
|
-
version = "
|
|
3
|
+
version = "5.0.0"
|
|
4
4
|
description = "A strict, framework-free implementation of the HTTP message contract: requests, responses, streams, URIs, uploaded files and one factory for all six."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Typing :: Typed",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"xtr-http-message-contracts>=
|
|
29
|
+
"xtr-http-message-contracts>=5.0,<6",
|
|
30
30
|
"typing-extensions>=4.4",
|
|
31
31
|
"uritools>=6.1,<7",
|
|
32
32
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-http-message"
|
|
3
|
-
version = "
|
|
3
|
+
version = "5.0.0"
|
|
4
4
|
description = "A strict, framework-free implementation of the HTTP message contract: requests, responses, streams, URIs, uploaded files and one factory for all six."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
# ours sees the string). Everything else the six classes need is in the
|
|
27
27
|
# standard library.
|
|
28
28
|
dependencies = [
|
|
29
|
-
"xtr-http-message-contracts>=
|
|
29
|
+
"xtr-http-message-contracts>=5.0,<6",
|
|
30
30
|
"typing-extensions>=4.4",
|
|
31
31
|
"uritools>=6.1,<7",
|
|
32
32
|
]
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xtr-http-message
|
|
3
|
-
description: How to build and change requests, responses, streams, URIs and uploaded files with xtr-http-message. Use when code needs a concrete HTTP message rather than the interface - building a request or response in a test or a client, reading or writing a message body, taking a URI apart or putting one together, handling an uploaded file, or taking the factory that builds any of these. Also use when a message body reads as empty, a with_ method seems not to have changed anything, a header is refused, or a URI will not parse - a host, a port or a missing host that RFC 3986 and RFC 9110 refuse.
|
|
3
|
+
description: How to build and change requests, responses, streams, URIs and uploaded files with xtr-http-message. Use when code needs a concrete HTTP message rather than the interface - building a request or response in a test or a client, reading or writing a message body, taking a URI apart or putting one together, handling an uploaded file, building a request over a connection whose body arrives late, or taking the factory that builds any of these. Also use when a message body reads as empty, a with_ method seems not to have changed anything, a header is refused, or a URI will not parse - a host, a port or a missing host that RFC 3986 and RFC 9110 refuse.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# xtr-http-message
|
|
7
7
|
|
|
8
|
-
The HTTP message contract, implemented. Six classes
|
|
9
|
-
only *handles* messages should depend on
|
|
10
|
-
application, a test or a client installs
|
|
8
|
+
The HTTP message contract, implemented. Six classes, one factory, a request that can be born
|
|
9
|
+
connected, no web framework. Code that only *handles* messages should depend on
|
|
10
|
+
`xtr-http-message-contracts` instead; this is what an application, a test or a client installs
|
|
11
|
+
to have something that actually is a message.
|
|
11
12
|
|
|
12
13
|
## Quick reference
|
|
13
14
|
|
|
@@ -15,9 +16,13 @@ application, a test or a client installs to have something that actually is a me
|
|
|
15
16
|
the URI unless you pass one.
|
|
16
17
|
- `ServerRequest(method, uri, headers=None, body=None, version="1.1", server_params=None)` — the
|
|
17
18
|
query parameters are read off the URI as it is built.
|
|
19
|
+
- `ServerRequest.over(method, uri, *, source, ...)` — the same class, born connected over a
|
|
20
|
+
`RequestSource`: the head passed eagerly, the body side answered late.
|
|
18
21
|
- `Response(status=200, headers=None, body=None, version="1.1", reason=None)` — `reason=None`
|
|
19
22
|
takes the recommended phrase, `reason=""` leaves the response with none.
|
|
20
23
|
- `Stream.create(bytes | BinaryIO | StreamInterface)` — no `str`: a body is bytes.
|
|
24
|
+
- `LazyStream(materialise)` — a body that stands in unread: read once on the first read,
|
|
25
|
+
buffered after.
|
|
21
26
|
- `Uri(text)` — read as RFC 3986 defines a URI reference;
|
|
22
27
|
`UploadedFile(stream_or_file, size, error, client_filename=None, client_media_type=None)`.
|
|
23
28
|
- `HttpFactory()` is all six factory interfaces at once; it holds no state.
|
|
@@ -89,6 +94,47 @@ async def main() -> None:
|
|
|
89
94
|
asyncio.run(main())
|
|
90
95
|
```
|
|
91
96
|
|
|
97
|
+
## Build a request born connected
|
|
98
|
+
|
|
99
|
+
A request that arrived is not always known in full when it becomes a message — a connection
|
|
100
|
+
hands over its head at once, but its body is still on the wire. `ServerRequest.over()` builds
|
|
101
|
+
the one class both ways, over a `RequestSource`:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from typing import Mapping
|
|
105
|
+
|
|
106
|
+
from xtr_http_message import RequestSource, ServerRequest
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Connection(RequestSource):
|
|
110
|
+
async def materialise(self) -> bytes:
|
|
111
|
+
"""Read the whole body once, memoised; fill the parsed side as a side effect."""
|
|
112
|
+
|
|
113
|
+
def parsed_body(self) -> object | None: ... # a sync peek; None until materialised
|
|
114
|
+
def uploaded_files(self) -> Mapping[str, object]: ... # a sync peek; {} until then
|
|
115
|
+
def attributes(self) -> Mapping[str, object]: ... # live; routing fills it late
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
served = ServerRequest.over(
|
|
119
|
+
"POST",
|
|
120
|
+
"https://example.com/orders",
|
|
121
|
+
source=Connection(),
|
|
122
|
+
headers={"host": "example.com"},
|
|
123
|
+
cookies={"session": "abc"},
|
|
124
|
+
query={"page": "2"},
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- The head is passed eagerly; the body is one `LazyStream` over `source.materialise`, and the
|
|
129
|
+
first read materialises the whole body once.
|
|
130
|
+
- `get_parsed_body()` and `get_uploaded_files()` answer what materialisation has yielded so
|
|
131
|
+
far; `await served.ensure_parsed()` materialises it at the one seam that can still await.
|
|
132
|
+
- `get_attributes()` reads the source live, each time.
|
|
133
|
+
- The `with_*` members detach: plain eager copies — attributes frozen to the snapshot, parsed
|
|
134
|
+
body and files to the peek, body the one shared lazy stream — that never read the source
|
|
135
|
+
again.
|
|
136
|
+
- `body=` is refused alongside `source=`: one owner of the body.
|
|
137
|
+
|
|
92
138
|
## Take a URI apart and put one together
|
|
93
139
|
|
|
94
140
|
```python
|
|
@@ -276,6 +322,9 @@ implementation can catch `HttpMessageError` and get only its failures.
|
|
|
276
322
|
- Do not expect a `with_*` method to change the message it was called on: it returns a new one,
|
|
277
323
|
and returns the same one when nothing would change. `request.with_header(...)` on its own does
|
|
278
324
|
nothing — keep the result.
|
|
325
|
+
- Do not expect `get_parsed_body()` or `get_uploaded_files()` on a connected request to answer
|
|
326
|
+
before its body has been read: they answer what materialisation has yielded, and
|
|
327
|
+
`await served.ensure_parsed()` is the seam that materialises it.
|
|
279
328
|
- Do not change a mapping or list one of the getters handed you and expect the message to follow:
|
|
280
329
|
every one of them hands back a fresh copy.
|
|
281
330
|
- Do not import these classes in a library that only handles messages — depend on
|
|
@@ -11,11 +11,16 @@ What is here is one class per interface, and nothing besides:
|
|
|
11
11
|
- :class:`~xtr_http_message.server_request.ServerRequest` — a request that
|
|
12
12
|
arrived, with the environment parameters, the cookies, the query string
|
|
13
13
|
parsed, the files that came with it, the parsed body and the attributes a
|
|
14
|
-
handler annotates it with.
|
|
14
|
+
handler annotates it with. Born complete, or born connected over a
|
|
15
|
+
:class:`~xtr_http_message.request_source.RequestSource` — a connection whose
|
|
16
|
+
body side arrives as it is read.
|
|
15
17
|
- :class:`~xtr_http_message.response.Response` — a status code, the reason
|
|
16
18
|
phrase recommended for it, headers and a body.
|
|
17
19
|
- :class:`~xtr_http_message.stream.Stream` — a body, as a cursor over bytes
|
|
18
20
|
behind an open binary file object.
|
|
21
|
+
- :class:`~xtr_http_message.lazy_stream.LazyStream` — a body that stands in
|
|
22
|
+
unread: the whole of it is read once, on the first read, and buffered
|
|
23
|
+
after.
|
|
19
24
|
- :class:`~xtr_http_message.uri.Uri` — a URI taken apart into components and
|
|
20
25
|
put back together as text.
|
|
21
26
|
- :class:`~xtr_http_message.uploaded_file.UploadedFile` — one file that came
|
|
@@ -55,7 +60,9 @@ from .exception import (
|
|
|
55
60
|
UploadedFileError,
|
|
56
61
|
)
|
|
57
62
|
from .factory import HttpFactory
|
|
63
|
+
from .lazy_stream import LazyStream
|
|
58
64
|
from .request import Request
|
|
65
|
+
from .request_source import RequestSource
|
|
59
66
|
from .response import Response
|
|
60
67
|
from .server_request import ServerRequest
|
|
61
68
|
from .stream import Stream
|
|
@@ -73,7 +80,9 @@ __all__ = [
|
|
|
73
80
|
"HttpFactory",
|
|
74
81
|
"HttpMessageError",
|
|
75
82
|
"InvalidArgumentError",
|
|
83
|
+
"LazyStream",
|
|
76
84
|
"Request",
|
|
85
|
+
"RequestSource",
|
|
77
86
|
"Response",
|
|
78
87
|
"ServerRequest",
|
|
79
88
|
"Stream",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""The connected side of a server request: born over a source, read through it.
|
|
2
|
+
|
|
3
|
+
A request that arrived is not always known in full when it becomes a
|
|
4
|
+
message — a connection hands over its head at once, but its body is
|
|
5
|
+
still on the wire. This is the plumbing that lets the one server request
|
|
6
|
+
class be born that way, connected to a source that answers the body side
|
|
7
|
+
late, while the eager path the class has always had stays exactly as it
|
|
8
|
+
was. A mixin, for the same reason the message's own traits are: this is
|
|
9
|
+
one part of what a server request answers, and the class composing it
|
|
10
|
+
carries the rest.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, ClassVar, Self
|
|
16
|
+
|
|
17
|
+
from typing_extensions import override
|
|
18
|
+
|
|
19
|
+
from ._request_trait import RequestTrait
|
|
20
|
+
from .lazy_stream import LazyStream
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from xtr_http_message_contracts import StreamInterface, UriInterface
|
|
24
|
+
|
|
25
|
+
from .request_source import RequestSource
|
|
26
|
+
|
|
27
|
+
__all__ = ["ConnectedTrait"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConnectedTrait(RequestTrait):
|
|
31
|
+
"""The body side of a server request that arrives late, over a source.
|
|
32
|
+
|
|
33
|
+
The ``with_*`` members a connected request answers detach: the copy
|
|
34
|
+
is a plain eager request — the live parts frozen to what they answer
|
|
35
|
+
at that moment — so a change a listener makes never reaches the
|
|
36
|
+
connection. Because the copy is the same class, the ``Self`` the
|
|
37
|
+
``with_*`` members promise is finally the whole truth.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
__slots__: ClassVar[tuple[str, ...]] = ("_source",)
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
"""Start connected to nothing; :meth:`ServerRequest.over` connects."""
|
|
44
|
+
super().__init__()
|
|
45
|
+
self._source: RequestSource | None = None
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def get_body(self) -> StreamInterface:
|
|
49
|
+
"""Return the body of the message.
|
|
50
|
+
|
|
51
|
+
A connected request's body is one lazy stream over its source,
|
|
52
|
+
handed out however many times the body is asked for: the first
|
|
53
|
+
read materialises the whole body once, and every later read is
|
|
54
|
+
served out of the buffer.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The body, as a stream.
|
|
58
|
+
"""
|
|
59
|
+
if self._source is not None and self._stream is None:
|
|
60
|
+
self._stream: StreamInterface | None = LazyStream(self._source.materialise)
|
|
61
|
+
|
|
62
|
+
return super().get_body()
|
|
63
|
+
|
|
64
|
+
async def ensure_parsed(self) -> None:
|
|
65
|
+
"""Materialise the body side, when connected and nothing has read it yet.
|
|
66
|
+
|
|
67
|
+
The seam that can still await calls this before the message is
|
|
68
|
+
handed on: a consumer written against the contract reads
|
|
69
|
+
:meth:`~xtr_http_message.server_request.ServerRequest.get_parsed_body`
|
|
70
|
+
without reading the stream first, and only a body that has been
|
|
71
|
+
read can answer it. A no-op for an eager request, and for one
|
|
72
|
+
whose body has already been read.
|
|
73
|
+
"""
|
|
74
|
+
if self._source is None:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
body = self.get_body()
|
|
78
|
+
if isinstance(body, LazyStream) and not body.materialised:
|
|
79
|
+
_ = await body.get_contents()
|
|
80
|
+
|
|
81
|
+
@override
|
|
82
|
+
def with_protocol_version(self, version: str) -> Self:
|
|
83
|
+
"""Return an instance carrying ``version``; a connected one detaches first."""
|
|
84
|
+
if self._source is not None:
|
|
85
|
+
return self._copy().with_protocol_version(version)
|
|
86
|
+
|
|
87
|
+
return super().with_protocol_version(version)
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
def without_header(self, name: str) -> Self:
|
|
91
|
+
"""Return an instance with the ``name`` header gone; a connected one detaches first."""
|
|
92
|
+
if self._source is not None:
|
|
93
|
+
return self._copy().without_header(name)
|
|
94
|
+
|
|
95
|
+
return super().without_header(name)
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
def with_body(self, body: StreamInterface) -> Self:
|
|
99
|
+
"""Return an instance reading its body from ``body``; a connected one detaches first."""
|
|
100
|
+
if self._source is not None:
|
|
101
|
+
return self._copy().with_body(body)
|
|
102
|
+
|
|
103
|
+
return super().with_body(body)
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
def with_uri(self, uri: UriInterface, preserve_host: bool = False) -> Self:
|
|
107
|
+
"""Return an instance for ``uri``; a connected one detaches first.
|
|
108
|
+
|
|
109
|
+
The copy is a plain eager request, so the URI it carries is a
|
|
110
|
+
value, not a view — and a ``Host`` header is kept where asked.
|
|
111
|
+
"""
|
|
112
|
+
if self._source is not None:
|
|
113
|
+
return self._copy().with_uri(uri, preserve_host)
|
|
114
|
+
|
|
115
|
+
return super().with_uri(uri, preserve_host)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""A body stream that reads its bytes once, on its first read.
|
|
2
|
+
|
|
3
|
+
A request built over a connection must hand out a stream, but nobody
|
|
4
|
+
has read that connection's body until something asks to — reading it
|
|
5
|
+
eagerly is exactly what a lazy request may not do. This stream stands
|
|
6
|
+
in for the body until the first read, then materialises the whole of it
|
|
7
|
+
once and serves every later read out of the buffer: seekable and
|
|
8
|
+
rewindable, so a reader that inspects can rewind for whoever reads
|
|
9
|
+
after it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from typing import TYPE_CHECKING, final
|
|
16
|
+
|
|
17
|
+
from typing_extensions import override
|
|
18
|
+
from xtr_http_message_contracts import StreamInterface
|
|
19
|
+
|
|
20
|
+
from .stream import Stream
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Awaitable, Callable
|
|
24
|
+
from typing import BinaryIO
|
|
25
|
+
|
|
26
|
+
__all__ = ["LazyStream"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@final
|
|
30
|
+
class LazyStream(StreamInterface):
|
|
31
|
+
"""A body stream that reads its bytes once, on its first read.
|
|
32
|
+
|
|
33
|
+
Built with a coroutine that returns the buffered body — one that
|
|
34
|
+
reads the connection and parses it per its content type — and holds
|
|
35
|
+
nothing until the first :meth:`read` or :meth:`get_contents` calls
|
|
36
|
+
it. From then on it is a plain view over the buffer, and every
|
|
37
|
+
question the contract asks of an unread stream is answered for the
|
|
38
|
+
buffer it will be.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__slots__ = ("_buffered", "_materialise")
|
|
42
|
+
|
|
43
|
+
def __init__(self, materialise: Callable[[], Awaitable[bytes]]) -> None:
|
|
44
|
+
"""Read the body through ``materialise``, when anything first asks to.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
materialise: Reads the whole body and returns its bytes.
|
|
48
|
+
"""
|
|
49
|
+
self._materialise = materialise
|
|
50
|
+
self._buffered: StreamInterface | None = None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def materialised(self) -> bool:
|
|
54
|
+
"""Tell whether the body has been read into memory."""
|
|
55
|
+
return self._buffered is not None
|
|
56
|
+
|
|
57
|
+
async def _held(self) -> StreamInterface:
|
|
58
|
+
"""Return the buffered body, reading it off the connection if nothing has yet."""
|
|
59
|
+
if self._buffered is None:
|
|
60
|
+
self._buffered = Stream.create(await self._materialise())
|
|
61
|
+
return self._buffered
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
async def close(self) -> None:
|
|
65
|
+
"""Close the buffer, if there is one to close.
|
|
66
|
+
|
|
67
|
+
An unread body holds nothing open, so closing one before the
|
|
68
|
+
first read is not a read: it is a no-op, and the next reader
|
|
69
|
+
still gets the bytes off the connection.
|
|
70
|
+
"""
|
|
71
|
+
if self._buffered is not None:
|
|
72
|
+
await self._buffered.close()
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
def detach(self) -> BinaryIO | None:
|
|
76
|
+
"""Hand out the buffer's file object, or ``None`` while there is none."""
|
|
77
|
+
return self._buffered.detach() if self._buffered is not None else None
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
def get_size(self) -> int | None:
|
|
81
|
+
"""Return the size of the buffered body, or ``None`` before there is one."""
|
|
82
|
+
return self._buffered.get_size() if self._buffered is not None else None
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
def tell(self) -> int:
|
|
86
|
+
"""Return where the cursor sits: the first byte, until there is a buffer."""
|
|
87
|
+
return self._buffered.tell() if self._buffered is not None else 0
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
def eof(self) -> bool:
|
|
91
|
+
"""Tell whether a read reached the end: no read has, until there is a buffer."""
|
|
92
|
+
return self._buffered.eof() if self._buffered is not None else False
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
def is_seekable(self) -> bool:
|
|
96
|
+
"""Tell whether the cursor moves: the buffer it will be is over bytes in memory."""
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
@override
|
|
100
|
+
async def seek(self, offset: int, whence: int = os.SEEK_SET) -> None:
|
|
101
|
+
"""Move the cursor, materialising the body if nothing has read it yet.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
StreamError: When the buffer cannot be moved, once there is one.
|
|
105
|
+
"""
|
|
106
|
+
await (await self._held()).seek(offset, whence)
|
|
107
|
+
|
|
108
|
+
@override
|
|
109
|
+
async def rewind(self) -> None:
|
|
110
|
+
"""Move the cursor back to the first byte, materialising if nothing has read."""
|
|
111
|
+
await (await self._held()).rewind()
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
def is_writable(self) -> bool:
|
|
115
|
+
"""Tell whether bytes can be written to the buffer: it is a buffer, so yes."""
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
@override
|
|
119
|
+
async def write(self, data: bytes) -> int:
|
|
120
|
+
"""Write at the cursor, materialising the body if nothing has read it yet.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
StreamError: When the buffer cannot be written to.
|
|
124
|
+
"""
|
|
125
|
+
return await (await self._held()).write(data)
|
|
126
|
+
|
|
127
|
+
@override
|
|
128
|
+
def is_readable(self) -> bool:
|
|
129
|
+
"""Tell whether bytes can be read: they can, once anything has been read."""
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
@override
|
|
133
|
+
async def read(self, length: int) -> bytes:
|
|
134
|
+
"""Read up to ``length`` bytes, materialising the body if nothing has read it yet.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
StreamError: When the buffer cannot be read.
|
|
138
|
+
"""
|
|
139
|
+
return await (await self._held()).read(length)
|
|
140
|
+
|
|
141
|
+
@override
|
|
142
|
+
async def get_contents(self) -> bytes:
|
|
143
|
+
"""Return every byte of the body, materialising it if nothing has read it yet.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
StreamError: When the buffer cannot be read.
|
|
147
|
+
"""
|
|
148
|
+
return await (await self._held()).get_contents()
|
|
149
|
+
|
|
150
|
+
@override
|
|
151
|
+
def get_metadata(self, key: str | None = None) -> object:
|
|
152
|
+
"""Return what the buffer knows about itself, or nothing while there is none."""
|
|
153
|
+
return (
|
|
154
|
+
self._buffered.get_metadata(key)
|
|
155
|
+
if self._buffered is not None
|
|
156
|
+
else ({} if key is None else None)
|
|
157
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Where a connected request reads its late answers from.
|
|
2
|
+
|
|
3
|
+
A request built over a connection is born before anything has read its
|
|
4
|
+
body: the head is already on the connection, but the body, the parsed
|
|
5
|
+
form and the uploaded files arrive only as the body is read. This is
|
|
6
|
+
the seam between the two moments — framework-free, so the message
|
|
7
|
+
package stays tied to no web framework, and small enough that a
|
|
8
|
+
connection adapter is all an integration needs to write.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
|
|
18
|
+
__all__ = ["RequestSource"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@runtime_checkable
|
|
22
|
+
class RequestSource(Protocol):
|
|
23
|
+
"""Where a connected request reads its late answers from.
|
|
24
|
+
|
|
25
|
+
A request built over a connection materialises its body side
|
|
26
|
+
lazily: the sync getters answer what materialisation has yielded so
|
|
27
|
+
far, and the body stream reads the rest through here on its first
|
|
28
|
+
read. The head — method, URI, headers, cookies, query, environment —
|
|
29
|
+
never comes from here: it is passed eagerly when the request is
|
|
30
|
+
built, because it costs no input and never changes mid-request.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
async def materialise(self) -> bytes:
|
|
34
|
+
"""Read the whole body once, and answer its bytes.
|
|
35
|
+
|
|
36
|
+
Runs at most once — the implementation memoises — filling the
|
|
37
|
+
parsed side and the uploaded files as a side effect, so the sync
|
|
38
|
+
peeks answer from then on. The bytes answered are the raw body
|
|
39
|
+
bytes: for a submitted form, the raw form bytes, with the form
|
|
40
|
+
itself landing in the parsed side.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The raw bytes of the body.
|
|
44
|
+
"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def parsed_body(self) -> object | None:
|
|
48
|
+
"""Return what parsing the body yielded, or ``None`` until it has.
|
|
49
|
+
|
|
50
|
+
A sync peek at what :meth:`materialise` has yielded so far: the
|
|
51
|
+
form for form content types, a JSON object for JSON, ``None``
|
|
52
|
+
before any body read.
|
|
53
|
+
"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def uploaded_files(self) -> Mapping[str, object]:
|
|
57
|
+
"""Return the files that came with the body, empty until it is read.
|
|
58
|
+
|
|
59
|
+
A sync peek, as :meth:`parsed_body` is one: the files are
|
|
60
|
+
gathered when the body is materialised, so before any body read
|
|
61
|
+
this answers an empty mapping.
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def attributes(self) -> Mapping[str, object]:
|
|
66
|
+
"""Return what has been derived from the request while handling it.
|
|
67
|
+
|
|
68
|
+
A live view: routing fills the named path parameters after the
|
|
69
|
+
request exists, and each read answers them as they are now.
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
@@ -9,7 +9,7 @@ from urllib.parse import parse_qsl
|
|
|
9
9
|
from typing_extensions import override
|
|
10
10
|
from xtr_http_message_contracts import ServerRequestInterface
|
|
11
11
|
|
|
12
|
-
from .
|
|
12
|
+
from ._connected_trait import ConnectedTrait
|
|
13
13
|
from .exception import InvalidArgumentError
|
|
14
14
|
from .uri import Uri
|
|
15
15
|
|
|
@@ -19,6 +19,8 @@ if TYPE_CHECKING:
|
|
|
19
19
|
|
|
20
20
|
from xtr_http_message_contracts import StreamInterface, UriInterface
|
|
21
21
|
|
|
22
|
+
from .request_source import RequestSource
|
|
23
|
+
|
|
22
24
|
__all__ = ["ServerRequest"]
|
|
23
25
|
|
|
24
26
|
_HOST = "Host"
|
|
@@ -61,7 +63,7 @@ def _parse_query(query: str) -> dict[str, object]:
|
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
@final
|
|
64
|
-
class ServerRequest(
|
|
66
|
+
class ServerRequest(ConnectedTrait, ServerRequestInterface):
|
|
65
67
|
"""An incoming, server-side request.
|
|
66
68
|
|
|
67
69
|
On top of what every request carries, this one gathers what reached the
|
|
@@ -88,6 +90,14 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
88
90
|
mapping of a kind that answers more than a single value per name reaches
|
|
89
91
|
whoever reads it back as that kind, rather than flattened into a plain
|
|
90
92
|
one.
|
|
93
|
+
|
|
94
|
+
A request is also born connected, over a
|
|
95
|
+
:class:`~xtr_http_message.request_source.RequestSource` — see
|
|
96
|
+
:meth:`over`. Its head is passed eagerly; its body is one lazy stream
|
|
97
|
+
that materialises on first read; its parsed body, uploaded files and
|
|
98
|
+
attributes answer what the source has yielded so far, the attributes
|
|
99
|
+
live. The ``with_*`` members of such a request detach: plain eager
|
|
100
|
+
copies, so a change a handler makes never reaches the connection.
|
|
91
101
|
"""
|
|
92
102
|
|
|
93
103
|
__slots__ = (
|
|
@@ -146,6 +156,77 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
146
156
|
|
|
147
157
|
self._set_body(body)
|
|
148
158
|
|
|
159
|
+
@classmethod
|
|
160
|
+
def over(
|
|
161
|
+
cls,
|
|
162
|
+
method: str,
|
|
163
|
+
uri: UriInterface | str,
|
|
164
|
+
*,
|
|
165
|
+
source: RequestSource,
|
|
166
|
+
body: bytes | BinaryIO | StreamInterface | None = None,
|
|
167
|
+
headers: Mapping[str, str | Sequence[str]] | None = None,
|
|
168
|
+
server_params: Mapping[str, object] | None = None,
|
|
169
|
+
cookies: Mapping[str, str] | None = None,
|
|
170
|
+
query: Mapping[str, object] | None = None,
|
|
171
|
+
protocol_version: str = "1.1",
|
|
172
|
+
) -> Self:
|
|
173
|
+
"""Build a server request over ``source``, born connected to it.
|
|
174
|
+
|
|
175
|
+
The head is passed eagerly — it costs no input and never changes
|
|
176
|
+
mid-request — and the body side comes from the source: the body
|
|
177
|
+
is one lazy stream that materialises on its first read, and the
|
|
178
|
+
parsed body and the uploaded files answer what materialisation
|
|
179
|
+
has yielded so far.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
method: The HTTP method, kept exactly as given.
|
|
183
|
+
uri: The URI of the request. Given as text, it is read into a
|
|
184
|
+
:class:`~xtr_http_message.uri.Uri`.
|
|
185
|
+
source: Where the connected request reads its late answers
|
|
186
|
+
from.
|
|
187
|
+
body: Refused — the body of a request built over a source is
|
|
188
|
+
the source's to materialise, and there is one owner of
|
|
189
|
+
the body. The parameter exists so a caller used to the
|
|
190
|
+
constructor's shape is told the rule rather than left
|
|
191
|
+
with an unexpected keyword.
|
|
192
|
+
headers: Each header name mapped to one value or to several.
|
|
193
|
+
A header the message grammar refuses is left out rather
|
|
194
|
+
than refusing the request: a connection let it through,
|
|
195
|
+
and a message is no place to re-litigate that.
|
|
196
|
+
server_params: The parameters of the environment the request
|
|
197
|
+
arrived in, taken exactly as given.
|
|
198
|
+
cookies: The cookies the client sent; left out, none.
|
|
199
|
+
query: The query string, parsed into parameters; left out,
|
|
200
|
+
the URI's own query stands.
|
|
201
|
+
protocol_version: The HTTP version number alone.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
The request, connected to ``source``.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
InvalidArgumentError: When ``body`` is given alongside
|
|
208
|
+
``source``.
|
|
209
|
+
"""
|
|
210
|
+
if body is not None:
|
|
211
|
+
raise InvalidArgumentError(
|
|
212
|
+
"One owner of the body: body= and source= cannot both be given"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
request = cls(method, uri, None, None, protocol_version, server_params)
|
|
216
|
+
for name, values in (headers or {}).items():
|
|
217
|
+
try:
|
|
218
|
+
request = request.with_header(name, values)
|
|
219
|
+
except ValueError:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
if cookies is not None:
|
|
223
|
+
request = request.with_cookie_params(cookies)
|
|
224
|
+
if query is not None:
|
|
225
|
+
request = request.with_query_params(query)
|
|
226
|
+
|
|
227
|
+
request._source = source # noqa: SLF001 — a fresh instance of this very class.
|
|
228
|
+
return request
|
|
229
|
+
|
|
149
230
|
@override
|
|
150
231
|
def get_server_params(self) -> Mapping[str, object]:
|
|
151
232
|
"""Return the parameters of the environment the request arrived in.
|
|
@@ -225,8 +306,13 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
225
306
|
A mapping shaped like the names the files were submitted under,
|
|
226
307
|
every leaf of it an uploaded file — the mapping as it was given
|
|
227
308
|
where nothing can change it, otherwise a fresh one. Empty when no
|
|
228
|
-
file came with the request.
|
|
309
|
+
file came with the request. A request built over a connection
|
|
310
|
+
answers what its body's materialisation has gathered so far:
|
|
311
|
+
empty until the body has been read.
|
|
229
312
|
"""
|
|
313
|
+
if self._source is not None:
|
|
314
|
+
return self._source.uploaded_files()
|
|
315
|
+
|
|
230
316
|
return _held(self._uploaded_files)
|
|
231
317
|
|
|
232
318
|
@override
|
|
@@ -251,8 +337,13 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
251
337
|
|
|
252
338
|
Returns:
|
|
253
339
|
Whatever parsing the body yielded, or ``None`` when nothing has
|
|
254
|
-
been parsed into the request.
|
|
340
|
+
been parsed into the request. A request built over a connection
|
|
341
|
+
answers what its body's materialisation has yielded so far:
|
|
342
|
+
``None`` until the body has been read.
|
|
255
343
|
"""
|
|
344
|
+
if self._source is not None:
|
|
345
|
+
return self._source.parsed_body()
|
|
346
|
+
|
|
256
347
|
return self._parsed_body
|
|
257
348
|
|
|
258
349
|
@override
|
|
@@ -287,8 +378,14 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
287
378
|
"""Return what was derived from the request while handling it.
|
|
288
379
|
|
|
289
380
|
Returns:
|
|
290
|
-
A fresh mapping of each attribute name to its value.
|
|
381
|
+
A fresh mapping of each attribute name to its value. A request
|
|
382
|
+
built over a connection reads them live, off its source: the
|
|
383
|
+
named parameters the matched route fills in are there as soon
|
|
384
|
+
as routing has filled them — which is after the request exists.
|
|
291
385
|
"""
|
|
386
|
+
if self._source is not None:
|
|
387
|
+
return self._source.attributes()
|
|
388
|
+
|
|
292
389
|
return dict(self._attributes)
|
|
293
390
|
|
|
294
391
|
@override
|
|
@@ -307,6 +404,9 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
307
404
|
Returns:
|
|
308
405
|
The value under ``name``, or ``default``.
|
|
309
406
|
"""
|
|
407
|
+
if self._source is not None:
|
|
408
|
+
return self._source.attributes().get(name, default)
|
|
409
|
+
|
|
310
410
|
if name not in self._attributes:
|
|
311
411
|
return default
|
|
312
412
|
|
|
@@ -324,7 +424,9 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
324
424
|
A new instance; this one is left as it was.
|
|
325
425
|
"""
|
|
326
426
|
new = self._copy()
|
|
327
|
-
|
|
427
|
+
# Read the copy's bag, not this instance's: a connected request's own
|
|
428
|
+
# is empty, its attributes living on the source the copy detached from.
|
|
429
|
+
new._attributes = {**new._attributes, name: value} # noqa: SLF001 — a copy of this class.
|
|
328
430
|
|
|
329
431
|
return new
|
|
330
432
|
|
|
@@ -337,8 +439,19 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
337
439
|
|
|
338
440
|
Returns:
|
|
339
441
|
A new instance, or this one when the request carries no such
|
|
340
|
-
attribute.
|
|
442
|
+
attribute. A connected request always answers a copy: its
|
|
443
|
+
attributes are live, so the name is looked for in what they
|
|
444
|
+
answer now, and the copy detaches either way.
|
|
341
445
|
"""
|
|
446
|
+
source = self._source
|
|
447
|
+
if source is not None:
|
|
448
|
+
new = self._copy()
|
|
449
|
+
new._attributes = { # noqa: SLF001 — a copy of this very class.
|
|
450
|
+
held: value for held, value in source.attributes().items() if held != name
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return new
|
|
454
|
+
|
|
342
455
|
if name not in self._attributes:
|
|
343
456
|
return self
|
|
344
457
|
|
|
@@ -348,3 +461,28 @@ class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
|
348
461
|
}
|
|
349
462
|
|
|
350
463
|
return new
|
|
464
|
+
|
|
465
|
+
@override
|
|
466
|
+
def _copy(self) -> Self:
|
|
467
|
+
"""Return a copy to change, leaving this instance as it is.
|
|
468
|
+
|
|
469
|
+
A connected request's copy detaches: it is a plain eager request,
|
|
470
|
+
carrying the head as it was passed and the live parts frozen to
|
|
471
|
+
what they answer at this moment — the attributes as a snapshot,
|
|
472
|
+
the parsed body and the uploaded files as the source's peeks. The
|
|
473
|
+
body is the one lazy stream, shared: one materialisation serves
|
|
474
|
+
the request and every copy of it, and the copy never reads the
|
|
475
|
+
source again.
|
|
476
|
+
"""
|
|
477
|
+
source = self._source
|
|
478
|
+
if source is None:
|
|
479
|
+
return super()._copy()
|
|
480
|
+
|
|
481
|
+
new = super()._copy()
|
|
482
|
+
new._stream = self.get_body() # noqa: SLF001 — a copy of this very class.
|
|
483
|
+
new._attributes = dict(source.attributes()) # noqa: SLF001 — a copy of this class.
|
|
484
|
+
new._parsed_body = source.parsed_body() # noqa: SLF001 — a copy of this class.
|
|
485
|
+
new._uploaded_files = source.uploaded_files() # noqa: SLF001 — a copy of this class.
|
|
486
|
+
new._source = None # noqa: SLF001 — a copy of this very class.
|
|
487
|
+
|
|
488
|
+
return new
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/exception/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/exception/stream_error.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xtr_http_message-4.0.4 → xtr_http_message-5.0.0}/src/xtr_http_message/factory/http_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|