xtr-cache-contracts 1.2.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xterr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtr-cache-contracts
3
+ Version: 1.2.0
4
+ Summary: The caching contract: compute-once reads, item pools, tags and namespaces, with no backend attached.
5
+ Keywords: cache,contracts,interface,protocol,asyncio,stampede
6
+ Author: Razvan Ceana
7
+ Author-email: Razvan Ceana <razvan@ceana.ro>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: typing-extensions>=4.4
19
+ Requires-Dist: xtr-clock>=1.0,<2
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+
23
+ <div align="center">
24
+
25
+ # xtr-cache-contracts
26
+
27
+ **The caching contract, and nothing else — so a library that caches installs nothing else.**
28
+
29
+ <img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
30
+ <img alt="asyncio" src="https://img.shields.io/badge/asyncio-native-1f6feb">
31
+ <img alt="core dependencies: 2" src="https://img.shields.io/badge/core%20deps-2-3FB950">
32
+ <img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
33
+ <img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
34
+
35
+ </div>
36
+
37
+ ---
38
+
39
+ ## Why?
40
+
41
+ A library that caches should not decide where values go. It takes a `CacheInterface`, hands it a
42
+ key and the function that computes the value, and leaves memory, files or a server to the
43
+ application that wires it.
44
+
45
+ That needs the *contract*, not a backend. This package is that dependency, reduced to what the
46
+ seam is made of:
47
+
48
+ - 🔁 **`CacheInterface`** — fetch-or-compute in one call, and delete.
49
+ - 🏷ïļ **`TagAwareCacheInterface`** — drop every value carrying a tag, whatever its key.
50
+ - 🗃ïļ **`CacheItemPoolInterface`** — the items underneath: hits told apart from misses, several
51
+ keys per round trip, batched writes.
52
+ - 📁 **`NamespacedPoolInterface`** — a view of a pool confined to a sub-namespace.
53
+ - ðŸ§Đ **`CacheMixin`** — `CacheInterface` for free on any pool, with early recomputation.
54
+ - ðŸŠķ **Two small dependencies** — `typing-extensions`, for `@override` on 3.11, and
55
+ `xtr-clock`, which has none of its own.
56
+
57
+ ```python
58
+ from xtr_cache_contracts import CacheInterface, ItemInterface
59
+
60
+
61
+ class Profiles:
62
+ def __init__(self, cache: CacheInterface) -> None:
63
+ self._cache = cache
64
+
65
+ async def get(self, user_id: int) -> Profile:
66
+ async def load(item: ItemInterface) -> Profile:
67
+ item.expires_after(3600)
68
+ return await fetch_profile(user_id)
69
+
70
+ return await self._cache.get(f"profile.{user_id}", load)
71
+ ```
72
+
73
+ ## Install
74
+
75
+ ```sh
76
+ uv add xtr-cache-contracts
77
+ ```
78
+
79
+ Requires Python 3.11+.
80
+
81
+ ## Who installs what
82
+
83
+ | | Depends on |
84
+ | --- | --- |
85
+ | **A library that caches** | `xtr-cache-contracts` at runtime. |
86
+ | **An application** | `xtr-cache`, which implements this contract with adapters and wires pools from configuration. |
87
+
88
+ `xtr-cache` **re-exports** every symbol here rather than redefining it, so
89
+ `xtr_cache.CacheInterface is xtr_cache_contracts.CacheInterface`. That identity is what lets a
90
+ container register a pool under the interface and a library, which never imported `xtr-cache`,
91
+ receive it.
92
+
93
+ ## Fetch-or-compute
94
+
95
+ ```python
96
+ class CacheInterface(Protocol):
97
+ async def get(
98
+ self,
99
+ key: str,
100
+ callback: Callback[T],
101
+ /,
102
+ *,
103
+ beta: float | None = None,
104
+ metadata: Metadata | None = None,
105
+ ) -> T: ...
106
+
107
+ async def delete(self, key: str, /) -> bool: ...
108
+ ```
109
+
110
+ On a miss, `callback` is awaited with the item for `key`; what it returns is saved and returned.
111
+ On a hit, the stored value comes back as the type `callback` returns — keep one key for one type.
112
+
113
+ Checking for a value, computing it and saving it is three steps with a race between each. Handing
114
+ the cache the computation instead lets it decide when to run it, which is what makes stampede
115
+ protection possible: computing a missing key once for every caller waiting on it, or refreshing
116
+ a value just before it expires.
117
+
118
+ - **The callback** is any `async def` taking the item, or an object with an `async def __call__`.
119
+ It sets the value's lifetime and tags through the item. If it raises, nothing is stored and
120
+ the error reaches the caller unchanged.
121
+ - **`beta`** controls early recomputation. A hit whose metadata says when it expires and how long
122
+ it took to compute may be recomputed before it expires, with a chance that grows as expiry
123
+ nears and as `beta` grows. `0` disables it, `math.inf` forces recomputation now, and `None`
124
+ leaves the choice to the implementation (`1.0` in `CacheMixin`).
125
+ - **`metadata`** is a dict the cache fills: `expiry` (Unix timestamp), `ctime` (milliseconds the
126
+ value took to compute), `tags`, and `save_failed` when a computed value could not be stored.
127
+
128
+ ```python
129
+ from xtr_cache_contracts import Metadata
130
+
131
+ metadata: Metadata = {}
132
+ report = await cache.get("report.daily", build_report, metadata=metadata)
133
+ if metadata.get("save_failed"):
134
+ ...
135
+ ```
136
+
137
+ ## Items and pools
138
+
139
+ `CacheItemPoolInterface` is the level below: for code that needs a hit told apart from a miss,
140
+ several keys in one round trip, or writes batched until `commit()` — and the level a backend
141
+ implements.
142
+
143
+ ```python
144
+ item = await pool.get_item("rate.42")
145
+ if not item.is_hit():
146
+ await pool.save(item.set(0).expires_after(60))
147
+
148
+ items = await pool.get_items(["a", "b", "c"]) # every key, hit or miss, in the order asked
149
+ await pool.save_deferred(item) # queued ...
150
+ await pool.commit() # ... stored
151
+ ```
152
+
153
+ An item is returned for every key, found or not, so `None` is a value like any other. Changing
154
+ an item reaches the backend only once it is saved. A pool refuses, with `False`, an item of a kind
155
+ it does not store.
156
+
157
+ The rules:
158
+
159
+ - **Keys** are non-empty strings without any of `{}()/\@:` (`RESERVED_CHARACTERS`). Letters,
160
+ digits, `_` and `.` up to 64 characters work everywhere; a pool may accept more. Tags follow
161
+ the same rules.
162
+ - **A backend failure never raises.** The call returns `False`, or reads as a miss, and the
163
+ implementation logs it: code that caches keeps working when the cache does not. What raises
164
+ is a mistake in the calling code — an invalid key, a tag on an item whose pool cannot store
165
+ tags.
166
+ - **Lifetimes** are set on the item: `expires_after(seconds or timedelta)` or
167
+ `expires_at(datetime)`. `None` falls back to the pool's default; a lifetime of zero or less
168
+ removes the key when the item is saved.
169
+ - **Deferred items** are committed by `commit()`, or before their key is read from the same pool.
170
+ Nothing commits them when the pool is discarded.
171
+
172
+ ## Tags and namespaces
173
+
174
+ A tag-aware cache drops values by tag rather than by key. Tag a value when computing it:
175
+
176
+ ```python
177
+ async def load_invoice(item: ItemInterface) -> Invoice:
178
+ item.tag([f"customer.{customer_id}", "invoices"])
179
+ return await fetch_invoice(invoice_id)
180
+
181
+
182
+ await cache.get(f"invoice.{invoice_id}", load_invoice)
183
+ await cache.invalidate_tags([f"customer.{customer_id}"]) # every invoice of that customer
184
+ ```
185
+
186
+ A namespaced pool hands out a view of itself whose keys live under a sub-namespace, so a group of
187
+ keys can be cleared together: `pool.with_sub_namespace("tenant42")`. The original pool is left as
188
+ it was. Tags ignore sub-namespaces.
189
+
190
+ ## Implementing a pool
191
+
192
+ Implement `CacheItemPoolInterface` and derive from `CacheMixin` to get `get()` and `delete()`
193
+ built on your `get_item()`, `save()` and `delete_item()`:
194
+
195
+ ```python
196
+ from xtr_cache_contracts import CacheItemPoolInterface, CacheMixin
197
+
198
+
199
+ class MyPool(CacheItemPoolInterface, CacheMixin): ...
200
+ ```
201
+
202
+ `CacheMixin` computes a miss, saves it, reports a failed save in `metadata`, and recomputes a hit
203
+ early when its metadata allows. It reads the time from the clock in force (`xtr_clock.now()`),
204
+ so `mock_time()` or a `MockClock` installed by a test freezes it too. It does not make concurrent misses on one key compute once — that
205
+ needs a lock or a shared in-flight computation, and is the implementation's to add by overriding
206
+ `get()`.
207
+
208
+ ## What is not here
209
+
210
+ Everything that stores or acts on values: the item class, adapters for memory, files and Redis,
211
+ serialisation, stampede locking, chaining, and the bundle. All of that is
212
+ [xtr-cache](https://github.com/xterr/python-xtr-cache).
213
+
214
+ There is no simple key-value interface (`get(key, default)` / `set(key, value, ttl)`): it cannot
215
+ tell a cached `None` from a miss, and checking then reading is a race. Fetch-or-compute covers the
216
+ common case, and the item pool covers the rest.
217
+
218
+ ## Errors
219
+
220
+ | Error | Raised when |
221
+ | --- | --- |
222
+ | `CacheError` | Never directly — the base every caching error derives from, `xtr-cache`'s included, such as the one `tag()` raises on an item whose pool cannot store tags |
223
+ | `InvalidArgumentError` | A key, a tag, a namespace or `beta` is invalid (also a `ValueError`); what is wrong is in `reason` |
224
+
225
+ ## Development
226
+
227
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
228
+ `packages/xtr-cache-contracts`; run the commands below from there. The
229
+ `python-xtr-cache-contracts` repository is a read-only copy, so send issues and pull requests to
230
+ the monorepo.
231
+
232
+ ```sh
233
+ uv sync
234
+ uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest
235
+ ```
236
+
237
+ ## License
238
+
239
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,217 @@
1
+ <div align="center">
2
+
3
+ # xtr-cache-contracts
4
+
5
+ **The caching contract, and nothing else — so a library that caches installs nothing else.**
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="asyncio" src="https://img.shields.io/badge/asyncio-native-1f6feb">
9
+ <img alt="core dependencies: 2" src="https://img.shields.io/badge/core%20deps-2-3FB950">
10
+ <img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
11
+ <img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ ## Why?
18
+
19
+ A library that caches should not decide where values go. It takes a `CacheInterface`, hands it a
20
+ key and the function that computes the value, and leaves memory, files or a server to the
21
+ application that wires it.
22
+
23
+ That needs the *contract*, not a backend. This package is that dependency, reduced to what the
24
+ seam is made of:
25
+
26
+ - 🔁 **`CacheInterface`** — fetch-or-compute in one call, and delete.
27
+ - 🏷ïļ **`TagAwareCacheInterface`** — drop every value carrying a tag, whatever its key.
28
+ - 🗃ïļ **`CacheItemPoolInterface`** — the items underneath: hits told apart from misses, several
29
+ keys per round trip, batched writes.
30
+ - 📁 **`NamespacedPoolInterface`** — a view of a pool confined to a sub-namespace.
31
+ - ðŸ§Đ **`CacheMixin`** — `CacheInterface` for free on any pool, with early recomputation.
32
+ - ðŸŠķ **Two small dependencies** — `typing-extensions`, for `@override` on 3.11, and
33
+ `xtr-clock`, which has none of its own.
34
+
35
+ ```python
36
+ from xtr_cache_contracts import CacheInterface, ItemInterface
37
+
38
+
39
+ class Profiles:
40
+ def __init__(self, cache: CacheInterface) -> None:
41
+ self._cache = cache
42
+
43
+ async def get(self, user_id: int) -> Profile:
44
+ async def load(item: ItemInterface) -> Profile:
45
+ item.expires_after(3600)
46
+ return await fetch_profile(user_id)
47
+
48
+ return await self._cache.get(f"profile.{user_id}", load)
49
+ ```
50
+
51
+ ## Install
52
+
53
+ ```sh
54
+ uv add xtr-cache-contracts
55
+ ```
56
+
57
+ Requires Python 3.11+.
58
+
59
+ ## Who installs what
60
+
61
+ | | Depends on |
62
+ | --- | --- |
63
+ | **A library that caches** | `xtr-cache-contracts` at runtime. |
64
+ | **An application** | `xtr-cache`, which implements this contract with adapters and wires pools from configuration. |
65
+
66
+ `xtr-cache` **re-exports** every symbol here rather than redefining it, so
67
+ `xtr_cache.CacheInterface is xtr_cache_contracts.CacheInterface`. That identity is what lets a
68
+ container register a pool under the interface and a library, which never imported `xtr-cache`,
69
+ receive it.
70
+
71
+ ## Fetch-or-compute
72
+
73
+ ```python
74
+ class CacheInterface(Protocol):
75
+ async def get(
76
+ self,
77
+ key: str,
78
+ callback: Callback[T],
79
+ /,
80
+ *,
81
+ beta: float | None = None,
82
+ metadata: Metadata | None = None,
83
+ ) -> T: ...
84
+
85
+ async def delete(self, key: str, /) -> bool: ...
86
+ ```
87
+
88
+ On a miss, `callback` is awaited with the item for `key`; what it returns is saved and returned.
89
+ On a hit, the stored value comes back as the type `callback` returns — keep one key for one type.
90
+
91
+ Checking for a value, computing it and saving it is three steps with a race between each. Handing
92
+ the cache the computation instead lets it decide when to run it, which is what makes stampede
93
+ protection possible: computing a missing key once for every caller waiting on it, or refreshing
94
+ a value just before it expires.
95
+
96
+ - **The callback** is any `async def` taking the item, or an object with an `async def __call__`.
97
+ It sets the value's lifetime and tags through the item. If it raises, nothing is stored and
98
+ the error reaches the caller unchanged.
99
+ - **`beta`** controls early recomputation. A hit whose metadata says when it expires and how long
100
+ it took to compute may be recomputed before it expires, with a chance that grows as expiry
101
+ nears and as `beta` grows. `0` disables it, `math.inf` forces recomputation now, and `None`
102
+ leaves the choice to the implementation (`1.0` in `CacheMixin`).
103
+ - **`metadata`** is a dict the cache fills: `expiry` (Unix timestamp), `ctime` (milliseconds the
104
+ value took to compute), `tags`, and `save_failed` when a computed value could not be stored.
105
+
106
+ ```python
107
+ from xtr_cache_contracts import Metadata
108
+
109
+ metadata: Metadata = {}
110
+ report = await cache.get("report.daily", build_report, metadata=metadata)
111
+ if metadata.get("save_failed"):
112
+ ...
113
+ ```
114
+
115
+ ## Items and pools
116
+
117
+ `CacheItemPoolInterface` is the level below: for code that needs a hit told apart from a miss,
118
+ several keys in one round trip, or writes batched until `commit()` — and the level a backend
119
+ implements.
120
+
121
+ ```python
122
+ item = await pool.get_item("rate.42")
123
+ if not item.is_hit():
124
+ await pool.save(item.set(0).expires_after(60))
125
+
126
+ items = await pool.get_items(["a", "b", "c"]) # every key, hit or miss, in the order asked
127
+ await pool.save_deferred(item) # queued ...
128
+ await pool.commit() # ... stored
129
+ ```
130
+
131
+ An item is returned for every key, found or not, so `None` is a value like any other. Changing
132
+ an item reaches the backend only once it is saved. A pool refuses, with `False`, an item of a kind
133
+ it does not store.
134
+
135
+ The rules:
136
+
137
+ - **Keys** are non-empty strings without any of `{}()/\@:` (`RESERVED_CHARACTERS`). Letters,
138
+ digits, `_` and `.` up to 64 characters work everywhere; a pool may accept more. Tags follow
139
+ the same rules.
140
+ - **A backend failure never raises.** The call returns `False`, or reads as a miss, and the
141
+ implementation logs it: code that caches keeps working when the cache does not. What raises
142
+ is a mistake in the calling code — an invalid key, a tag on an item whose pool cannot store
143
+ tags.
144
+ - **Lifetimes** are set on the item: `expires_after(seconds or timedelta)` or
145
+ `expires_at(datetime)`. `None` falls back to the pool's default; a lifetime of zero or less
146
+ removes the key when the item is saved.
147
+ - **Deferred items** are committed by `commit()`, or before their key is read from the same pool.
148
+ Nothing commits them when the pool is discarded.
149
+
150
+ ## Tags and namespaces
151
+
152
+ A tag-aware cache drops values by tag rather than by key. Tag a value when computing it:
153
+
154
+ ```python
155
+ async def load_invoice(item: ItemInterface) -> Invoice:
156
+ item.tag([f"customer.{customer_id}", "invoices"])
157
+ return await fetch_invoice(invoice_id)
158
+
159
+
160
+ await cache.get(f"invoice.{invoice_id}", load_invoice)
161
+ await cache.invalidate_tags([f"customer.{customer_id}"]) # every invoice of that customer
162
+ ```
163
+
164
+ A namespaced pool hands out a view of itself whose keys live under a sub-namespace, so a group of
165
+ keys can be cleared together: `pool.with_sub_namespace("tenant42")`. The original pool is left as
166
+ it was. Tags ignore sub-namespaces.
167
+
168
+ ## Implementing a pool
169
+
170
+ Implement `CacheItemPoolInterface` and derive from `CacheMixin` to get `get()` and `delete()`
171
+ built on your `get_item()`, `save()` and `delete_item()`:
172
+
173
+ ```python
174
+ from xtr_cache_contracts import CacheItemPoolInterface, CacheMixin
175
+
176
+
177
+ class MyPool(CacheItemPoolInterface, CacheMixin): ...
178
+ ```
179
+
180
+ `CacheMixin` computes a miss, saves it, reports a failed save in `metadata`, and recomputes a hit
181
+ early when its metadata allows. It reads the time from the clock in force (`xtr_clock.now()`),
182
+ so `mock_time()` or a `MockClock` installed by a test freezes it too. It does not make concurrent misses on one key compute once — that
183
+ needs a lock or a shared in-flight computation, and is the implementation's to add by overriding
184
+ `get()`.
185
+
186
+ ## What is not here
187
+
188
+ Everything that stores or acts on values: the item class, adapters for memory, files and Redis,
189
+ serialisation, stampede locking, chaining, and the bundle. All of that is
190
+ [xtr-cache](https://github.com/xterr/python-xtr-cache).
191
+
192
+ There is no simple key-value interface (`get(key, default)` / `set(key, value, ttl)`): it cannot
193
+ tell a cached `None` from a miss, and checking then reading is a race. Fetch-or-compute covers the
194
+ common case, and the item pool covers the rest.
195
+
196
+ ## Errors
197
+
198
+ | Error | Raised when |
199
+ | --- | --- |
200
+ | `CacheError` | Never directly — the base every caching error derives from, `xtr-cache`'s included, such as the one `tag()` raises on an item whose pool cannot store tags |
201
+ | `InvalidArgumentError` | A key, a tag, a namespace or `beta` is invalid (also a `ValueError`); what is wrong is in `reason` |
202
+
203
+ ## Development
204
+
205
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
206
+ `packages/xtr-cache-contracts`; run the commands below from there. The
207
+ `python-xtr-cache-contracts` repository is a read-only copy, so send issues and pull requests to
208
+ the monorepo.
209
+
210
+ ```sh
211
+ uv sync
212
+ uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest
213
+ ```
214
+
215
+ ## License
216
+
217
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,143 @@
1
+ [project]
2
+ name = "xtr-cache-contracts"
3
+ version = "1.2.0"
4
+ description = "The caching contract: compute-once reads, item pools, tags and namespaces, with no backend attached."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "cache",
11
+ "contracts",
12
+ "interface",
13
+ "protocol",
14
+ "asyncio",
15
+ "stampede",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Framework :: AsyncIO",
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
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "typing-extensions>=4.4",
29
+ "xtr-clock>=1.0,<2",
30
+ ]
31
+
32
+ [[project.authors]]
33
+ name = "Razvan Ceana"
34
+ email = "razvan@ceana.ro"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "basedpyright>=1.21",
39
+ "ruff>=0.8",
40
+ "pytest>=8",
41
+ "pytest-cov>=5",
42
+ "anyio>=4.0",
43
+ "ty>=0.0.83",
44
+ ]
45
+
46
+ [build-system]
47
+ requires = ["uv_build>=0.9.18,<0.10.0"]
48
+ build-backend = "uv_build"
49
+
50
+ [tool.basedpyright]
51
+ typeCheckingMode = "all"
52
+ pythonVersion = "3.11"
53
+ pythonPlatform = "All"
54
+ include = [
55
+ "src",
56
+ "tests",
57
+ ]
58
+ exclude = [
59
+ "**/__pycache__",
60
+ "**/.venv",
61
+ "**/build",
62
+ "**/dist",
63
+ ".tmp",
64
+ ]
65
+ reportUnusedCallResult = "warning"
66
+ reportUnnecessaryTypeIgnoreComment = "error"
67
+ reportUnusedVariable = "error"
68
+ reportMissingParameterType = "error"
69
+ reportPrivateUsage = "error"
70
+
71
+ [tool.ruff]
72
+ target-version = "py311"
73
+ line-length = 100
74
+ src = [
75
+ "src",
76
+ "tests",
77
+ ]
78
+
79
+ [tool.ruff.lint]
80
+ select = ["ALL"]
81
+ ignore = [
82
+ "COM812",
83
+ "ISC001",
84
+ "D203",
85
+ "D213",
86
+ "CPY001",
87
+ "FBT001",
88
+ "FBT002",
89
+ "TD002",
90
+ "TD003",
91
+ "FIX002",
92
+ "TRY003",
93
+ "EM101",
94
+ "EM102",
95
+ ]
96
+ fixable = ["ALL"]
97
+ unfixable = []
98
+
99
+ [tool.ruff.lint.per-file-ignores]
100
+ "tests/**/*.py" = [
101
+ "S101",
102
+ "ARG",
103
+ "PLR2004",
104
+ "SLF001",
105
+ "D",
106
+ ]
107
+
108
+ [tool.ruff.lint.pydocstyle]
109
+ convention = "google"
110
+
111
+ [tool.ruff.format]
112
+ quote-style = "double"
113
+ indent-style = "space"
114
+ docstring-code-format = true
115
+ docstring-code-line-length = "dynamic"
116
+
117
+ [tool.ty.src]
118
+ include = [
119
+ "src",
120
+ "tests",
121
+ ]
122
+
123
+ [tool.pytest.ini_options]
124
+ minversion = "8.0"
125
+ testpaths = ["tests"]
126
+ addopts = [
127
+ "-ra",
128
+ "--strict-config",
129
+ "--strict-markers",
130
+ ]
131
+ filterwarnings = ["error"]
132
+
133
+ [tool.coverage.run]
134
+ source = ["src"]
135
+ branch = true
136
+
137
+ [tool.coverage.report]
138
+ exclude_lines = [
139
+ "pragma: no cover",
140
+ "if TYPE_CHECKING:",
141
+ "raise NotImplementedError",
142
+ '^\s*\.\.\.$',
143
+ ]