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