coredis-utils 0.1.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,112 @@
1
+ .DS_Store
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+ tmp.py
7
+ *.override.yml
8
+
9
+ # C extensions
10
+ *.so
11
+ data/
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ target/
75
+
76
+ # Jupyter Notebook
77
+ .ipynb_checkpoints
78
+ *.ipynb
79
+
80
+ # celery beat schedule file
81
+ celerybeat-schedule
82
+
83
+ # SageMath parsed files
84
+ *.sage.py
85
+
86
+ # Environments
87
+ .env
88
+ .venv
89
+ env/
90
+ venv/
91
+ ENV/
92
+ env.bak/
93
+ venv.bak/
94
+
95
+ # Spyder project settings
96
+ .spyderproject
97
+ .spyproject
98
+
99
+ # Rope project settings
100
+ .ropeproject
101
+
102
+ # mypy
103
+ .mypy_cache/
104
+
105
+ # idea
106
+ .idea
107
+ *.iml
108
+ *.ipr
109
+
110
+ .vscode/
111
+
112
+ main.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tastyware
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,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: coredis-utils
3
+ Version: 0.1.0
4
+ Summary: A collection of helpful utilities for coredis.
5
+ Project-URL: Homepage, https://github.com/tastyware/coredis-utils
6
+ Project-URL: Funding, https://github.com/sponsors/tastyware
7
+ Project-URL: Source, https://github.com/tastyware/coredis-utils
8
+ Project-URL: Changelog, https://github.com/tastyware/coredis-utils/releases
9
+ Author-email: Graeme Holliday <graeme@tastyware.dev>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 tastyware
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Environment :: Console
34
+ Classifier: Framework :: AnyIO
35
+ Classifier: Framework :: AsyncIO
36
+ Classifier: Framework :: Django
37
+ Classifier: Framework :: FastAPI
38
+ Classifier: Framework :: Flask
39
+ Classifier: Framework :: Trio
40
+ Classifier: Intended Audience :: Developers
41
+ Classifier: Intended Audience :: Information Technology
42
+ Classifier: Intended Audience :: System Administrators
43
+ Classifier: License :: OSI Approved :: MIT License
44
+ Classifier: Operating System :: MacOS :: MacOS X
45
+ Classifier: Operating System :: Microsoft :: Windows
46
+ Classifier: Operating System :: POSIX :: Linux
47
+ Classifier: Operating System :: Unix
48
+ Classifier: Programming Language :: Python
49
+ Classifier: Programming Language :: Python :: 3
50
+ Classifier: Programming Language :: Python :: 3 :: Only
51
+ Classifier: Programming Language :: Python :: 3.11
52
+ Classifier: Programming Language :: Python :: 3.12
53
+ Classifier: Programming Language :: Python :: 3.13
54
+ Classifier: Programming Language :: Python :: 3.14
55
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
56
+ Classifier: Topic :: System :: Clustering
57
+ Classifier: Topic :: System :: Distributed Computing
58
+ Classifier: Topic :: System :: Monitoring
59
+ Classifier: Topic :: System :: Systems Administration
60
+ Classifier: Typing :: Typed
61
+ Requires-Python: >=3.11
62
+ Requires-Dist: coredis>=6.6.1
63
+ Description-Content-Type: text/markdown
64
+
65
+ [![PyPI](https://img.shields.io/pypi/v/coredis-utils)](https://pypi.org/project/coredis-utils)
66
+ [![Downloads](https://static.pepy.tech/badge/coredis-utils)](https://pepy.tech/project/coredis-utils)
67
+ [![Release](https://img.shields.io/github/v/release/tastyware/coredis-utils?label=release%20notes)](https://github.com/tastyware/coredis-utils/releases)
68
+
69
+ # coredis-utils
70
+
71
+ A collection of helpful utilities for [coredis](https://coredis.readthedocs.io/en/latest/).
72
+
73
+ ## Features
74
+
75
+ - Caching decorator with thundering herd protection and error caching
76
+ - Idempotency keys
77
+ - Fixed-window rate limiting
78
+
79
+ ## Installation
80
+
81
+ ```console
82
+ $ pip install coredis-utils
83
+ ```
84
+
85
+ ## Getting started
86
+
87
+ First, create a `CoredisUtils` object wrapping a `coredis.Redis` instance:
88
+
89
+ ```python
90
+ from coredis import Redis
91
+ from coredis_utils import CoredisUtils
92
+
93
+ client = Redis(...)
94
+ utils = CoredisUtils(client)
95
+ ```
96
+
97
+ Caching is implemented with a decorator:
98
+
99
+ ```python
100
+ @utils.cached(ttl=60)
101
+ async def my_task() -> int: ...
102
+ ```
103
+
104
+ Idempotency uses a simple check:
105
+
106
+ ```python
107
+ if await utils.idempotent("my-key", ttl=60):
108
+ ... # code in this block can only run once
109
+ ```
110
+
111
+ Rate limiting is similar:
112
+
113
+ ```python
114
+ for _ in range(15):
115
+ print(await utils.limit("my-ip-addr", 10, 1)) # limit to 10/second
116
+ ```
117
+
118
+ ```python
119
+ True
120
+ True
121
+ True
122
+ True
123
+ True
124
+ True
125
+ True
126
+ True
127
+ True
128
+ True
129
+ False
130
+ False
131
+ False
132
+ False
133
+ False
134
+ ```
@@ -0,0 +1,70 @@
1
+ [![PyPI](https://img.shields.io/pypi/v/coredis-utils)](https://pypi.org/project/coredis-utils)
2
+ [![Downloads](https://static.pepy.tech/badge/coredis-utils)](https://pepy.tech/project/coredis-utils)
3
+ [![Release](https://img.shields.io/github/v/release/tastyware/coredis-utils?label=release%20notes)](https://github.com/tastyware/coredis-utils/releases)
4
+
5
+ # coredis-utils
6
+
7
+ A collection of helpful utilities for [coredis](https://coredis.readthedocs.io/en/latest/).
8
+
9
+ ## Features
10
+
11
+ - Caching decorator with thundering herd protection and error caching
12
+ - Idempotency keys
13
+ - Fixed-window rate limiting
14
+
15
+ ## Installation
16
+
17
+ ```console
18
+ $ pip install coredis-utils
19
+ ```
20
+
21
+ ## Getting started
22
+
23
+ First, create a `CoredisUtils` object wrapping a `coredis.Redis` instance:
24
+
25
+ ```python
26
+ from coredis import Redis
27
+ from coredis_utils import CoredisUtils
28
+
29
+ client = Redis(...)
30
+ utils = CoredisUtils(client)
31
+ ```
32
+
33
+ Caching is implemented with a decorator:
34
+
35
+ ```python
36
+ @utils.cached(ttl=60)
37
+ async def my_task() -> int: ...
38
+ ```
39
+
40
+ Idempotency uses a simple check:
41
+
42
+ ```python
43
+ if await utils.idempotent("my-key", ttl=60):
44
+ ... # code in this block can only run once
45
+ ```
46
+
47
+ Rate limiting is similar:
48
+
49
+ ```python
50
+ for _ in range(15):
51
+ print(await utils.limit("my-ip-addr", 10, 1)) # limit to 10/second
52
+ ```
53
+
54
+ ```python
55
+ True
56
+ True
57
+ True
58
+ True
59
+ True
60
+ True
61
+ True
62
+ True
63
+ True
64
+ True
65
+ False
66
+ False
67
+ False
68
+ False
69
+ False
70
+ ```
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ import hmac
4
+ import inspect
5
+ import pickle
6
+ import time
7
+ from collections.abc import Awaitable, Callable
8
+ from dataclasses import dataclass
9
+ from datetime import timedelta
10
+ from functools import wraps
11
+ from hashlib import sha256
12
+ from typing import Any, AnyStr, Generic, ParamSpec, TypeVar, overload
13
+
14
+ from anyio import sleep
15
+ from coredis import PureToken, Redis, RedisCluster
16
+ from coredis.commands import CommandRequest
17
+ from coredis.typing import KeyT
18
+
19
+ P = ParamSpec("P")
20
+ R = TypeVar("R")
21
+
22
+ VERSION = "0.1.0"
23
+ LIMITER_SCRIPT = """
24
+ local val = redis.call('incr', KEYS[1])
25
+ if val == 1 then
26
+ redis.call('expire', KEYS[1], ARGV[1])
27
+ end
28
+ return val
29
+ """
30
+ __version__ = VERSION
31
+
32
+
33
+ def _limit(key: KeyT, period: int) -> CommandRequest[int]: ...
34
+
35
+
36
+ def _make_cache_key(
37
+ prefix: str,
38
+ fn: Callable[..., Any],
39
+ signature: inspect.Signature,
40
+ args: tuple[Any, ...],
41
+ kwargs: dict[str, Any],
42
+ ) -> str:
43
+ bound = signature.bind(*args, **kwargs)
44
+ bound.apply_defaults()
45
+ canonical = pickle.dumps(tuple(bound.arguments.items()))
46
+ digest = sha256(canonical).hexdigest()[:16]
47
+ return f"{prefix}cache:{fn.__module__}.{fn.__qualname__}:{digest}"
48
+
49
+
50
+ def _ttl_seconds(ttl: timedelta | int) -> int:
51
+ if isinstance(ttl, timedelta):
52
+ return round(ttl.total_seconds())
53
+ return ttl
54
+
55
+
56
+ # cache envelopes to distinguish a stored value from a stored error
57
+ @dataclass(slots=True, frozen=True)
58
+ class _Ok:
59
+ value: Any
60
+
61
+
62
+ @dataclass(slots=True, frozen=True)
63
+ class _Err:
64
+ exc: BaseException
65
+
66
+
67
+ class CoredisUtils(Generic[AnyStr]):
68
+ """
69
+ Wraps a coredis client to provide additional functionality.
70
+
71
+ :param client: basic or cluster client to use
72
+ :param prefix: prefix for all keys used in Redis
73
+ :param ttl: default TTL for cached results/idempotency keys, defaults to 5 minutes
74
+ :param signing_secret:
75
+ if provided, used to sign results cached in Redis, which improves security since
76
+ serialization uses pickle. To generate a key, try: `secrets.token_urlsafe(32)`
77
+ """
78
+
79
+ __slots__ = ("_client", "_limit", "prefix", "signing_secret", "ttl")
80
+
81
+ @overload
82
+ def __init__(
83
+ self: CoredisUtils[bytes],
84
+ client: Redis[bytes] | RedisCluster[bytes],
85
+ *,
86
+ prefix: str | None = ...,
87
+ ttl: timedelta | int | None = ...,
88
+ signing_secret: str | None = ...,
89
+ ) -> None: ...
90
+
91
+ @overload
92
+ def __init__(
93
+ self: CoredisUtils[str],
94
+ client: Redis[str] | RedisCluster[str],
95
+ *,
96
+ prefix: str | None = ...,
97
+ ttl: timedelta | int | None = ...,
98
+ signing_secret: str | None = ...,
99
+ ) -> None: ...
100
+
101
+ def __init__(
102
+ self: CoredisUtils[Any],
103
+ client: Redis[Any] | RedisCluster[Any],
104
+ *,
105
+ prefix: str | None = "coredis-utils",
106
+ ttl: timedelta | int | None = 300,
107
+ signing_secret: str | None = None,
108
+ ) -> None:
109
+ # Redis connection
110
+ self._client = client
111
+ self.prefix = prefix + ":" if prefix else ""
112
+ self.ttl = ttl
113
+ self.signing_secret = signing_secret.encode() if signing_secret else None
114
+ # coredis FFI stubs for Lua script
115
+ self._limit = client.register_script(LIMITER_SCRIPT).wraps()(_limit)
116
+
117
+ async def idempotent(self, key: str, ttl: timedelta | int | None = 60) -> bool:
118
+ """
119
+ Shields code from being run multiple times.
120
+
121
+ :param key: idempotency key to use
122
+ :param ttl: how long to prevent duplicate runs, defaults to 1 minute
123
+ """
124
+ ttl = ttl or self.ttl
125
+ return await self._client.set(
126
+ f"{self.prefix}idempotent:{key}", 1, condition=PureToken.NX, ex=ttl
127
+ )
128
+
129
+ async def limit(self, key: str, limit: int, period: timedelta | int) -> bool:
130
+ """
131
+ Limits the number of successful calls per period to the given number using a
132
+ fixed-window rate limiting algorithm.
133
+
134
+ :param key: unique identifier, usually some combination of user/IP/route
135
+ :param limit: maximum number of calls that can succeed per period
136
+ :param period: duration of window before more calls can succeed
137
+ """
138
+ count = await self._limit(f"{self.prefix}limit:{key}", _ttl_seconds(period))
139
+ return count <= limit
140
+
141
+ def _serialize(self, data: Any) -> str | bytes:
142
+ try:
143
+ serialized = pickle.dumps(data)
144
+ except Exception as e:
145
+ raise RuntimeError(f"Failed to serialize data: {data}") from e
146
+ if self.signing_secret:
147
+ serialized += hmac.digest(self.signing_secret, serialized, "sha256")
148
+ return serialized
149
+
150
+ def _deserialize(self, data: Any) -> Any:
151
+ if self.signing_secret:
152
+ data_bytes, signature = data[:-32], data[-32:]
153
+ verify = hmac.digest(self.signing_secret, data_bytes, "sha256")
154
+ if not hmac.compare_digest(signature, verify):
155
+ raise RuntimeError("Invalid signature for task data!")
156
+ data = data_bytes
157
+ try:
158
+ return pickle.loads(data)
159
+ except Exception as e:
160
+ raise RuntimeError(f"Failed to deserialize data: {data}") from e
161
+
162
+ async def _try_read(self, key: str) -> _Ok | None:
163
+ raw = await self._client.get(key)
164
+ if raw is not None:
165
+ val = self._deserialize(raw)
166
+ if isinstance(val, _Err):
167
+ raise val.exc
168
+ if isinstance(val, _Ok):
169
+ return val
170
+
171
+ @overload
172
+ def cached(self, fn: Callable[P, Awaitable[R]], /) -> Callable[P, Awaitable[R]]: ...
173
+
174
+ @overload
175
+ def cached(
176
+ self,
177
+ *,
178
+ ttl: timedelta | int | None = ...,
179
+ error_ttl: timedelta | int | None = ...,
180
+ lock_timeout: timedelta | int = ...,
181
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: ...
182
+
183
+ def cached(
184
+ self,
185
+ fn: Callable[P, Awaitable[R]] | None = None,
186
+ *,
187
+ ttl: timedelta | int | None = None,
188
+ error_ttl: timedelta | int | None = None,
189
+ lock_timeout: timedelta | int = 60,
190
+ ) -> Any:
191
+ """
192
+ Cache the function's results in Redis. Uses a lock to implement "singleflight",
193
+ protecting against thundering herds.
194
+
195
+ :param ttl: duration to cache results, defaults to `cache_ttl`
196
+ :param error_ttl:
197
+ duration to cache errors, defaults to `cache_ttl`, 0 means disabled
198
+ :param lock_timeout: TTL of the stampede protection lock, defaults to 1 minute
199
+ """
200
+ ttl = ttl or self.ttl
201
+ error_ttl = error_ttl if error_ttl is not None else (self.ttl or 0)
202
+
203
+ def decorator(_fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
204
+ sig = inspect.signature(_fn)
205
+
206
+ @wraps(_fn)
207
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
208
+ key = _make_cache_key(self.prefix, _fn, sig, args, kwargs)
209
+ # fast path: cache hit
210
+ if res := await self._try_read(key):
211
+ return res.value
212
+ # slow path: add lock for stampede protection
213
+ lock = self._client.lock(
214
+ f"{self.prefix}lock:{key}",
215
+ timeout=_ttl_seconds(lock_timeout),
216
+ blocking=False,
217
+ )
218
+ if await lock.acquire():
219
+ try:
220
+ if res := await self._try_read(key):
221
+ return res.value
222
+ value = await _fn(*args, **kwargs)
223
+ await self._client.set(key, self._serialize(_Ok(value)), ex=ttl)
224
+ return value
225
+ except Exception as e:
226
+ if error_ttl != 0:
227
+ await self._client.set(
228
+ key, self._serialize(_Err(exc=e)), ex=error_ttl
229
+ )
230
+ raise
231
+ finally:
232
+ await lock.release()
233
+ # wait for leader's result by polling the cache
234
+ deadline = time.monotonic() + _ttl_seconds(lock_timeout)
235
+ sleep_time = 0.005 # 5ms initial backoff
236
+ while time.monotonic() < deadline:
237
+ await sleep(sleep_time)
238
+ if res := await self._try_read(key):
239
+ return res.value
240
+ sleep_time = min(sleep_time * 2, 0.1) # capped exponential backoff
241
+ # leader didn't complete in time, just run ourselves
242
+ return await _fn(*args, **kwargs)
243
+
244
+ return wrapper
245
+
246
+ if fn is None:
247
+ return decorator
248
+ return decorator(fn)
249
+
250
+
251
+ __all__ = ["CoredisUtils"]
File without changes
@@ -0,0 +1,83 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.version]
6
+ path = "coredis_utils/__init__.py"
7
+
8
+ [tool.hatch.build.targets.wheel]
9
+ include = ["coredis_utils/**/*"]
10
+ exclude = ["**/__pycache__/**"]
11
+
12
+ [tool.hatch.build.targets.sdist]
13
+ include = ["coredis_utils/**/*"]
14
+ exclude = ["**/__pycache__/**"]
15
+
16
+ [project]
17
+ name = "coredis-utils"
18
+ description = "A collection of helpful utilities for coredis."
19
+ readme = "README.md"
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Environment :: Console",
23
+ "Framework :: AsyncIO",
24
+ "Framework :: AnyIO",
25
+ "Framework :: Django",
26
+ "Framework :: FastAPI",
27
+ "Framework :: Flask",
28
+ "Framework :: Trio",
29
+ "Intended Audience :: Developers",
30
+ "Intended Audience :: Information Technology",
31
+ "Intended Audience :: System Administrators",
32
+ "License :: OSI Approved :: MIT License",
33
+ "Operating System :: MacOS :: MacOS X",
34
+ "Operating System :: Microsoft :: Windows",
35
+ "Operating System :: Unix",
36
+ "Operating System :: POSIX :: Linux",
37
+ "Programming Language :: Python",
38
+ "Programming Language :: Python :: 3",
39
+ "Programming Language :: Python :: 3 :: Only",
40
+ "Programming Language :: Python :: 3.11",
41
+ "Programming Language :: Python :: 3.12",
42
+ "Programming Language :: Python :: 3.13",
43
+ "Programming Language :: Python :: 3.14",
44
+ "Topic :: Software Development :: Libraries :: Python Modules",
45
+ "Topic :: System :: Clustering",
46
+ "Topic :: System :: Distributed Computing",
47
+ "Topic :: System :: Monitoring",
48
+ "Topic :: System :: Systems Administration",
49
+ "Typing :: Typed",
50
+ ]
51
+ requires-python = ">=3.11"
52
+ license = {file = "LICENSE"}
53
+ authors = [
54
+ { name = "Graeme Holliday", email = "graeme@tastyware.dev" }
55
+ ]
56
+ dependencies = [
57
+ "coredis>=6.6.1",
58
+ ]
59
+ dynamic = ["version"]
60
+
61
+ [project.urls]
62
+ Homepage = "https://github.com/tastyware/coredis-utils"
63
+ Funding = "https://github.com/sponsors/tastyware"
64
+ Source = "https://github.com/tastyware/coredis-utils"
65
+ Changelog = "https://github.com/tastyware/coredis-utils/releases"
66
+
67
+ [dependency-groups]
68
+ dev = [
69
+ "pyright>=1.1.406",
70
+ "pytest>=8.4.2",
71
+ "ruff>=0.13.1",
72
+ "trio>=0.30.0",
73
+ "uvloop>=0.22.1",
74
+ ]
75
+
76
+ [tool.pytest.ini_options]
77
+ testpaths = "tests"
78
+
79
+ [tool.ruff.lint]
80
+ select = ["E", "F", "I", "UP"]
81
+
82
+ [tool.pyright]
83
+ strict = ["coredis_utils/"]