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.
Files changed (22) hide show
  1. xtr_http_message_contracts-4.0.2/LICENSE +23 -0
  2. xtr_http_message_contracts-4.0.2/PKG-INFO +184 -0
  3. xtr_http_message_contracts-4.0.2/README.md +163 -0
  4. xtr_http_message_contracts-4.0.2/pyproject.toml +138 -0
  5. xtr_http_message_contracts-4.0.2/pyproject.toml.orig +132 -0
  6. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/__init__.py +89 -0
  7. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/__init__.py +41 -0
  8. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/request_factory_interface.py +29 -0
  9. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/response_factory_interface.py +35 -0
  10. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/server_request_factory_interface.py +42 -0
  11. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/stream_factory_interface.py +63 -0
  12. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/uploaded_file_factory_interface.py +46 -0
  13. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/factory/uri_factory_interface.py +29 -0
  14. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/message_interface.py +177 -0
  15. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/py.typed +0 -0
  16. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/request_interface.py +119 -0
  17. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/response_interface.py +64 -0
  18. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/server_request_interface.py +239 -0
  19. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/stream_interface.py +191 -0
  20. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/upload_error.py +45 -0
  21. xtr_http_message_contracts-4.0.2/src/xtr_http_message_contracts/uploaded_file_interface.py +110 -0
  22. 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
+ ]