xtr-http-message 4.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xtr_http_message/.agents/skills/xtr-http-message/SKILL.md +296 -0
- xtr_http_message/__init__.py +85 -0
- xtr_http_message/_ascii.py +26 -0
- xtr_http_message/_message_trait.py +354 -0
- xtr_http_message/_request_trait.py +164 -0
- xtr_http_message/exception/__init__.py +24 -0
- xtr_http_message/exception/http_message_error.py +14 -0
- xtr_http_message/exception/invalid_argument_error.py +17 -0
- xtr_http_message/exception/stream_error.py +17 -0
- xtr_http_message/exception/uploaded_file_error.py +17 -0
- xtr_http_message/factory/__init__.py +20 -0
- xtr_http_message/factory/http_factory.py +235 -0
- xtr_http_message/py.typed +0 -0
- xtr_http_message/request.py +74 -0
- xtr_http_message/response.py +196 -0
- xtr_http_message/server_request.py +350 -0
- xtr_http_message/stream.py +404 -0
- xtr_http_message/uploaded_file.py +270 -0
- xtr_http_message/uri.py +660 -0
- xtr_http_message-4.0.2.dist-info/METADATA +280 -0
- xtr_http_message-4.0.2.dist-info/RECORD +23 -0
- xtr_http_message-4.0.2.dist-info/WHEEL +4 -0
- xtr_http_message-4.0.2.dist-info/licenses/LICENSE +23 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""A request as it arrived, with everything the server learned alongside it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, MutableMapping
|
|
6
|
+
from typing import TYPE_CHECKING, Self, TypeVar, final
|
|
7
|
+
from urllib.parse import parse_qsl
|
|
8
|
+
|
|
9
|
+
from typing_extensions import override
|
|
10
|
+
from xtr_http_message_contracts import ServerRequestInterface
|
|
11
|
+
|
|
12
|
+
from ._request_trait import RequestTrait
|
|
13
|
+
from .exception import InvalidArgumentError
|
|
14
|
+
from .uri import Uri
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from typing import BinaryIO
|
|
19
|
+
|
|
20
|
+
from xtr_http_message_contracts import StreamInterface, UriInterface
|
|
21
|
+
|
|
22
|
+
__all__ = ["ServerRequest"]
|
|
23
|
+
|
|
24
|
+
_HOST = "Host"
|
|
25
|
+
|
|
26
|
+
_K = TypeVar("_K")
|
|
27
|
+
_V = TypeVar("_V")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _held(given: Mapping[_K, _V]) -> Mapping[_K, _V]:
|
|
31
|
+
"""Return the mapping to hold for one given to the request.
|
|
32
|
+
|
|
33
|
+
A mapping nobody can change is held as it is: copying it would only cost a
|
|
34
|
+
read of every entry, and would turn whatever kind of mapping it is — one
|
|
35
|
+
that answers every value of a repeated name, say — into a plain one that
|
|
36
|
+
answers a single value per name. A mutable mapping is copied, since the
|
|
37
|
+
caller keeps a reference to it and a request must not change behind
|
|
38
|
+
whoever was handed it.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
given: The mapping the caller passed.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The mapping itself when it cannot be changed, otherwise a copy of it.
|
|
45
|
+
"""
|
|
46
|
+
return dict(given) if isinstance(given, MutableMapping) else given
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_query(query: str) -> dict[str, object]:
|
|
50
|
+
"""Read a query string into parameters, the last of a repeated name winning.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
query: The query string, without its leading ``?``.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Each parameter name mapped to its value. A name with no value reads as
|
|
57
|
+
an empty string rather than being dropped, since whether a parameter
|
|
58
|
+
was sent at all is often the question.
|
|
59
|
+
"""
|
|
60
|
+
return dict(parse_qsl(query, keep_blank_values=True))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@final
|
|
64
|
+
class ServerRequest(RequestTrait, ServerRequestInterface):
|
|
65
|
+
"""An incoming, server-side request.
|
|
66
|
+
|
|
67
|
+
On top of what every request carries, this one gathers what reached the
|
|
68
|
+
application with it: the parameters of the environment it arrived in, the
|
|
69
|
+
cookies, the query string parsed, the files that came with it and the body
|
|
70
|
+
parsed into parameters — plus the attributes a handler annotates it with
|
|
71
|
+
as it works out who sent it and what they asked for.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from xtr_http_message import ServerRequest
|
|
75
|
+
|
|
76
|
+
request = ServerRequest("GET", "/orders?page=2")
|
|
77
|
+
assert request.get_query_params() == {"page": "2"}
|
|
78
|
+
assert request.with_attribute("user", "ada").get_attribute("user") == "ada"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The query parameters are read off the URI as the request is built; nothing
|
|
82
|
+
keeps them in step with it afterwards, so a URI changed later leaves them
|
|
83
|
+
as they were — which is what lets a handler correct them.
|
|
84
|
+
|
|
85
|
+
The cookies, the query parameters and the uploaded files are held exactly
|
|
86
|
+
as they were given when nothing can change them — a mapping that is not a
|
|
87
|
+
:class:`~collections.abc.MutableMapping` — and copied when they can. So a
|
|
88
|
+
mapping of a kind that answers more than a single value per name reaches
|
|
89
|
+
whoever reads it back as that kind, rather than flattened into a plain
|
|
90
|
+
one.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
__slots__ = (
|
|
94
|
+
"_attributes",
|
|
95
|
+
"_cookie_params",
|
|
96
|
+
"_parsed_body",
|
|
97
|
+
"_query_params",
|
|
98
|
+
"_server_params",
|
|
99
|
+
"_uploaded_files",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
method: str,
|
|
105
|
+
uri: UriInterface | str,
|
|
106
|
+
headers: Mapping[str, str | Sequence[str]] | None = None,
|
|
107
|
+
body: bytes | BinaryIO | StreamInterface | None = None,
|
|
108
|
+
version: str = "1.1",
|
|
109
|
+
server_params: Mapping[str, object] | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Build a server request.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
method: The HTTP method, kept exactly as given.
|
|
115
|
+
uri: The URI of the request. Given as text, it is read into a
|
|
116
|
+
:class:`~xtr_http_message.uri.Uri`.
|
|
117
|
+
headers: Each header name mapped to one value or to several.
|
|
118
|
+
body: The payload, in any of the shapes a stream is built from.
|
|
119
|
+
Left out, nothing is opened until the body is asked for.
|
|
120
|
+
version: The HTTP version number alone.
|
|
121
|
+
server_params: The parameters of the environment the request
|
|
122
|
+
arrived in, taken exactly as given. ``None`` means none.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
InvalidArgumentError: When the URI cannot be read, or a header
|
|
126
|
+
name or value is not one.
|
|
127
|
+
"""
|
|
128
|
+
super().__init__()
|
|
129
|
+
|
|
130
|
+
self._server_params: Mapping[str, object] = (
|
|
131
|
+
{} if server_params is None else _held(server_params)
|
|
132
|
+
)
|
|
133
|
+
self._attributes: dict[str, object] = {}
|
|
134
|
+
self._cookie_params: Mapping[str, str] = {}
|
|
135
|
+
self._parsed_body: object | None = None
|
|
136
|
+
self._uploaded_files: Mapping[str, object] = {}
|
|
137
|
+
|
|
138
|
+
self._method = method
|
|
139
|
+
self._uri = Uri(uri) if isinstance(uri, str) else uri
|
|
140
|
+
self._set_headers(headers or {})
|
|
141
|
+
self._protocol = version
|
|
142
|
+
self._query_params: Mapping[str, object] = _parse_query(self._uri.get_query())
|
|
143
|
+
|
|
144
|
+
if not self.has_header(_HOST):
|
|
145
|
+
self._update_host_from_uri()
|
|
146
|
+
|
|
147
|
+
self._set_body(body)
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
def get_server_params(self) -> Mapping[str, object]:
|
|
151
|
+
"""Return the parameters of the environment the request arrived in.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Each parameter under the name the environment gave it — the
|
|
155
|
+
mapping as it was given where nothing can change it, otherwise a
|
|
156
|
+
fresh one. Nothing changes them: they are the state the request
|
|
157
|
+
arrived in.
|
|
158
|
+
"""
|
|
159
|
+
return _held(self._server_params)
|
|
160
|
+
|
|
161
|
+
@override
|
|
162
|
+
def get_cookie_params(self) -> Mapping[str, str]:
|
|
163
|
+
"""Return the cookies the client sent.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Each cookie name mapped to its value — the mapping as it was given
|
|
167
|
+
where nothing can change it, otherwise a fresh one.
|
|
168
|
+
"""
|
|
169
|
+
return _held(self._cookie_params)
|
|
170
|
+
|
|
171
|
+
@override
|
|
172
|
+
def with_cookie_params(self, cookies: Mapping[str, str]) -> Self:
|
|
173
|
+
"""Return an instance carrying ``cookies``.
|
|
174
|
+
|
|
175
|
+
Neither the ``Cookie`` header of the new instance nor its environment
|
|
176
|
+
parameters change with them.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
cookies: Each cookie name mapped to its value.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A new instance; this one is left as it was.
|
|
183
|
+
"""
|
|
184
|
+
new = self._copy()
|
|
185
|
+
new._cookie_params = _held(cookies) # noqa: SLF001 — a copy of this very class.
|
|
186
|
+
|
|
187
|
+
return new
|
|
188
|
+
|
|
189
|
+
@override
|
|
190
|
+
def get_query_params(self) -> Mapping[str, object]:
|
|
191
|
+
"""Return the query string, parsed into parameters.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Each parameter name mapped to its value — the mapping as it was
|
|
195
|
+
given where nothing can change it, otherwise a fresh one. These
|
|
196
|
+
were read off the URI when the request was built and may have been
|
|
197
|
+
replaced since; parse ``get_uri().get_query()`` to be sure of what
|
|
198
|
+
arrived.
|
|
199
|
+
"""
|
|
200
|
+
return _held(self._query_params)
|
|
201
|
+
|
|
202
|
+
@override
|
|
203
|
+
def with_query_params(self, query: Mapping[str, object]) -> Self:
|
|
204
|
+
"""Return an instance carrying ``query``.
|
|
205
|
+
|
|
206
|
+
Neither the URI the request holds nor its environment parameters
|
|
207
|
+
change with them.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
query: Each parameter name mapped to its value.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
A new instance; this one is left as it was.
|
|
214
|
+
"""
|
|
215
|
+
new = self._copy()
|
|
216
|
+
new._query_params = _held(query) # noqa: SLF001 — a copy of this very class.
|
|
217
|
+
|
|
218
|
+
return new
|
|
219
|
+
|
|
220
|
+
@override
|
|
221
|
+
def get_uploaded_files(self) -> Mapping[str, object]:
|
|
222
|
+
"""Return the files that came with the request.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
A mapping shaped like the names the files were submitted under,
|
|
226
|
+
every leaf of it an uploaded file — the mapping as it was given
|
|
227
|
+
where nothing can change it, otherwise a fresh one. Empty when no
|
|
228
|
+
file came with the request.
|
|
229
|
+
"""
|
|
230
|
+
return _held(self._uploaded_files)
|
|
231
|
+
|
|
232
|
+
@override
|
|
233
|
+
def with_uploaded_files(self, uploaded_files: Mapping[str, object]) -> Self:
|
|
234
|
+
"""Return an instance carrying ``uploaded_files``.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
uploaded_files: A tree of uploaded files, shaped as
|
|
238
|
+
:meth:`get_uploaded_files` describes.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
A new instance; this one is left as it was.
|
|
242
|
+
"""
|
|
243
|
+
new = self._copy()
|
|
244
|
+
new._uploaded_files = _held(uploaded_files) # noqa: SLF001 — a copy of this class.
|
|
245
|
+
|
|
246
|
+
return new
|
|
247
|
+
|
|
248
|
+
@override
|
|
249
|
+
def get_parsed_body(self) -> object | None:
|
|
250
|
+
"""Return the parameters found in the body of the request.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Whatever parsing the body yielded, or ``None`` when nothing has
|
|
254
|
+
been parsed into the request.
|
|
255
|
+
"""
|
|
256
|
+
return self._parsed_body
|
|
257
|
+
|
|
258
|
+
@override
|
|
259
|
+
def with_parsed_body(self, data: object | None) -> Self:
|
|
260
|
+
"""Return an instance carrying ``data`` as its body parameters.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
data: The parsed body parameters — a mapping, or an object
|
|
264
|
+
standing for the structure the body held — or ``None`` when
|
|
265
|
+
there was nothing to parse.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
A new instance; this one is left as it was.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
InvalidArgumentError: When the data is a scalar: parsing a body
|
|
272
|
+
yields structure, so a number or a string cannot be what it
|
|
273
|
+
yielded.
|
|
274
|
+
"""
|
|
275
|
+
if isinstance(data, (bool, bytes, complex, float, int, str)):
|
|
276
|
+
raise InvalidArgumentError(
|
|
277
|
+
"First parameter to with_parsed_body MUST be object, mapping or None"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
new = self._copy()
|
|
281
|
+
new._parsed_body = data # noqa: SLF001 — a copy of this very class.
|
|
282
|
+
|
|
283
|
+
return new
|
|
284
|
+
|
|
285
|
+
@override
|
|
286
|
+
def get_attributes(self) -> Mapping[str, object]:
|
|
287
|
+
"""Return what was derived from the request while handling it.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
A fresh mapping of each attribute name to its value.
|
|
291
|
+
"""
|
|
292
|
+
return dict(self._attributes)
|
|
293
|
+
|
|
294
|
+
@override
|
|
295
|
+
def get_attribute(self, name: str, default: object | None = None) -> object | None:
|
|
296
|
+
"""Return one of the attributes :meth:`get_attributes` describes.
|
|
297
|
+
|
|
298
|
+
An attribute recorded as ``None`` is told apart from one that was
|
|
299
|
+
never recorded: the first reads as ``None``, the second as
|
|
300
|
+
``default``.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
name: The attribute name.
|
|
304
|
+
default: What to answer with when the request carries no such
|
|
305
|
+
attribute.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
The value under ``name``, or ``default``.
|
|
309
|
+
"""
|
|
310
|
+
if name not in self._attributes:
|
|
311
|
+
return default
|
|
312
|
+
|
|
313
|
+
return self._attributes[name]
|
|
314
|
+
|
|
315
|
+
@override
|
|
316
|
+
def with_attribute(self, name: str, value: object) -> Self:
|
|
317
|
+
"""Return an instance carrying one more attribute.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
name: The attribute name.
|
|
321
|
+
value: The value to record under it.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
A new instance; this one is left as it was.
|
|
325
|
+
"""
|
|
326
|
+
new = self._copy()
|
|
327
|
+
new._attributes = {**self._attributes, name: value} # noqa: SLF001 — a copy of this class.
|
|
328
|
+
|
|
329
|
+
return new
|
|
330
|
+
|
|
331
|
+
@override
|
|
332
|
+
def without_attribute(self, name: str) -> Self:
|
|
333
|
+
"""Return an instance with the ``name`` attribute gone.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
name: The attribute name.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
A new instance, or this one when the request carries no such
|
|
340
|
+
attribute.
|
|
341
|
+
"""
|
|
342
|
+
if name not in self._attributes:
|
|
343
|
+
return self
|
|
344
|
+
|
|
345
|
+
new = self._copy()
|
|
346
|
+
new._attributes = { # noqa: SLF001 — a copy of this very class.
|
|
347
|
+
held: value for held, value in self._attributes.items() if held != name
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return new
|