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,235 @@
|
|
|
1
|
+
"""One factory for every kind of message, so a caller need name no class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, cast, final
|
|
7
|
+
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
from xtr_http_message_contracts import UploadError
|
|
10
|
+
from xtr_http_message_contracts.factory import (
|
|
11
|
+
RequestFactoryInterface,
|
|
12
|
+
ResponseFactoryInterface,
|
|
13
|
+
ServerRequestFactoryInterface,
|
|
14
|
+
StreamFactoryInterface,
|
|
15
|
+
UploadedFileFactoryInterface,
|
|
16
|
+
UriFactoryInterface,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from xtr_http_message.exception import InvalidArgumentError, StreamError
|
|
20
|
+
from xtr_http_message.request import Request
|
|
21
|
+
from xtr_http_message.response import Response
|
|
22
|
+
from xtr_http_message.server_request import ServerRequest
|
|
23
|
+
from xtr_http_message.stream import Stream
|
|
24
|
+
from xtr_http_message.uploaded_file import UploadedFile
|
|
25
|
+
from xtr_http_message.uri import Uri
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Mapping
|
|
29
|
+
from typing import BinaryIO
|
|
30
|
+
|
|
31
|
+
from xtr_http_message_contracts import (
|
|
32
|
+
RequestInterface,
|
|
33
|
+
ResponseInterface,
|
|
34
|
+
ServerRequestInterface,
|
|
35
|
+
StreamInterface,
|
|
36
|
+
UploadedFileInterface,
|
|
37
|
+
UriInterface,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = ["HttpFactory"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@final
|
|
44
|
+
class HttpFactory(
|
|
45
|
+
RequestFactoryInterface,
|
|
46
|
+
ResponseFactoryInterface,
|
|
47
|
+
ServerRequestFactoryInterface,
|
|
48
|
+
StreamFactoryInterface,
|
|
49
|
+
UploadedFileFactoryInterface,
|
|
50
|
+
UriFactoryInterface,
|
|
51
|
+
):
|
|
52
|
+
"""Builds every kind of message this package implements.
|
|
53
|
+
|
|
54
|
+
One object rather than six, because the six belong together: a library
|
|
55
|
+
given this one can build whatever it needs, and an application that wants
|
|
56
|
+
to swap the implementation out swaps one registration.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from xtr_http_message.factory import HttpFactory
|
|
60
|
+
|
|
61
|
+
factory = HttpFactory()
|
|
62
|
+
response = factory.create_response(404)
|
|
63
|
+
assert response.get_reason_phrase() == "Not Found"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It holds no state, so one instance serves a whole application.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
__slots__ = ()
|
|
70
|
+
|
|
71
|
+
@override
|
|
72
|
+
def create_request(self, method: str, uri: UriInterface | str) -> RequestInterface:
|
|
73
|
+
"""Return a new request for ``method`` and ``uri``.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
method: The HTTP method of the request.
|
|
77
|
+
uri: The URI of the request. Given as text, it is read into one.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The request.
|
|
81
|
+
"""
|
|
82
|
+
return Request(method, uri)
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
def create_response(
|
|
86
|
+
self,
|
|
87
|
+
code: int = 200,
|
|
88
|
+
reason_phrase: str | None = None,
|
|
89
|
+
) -> ResponseInterface:
|
|
90
|
+
"""Return a new response for ``code``.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
code: The status code.
|
|
94
|
+
reason_phrase: The phrase to go with ``code``. Left out, the
|
|
95
|
+
recommended phrase for the code is filled in where there is
|
|
96
|
+
one; given as an empty string, the response carries no phrase
|
|
97
|
+
at all. That is the difference between asking for the usual
|
|
98
|
+
phrase and asking for none.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The response.
|
|
102
|
+
"""
|
|
103
|
+
return Response(code, None, None, "1.1", reason_phrase)
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
def create_server_request(
|
|
107
|
+
self,
|
|
108
|
+
method: str,
|
|
109
|
+
uri: UriInterface | str,
|
|
110
|
+
server_params: Mapping[str, object] | None = None,
|
|
111
|
+
) -> ServerRequestInterface:
|
|
112
|
+
"""Return a new server request for ``method`` and ``uri``.
|
|
113
|
+
|
|
114
|
+
The environment parameters are taken exactly as given: nothing in them
|
|
115
|
+
is read, and in particular neither the method nor the URI, which is
|
|
116
|
+
why both have to be passed.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
method: The HTTP method of the request.
|
|
120
|
+
uri: The URI of the request. Given as text, it is read into one.
|
|
121
|
+
server_params: The environment parameters to seed the request
|
|
122
|
+
with. ``None`` seeds it with none.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The server request.
|
|
126
|
+
"""
|
|
127
|
+
return ServerRequest(method, uri, None, None, "1.1", server_params)
|
|
128
|
+
|
|
129
|
+
@override
|
|
130
|
+
def create_stream(self, content: bytes = b"") -> StreamInterface:
|
|
131
|
+
"""Return a new stream over ``content``, held in memory.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
content: The bytes the stream reads back.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The stream, with its cursor at the first byte.
|
|
138
|
+
"""
|
|
139
|
+
return Stream.create(content)
|
|
140
|
+
|
|
141
|
+
@override
|
|
142
|
+
def create_stream_from_file(self, filename: str, mode: str = "r") -> StreamInterface:
|
|
143
|
+
"""Return a new stream over the file at ``filename``.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
filename: The path to open.
|
|
147
|
+
mode: How to open it. The payload is bytes, so the mode is taken
|
|
148
|
+
as a binary one whether or not it says so.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The stream.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
InvalidArgumentError: When the mode is not a mode.
|
|
155
|
+
StreamError: When the path is empty, or the file cannot be opened.
|
|
156
|
+
"""
|
|
157
|
+
if filename == "":
|
|
158
|
+
raise StreamError("Path cannot be empty")
|
|
159
|
+
|
|
160
|
+
binary = mode if "b" in mode else f"{mode}b"
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# The stream takes the file over and closes it, so there is no
|
|
164
|
+
# context manager here to close it first (SIM115). The mode is
|
|
165
|
+
# binary by the line above, which a mode computed at run time
|
|
166
|
+
# cannot say in a type.
|
|
167
|
+
opened = Path(filename).open(binary) # noqa: SIM115
|
|
168
|
+
except ValueError as invalid:
|
|
169
|
+
# Not a mode at all, which is worth telling apart from a file that
|
|
170
|
+
# will not open.
|
|
171
|
+
message = f'The mode "{mode}" is invalid.'
|
|
172
|
+
raise InvalidArgumentError(message) from invalid
|
|
173
|
+
except OSError as failure:
|
|
174
|
+
message = f'The file "{filename}" cannot be opened: {failure}'
|
|
175
|
+
raise StreamError(message) from failure
|
|
176
|
+
|
|
177
|
+
return Stream.create(cast("BinaryIO", opened))
|
|
178
|
+
|
|
179
|
+
@override
|
|
180
|
+
def create_stream_from_resource(self, resource: BinaryIO) -> StreamInterface:
|
|
181
|
+
"""Return a new stream over an open binary file object.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
resource: The file object. The stream takes it over: closing the
|
|
185
|
+
stream closes it.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The stream.
|
|
189
|
+
"""
|
|
190
|
+
return Stream.create(resource)
|
|
191
|
+
|
|
192
|
+
@override
|
|
193
|
+
def create_uploaded_file(
|
|
194
|
+
self,
|
|
195
|
+
stream: StreamInterface,
|
|
196
|
+
size: int | None = None,
|
|
197
|
+
error: int = UploadError.OK,
|
|
198
|
+
client_filename: str | None = None,
|
|
199
|
+
client_media_type: str | None = None,
|
|
200
|
+
) -> UploadedFileInterface:
|
|
201
|
+
"""Return a new uploaded file reading from ``stream``.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
stream: The contents of the uploaded file.
|
|
205
|
+
size: The size in bytes. Left out, it is read off the stream.
|
|
206
|
+
error: How the upload went.
|
|
207
|
+
client_filename: The file name the client sent, if any.
|
|
208
|
+
client_media_type: The media type the client sent, if any.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The uploaded file.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
InvalidArgumentError: When ``error`` is not one of the upload
|
|
215
|
+
outcomes.
|
|
216
|
+
"""
|
|
217
|
+
if size is None:
|
|
218
|
+
size = stream.get_size()
|
|
219
|
+
|
|
220
|
+
return UploadedFile(stream, size, error, client_filename, client_media_type)
|
|
221
|
+
|
|
222
|
+
@override
|
|
223
|
+
def create_uri(self, uri: str = "") -> UriInterface:
|
|
224
|
+
"""Return a new URI read from ``uri``.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
uri: The URI as text. Empty builds one with no component at all.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
The URI.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
InvalidArgumentError: When the text cannot be read as a URI.
|
|
234
|
+
"""
|
|
235
|
+
return Uri(uri)
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""A request on its way out, as a client sends it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
|
|
7
|
+
from xtr_http_message_contracts import RequestInterface
|
|
8
|
+
|
|
9
|
+
from ._request_trait import RequestTrait
|
|
10
|
+
from .uri import Uri
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Mapping, Sequence
|
|
14
|
+
from typing import BinaryIO
|
|
15
|
+
|
|
16
|
+
from xtr_http_message_contracts import StreamInterface, UriInterface
|
|
17
|
+
|
|
18
|
+
__all__ = ["Request"]
|
|
19
|
+
|
|
20
|
+
_HOST = "Host"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@final
|
|
24
|
+
class Request(RequestTrait, RequestInterface):
|
|
25
|
+
"""An outgoing, client-side request: a method, a URI, headers and a body.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from xtr_http_message import Request
|
|
29
|
+
|
|
30
|
+
request = Request("GET", "https://example.com/orders?page=2")
|
|
31
|
+
assert request.get_header_line("host") == "example.com"
|
|
32
|
+
assert request.get_request_target() == "/orders?page=2"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The ``Host`` header is taken from the URI unless one was given with the
|
|
36
|
+
request, and is sent in front of every other header.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ()
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
method: str,
|
|
44
|
+
uri: UriInterface | str,
|
|
45
|
+
headers: Mapping[str, str | Sequence[str]] | None = None,
|
|
46
|
+
body: bytes | BinaryIO | StreamInterface | None = None,
|
|
47
|
+
version: str = "1.1",
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Build a request.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
method: The HTTP method, kept exactly as given.
|
|
53
|
+
uri: The URI of the request. Given as text, it is read into a
|
|
54
|
+
:class:`~xtr_http_message.uri.Uri`.
|
|
55
|
+
headers: Each header name mapped to one value or to several.
|
|
56
|
+
body: The payload, in any of the shapes a stream is built from.
|
|
57
|
+
Left out, nothing is opened until the body is asked for.
|
|
58
|
+
version: The HTTP version number alone.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
InvalidArgumentError: When the URI cannot be read, or a header
|
|
62
|
+
name or value is not one.
|
|
63
|
+
"""
|
|
64
|
+
super().__init__()
|
|
65
|
+
|
|
66
|
+
self._method = method
|
|
67
|
+
self._uri = Uri(uri) if isinstance(uri, str) else uri
|
|
68
|
+
self._set_headers(headers or {})
|
|
69
|
+
self._protocol = version
|
|
70
|
+
|
|
71
|
+
if not self.has_header(_HOST):
|
|
72
|
+
self._update_host_from_uri()
|
|
73
|
+
|
|
74
|
+
self._set_body(body)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""A response on its way out, as a server sends it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from typing import TYPE_CHECKING, Final, Self, final
|
|
7
|
+
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
from xtr_http_message_contracts import ResponseInterface
|
|
10
|
+
|
|
11
|
+
from ._message_trait import MessageTrait
|
|
12
|
+
from .exception import InvalidArgumentError
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Mapping, Sequence
|
|
16
|
+
from typing import BinaryIO
|
|
17
|
+
|
|
18
|
+
from xtr_http_message_contracts import StreamInterface
|
|
19
|
+
|
|
20
|
+
__all__ = ["Response"]
|
|
21
|
+
|
|
22
|
+
_LOWEST_STATUS = 100
|
|
23
|
+
_HIGHEST_STATUS = 599
|
|
24
|
+
|
|
25
|
+
# The reason phrase recommended for each status code that has one. A code
|
|
26
|
+
# absent from here is still a status code; it simply has no phrase to offer.
|
|
27
|
+
_PHRASES: Final[Mapping[int, str]] = MappingProxyType(
|
|
28
|
+
{
|
|
29
|
+
100: "Continue",
|
|
30
|
+
101: "Switching Protocols",
|
|
31
|
+
102: "Processing",
|
|
32
|
+
200: "OK",
|
|
33
|
+
201: "Created",
|
|
34
|
+
202: "Accepted",
|
|
35
|
+
203: "Non-Authoritative Information",
|
|
36
|
+
204: "No Content",
|
|
37
|
+
205: "Reset Content",
|
|
38
|
+
206: "Partial Content",
|
|
39
|
+
207: "Multi-status",
|
|
40
|
+
208: "Already Reported",
|
|
41
|
+
300: "Multiple Choices",
|
|
42
|
+
301: "Moved Permanently",
|
|
43
|
+
302: "Found",
|
|
44
|
+
303: "See Other",
|
|
45
|
+
304: "Not Modified",
|
|
46
|
+
305: "Use Proxy",
|
|
47
|
+
306: "Switch Proxy",
|
|
48
|
+
307: "Temporary Redirect",
|
|
49
|
+
400: "Bad Request",
|
|
50
|
+
401: "Unauthorized",
|
|
51
|
+
402: "Payment Required",
|
|
52
|
+
403: "Forbidden",
|
|
53
|
+
404: "Not Found",
|
|
54
|
+
405: "Method Not Allowed",
|
|
55
|
+
406: "Not Acceptable",
|
|
56
|
+
407: "Proxy Authentication Required",
|
|
57
|
+
408: "Request Time-out",
|
|
58
|
+
409: "Conflict",
|
|
59
|
+
410: "Gone",
|
|
60
|
+
411: "Length Required",
|
|
61
|
+
412: "Precondition Failed",
|
|
62
|
+
413: "Request Entity Too Large",
|
|
63
|
+
414: "Request-URI Too Large",
|
|
64
|
+
415: "Unsupported Media Type",
|
|
65
|
+
416: "Requested range not satisfiable",
|
|
66
|
+
417: "Expectation Failed",
|
|
67
|
+
418: "I'm a teapot",
|
|
68
|
+
422: "Unprocessable Entity",
|
|
69
|
+
423: "Locked",
|
|
70
|
+
424: "Failed Dependency",
|
|
71
|
+
425: "Unordered Collection",
|
|
72
|
+
426: "Upgrade Required",
|
|
73
|
+
428: "Precondition Required",
|
|
74
|
+
429: "Too Many Requests",
|
|
75
|
+
431: "Request Header Fields Too Large",
|
|
76
|
+
451: "Unavailable For Legal Reasons",
|
|
77
|
+
500: "Internal Server Error",
|
|
78
|
+
501: "Not Implemented",
|
|
79
|
+
502: "Bad Gateway",
|
|
80
|
+
503: "Service Unavailable",
|
|
81
|
+
504: "Gateway Time-out",
|
|
82
|
+
505: "HTTP Version not supported",
|
|
83
|
+
506: "Variant Also Negotiates",
|
|
84
|
+
507: "Insufficient Storage",
|
|
85
|
+
508: "Loop Detected",
|
|
86
|
+
511: "Network Authentication Required",
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@final
|
|
92
|
+
class Response(MessageTrait, ResponseInterface):
|
|
93
|
+
"""A response: a status code, the phrase that goes with it, headers and a body.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from xtr_http_message import Response
|
|
97
|
+
|
|
98
|
+
response = Response(201, {"content-type": "application/json"}, b'{"id": 7}')
|
|
99
|
+
assert response.get_reason_phrase() == "Created"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Given no reason phrase, the one recommended for the status code is filled
|
|
103
|
+
in; given an empty one, the response carries none, since a status line
|
|
104
|
+
need not have a phrase at all.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
__slots__ = ("_reason_phrase", "_status_code")
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
status: int = 200,
|
|
112
|
+
headers: Mapping[str, str | Sequence[str]] | None = None,
|
|
113
|
+
body: bytes | BinaryIO | StreamInterface | None = None,
|
|
114
|
+
version: str = "1.1",
|
|
115
|
+
reason: str | None = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Build a response.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
status: The status code.
|
|
121
|
+
headers: Each header name mapped to one value or to several.
|
|
122
|
+
body: The payload, in any of the shapes a stream is built from.
|
|
123
|
+
Left out, nothing is opened until the body is asked for.
|
|
124
|
+
version: The HTTP version number alone.
|
|
125
|
+
reason: The reason phrase. ``None`` — the default — takes the one
|
|
126
|
+
recommended for ``status``; an empty string leaves the
|
|
127
|
+
response with none.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
InvalidArgumentError: When a header name or value is not one.
|
|
131
|
+
"""
|
|
132
|
+
super().__init__()
|
|
133
|
+
|
|
134
|
+
self._set_body(body)
|
|
135
|
+
|
|
136
|
+
self._status_code: int = status
|
|
137
|
+
self._set_headers(headers or {})
|
|
138
|
+
|
|
139
|
+
if reason is None:
|
|
140
|
+
self._reason_phrase: str = _PHRASES.get(status, "")
|
|
141
|
+
else:
|
|
142
|
+
self._reason_phrase = reason
|
|
143
|
+
|
|
144
|
+
self._protocol = version
|
|
145
|
+
|
|
146
|
+
@override
|
|
147
|
+
def get_status_code(self) -> int:
|
|
148
|
+
"""Return the status code of the response.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The three-digit code saying how the server got on with the
|
|
152
|
+
request.
|
|
153
|
+
"""
|
|
154
|
+
return self._status_code
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
def get_reason_phrase(self) -> str:
|
|
158
|
+
"""Return the reason phrase that goes with the status code.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
The reason phrase, or an empty string when the response carries
|
|
162
|
+
none.
|
|
163
|
+
"""
|
|
164
|
+
return self._reason_phrase
|
|
165
|
+
|
|
166
|
+
@override
|
|
167
|
+
def with_status(self, code: int, reason_phrase: str = "") -> Self:
|
|
168
|
+
"""Return an instance carrying ``code`` and, optionally, ``reason_phrase``.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
code: The status code to set.
|
|
172
|
+
reason_phrase: The phrase to go with ``code``. Left out or empty,
|
|
173
|
+
the one recommended for ``code`` is filled in where there is
|
|
174
|
+
one.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A new instance; this one is left as it was.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
InvalidArgumentError: When the code is not a status code.
|
|
181
|
+
"""
|
|
182
|
+
if code < _LOWEST_STATUS or code > _HIGHEST_STATUS:
|
|
183
|
+
message = (
|
|
184
|
+
"Status code has to be an integer between 100 and 599. "
|
|
185
|
+
f"A status code of {code} was given"
|
|
186
|
+
)
|
|
187
|
+
raise InvalidArgumentError(message)
|
|
188
|
+
|
|
189
|
+
if reason_phrase == "":
|
|
190
|
+
reason_phrase = _PHRASES.get(code, "")
|
|
191
|
+
|
|
192
|
+
new = self._copy()
|
|
193
|
+
new._status_code = code # noqa: SLF001 — a copy of this very class.
|
|
194
|
+
new._reason_phrase = reason_phrase # noqa: SLF001 — a copy of this very class.
|
|
195
|
+
|
|
196
|
+
return new
|