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.
@@ -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