xtr-http-message-contracts 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_contracts/__init__.py +89 -0
- xtr_http_message_contracts/factory/__init__.py +41 -0
- xtr_http_message_contracts/factory/request_factory_interface.py +29 -0
- xtr_http_message_contracts/factory/response_factory_interface.py +35 -0
- xtr_http_message_contracts/factory/server_request_factory_interface.py +42 -0
- xtr_http_message_contracts/factory/stream_factory_interface.py +63 -0
- xtr_http_message_contracts/factory/uploaded_file_factory_interface.py +46 -0
- xtr_http_message_contracts/factory/uri_factory_interface.py +29 -0
- xtr_http_message_contracts/message_interface.py +177 -0
- xtr_http_message_contracts/py.typed +0 -0
- xtr_http_message_contracts/request_interface.py +119 -0
- xtr_http_message_contracts/response_interface.py +64 -0
- xtr_http_message_contracts/server_request_interface.py +239 -0
- xtr_http_message_contracts/stream_interface.py +191 -0
- xtr_http_message_contracts/upload_error.py +45 -0
- xtr_http_message_contracts/uploaded_file_interface.py +110 -0
- xtr_http_message_contracts/uri_interface.py +265 -0
- xtr_http_message_contracts-4.0.2.dist-info/METADATA +184 -0
- xtr_http_message_contracts-4.0.2.dist-info/RECORD +21 -0
- xtr_http_message_contracts-4.0.2.dist-info/WHEEL +4 -0
- xtr_http_message_contracts-4.0.2.dist-info/licenses/LICENSE +23 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""The HTTP message contract: what code that handles requests depends on, and nothing more.
|
|
2
|
+
|
|
3
|
+
A library that reads a request or fills in a response should not decide which
|
|
4
|
+
class is one. It takes a :class:`ServerRequestInterface` and a
|
|
5
|
+
:class:`ResponseInterface`, or the factory that makes them, and leaves the
|
|
6
|
+
choice of implementation to the application — so two libraries written against
|
|
7
|
+
this contract pass messages to each other without either knowing what the
|
|
8
|
+
other installed.
|
|
9
|
+
|
|
10
|
+
What is here is only what appears in that contract:
|
|
11
|
+
|
|
12
|
+
- :class:`MessageInterface` — the protocol version, headers and body every
|
|
13
|
+
message has.
|
|
14
|
+
- :class:`RequestInterface` / :class:`ServerRequestInterface` — a request
|
|
15
|
+
going out, and one that arrived with everything the server learned
|
|
16
|
+
alongside it.
|
|
17
|
+
- :class:`ResponseInterface` — a status code, a reason phrase, and a message.
|
|
18
|
+
- :class:`StreamInterface` — the body, as a cursor over bytes.
|
|
19
|
+
- :class:`UriInterface` — a URI taken apart into its components.
|
|
20
|
+
- :class:`UploadedFileInterface` and :class:`UploadError` — a file that came
|
|
21
|
+
in with a request, and how its upload went.
|
|
22
|
+
- The six factories, also under ``xtr_http_message_contracts.factory``, for
|
|
23
|
+
building any of the above without naming a class.
|
|
24
|
+
|
|
25
|
+
Every message is immutable: a ``with_*`` method leaves the instance it was
|
|
26
|
+
called on alone and returns one carrying the change.
|
|
27
|
+
|
|
28
|
+
A body moves bytes, so reading and writing one is awaited.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from xtr_http_message_contracts import ResponseInterface, ServerRequestInterface
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def whoami(request: ServerRequestInterface, response: ResponseInterface) -> ResponseInterface:
|
|
35
|
+
_ = await response.get_body().write(request.get_header_line("user-agent").encode())
|
|
36
|
+
return response.with_header("content-type", "text/plain")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
An implementation builds on these types rather than redefining them, so a
|
|
40
|
+
container can register one under an interface and a library that never
|
|
41
|
+
imported it receives it. Bridging a message to the web framework an
|
|
42
|
+
application runs on belongs with that framework's integration, not here.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
48
|
+
|
|
49
|
+
from .factory import (
|
|
50
|
+
RequestFactoryInterface,
|
|
51
|
+
ResponseFactoryInterface,
|
|
52
|
+
ServerRequestFactoryInterface,
|
|
53
|
+
StreamFactoryInterface,
|
|
54
|
+
UploadedFileFactoryInterface,
|
|
55
|
+
UriFactoryInterface,
|
|
56
|
+
)
|
|
57
|
+
from .message_interface import MessageInterface
|
|
58
|
+
from .request_interface import RequestInterface
|
|
59
|
+
from .response_interface import ResponseInterface
|
|
60
|
+
from .server_request_interface import ServerRequestInterface
|
|
61
|
+
from .stream_interface import StreamInterface
|
|
62
|
+
from .upload_error import UploadError
|
|
63
|
+
from .uploaded_file_interface import UploadedFileInterface
|
|
64
|
+
from .uri_interface import UriInterface
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
__version__ = version("xtr-http-message-contracts")
|
|
68
|
+
except PackageNotFoundError: # pragma: no cover
|
|
69
|
+
# Running from a source tree or a vendored copy, with no installed
|
|
70
|
+
# metadata to read. Having no version is better than refusing to import.
|
|
71
|
+
__version__ = "0+unknown"
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"MessageInterface",
|
|
75
|
+
"RequestFactoryInterface",
|
|
76
|
+
"RequestInterface",
|
|
77
|
+
"ResponseFactoryInterface",
|
|
78
|
+
"ResponseInterface",
|
|
79
|
+
"ServerRequestFactoryInterface",
|
|
80
|
+
"ServerRequestInterface",
|
|
81
|
+
"StreamFactoryInterface",
|
|
82
|
+
"StreamInterface",
|
|
83
|
+
"UploadError",
|
|
84
|
+
"UploadedFileFactoryInterface",
|
|
85
|
+
"UploadedFileInterface",
|
|
86
|
+
"UriFactoryInterface",
|
|
87
|
+
"UriInterface",
|
|
88
|
+
"__version__",
|
|
89
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""The six factories, so that code which builds messages names no class.
|
|
2
|
+
|
|
3
|
+
A library that has to make a request, a response, a stream, a URI or an
|
|
4
|
+
uploaded file takes the matching factory and stays free of whichever
|
|
5
|
+
implementation the application installed:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
class Probe:
|
|
9
|
+
def __init__(self, requests: RequestFactoryInterface) -> None:
|
|
10
|
+
self._requests = requests
|
|
11
|
+
|
|
12
|
+
def ping(self, url: str) -> RequestInterface:
|
|
13
|
+
return self._requests.create_request("HEAD", url)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- :class:`RequestFactoryInterface` / :class:`ServerRequestFactoryInterface` —
|
|
17
|
+
a request going out, and one that arrived.
|
|
18
|
+
- :class:`ResponseFactoryInterface` — a response.
|
|
19
|
+
- :class:`StreamFactoryInterface` — a message body, from bytes, from a file, or
|
|
20
|
+
around a file object already open.
|
|
21
|
+
- :class:`UriFactoryInterface` — a URI parsed from text.
|
|
22
|
+
- :class:`UploadedFileFactoryInterface` — a file that came in with a request.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from .request_factory_interface import RequestFactoryInterface
|
|
28
|
+
from .response_factory_interface import ResponseFactoryInterface
|
|
29
|
+
from .server_request_factory_interface import ServerRequestFactoryInterface
|
|
30
|
+
from .stream_factory_interface import StreamFactoryInterface
|
|
31
|
+
from .uploaded_file_factory_interface import UploadedFileFactoryInterface
|
|
32
|
+
from .uri_factory_interface import UriFactoryInterface
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"RequestFactoryInterface",
|
|
36
|
+
"ResponseFactoryInterface",
|
|
37
|
+
"ServerRequestFactoryInterface",
|
|
38
|
+
"StreamFactoryInterface",
|
|
39
|
+
"UploadedFileFactoryInterface",
|
|
40
|
+
"UriFactoryInterface",
|
|
41
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Build a request."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from xtr_http_message_contracts.request_interface import RequestInterface
|
|
9
|
+
from xtr_http_message_contracts.uri_interface import UriInterface
|
|
10
|
+
|
|
11
|
+
__all__ = ["RequestFactoryInterface"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class RequestFactoryInterface(Protocol):
|
|
16
|
+
"""Makes a request, so that a caller need not name the class that is one."""
|
|
17
|
+
|
|
18
|
+
def create_request(self, method: str, uri: UriInterface | str) -> RequestInterface:
|
|
19
|
+
"""Return a new request for ``method`` and ``uri``.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
method: The HTTP method of the request.
|
|
23
|
+
uri: The URI of the request. Given as text, the factory builds the
|
|
24
|
+
URI from it.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The request.
|
|
28
|
+
"""
|
|
29
|
+
...
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Build a response."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from xtr_http_message_contracts.response_interface import ResponseInterface
|
|
9
|
+
|
|
10
|
+
__all__ = ["ResponseFactoryInterface"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class ResponseFactoryInterface(Protocol):
|
|
15
|
+
"""Makes a response, so that a caller need not name the class that is one."""
|
|
16
|
+
|
|
17
|
+
def create_response(
|
|
18
|
+
self,
|
|
19
|
+
code: int = 200,
|
|
20
|
+
reason_phrase: str | None = None,
|
|
21
|
+
) -> ResponseInterface:
|
|
22
|
+
"""Return a new response carrying ``code`` and ``reason_phrase``.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
code: The status code; ``200`` unless another is asked for.
|
|
26
|
+
reason_phrase: The phrase to go with ``code`` in the response.
|
|
27
|
+
``None`` — nothing given — lets the factory fill in the
|
|
28
|
+
phrase recommended for the code; ``""`` asks for a response
|
|
29
|
+
with no phrase at all. Those are two different requests, and
|
|
30
|
+
only this tells them apart.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The response.
|
|
34
|
+
"""
|
|
35
|
+
...
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Build a server request."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
|
|
10
|
+
from xtr_http_message_contracts.server_request_interface import ServerRequestInterface
|
|
11
|
+
from xtr_http_message_contracts.uri_interface import UriInterface
|
|
12
|
+
|
|
13
|
+
__all__ = ["ServerRequestFactoryInterface"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class ServerRequestFactoryInterface(Protocol):
|
|
18
|
+
"""Makes a server request, so that a caller need not name the class that is one."""
|
|
19
|
+
|
|
20
|
+
def create_server_request(
|
|
21
|
+
self,
|
|
22
|
+
method: str,
|
|
23
|
+
uri: UriInterface | str,
|
|
24
|
+
server_params: Mapping[str, object] | None = None,
|
|
25
|
+
) -> ServerRequestInterface:
|
|
26
|
+
"""Return a new server request for ``method`` and ``uri``.
|
|
27
|
+
|
|
28
|
+
The environment parameters are taken exactly as given: nothing in them
|
|
29
|
+
is parsed or processed, and in particular neither the method nor the
|
|
30
|
+
URI is read out of them, which is why both have to be passed.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
method: The HTTP method of the request.
|
|
34
|
+
uri: The URI of the request. Given as text, the factory builds the
|
|
35
|
+
URI from it.
|
|
36
|
+
server_params: The environment parameters to seed the request
|
|
37
|
+
with. ``None`` seeds it with none.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The server request.
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Build a stream, from bytes, from a file, or around one already open."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import BinaryIO
|
|
9
|
+
|
|
10
|
+
from xtr_http_message_contracts.stream_interface import StreamInterface
|
|
11
|
+
|
|
12
|
+
__all__ = ["StreamFactoryInterface"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class StreamFactoryInterface(Protocol):
|
|
17
|
+
"""Makes a stream, so that a caller need not name the class that is one."""
|
|
18
|
+
|
|
19
|
+
def create_stream(self, content: bytes = b"") -> StreamInterface:
|
|
20
|
+
"""Return a new stream holding ``content``.
|
|
21
|
+
|
|
22
|
+
The stream should be backed by something temporary rather than by a
|
|
23
|
+
file the caller has to clean up.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
content: The bytes the stream starts out holding.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The stream.
|
|
30
|
+
"""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def create_stream_from_file(self, filename: str, mode: str = "r") -> StreamInterface:
|
|
34
|
+
"""Return a new stream reading a file that already exists.
|
|
35
|
+
|
|
36
|
+
The file is opened in ``mode``, which may be any mode the host's file
|
|
37
|
+
opener accepts, and ``filename`` may be anything it can open.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
filename: The file, or the address of the stream, to read.
|
|
41
|
+
mode: How to open it.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The stream.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
RuntimeError: When the file cannot be opened.
|
|
48
|
+
ValueError: When the mode is not a usable one.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def create_stream_from_resource(self, resource: BinaryIO) -> StreamInterface:
|
|
53
|
+
"""Return a new stream around a file object that is already open.
|
|
54
|
+
|
|
55
|
+
The stream must be readable, and may be writable.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
resource: The open file object to wrap.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The stream.
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Build an uploaded file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from xtr_http_message_contracts.upload_error import UploadError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from xtr_http_message_contracts.stream_interface import StreamInterface
|
|
11
|
+
from xtr_http_message_contracts.uploaded_file_interface import UploadedFileInterface
|
|
12
|
+
|
|
13
|
+
__all__ = ["UploadedFileFactoryInterface"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class UploadedFileFactoryInterface(Protocol):
|
|
18
|
+
"""Makes an uploaded file, so that a caller need not name the class that is one."""
|
|
19
|
+
|
|
20
|
+
def create_uploaded_file(
|
|
21
|
+
self,
|
|
22
|
+
stream: StreamInterface,
|
|
23
|
+
size: int | None = None,
|
|
24
|
+
error: int = UploadError.OK,
|
|
25
|
+
client_filename: str | None = None,
|
|
26
|
+
client_media_type: str | None = None,
|
|
27
|
+
) -> UploadedFileInterface:
|
|
28
|
+
"""Return a new uploaded file reading from ``stream``.
|
|
29
|
+
|
|
30
|
+
Given no size, the factory works it out from the stream itself.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
stream: The contents of the uploaded file.
|
|
34
|
+
size: The size in bytes, when it is already known.
|
|
35
|
+
error: How the upload went, as one of
|
|
36
|
+
:class:`~xtr_http_message_contracts.upload_error.UploadError`.
|
|
37
|
+
client_filename: The file name the client sent, if any.
|
|
38
|
+
client_media_type: The media type the client sent, if any.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The uploaded file.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: When the stream cannot be read.
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Build a URI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from xtr_http_message_contracts.uri_interface import UriInterface
|
|
9
|
+
|
|
10
|
+
__all__ = ["UriFactoryInterface"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class UriFactoryInterface(Protocol):
|
|
15
|
+
"""Makes a URI, so that a caller need not name the class that is one."""
|
|
16
|
+
|
|
17
|
+
def create_uri(self, uri: str = "") -> UriInterface:
|
|
18
|
+
"""Return a new URI parsed from ``uri``.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
uri: The URI as text; empty for a URI with no components at all.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The URI.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: When the text cannot be parsed as a URI.
|
|
28
|
+
"""
|
|
29
|
+
...
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""What a request and a response have in common."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
from .stream_interface import StreamInterface
|
|
11
|
+
|
|
12
|
+
__all__ = ["MessageInterface"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class MessageInterface(Protocol):
|
|
17
|
+
"""An HTTP message: a protocol version, headers and a body.
|
|
18
|
+
|
|
19
|
+
Requests travel from a client to a server and responses back the other
|
|
20
|
+
way; everything they share is here.
|
|
21
|
+
|
|
22
|
+
Messages are immutable. Every ``with_*`` method keeps the instance it was
|
|
23
|
+
called on as it was and returns one carrying the change, so a message
|
|
24
|
+
handed around cannot be altered behind the back of whoever handed it on.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def get_protocol_version(self) -> str:
|
|
28
|
+
"""Return the HTTP version of the message.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The version number alone, such as ``"1.1"`` or ``"1.0"`` — never
|
|
32
|
+
the protocol name with it.
|
|
33
|
+
"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def with_protocol_version(self, version: str) -> Self:
|
|
37
|
+
"""Return an instance carrying ``version``.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
version: The HTTP version number alone, such as ``"1.1"``.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A new instance; this one is left as it was.
|
|
44
|
+
"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def get_headers(self) -> Mapping[str, Sequence[str]]:
|
|
48
|
+
"""Return every header, each name mapped to all of its values.
|
|
49
|
+
|
|
50
|
+
A name comes back in the case it was first given in, even though names
|
|
51
|
+
are matched without regard to case elsewhere, so a message can be
|
|
52
|
+
written out the way it arrived:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
for name, values in message.get_headers().items():
|
|
56
|
+
print(f"{name}: {', '.join(values)}")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Each header name as it will go over the wire, mapped to the
|
|
61
|
+
values recorded under it.
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def has_header(self, name: str) -> bool:
|
|
66
|
+
"""Tell whether a header is present, matching ``name`` without case.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
name: The header name, in any case.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
``True`` when some header name matches ``name`` ignoring case;
|
|
73
|
+
``False`` when none does.
|
|
74
|
+
"""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
def get_header(self, name: str) -> Sequence[str]:
|
|
78
|
+
"""Return every value recorded under a header, matched without case.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
name: The header name, in any case.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
All the values under that header, in order. A header the message
|
|
85
|
+
does not carry gives an empty sequence.
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
def get_header_line(self, name: str) -> str:
|
|
90
|
+
"""Return the values of a header joined with commas, matched without case.
|
|
91
|
+
|
|
92
|
+
Not every header can be written this way; where comma-joining would
|
|
93
|
+
change the meaning, read :meth:`get_header` and join the values with
|
|
94
|
+
whatever separator that header calls for.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
name: The header name, in any case.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The values joined with commas. A header the message does not
|
|
101
|
+
carry gives an empty string.
|
|
102
|
+
"""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
def with_header(self, name: str, value: str | Sequence[str]) -> Self:
|
|
106
|
+
"""Return an instance whose ``name`` header holds ``value`` and nothing else.
|
|
107
|
+
|
|
108
|
+
Whatever was recorded under that header is replaced. The case of
|
|
109
|
+
``name`` is kept and is what :meth:`get_headers` reports, even though
|
|
110
|
+
the header it replaces is found without regard to case.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
name: The header name, in any case.
|
|
114
|
+
value: One value, or every value the header is to hold.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A new instance; this one is left as it was.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: When the name or a value is not a valid header name or
|
|
121
|
+
value.
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
def with_added_header(self, name: str, value: str | Sequence[str]) -> Self:
|
|
126
|
+
"""Return an instance with ``value`` appended to the ``name`` header.
|
|
127
|
+
|
|
128
|
+
What the header already held stays; the new values go after it. A
|
|
129
|
+
header the message did not carry is added.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
name: The header name, in any case.
|
|
133
|
+
value: One value, or every value to append.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
A new instance; this one is left as it was.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: When the name or a value is not a valid header name or
|
|
140
|
+
value.
|
|
141
|
+
"""
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
def without_header(self, name: str) -> Self:
|
|
145
|
+
"""Return an instance with the ``name`` header gone.
|
|
146
|
+
|
|
147
|
+
The header to drop is found without regard to case.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: The header name, in any case.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A new instance; this one is left as it was.
|
|
154
|
+
"""
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
def get_body(self) -> StreamInterface:
|
|
158
|
+
"""Return the body of the message.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
The body, as a stream.
|
|
162
|
+
"""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
def with_body(self, body: StreamInterface) -> Self:
|
|
166
|
+
"""Return an instance carrying ``body``.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
body: The stream the new instance reads its body from.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A new instance; this one is left as it was.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: When the body cannot be used as one.
|
|
176
|
+
"""
|
|
177
|
+
...
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
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, Protocol, Self, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from .message_interface import MessageInterface
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .uri_interface import UriInterface
|
|
11
|
+
|
|
12
|
+
__all__ = ["RequestInterface"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class RequestInterface(MessageInterface, Protocol):
|
|
17
|
+
"""An outgoing, client-side request.
|
|
18
|
+
|
|
19
|
+
On top of what every message carries — a protocol version, headers and a
|
|
20
|
+
body — a request carries a method and a URI.
|
|
21
|
+
|
|
22
|
+
A request being built must take its ``Host`` header from the URI it is
|
|
23
|
+
given whenever no ``Host`` header was given with it.
|
|
24
|
+
|
|
25
|
+
Requests are immutable. Every ``with_*`` method keeps the instance it was
|
|
26
|
+
called on as it was and returns one carrying the change.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def get_request_target(self) -> str:
|
|
30
|
+
"""Return the request-target of the message.
|
|
31
|
+
|
|
32
|
+
That is the target as it will go out for a client, as it came in for a
|
|
33
|
+
server, or exactly as :meth:`with_request_target` set it.
|
|
34
|
+
|
|
35
|
+
Usually it is the origin-form of the URI the request carries — the
|
|
36
|
+
path with the query string after it — unless something else was set
|
|
37
|
+
explicitly.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The request-target, or ``"/"`` when there is no URI to build one
|
|
41
|
+
from and none was set.
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def with_request_target(self, request_target: str) -> Self:
|
|
46
|
+
"""Return an instance carrying ``request_target`` exactly as given.
|
|
47
|
+
|
|
48
|
+
Use it where the origin-form will not do: an absolute-form target, an
|
|
49
|
+
authority-form one, or the asterisk-form.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
request_target: The request-target, taken verbatim.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A new instance; this one is left as it was.
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def get_method(self) -> str:
|
|
60
|
+
"""Return the HTTP method of the request.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The method, in the case it was given in.
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def with_method(self, method: str) -> Self:
|
|
68
|
+
"""Return an instance carrying ``method``.
|
|
69
|
+
|
|
70
|
+
Methods are written in capitals by convention but compared with
|
|
71
|
+
regard to case, so what is given should be kept as it is rather than
|
|
72
|
+
recased.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
method: The method, case-sensitive.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A new instance; this one is left as it was.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: When the method is not a valid HTTP method.
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
def get_uri(self) -> UriInterface:
|
|
86
|
+
"""Return the URI the request is for.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The URI of the request.
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def with_uri(self, uri: UriInterface, preserve_host: bool = False) -> Self:
|
|
94
|
+
"""Return an instance carrying ``uri``.
|
|
95
|
+
|
|
96
|
+
By default the ``Host`` header of the new instance follows the URI: it
|
|
97
|
+
is set from the URI's host component when the URI has one, and a
|
|
98
|
+
``Host`` header already present carries over untouched when it has
|
|
99
|
+
none.
|
|
100
|
+
|
|
101
|
+
Passing ``preserve_host=True`` asks instead that an existing ``Host``
|
|
102
|
+
header be kept, which plays out in three ways:
|
|
103
|
+
|
|
104
|
+
- no ``Host`` header, or an empty one, and a URI with a host: the
|
|
105
|
+
``Host`` header of the new instance is set from the URI;
|
|
106
|
+
- no ``Host`` header, or an empty one, and a URI without a host: the
|
|
107
|
+
new instance gets no ``Host`` header either;
|
|
108
|
+
- a ``Host`` header that is present and not empty: it is left exactly
|
|
109
|
+
as it is.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
uri: The URI the new instance is for.
|
|
113
|
+
preserve_host: Whether to keep the ``Host`` header the request
|
|
114
|
+
already carries.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A new instance; this one is left as it was.
|
|
118
|
+
"""
|
|
119
|
+
...
|