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,354 @@
|
|
|
1
|
+
"""What a request and a response have in common, in one place."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import TYPE_CHECKING, ClassVar, Self
|
|
9
|
+
|
|
10
|
+
from ._ascii import lower
|
|
11
|
+
from .exception import InvalidArgumentError
|
|
12
|
+
from .stream import Stream
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from typing import BinaryIO
|
|
17
|
+
|
|
18
|
+
from xtr_http_message_contracts import StreamInterface
|
|
19
|
+
|
|
20
|
+
__all__ = ["MessageTrait"]
|
|
21
|
+
|
|
22
|
+
_DEFAULT_PROTOCOL = "1.1"
|
|
23
|
+
|
|
24
|
+
# A header name is one or more token characters and nothing else, so that no
|
|
25
|
+
# name can carry the delimiters that would let it inject a header of its own.
|
|
26
|
+
_HEADER_NAME = re.compile(r"[!#$%&'*+.^_`|~0-9A-Za-z-]+")
|
|
27
|
+
# A header value is visible characters and inner blanks. The upper half of the
|
|
28
|
+
# range is written as the bytes it is, so a character above U+00FF — which no
|
|
29
|
+
# header field can carry undeclared — is refused rather than quietly sent.
|
|
30
|
+
_HEADER_VALUE = re.compile("[ \t\x21-\x7e\x80-\xff]*")
|
|
31
|
+
# The blanks around a header value belong to the field, not to the value.
|
|
32
|
+
_OPTIONAL_WHITESPACE = " \t"
|
|
33
|
+
|
|
34
|
+
_BAD_NAME = "Header name must be an RFC 7230 compatible string"
|
|
35
|
+
_BAD_VALUE = "Header values must be RFC 7230 compatible strings"
|
|
36
|
+
_NO_VALUE = "Header values must be a string or an array of strings, empty array given"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _trimmed_value(value: object) -> str:
|
|
40
|
+
"""Return a header value with its surrounding blanks taken off.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
value: What is being offered as a value.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The value, trimmed.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
InvalidArgumentError: When it is not a header value — not a string at
|
|
50
|
+
all, or a string carrying what no header field can.
|
|
51
|
+
"""
|
|
52
|
+
if not isinstance(value, str) or _HEADER_VALUE.fullmatch(value) is None:
|
|
53
|
+
raise InvalidArgumentError(_BAD_VALUE)
|
|
54
|
+
|
|
55
|
+
return value.strip(_OPTIONAL_WHITESPACE)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _validate_and_trim(name: str, value: object) -> list[str]:
|
|
59
|
+
"""Check a header against the grammar and return its values, trimmed.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
name: The header name.
|
|
63
|
+
value: One value, or every value the header is to hold. Taken as
|
|
64
|
+
anything at all, so that a caller whose types went unchecked is
|
|
65
|
+
told what was wrong with what it passed rather than left with a
|
|
66
|
+
type error from deeper in.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The values, each with its surrounding blanks taken off.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
InvalidArgumentError: When the name is not a header name, a value is
|
|
73
|
+
not a header value, or a header is given no value at all.
|
|
74
|
+
"""
|
|
75
|
+
if _HEADER_NAME.fullmatch(name) is None:
|
|
76
|
+
raise InvalidArgumentError(_BAD_NAME)
|
|
77
|
+
|
|
78
|
+
if isinstance(value, str):
|
|
79
|
+
return [_trimmed_value(value)]
|
|
80
|
+
|
|
81
|
+
if not isinstance(value, Sequence):
|
|
82
|
+
raise InvalidArgumentError(_BAD_VALUE)
|
|
83
|
+
|
|
84
|
+
offered: Sequence[object] = value
|
|
85
|
+
if len(offered) == 0:
|
|
86
|
+
raise InvalidArgumentError(_NO_VALUE)
|
|
87
|
+
|
|
88
|
+
return [_trimmed_value(one) for one in offered]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class MessageTrait:
|
|
92
|
+
"""The protocol version, the headers and the body of any message.
|
|
93
|
+
|
|
94
|
+
A mixin rather than a class of its own: nothing here is an HTTP message by
|
|
95
|
+
itself, and the two things that are — a request and a response — differ in
|
|
96
|
+
everything else. A subclass sets the state up in its own ``__init__``
|
|
97
|
+
through :meth:`_set_headers`, then inherits every reader and every
|
|
98
|
+
``with_*`` method from here.
|
|
99
|
+
|
|
100
|
+
Names are recorded in the case they were first given in and matched
|
|
101
|
+
without regard to case, which is what the two mappings are for: one holds
|
|
102
|
+
the headers under the name they will go out as, the other maps a
|
|
103
|
+
lowercased name to that one.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
__slots__: ClassVar[tuple[str, ...]] = ("_header_names", "_headers", "_protocol", "_stream")
|
|
107
|
+
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
"""Start with no header, the usual protocol version and no body yet."""
|
|
110
|
+
self._headers: dict[str, list[str]] = {}
|
|
111
|
+
self._header_names: dict[str, str] = {}
|
|
112
|
+
self._protocol: str = _DEFAULT_PROTOCOL
|
|
113
|
+
self._stream: StreamInterface | None = None
|
|
114
|
+
|
|
115
|
+
def get_protocol_version(self) -> str:
|
|
116
|
+
"""Return the HTTP version of the message.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The version number alone, such as ``"1.1"``.
|
|
120
|
+
"""
|
|
121
|
+
return self._protocol
|
|
122
|
+
|
|
123
|
+
def with_protocol_version(self, version: str) -> Self:
|
|
124
|
+
"""Return an instance carrying ``version``.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
version: The HTTP version number alone, such as ``"1.1"``.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
A new instance, or this one when the version is already that.
|
|
131
|
+
"""
|
|
132
|
+
if self._protocol == version:
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
new = self._copy()
|
|
136
|
+
new._protocol = version # noqa: SLF001 — a copy of this very class.
|
|
137
|
+
|
|
138
|
+
return new
|
|
139
|
+
|
|
140
|
+
def get_headers(self) -> Mapping[str, Sequence[str]]:
|
|
141
|
+
"""Return every header, each name mapped to all of its values.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
A fresh mapping of each header name, in the case it will go out
|
|
145
|
+
as, to the values recorded under it. Changing what comes back
|
|
146
|
+
cannot change the message.
|
|
147
|
+
"""
|
|
148
|
+
return {name: list(values) for name, values in self._headers.items()}
|
|
149
|
+
|
|
150
|
+
def has_header(self, name: str) -> bool:
|
|
151
|
+
"""Tell whether a header is present, matching ``name`` without case.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: The header name, in any case.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
``True`` when some header name matches ``name`` ignoring case.
|
|
158
|
+
"""
|
|
159
|
+
return lower(name) in self._header_names
|
|
160
|
+
|
|
161
|
+
def get_header(self, name: str) -> Sequence[str]:
|
|
162
|
+
"""Return every value recorded under a header, matched without case.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name: The header name, in any case.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The values under that header, in order; an empty list for a header
|
|
169
|
+
the message does not carry.
|
|
170
|
+
"""
|
|
171
|
+
recorded = self._header_names.get(lower(name))
|
|
172
|
+
if recorded is None:
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
return list(self._headers[recorded])
|
|
176
|
+
|
|
177
|
+
def get_header_line(self, name: str) -> str:
|
|
178
|
+
"""Return the values of a header joined with commas, matched without case.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
name: The header name, in any case.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The values joined with ``", "``; an empty string for a header the
|
|
185
|
+
message does not carry.
|
|
186
|
+
"""
|
|
187
|
+
return ", ".join(self.get_header(name))
|
|
188
|
+
|
|
189
|
+
def with_header(self, name: str, value: str | Sequence[str]) -> Self:
|
|
190
|
+
"""Return an instance whose ``name`` header holds ``value`` and nothing else.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
name: The header name, in any case. Its case is what the new
|
|
194
|
+
instance will report and send, even when it replaces a header
|
|
195
|
+
recorded under another.
|
|
196
|
+
value: One value, or every value the header is to hold.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
A new instance; this one is left as it was.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
InvalidArgumentError: When the name or a value is not one.
|
|
203
|
+
"""
|
|
204
|
+
values = _validate_and_trim(name, value)
|
|
205
|
+
normalized = lower(name)
|
|
206
|
+
|
|
207
|
+
new = self._copy()
|
|
208
|
+
replaced = new._header_names.get(normalized) # noqa: SLF001 — a copy of this very class.
|
|
209
|
+
if replaced is not None:
|
|
210
|
+
del new._headers[replaced] # noqa: SLF001 — a copy of this very class.
|
|
211
|
+
new._header_names[normalized] = name # noqa: SLF001 — a copy of this very class.
|
|
212
|
+
new._headers[name] = values # noqa: SLF001 — a copy of this very class.
|
|
213
|
+
|
|
214
|
+
return new
|
|
215
|
+
|
|
216
|
+
def with_added_header(self, name: str, value: str | Sequence[str]) -> Self:
|
|
217
|
+
"""Return an instance with ``value`` appended to the ``name`` header.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
name: The header name, in any case. A header already recorded
|
|
221
|
+
under another case keeps the case it has.
|
|
222
|
+
value: One value, or every value to append.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
A new instance; this one is left as it was.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
InvalidArgumentError: When the name or a value is not one.
|
|
229
|
+
"""
|
|
230
|
+
new = self._copy()
|
|
231
|
+
new._set_headers({name: value}) # noqa: SLF001 — a copy of this very class.
|
|
232
|
+
|
|
233
|
+
return new
|
|
234
|
+
|
|
235
|
+
def without_header(self, name: str) -> Self:
|
|
236
|
+
"""Return an instance with the ``name`` header gone.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
name: The header name, in any case.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
A new instance, or this one when the message does not carry that
|
|
243
|
+
header.
|
|
244
|
+
"""
|
|
245
|
+
normalized = lower(name)
|
|
246
|
+
recorded = self._header_names.get(normalized)
|
|
247
|
+
if recorded is None:
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
new = self._copy()
|
|
251
|
+
del new._headers[recorded] # noqa: SLF001 — a copy of this very class.
|
|
252
|
+
del new._header_names[normalized] # noqa: SLF001 — a copy of this very class.
|
|
253
|
+
|
|
254
|
+
return new
|
|
255
|
+
|
|
256
|
+
def get_body(self) -> StreamInterface:
|
|
257
|
+
"""Return the body of the message.
|
|
258
|
+
|
|
259
|
+
A message built with no body has none until it is asked for: the empty
|
|
260
|
+
stream is opened here, the first time, rather than for every message
|
|
261
|
+
that never reads one.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
The body, as a stream.
|
|
265
|
+
"""
|
|
266
|
+
if self._stream is None:
|
|
267
|
+
self._stream = Stream.create()
|
|
268
|
+
|
|
269
|
+
return self._stream
|
|
270
|
+
|
|
271
|
+
def with_body(self, body: StreamInterface) -> Self:
|
|
272
|
+
"""Return an instance carrying ``body``.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
body: The stream the new instance reads its body from.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
A new instance, or this one when it already reads that very
|
|
279
|
+
stream.
|
|
280
|
+
"""
|
|
281
|
+
if body is self._stream:
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
new = self._copy()
|
|
285
|
+
new._stream = body # noqa: SLF001 — a copy of this very class.
|
|
286
|
+
|
|
287
|
+
return new
|
|
288
|
+
|
|
289
|
+
def _copy(self) -> Self:
|
|
290
|
+
"""Return a copy to change, leaving this instance as it is.
|
|
291
|
+
|
|
292
|
+
The two header mappings are copied rather than shared, so that a
|
|
293
|
+
change made to the copy cannot reach back into this message. The body
|
|
294
|
+
is shared: a stream is the one thing here that is not a value.
|
|
295
|
+
"""
|
|
296
|
+
new = copy.copy(self)
|
|
297
|
+
new._headers = { # noqa: SLF001 — a copy of this very class.
|
|
298
|
+
name: list(values) for name, values in self._headers.items()
|
|
299
|
+
}
|
|
300
|
+
new._header_names = dict(self._header_names) # noqa: SLF001 — a copy of this very class.
|
|
301
|
+
|
|
302
|
+
return new
|
|
303
|
+
|
|
304
|
+
def _set_headers(self, headers: Mapping[str, str | Sequence[str]]) -> None:
|
|
305
|
+
"""Record headers on this instance, appending to any already there.
|
|
306
|
+
|
|
307
|
+
Only for a constructor and for :meth:`with_added_header`: it changes
|
|
308
|
+
the instance it is called on, which is either brand new or a copy
|
|
309
|
+
nobody else holds yet.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
headers: Each header name mapped to one value or to several.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
InvalidArgumentError: When a name or a value is not one.
|
|
316
|
+
"""
|
|
317
|
+
for name, value in headers.items():
|
|
318
|
+
values = _validate_and_trim(name, value)
|
|
319
|
+
normalized = lower(name)
|
|
320
|
+
recorded = self._header_names.get(normalized)
|
|
321
|
+
if recorded is None:
|
|
322
|
+
self._header_names[normalized] = name
|
|
323
|
+
self._headers[name] = values
|
|
324
|
+
else:
|
|
325
|
+
self._headers[recorded].extend(values)
|
|
326
|
+
|
|
327
|
+
def _set_body(self, body: bytes | BinaryIO | StreamInterface | None) -> None:
|
|
328
|
+
"""Record the body of a message being built, leaving an empty one unopened.
|
|
329
|
+
|
|
330
|
+
Only for a constructor. Nothing is opened for a message with no body:
|
|
331
|
+
:meth:`get_body` sees to that if it is ever asked for.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
body: The payload, in any of the shapes a stream is built from, or
|
|
335
|
+
``None``.
|
|
336
|
+
"""
|
|
337
|
+
if body is not None and body != b"":
|
|
338
|
+
self._stream = Stream.create(body)
|
|
339
|
+
|
|
340
|
+
def _put_header_first(self, name: str, values: list[str]) -> None:
|
|
341
|
+
"""Record a header in front of every other, dropping where it was.
|
|
342
|
+
|
|
343
|
+
Only for a constructor and for the ``with_*`` methods of a request: it
|
|
344
|
+
changes the instance it is called on. A request sends its ``Host``
|
|
345
|
+
header first, which is the one thing the order of these mappings is
|
|
346
|
+
for.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
name: The header name, already in the case it will go out as.
|
|
350
|
+
values: What the header is to hold.
|
|
351
|
+
"""
|
|
352
|
+
rest = [(other, held) for other, held in self._headers.items() if other != name]
|
|
353
|
+
self._headers = {name: values}
|
|
354
|
+
self._headers.update(rest)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""What an outgoing request and an incoming one have in common, in one place."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, Self
|
|
7
|
+
|
|
8
|
+
from ._message_trait import MessageTrait
|
|
9
|
+
from .exception import InvalidArgumentError
|
|
10
|
+
from .uri import Uri
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from xtr_http_message_contracts import UriInterface
|
|
14
|
+
|
|
15
|
+
__all__ = ["RequestTrait"]
|
|
16
|
+
|
|
17
|
+
_WHITESPACE = re.compile(r"\s")
|
|
18
|
+
_HOST = "Host"
|
|
19
|
+
_NORMALIZED_HOST = "host"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RequestTrait(MessageTrait):
|
|
23
|
+
"""The method, the request-target and the URI of any request.
|
|
24
|
+
|
|
25
|
+
A mixin, for the same reason
|
|
26
|
+
:class:`~xtr_http_message._message_trait.MessageTrait` is one: a request
|
|
27
|
+
going out and a request that arrived carry all of this and differ in what
|
|
28
|
+
else they carry. It extends that one, since every request is a message.
|
|
29
|
+
|
|
30
|
+
The ``Host`` header follows the URI unless a caller asks for it not to,
|
|
31
|
+
and is sent in front of every other header.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
__slots__: ClassVar[tuple[str, ...]] = ("_method", "_request_target", "_uri")
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
"""Start with no method, no request-target set by hand and no URI."""
|
|
38
|
+
super().__init__()
|
|
39
|
+
self._method: str = ""
|
|
40
|
+
self._request_target: str | None = None
|
|
41
|
+
self._uri: UriInterface = Uri()
|
|
42
|
+
|
|
43
|
+
def get_request_target(self) -> str:
|
|
44
|
+
"""Return the request-target of the message.
|
|
45
|
+
|
|
46
|
+
Built from the URI — the path with the query string after it — unless
|
|
47
|
+
:meth:`with_request_target` set one by hand, in which case that is
|
|
48
|
+
returned exactly as it was given.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The request-target, which is ``"/"`` when the URI has no path to
|
|
52
|
+
build one from.
|
|
53
|
+
"""
|
|
54
|
+
if self._request_target is not None:
|
|
55
|
+
return self._request_target
|
|
56
|
+
|
|
57
|
+
target = self._uri.get_path()
|
|
58
|
+
if target == "":
|
|
59
|
+
target = "/"
|
|
60
|
+
|
|
61
|
+
query = self._uri.get_query()
|
|
62
|
+
if query != "":
|
|
63
|
+
target = f"{target}?{query}"
|
|
64
|
+
|
|
65
|
+
return target
|
|
66
|
+
|
|
67
|
+
def with_request_target(self, request_target: str) -> Self:
|
|
68
|
+
"""Return an instance carrying ``request_target`` exactly as given.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
request_target: The request-target, taken verbatim. Use it where
|
|
72
|
+
the path-and-query form will not do: an absolute URI, an
|
|
73
|
+
authority alone, or the asterisk.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A new instance; this one is left as it was.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
InvalidArgumentError: When the request-target carries whitespace,
|
|
80
|
+
which would split the request line in two.
|
|
81
|
+
"""
|
|
82
|
+
if _WHITESPACE.search(request_target) is not None:
|
|
83
|
+
raise InvalidArgumentError("Invalid request target provided; cannot contain whitespace")
|
|
84
|
+
|
|
85
|
+
new = self._copy()
|
|
86
|
+
new._request_target = request_target # noqa: SLF001 — a copy of this very class.
|
|
87
|
+
|
|
88
|
+
return new
|
|
89
|
+
|
|
90
|
+
def get_method(self) -> str:
|
|
91
|
+
"""Return the HTTP method of the request.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The method, in the case it was given in.
|
|
95
|
+
"""
|
|
96
|
+
return self._method
|
|
97
|
+
|
|
98
|
+
def with_method(self, method: str) -> Self:
|
|
99
|
+
"""Return an instance carrying ``method``.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
method: The method, kept exactly as given: methods are compared
|
|
103
|
+
with regard to case, so recasing one would change it.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A new instance; this one is left as it was.
|
|
107
|
+
"""
|
|
108
|
+
new = self._copy()
|
|
109
|
+
new._method = method # noqa: SLF001 — a copy of this very class.
|
|
110
|
+
|
|
111
|
+
return new
|
|
112
|
+
|
|
113
|
+
def get_uri(self) -> UriInterface:
|
|
114
|
+
"""Return the URI the request is for.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The URI of the request.
|
|
118
|
+
"""
|
|
119
|
+
return self._uri
|
|
120
|
+
|
|
121
|
+
def with_uri(self, uri: UriInterface, preserve_host: bool = False) -> Self:
|
|
122
|
+
"""Return an instance carrying ``uri``.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
uri: The URI the new instance is for.
|
|
126
|
+
preserve_host: Whether to keep a ``Host`` header the request
|
|
127
|
+
already carries. Left to its default, the header follows the
|
|
128
|
+
new URI.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A new instance, or this one when it is already for that very URI.
|
|
132
|
+
"""
|
|
133
|
+
if uri is self._uri:
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
new = self._copy()
|
|
137
|
+
new._uri = uri # noqa: SLF001 — a copy of this very class.
|
|
138
|
+
|
|
139
|
+
if not preserve_host or not self.has_header(_HOST):
|
|
140
|
+
new._update_host_from_uri() # noqa: SLF001 — a copy of this very class.
|
|
141
|
+
|
|
142
|
+
return new
|
|
143
|
+
|
|
144
|
+
def _update_host_from_uri(self) -> None:
|
|
145
|
+
"""Set the ``Host`` header from the URI, in front of every other header.
|
|
146
|
+
|
|
147
|
+
Only for a constructor and for :meth:`with_uri`: it changes the
|
|
148
|
+
instance it is called on. A URI with no host leaves the header alone,
|
|
149
|
+
since there would be nothing to put in it.
|
|
150
|
+
"""
|
|
151
|
+
host = self._uri.get_host()
|
|
152
|
+
if host == "":
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
port = self._uri.get_port()
|
|
156
|
+
if port is not None:
|
|
157
|
+
host = f"{host}:{port}"
|
|
158
|
+
|
|
159
|
+
recorded = self._header_names.get(_NORMALIZED_HOST)
|
|
160
|
+
if recorded is None:
|
|
161
|
+
recorded = _HOST
|
|
162
|
+
self._header_names[_NORMALIZED_HOST] = recorded
|
|
163
|
+
|
|
164
|
+
self._put_header_first(recorded, [host])
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Everything this package raises, under one base class.
|
|
2
|
+
|
|
3
|
+
The contract names two builtins — :class:`ValueError` for an argument that
|
|
4
|
+
cannot be what it is being used as, :class:`RuntimeError` for an operation
|
|
5
|
+
that cannot be carried out now — so each error here derives from the builtin
|
|
6
|
+
the contract named *and* from
|
|
7
|
+
:class:`~xtr_http_message.exception.http_message_error.HttpMessageError`. A
|
|
8
|
+
caller written against the interfaces catches the builtin; one that chose this
|
|
9
|
+
implementation can catch the base class instead and get only its failures.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .http_message_error import HttpMessageError
|
|
15
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
16
|
+
from .stream_error import StreamError
|
|
17
|
+
from .uploaded_file_error import UploadedFileError
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"HttpMessageError",
|
|
21
|
+
"InvalidArgumentError",
|
|
22
|
+
"StreamError",
|
|
23
|
+
"UploadedFileError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""The one error every failure in this package derives from."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["HttpMessageError"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HttpMessageError(Exception):
|
|
9
|
+
"""Something went wrong building or using a message, a stream or a URI.
|
|
10
|
+
|
|
11
|
+
Nothing raises this class itself; it exists so that an application can
|
|
12
|
+
catch everything this package raises with one ``except`` clause, whichever
|
|
13
|
+
of the four concrete errors reached it.
|
|
14
|
+
"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""An argument cannot be what it is being used as."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .http_message_error import HttpMessageError
|
|
6
|
+
|
|
7
|
+
__all__ = ["InvalidArgumentError"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InvalidArgumentError(HttpMessageError, ValueError):
|
|
11
|
+
"""An argument's value rules it out: a malformed header name, an out-of-range port.
|
|
12
|
+
|
|
13
|
+
It derives from :class:`ValueError` as well as from
|
|
14
|
+
:class:`~xtr_http_message.exception.http_message_error.HttpMessageError`
|
|
15
|
+
because that is what the contract promises a caller: code written against
|
|
16
|
+
the interfaces catches the builtin and never names this package.
|
|
17
|
+
"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""A stream cannot do what is being asked of it now."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .http_message_error import HttpMessageError
|
|
6
|
+
|
|
7
|
+
__all__ = ["StreamError"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StreamError(HttpMessageError, RuntimeError):
|
|
11
|
+
"""A stream operation cannot be carried out: detached, not seekable, not readable.
|
|
12
|
+
|
|
13
|
+
It derives from :class:`RuntimeError` as well as from
|
|
14
|
+
:class:`~xtr_http_message.exception.http_message_error.HttpMessageError`
|
|
15
|
+
because that is what the contract promises a caller: code written against
|
|
16
|
+
the interfaces catches the builtin and never names this package.
|
|
17
|
+
"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""An uploaded file cannot do what is being asked of it now."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .http_message_error import HttpMessageError
|
|
6
|
+
|
|
7
|
+
__all__ = ["UploadedFileError"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UploadedFileError(HttpMessageError, RuntimeError):
|
|
11
|
+
"""An uploaded file is unusable: the upload failed, or the file is already moved.
|
|
12
|
+
|
|
13
|
+
It derives from :class:`RuntimeError` as well as from
|
|
14
|
+
:class:`~xtr_http_message.exception.http_message_error.HttpMessageError`
|
|
15
|
+
because that is what the contract promises a caller: code written against
|
|
16
|
+
the interfaces catches the builtin and never names this package.
|
|
17
|
+
"""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""The factory that builds every kind of message.
|
|
2
|
+
|
|
3
|
+
One class implements all six factory interfaces, so a library that is handed
|
|
4
|
+
it can build a request, a server request, a response, a stream, a URI or an
|
|
5
|
+
uploaded file without ever naming the class that is one.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from xtr_http_message.factory import HttpFactory
|
|
9
|
+
|
|
10
|
+
factory = HttpFactory()
|
|
11
|
+
request = factory.create_request("GET", "https://example.com/orders")
|
|
12
|
+
assert request.get_header_line("host") == "example.com"
|
|
13
|
+
```
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .http_factory import HttpFactory
|
|
19
|
+
|
|
20
|
+
__all__ = ["HttpFactory"]
|