xtr-http-message-contracts 4.0.2__tar.gz
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-4.0.2/LICENSE +23 -0
- xtr_http_message_contracts-4.0.2/PKG-INFO +184 -0
- xtr_http_message_contracts-4.0.2/README.md +163 -0
- xtr_http_message_contracts-4.0.2/pyproject.toml +138 -0
- xtr_http_message_contracts-4.0.2/pyproject.toml.orig +132 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/__init__.py +89 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/__init__.py +41 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/request_factory_interface.py +29 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/response_factory_interface.py +35 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/server_request_factory_interface.py +42 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/stream_factory_interface.py +63 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/uploaded_file_factory_interface.py +46 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/uri_factory_interface.py +29 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/message_interface.py +177 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/py.typed +0 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/request_interface.py +119 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/response_interface.py +64 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/server_request_interface.py +239 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/stream_interface.py +191 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/upload_error.py +45 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/uploaded_file_interface.py +110 -0
- xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/uri_interface.py +265 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xterr
|
|
4
|
+
Copyright (c) 2014 PHP Framework Interoperability Group
|
|
5
|
+
Copyright (c) 2018 PHP-FIG
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtr-http-message-contracts
|
|
3
|
+
Version: 4.0.2
|
|
4
|
+
Summary: The HTTP message contract alone: requests, responses, streams, URIs, uploaded files and the six factories that build them.
|
|
5
|
+
Keywords: http,http-message,request,response,contracts,interface,protocol
|
|
6
|
+
Author: Xterr
|
|
7
|
+
Author-email: Xterr <me@xterr.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: typing-extensions>=4.4
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
|
|
24
|
+
# xtr-http-message-contracts
|
|
25
|
+
|
|
26
|
+
**The HTTP message contract, and nothing else — so two libraries can pass a request between them without agreeing on a class.**
|
|
27
|
+
|
|
28
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
29
|
+
<img alt="core dependencies: 1" src="https://img.shields.io/badge/core%20deps-1-3FB950">
|
|
30
|
+
<img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
|
|
31
|
+
<img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Why?
|
|
38
|
+
|
|
39
|
+
An authorization server, a rate limiter and a firewall all read a request and fill in a response.
|
|
40
|
+
None of them should care whose request object it is — and if each brings its own, the application
|
|
41
|
+
ends up converting between three shapes of the same thing on every call.
|
|
42
|
+
|
|
43
|
+
So they need the *seam*, not an implementation. This package is that seam and nothing else:
|
|
44
|
+
thirteen protocols and an enumeration, built from the standard library, with one dependency —
|
|
45
|
+
`typing-extensions`, for `@override` on 3.11.
|
|
46
|
+
|
|
47
|
+
- 🧩 **`ServerRequestInterface`** — what arrived: method, URI, headers, body, cookies, query,
|
|
48
|
+
uploaded files, parsed body, plus the attributes a handler annotates it with.
|
|
49
|
+
- 📤 **`ResponseInterface`** — a status code, a reason phrase, headers and a body.
|
|
50
|
+
- 🌊 **`StreamInterface`** — the body itself, as a cursor over bytes.
|
|
51
|
+
- 🔗 **`UriInterface`** — a URI taken apart into components and put back together as text.
|
|
52
|
+
- 📎 **`UploadedFileInterface`** / **`UploadError`** — a file that came in, and how it went.
|
|
53
|
+
- 🏭 **Six factories** — build any of the above without naming a class.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from xtr_http_message_contracts import ResponseInterface, ServerRequestInterface
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def whoami(
|
|
60
|
+
request: ServerRequestInterface,
|
|
61
|
+
response: ResponseInterface,
|
|
62
|
+
) -> ResponseInterface:
|
|
63
|
+
_ = await response.get_body().write(request.get_header_line("user-agent").encode())
|
|
64
|
+
return response.with_header("content-type", "text/plain")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
uv add xtr-http-message-contracts
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Python 3.11+.
|
|
74
|
+
|
|
75
|
+
## Who installs what
|
|
76
|
+
|
|
77
|
+
| | Depends on |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| **A library that handles requests** | `xtr-http-message-contracts` at runtime, `xtr-http-message` as a dev dependency — its tests build real messages. |
|
|
80
|
+
| **An application** | `xtr-http-message`, which implements the contract, plus the integration that bridges these types to the web framework it runs on. |
|
|
81
|
+
|
|
82
|
+
An implementation **builds on** these types rather than redefining them. Import them from
|
|
83
|
+
`xtr_http_message_contracts`; that one set of types is what lets a container register an
|
|
84
|
+
implementation under an interface, and a library that never imported the implementation receive
|
|
85
|
+
it.
|
|
86
|
+
|
|
87
|
+
## The interfaces
|
|
88
|
+
|
|
89
|
+
| Interface | What it is |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `MessageInterface` | What a request and a response share: protocol version, headers, body. |
|
|
92
|
+
| `RequestInterface` | A message going out: a method and a URI on top. |
|
|
93
|
+
| `ServerRequestInterface` | A request that arrived, with everything the server learned alongside it. |
|
|
94
|
+
| `ResponseInterface` | A message coming back: a status code and a reason phrase on top. |
|
|
95
|
+
| `StreamInterface` | A message body: read, write, seek, and the whole of it in one awaited call. |
|
|
96
|
+
| `UriInterface` | A URI as a value, every component readable. |
|
|
97
|
+
| `UploadedFileInterface` | One file that came in with a request, movable once. |
|
|
98
|
+
|
|
99
|
+
Under `xtr_http_message_contracts.factory`, one factory each for a request, a server request, a
|
|
100
|
+
response, a stream, a URI and an uploaded file — `RequestFactoryInterface`,
|
|
101
|
+
`ServerRequestFactoryInterface`, `ResponseFactoryInterface`, `StreamFactoryInterface`,
|
|
102
|
+
`UriFactoryInterface`, `UploadedFileFactoryInterface`. They are re-exported at the top level too,
|
|
103
|
+
so either import works.
|
|
104
|
+
|
|
105
|
+
The rules that hold across all of them:
|
|
106
|
+
|
|
107
|
+
- **Messages are immutable.** Every `with_*` method leaves the instance it was called on as it
|
|
108
|
+
was and returns one carrying the change, so a message handed on cannot be altered behind the
|
|
109
|
+
caller's back. The return type is `Self`, so a subtype keeps its own type through a chain.
|
|
110
|
+
- **Header names are matched without regard to case**, and reported in the case they were first
|
|
111
|
+
given in. A header the message does not carry reads as an empty sequence from `get_header` and
|
|
112
|
+
an empty string from `get_header_line` — never as an error.
|
|
113
|
+
- **A component the URI has not got reads as an empty string**, and `get_port` reads as `None`
|
|
114
|
+
when there is no port worth reporting.
|
|
115
|
+
- **A stream is the one thing that is not immutable**: reading, writing and seeking move its
|
|
116
|
+
cursor in place. Its payload is `bytes`, since a body may be binary;
|
|
117
|
+
`await stream.get_contents()` rewinds and reads the whole of it, and raises when the body
|
|
118
|
+
cannot be read.
|
|
119
|
+
- **A body is awaited.** `read`, `write`, `get_contents`, `seek`, `rewind` and `close` are
|
|
120
|
+
`async`, so a body can come off the network as it is asked for; `move_to` on an uploaded file
|
|
121
|
+
is too. The methods that only report what the stream already knows — `get_size`, `tell`,
|
|
122
|
+
`eof`, `is_seekable`, `is_readable`, `is_writable`, `get_metadata`, `detach` — are not.
|
|
123
|
+
- **`with_uri` decides the `Host` header.** By default it follows the new URI, and a `Host`
|
|
124
|
+
header already there carries over only when the URI has no host; `preserve_host=True` keeps a
|
|
125
|
+
non-empty one exactly as it is.
|
|
126
|
+
|
|
127
|
+
### Upload errors
|
|
128
|
+
|
|
129
|
+
`get_error()` on an uploaded file, and `error=` when building one, carry one of these:
|
|
130
|
+
|
|
131
|
+
| Member | Value | Means |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `OK` | 0 | The file arrived whole. |
|
|
134
|
+
| `INI_SIZE` | 1 | Larger than the server configuration allows. |
|
|
135
|
+
| `FORM_SIZE` | 2 | Larger than the submitted form named. |
|
|
136
|
+
| `PARTIAL` | 3 | The upload stopped part way. |
|
|
137
|
+
| `NO_FILE` | 4 | The field was submitted with no file in it. |
|
|
138
|
+
| `NO_TMP_DIR` | 6 | Nowhere to keep the file while receiving it. |
|
|
139
|
+
| `CANT_WRITE` | 7 | It could not be written to where it was being kept. |
|
|
140
|
+
| `EXTENSION` | 8 | Something loaded on the server refused it. |
|
|
141
|
+
|
|
142
|
+
`UploadError` is an `IntEnum`, so a member passes anywhere the `int` the contract calls for is
|
|
143
|
+
expected. `5` is absent because that number was never assigned.
|
|
144
|
+
|
|
145
|
+
## What is not here
|
|
146
|
+
|
|
147
|
+
Anything that *is* a message rather than describing one: the request, response, stream, URI and
|
|
148
|
+
uploaded file classes, the status-code phrase table, URI parsing, and the factory that builds all
|
|
149
|
+
six. That is `xtr-http-message`.
|
|
150
|
+
|
|
151
|
+
Anything that bridges a message to a web framework — building one from the call in flight,
|
|
152
|
+
returning one from a route, registering either on a container — is not here either: it belongs
|
|
153
|
+
with that framework's integration, which for a FastAPI application is
|
|
154
|
+
[xtr-http-kernel](../xtr-http-kernel).
|
|
155
|
+
|
|
156
|
+
## Errors
|
|
157
|
+
|
|
158
|
+
This package defines no exception of its own; a contract with no behaviour has nothing to raise.
|
|
159
|
+
The interfaces name two builtins instead, so a caller catches the same thing whichever
|
|
160
|
+
implementation is behind them:
|
|
161
|
+
|
|
162
|
+
| Error | Raised when |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `ValueError` | An argument cannot be what it is being used as — a malformed header name, an out-of-range port, a status code that is not one |
|
|
165
|
+
| `RuntimeError` | An operation cannot be carried out now — a stream that will not seek, an uploaded file already moved |
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
|
|
170
|
+
`packages/xtr-http-message-contracts`; run the commands below from there. The
|
|
171
|
+
`python-xtr-http-message-contracts` repository is a read-only copy, so send issues and pull
|
|
172
|
+
requests to the monorepo.
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
uv sync
|
|
176
|
+
uv run ruff check . && uv run ruff format --check .
|
|
177
|
+
uv run basedpyright
|
|
178
|
+
uv run ty check
|
|
179
|
+
uv run pytest
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# xtr-http-message-contracts
|
|
4
|
+
|
|
5
|
+
**The HTTP message contract, and nothing else — so two libraries can pass a request between them without agreeing on a class.**
|
|
6
|
+
|
|
7
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
8
|
+
<img alt="core dependencies: 1" src="https://img.shields.io/badge/core%20deps-1-3FB950">
|
|
9
|
+
<img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
|
|
10
|
+
<img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why?
|
|
17
|
+
|
|
18
|
+
An authorization server, a rate limiter and a firewall all read a request and fill in a response.
|
|
19
|
+
None of them should care whose request object it is — and if each brings its own, the application
|
|
20
|
+
ends up converting between three shapes of the same thing on every call.
|
|
21
|
+
|
|
22
|
+
So they need the *seam*, not an implementation. This package is that seam and nothing else:
|
|
23
|
+
thirteen protocols and an enumeration, built from the standard library, with one dependency —
|
|
24
|
+
`typing-extensions`, for `@override` on 3.11.
|
|
25
|
+
|
|
26
|
+
- 🧩 **`ServerRequestInterface`** — what arrived: method, URI, headers, body, cookies, query,
|
|
27
|
+
uploaded files, parsed body, plus the attributes a handler annotates it with.
|
|
28
|
+
- 📤 **`ResponseInterface`** — a status code, a reason phrase, headers and a body.
|
|
29
|
+
- 🌊 **`StreamInterface`** — the body itself, as a cursor over bytes.
|
|
30
|
+
- 🔗 **`UriInterface`** — a URI taken apart into components and put back together as text.
|
|
31
|
+
- 📎 **`UploadedFileInterface`** / **`UploadError`** — a file that came in, and how it went.
|
|
32
|
+
- 🏭 **Six factories** — build any of the above without naming a class.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from xtr_http_message_contracts import ResponseInterface, ServerRequestInterface
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def whoami(
|
|
39
|
+
request: ServerRequestInterface,
|
|
40
|
+
response: ResponseInterface,
|
|
41
|
+
) -> ResponseInterface:
|
|
42
|
+
_ = await response.get_body().write(request.get_header_line("user-agent").encode())
|
|
43
|
+
return response.with_header("content-type", "text/plain")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
uv add xtr-http-message-contracts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requires Python 3.11+.
|
|
53
|
+
|
|
54
|
+
## Who installs what
|
|
55
|
+
|
|
56
|
+
| | Depends on |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| **A library that handles requests** | `xtr-http-message-contracts` at runtime, `xtr-http-message` as a dev dependency — its tests build real messages. |
|
|
59
|
+
| **An application** | `xtr-http-message`, which implements the contract, plus the integration that bridges these types to the web framework it runs on. |
|
|
60
|
+
|
|
61
|
+
An implementation **builds on** these types rather than redefining them. Import them from
|
|
62
|
+
`xtr_http_message_contracts`; that one set of types is what lets a container register an
|
|
63
|
+
implementation under an interface, and a library that never imported the implementation receive
|
|
64
|
+
it.
|
|
65
|
+
|
|
66
|
+
## The interfaces
|
|
67
|
+
|
|
68
|
+
| Interface | What it is |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `MessageInterface` | What a request and a response share: protocol version, headers, body. |
|
|
71
|
+
| `RequestInterface` | A message going out: a method and a URI on top. |
|
|
72
|
+
| `ServerRequestInterface` | A request that arrived, with everything the server learned alongside it. |
|
|
73
|
+
| `ResponseInterface` | A message coming back: a status code and a reason phrase on top. |
|
|
74
|
+
| `StreamInterface` | A message body: read, write, seek, and the whole of it in one awaited call. |
|
|
75
|
+
| `UriInterface` | A URI as a value, every component readable. |
|
|
76
|
+
| `UploadedFileInterface` | One file that came in with a request, movable once. |
|
|
77
|
+
|
|
78
|
+
Under `xtr_http_message_contracts.factory`, one factory each for a request, a server request, a
|
|
79
|
+
response, a stream, a URI and an uploaded file — `RequestFactoryInterface`,
|
|
80
|
+
`ServerRequestFactoryInterface`, `ResponseFactoryInterface`, `StreamFactoryInterface`,
|
|
81
|
+
`UriFactoryInterface`, `UploadedFileFactoryInterface`. They are re-exported at the top level too,
|
|
82
|
+
so either import works.
|
|
83
|
+
|
|
84
|
+
The rules that hold across all of them:
|
|
85
|
+
|
|
86
|
+
- **Messages are immutable.** Every `with_*` method leaves the instance it was called on as it
|
|
87
|
+
was and returns one carrying the change, so a message handed on cannot be altered behind the
|
|
88
|
+
caller's back. The return type is `Self`, so a subtype keeps its own type through a chain.
|
|
89
|
+
- **Header names are matched without regard to case**, and reported in the case they were first
|
|
90
|
+
given in. A header the message does not carry reads as an empty sequence from `get_header` and
|
|
91
|
+
an empty string from `get_header_line` — never as an error.
|
|
92
|
+
- **A component the URI has not got reads as an empty string**, and `get_port` reads as `None`
|
|
93
|
+
when there is no port worth reporting.
|
|
94
|
+
- **A stream is the one thing that is not immutable**: reading, writing and seeking move its
|
|
95
|
+
cursor in place. Its payload is `bytes`, since a body may be binary;
|
|
96
|
+
`await stream.get_contents()` rewinds and reads the whole of it, and raises when the body
|
|
97
|
+
cannot be read.
|
|
98
|
+
- **A body is awaited.** `read`, `write`, `get_contents`, `seek`, `rewind` and `close` are
|
|
99
|
+
`async`, so a body can come off the network as it is asked for; `move_to` on an uploaded file
|
|
100
|
+
is too. The methods that only report what the stream already knows — `get_size`, `tell`,
|
|
101
|
+
`eof`, `is_seekable`, `is_readable`, `is_writable`, `get_metadata`, `detach` — are not.
|
|
102
|
+
- **`with_uri` decides the `Host` header.** By default it follows the new URI, and a `Host`
|
|
103
|
+
header already there carries over only when the URI has no host; `preserve_host=True` keeps a
|
|
104
|
+
non-empty one exactly as it is.
|
|
105
|
+
|
|
106
|
+
### Upload errors
|
|
107
|
+
|
|
108
|
+
`get_error()` on an uploaded file, and `error=` when building one, carry one of these:
|
|
109
|
+
|
|
110
|
+
| Member | Value | Means |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `OK` | 0 | The file arrived whole. |
|
|
113
|
+
| `INI_SIZE` | 1 | Larger than the server configuration allows. |
|
|
114
|
+
| `FORM_SIZE` | 2 | Larger than the submitted form named. |
|
|
115
|
+
| `PARTIAL` | 3 | The upload stopped part way. |
|
|
116
|
+
| `NO_FILE` | 4 | The field was submitted with no file in it. |
|
|
117
|
+
| `NO_TMP_DIR` | 6 | Nowhere to keep the file while receiving it. |
|
|
118
|
+
| `CANT_WRITE` | 7 | It could not be written to where it was being kept. |
|
|
119
|
+
| `EXTENSION` | 8 | Something loaded on the server refused it. |
|
|
120
|
+
|
|
121
|
+
`UploadError` is an `IntEnum`, so a member passes anywhere the `int` the contract calls for is
|
|
122
|
+
expected. `5` is absent because that number was never assigned.
|
|
123
|
+
|
|
124
|
+
## What is not here
|
|
125
|
+
|
|
126
|
+
Anything that *is* a message rather than describing one: the request, response, stream, URI and
|
|
127
|
+
uploaded file classes, the status-code phrase table, URI parsing, and the factory that builds all
|
|
128
|
+
six. That is `xtr-http-message`.
|
|
129
|
+
|
|
130
|
+
Anything that bridges a message to a web framework — building one from the call in flight,
|
|
131
|
+
returning one from a route, registering either on a container — is not here either: it belongs
|
|
132
|
+
with that framework's integration, which for a FastAPI application is
|
|
133
|
+
[xtr-http-kernel](../xtr-http-kernel).
|
|
134
|
+
|
|
135
|
+
## Errors
|
|
136
|
+
|
|
137
|
+
This package defines no exception of its own; a contract with no behaviour has nothing to raise.
|
|
138
|
+
The interfaces name two builtins instead, so a caller catches the same thing whichever
|
|
139
|
+
implementation is behind them:
|
|
140
|
+
|
|
141
|
+
| Error | Raised when |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `ValueError` | An argument cannot be what it is being used as — a malformed header name, an out-of-range port, a status code that is not one |
|
|
144
|
+
| `RuntimeError` | An operation cannot be carried out now — a stream that will not seek, an uploaded file already moved |
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
|
|
149
|
+
`packages/xtr-http-message-contracts`; run the commands below from there. The
|
|
150
|
+
`python-xtr-http-message-contracts` repository is a read-only copy, so send issues and pull
|
|
151
|
+
requests to the monorepo.
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
uv sync
|
|
155
|
+
uv run ruff check . && uv run ruff format --check .
|
|
156
|
+
uv run basedpyright
|
|
157
|
+
uv run ty check
|
|
158
|
+
uv run pytest
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xtr-http-message-contracts"
|
|
3
|
+
version = "4.0.2"
|
|
4
|
+
description = "The HTTP message contract alone: requests, responses, streams, URIs, uploaded files and the six factories that build them."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"http",
|
|
11
|
+
"http-message",
|
|
12
|
+
"request",
|
|
13
|
+
"response",
|
|
14
|
+
"contracts",
|
|
15
|
+
"interface",
|
|
16
|
+
"protocol",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = ["typing-extensions>=4.4"]
|
|
29
|
+
|
|
30
|
+
[[project.authors]]
|
|
31
|
+
name = "Xterr"
|
|
32
|
+
email = "me@xterr.dev"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"basedpyright>=1.21",
|
|
37
|
+
"ruff>=0.8",
|
|
38
|
+
"pytest>=8",
|
|
39
|
+
"pytest-cov>=5",
|
|
40
|
+
"ty>=0.0.83",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[tool.basedpyright]
|
|
48
|
+
typeCheckingMode = "all"
|
|
49
|
+
pythonVersion = "3.11"
|
|
50
|
+
pythonPlatform = "All"
|
|
51
|
+
include = [
|
|
52
|
+
"src",
|
|
53
|
+
"tests",
|
|
54
|
+
]
|
|
55
|
+
exclude = [
|
|
56
|
+
"**/__pycache__",
|
|
57
|
+
"**/.venv",
|
|
58
|
+
"**/build",
|
|
59
|
+
"**/dist",
|
|
60
|
+
".tmp",
|
|
61
|
+
]
|
|
62
|
+
reportUnusedCallResult = "warning"
|
|
63
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
64
|
+
reportUnusedVariable = "error"
|
|
65
|
+
reportMissingParameterType = "error"
|
|
66
|
+
reportPrivateUsage = "error"
|
|
67
|
+
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
target-version = "py311"
|
|
70
|
+
line-length = 100
|
|
71
|
+
src = [
|
|
72
|
+
"src",
|
|
73
|
+
"tests",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
select = ["ALL"]
|
|
78
|
+
ignore = [
|
|
79
|
+
"COM812",
|
|
80
|
+
"ISC001",
|
|
81
|
+
"D203",
|
|
82
|
+
"D213",
|
|
83
|
+
"CPY001",
|
|
84
|
+
"FBT001",
|
|
85
|
+
"FBT002",
|
|
86
|
+
"TD002",
|
|
87
|
+
"TD003",
|
|
88
|
+
"FIX002",
|
|
89
|
+
]
|
|
90
|
+
fixable = ["ALL"]
|
|
91
|
+
unfixable = []
|
|
92
|
+
|
|
93
|
+
[tool.ruff.lint.per-file-ignores]
|
|
94
|
+
"tests/**/*.py" = [
|
|
95
|
+
"S101",
|
|
96
|
+
"ARG",
|
|
97
|
+
"PLR2004",
|
|
98
|
+
"SLF001",
|
|
99
|
+
"D",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[tool.ruff.lint.pydocstyle]
|
|
103
|
+
convention = "google"
|
|
104
|
+
|
|
105
|
+
[tool.ruff.format]
|
|
106
|
+
quote-style = "double"
|
|
107
|
+
indent-style = "space"
|
|
108
|
+
docstring-code-format = true
|
|
109
|
+
docstring-code-line-length = "dynamic"
|
|
110
|
+
|
|
111
|
+
[tool.ty.src]
|
|
112
|
+
include = [
|
|
113
|
+
"src",
|
|
114
|
+
"tests",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
[tool.pytest.ini_options]
|
|
118
|
+
minversion = "8.0"
|
|
119
|
+
testpaths = ["tests"]
|
|
120
|
+
addopts = [
|
|
121
|
+
"-ra",
|
|
122
|
+
"--strict-config",
|
|
123
|
+
"--strict-markers",
|
|
124
|
+
]
|
|
125
|
+
filterwarnings = ["error"]
|
|
126
|
+
|
|
127
|
+
[tool.coverage.run]
|
|
128
|
+
source = ["src"]
|
|
129
|
+
branch = true
|
|
130
|
+
|
|
131
|
+
[tool.coverage.report]
|
|
132
|
+
fail_under = 95
|
|
133
|
+
exclude_lines = [
|
|
134
|
+
"pragma: no cover",
|
|
135
|
+
"if TYPE_CHECKING:",
|
|
136
|
+
"raise NotImplementedError",
|
|
137
|
+
'^\s*\.\.\.$',
|
|
138
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xtr-http-message-contracts"
|
|
3
|
+
version = "4.0.2"
|
|
4
|
+
description = "The HTTP message contract alone: requests, responses, streams, URIs, uploaded files and the six factories that build them."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Xterr", email = "me@xterr.dev" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = ["http", "http-message", "request", "response", "contracts", "interface", "protocol"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
# One dependency, for `@override` on 3.11, where typing has none. Everything
|
|
24
|
+
# else the thirteen protocols and the enum need is in the standard library.
|
|
25
|
+
dependencies = [
|
|
26
|
+
"typing-extensions>=4.4",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"basedpyright>=1.21",
|
|
32
|
+
"ruff>=0.8",
|
|
33
|
+
"pytest>=8",
|
|
34
|
+
"pytest-cov>=5",
|
|
35
|
+
"ty>=0.0.83",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
40
|
+
build-backend = "uv_build"
|
|
41
|
+
|
|
42
|
+
# ─────────────────────────────────────────────────────────────────
|
|
43
|
+
# basedpyright — typeCheckingMode = "all" sets every report flag to error
|
|
44
|
+
# ─────────────────────────────────────────────────────────────────
|
|
45
|
+
[tool.basedpyright]
|
|
46
|
+
typeCheckingMode = "all"
|
|
47
|
+
pythonVersion = "3.11"
|
|
48
|
+
pythonPlatform = "All"
|
|
49
|
+
include = ["src", "tests"]
|
|
50
|
+
exclude = ["**/__pycache__", "**/.venv", "**/build", "**/dist", ".tmp"]
|
|
51
|
+
|
|
52
|
+
reportUnusedCallResult = "warning"
|
|
53
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
54
|
+
reportUnusedVariable = "error"
|
|
55
|
+
reportMissingParameterType = "error"
|
|
56
|
+
reportPrivateUsage = "error"
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────
|
|
59
|
+
# ruff — select = ["ALL"], minimal justified ignores
|
|
60
|
+
# ─────────────────────────────────────────────────────────────────
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
target-version = "py311"
|
|
63
|
+
line-length = 100
|
|
64
|
+
src = ["src", "tests"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["ALL"]
|
|
68
|
+
ignore = [
|
|
69
|
+
# Formatter conflicts (ruff documents these)
|
|
70
|
+
"COM812",
|
|
71
|
+
"ISC001",
|
|
72
|
+
# Mutually-exclusive docstring conventions; keep the modern one
|
|
73
|
+
"D203",
|
|
74
|
+
"D213",
|
|
75
|
+
# Project-specific noise
|
|
76
|
+
"CPY001",
|
|
77
|
+
"FBT001",
|
|
78
|
+
"FBT002",
|
|
79
|
+
"TD002",
|
|
80
|
+
"TD003",
|
|
81
|
+
"FIX002",
|
|
82
|
+
]
|
|
83
|
+
fixable = ["ALL"]
|
|
84
|
+
unfixable = []
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint.per-file-ignores]
|
|
87
|
+
"tests/**/*.py" = [
|
|
88
|
+
"S101", # assert is the point of pytest
|
|
89
|
+
"ARG", # fixtures look unused
|
|
90
|
+
"PLR2004", # magic numbers in test data
|
|
91
|
+
"SLF001", # tests reach into privates
|
|
92
|
+
"D", # test names are the docs
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint.pydocstyle]
|
|
96
|
+
convention = "google"
|
|
97
|
+
|
|
98
|
+
[tool.ruff.format]
|
|
99
|
+
quote-style = "double"
|
|
100
|
+
indent-style = "space"
|
|
101
|
+
docstring-code-format = true
|
|
102
|
+
docstring-code-line-length = "dynamic"
|
|
103
|
+
|
|
104
|
+
# ─────────────────────────────────────────────────────────────────
|
|
105
|
+
# ty — a second opinion on the source
|
|
106
|
+
# ─────────────────────────────────────────────────────────────────
|
|
107
|
+
[tool.ty.src]
|
|
108
|
+
include = ["src", "tests"]
|
|
109
|
+
|
|
110
|
+
# ─────────────────────────────────────────────────────────────────
|
|
111
|
+
# pytest
|
|
112
|
+
# ─────────────────────────────────────────────────────────────────
|
|
113
|
+
[tool.pytest.ini_options]
|
|
114
|
+
minversion = "8.0"
|
|
115
|
+
testpaths = ["tests"]
|
|
116
|
+
addopts = ["-ra", "--strict-config", "--strict-markers"]
|
|
117
|
+
filterwarnings = ["error"]
|
|
118
|
+
|
|
119
|
+
[tool.coverage.run]
|
|
120
|
+
source = ["src"]
|
|
121
|
+
branch = true
|
|
122
|
+
|
|
123
|
+
[tool.coverage.report]
|
|
124
|
+
# CI runs the tests with --cov; a change taking coverage below this fails there.
|
|
125
|
+
fail_under = 95
|
|
126
|
+
exclude_lines = [
|
|
127
|
+
"pragma: no cover",
|
|
128
|
+
"if TYPE_CHECKING:",
|
|
129
|
+
"raise NotImplementedError",
|
|
130
|
+
# A Protocol body states a contract; there is nothing there to run.
|
|
131
|
+
"^\\s*\\.\\.\\.$",
|
|
132
|
+
]
|