mirror of
https://github.com/d3vyce/taskiq-deduplication.git
synced 2026-08-05 03:14:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fb99e358e
|
||
|
|
66b418bff1 | ||
|
|
84fff031df | ||
|
|
bede6f8a40 | ||
|
|
446071924e | ||
|
|
3af968581d
|
||
|
|
1d15a36140
|
||
|
|
82a777ba14 | ||
|
|
de0d612f2c | ||
|
|
11cea69f44 | ||
|
|
11515ec72d | ||
|
|
1c31cc2633 | ||
|
|
f343ebe6a9 |
@@ -16,7 +16,7 @@ jobs:
|
|||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/configure-pages@v5
|
- uses: actions/configure-pages@v6
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
@@ -30,9 +30,9 @@ jobs:
|
|||||||
|
|
||||||
- run: uv run zensical build --clean
|
- run: uv run zensical build --clean
|
||||||
|
|
||||||
- uses: actions/upload-pages-artifact@v4
|
- uses: actions/upload-pages-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: site
|
path: site
|
||||||
|
|
||||||
- uses: actions/deploy-pages@v4
|
- uses: actions/deploy-pages@v5
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ except DuplicateTaskError:
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis queue lock, before they reach the broker.
|
- **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis lock, before they reach the broker.
|
||||||
- **Worker-side detection** — logs concurrent duplicate executions without raising, keeping `SmartRetryMiddleware` safe from retry storms.
|
- **Atomic lock release** — lock is released on completion or error via a Lua check-and-delete; only the owning task can release its lock.
|
||||||
- **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label.
|
- **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label.
|
||||||
- **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely.
|
- **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely.
|
||||||
- **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments.
|
- **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments.
|
||||||
|
|||||||
+2
-2
@@ -50,8 +50,8 @@ except DuplicateTaskError:
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis queue lock, before they reach the broker.
|
- **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis lock, before they reach the broker.
|
||||||
- **Worker-side detection** — logs concurrent duplicate executions without raising, keeping `SmartRetryMiddleware` safe from retry storms.
|
- **Atomic lock release** — lock is released on completion or error via a Lua check-and-delete; only the owning task can release its lock.
|
||||||
- **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label.
|
- **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label.
|
||||||
- **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely.
|
- **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely.
|
||||||
- **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments.
|
- **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments.
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends "base.html" %} {% block extrahead %}
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://analytics.d3vyce.fr/script.js"
|
||||||
|
data-website-id="07b1421a-a466-4816-a55b-ae76a101cb4b"
|
||||||
|
></script>
|
||||||
|
{{ super() }} {% endblock %}
|
||||||
+9
-8
@@ -1,13 +1,14 @@
|
|||||||
# API Reference
|
# API Reference
|
||||||
|
|
||||||
## Middleware
|
You can import them directly from `taskiq_deduplication`:
|
||||||
|
|
||||||
::: taskiq_deduplication.RedisDeduplicationMiddleware
|
```python
|
||||||
options:
|
from taskiq_deduplication import (
|
||||||
show_source: false
|
DuplicateTaskError,
|
||||||
|
RedisDeduplicationMiddleware,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
## Exceptions
|
## ::: taskiq_deduplication.RedisDeduplicationMiddleware
|
||||||
|
|
||||||
::: taskiq_deduplication.DuplicateTaskError
|
## ::: taskiq_deduplication.DuplicateTaskError
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|||||||
+6
-6
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "taskiq-deduplication"
|
name = "taskiq-deduplication"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
description = "Production-ready utilities for FastAPI applications"
|
description = "Redis-backed deduplication middleware for Taskiq"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
@@ -9,7 +9,7 @@ requires-python = ">=3.10"
|
|||||||
authors = [
|
authors = [
|
||||||
{ name = "d3vyce", email = "contact@d3vyce.fr" }
|
{ name = "d3vyce", email = "contact@d3vyce.fr" }
|
||||||
]
|
]
|
||||||
keywords = ["fastapi", "sqlalchemy", "postgresql"]
|
keywords = ["taskiq", "redis", "deduplication", "middleware", "task-queue"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Framework :: AsyncIO",
|
"Framework :: AsyncIO",
|
||||||
@@ -44,11 +44,11 @@ dev = [
|
|||||||
"taskiq_deduplication",
|
"taskiq_deduplication",
|
||||||
"prek>=0.3.8",
|
"prek>=0.3.8",
|
||||||
"ruff>=0.1.0",
|
"ruff>=0.1.0",
|
||||||
"ty>=0.0.1a0",
|
"ty>=0.0.34",
|
||||||
]
|
]
|
||||||
tests = [
|
tests = [
|
||||||
"coverage>=7.0.0",
|
"coverage>=7.0.0",
|
||||||
"fakeredis[lua]>=2.0.0",
|
"fakeredis[lua]>=2.35.1",
|
||||||
"pytest-anyio>=0.0.0",
|
"pytest-anyio>=0.0.0",
|
||||||
"pytest-cov>=4.0.0",
|
"pytest-cov>=4.0.0",
|
||||||
"pytest>=8.0.0",
|
"pytest>=8.0.0",
|
||||||
@@ -59,7 +59,7 @@ docs = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.10,<0.12.0"]
|
requires = ["uv_build>=0.11.8,<0.12.0"]
|
||||||
build-backend = "uv_build"
|
build-backend = "uv_build"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""FastAPI utilities package."""
|
"""Redis-backed deduplication middleware for Taskiq."""
|
||||||
|
|
||||||
from .middleware import DuplicateTaskError, RedisDeduplicationMiddleware
|
from .middleware import DuplicateTaskError, RedisDeduplicationMiddleware
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
__version__ = "1.0.2"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DuplicateTaskError",
|
"DuplicateTaskError",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
|
|||||||
if self._redis is not None:
|
if self._redis is not None:
|
||||||
await self._redis.aclose()
|
await self._redis.aclose()
|
||||||
|
|
||||||
def _build_deduplication_key(self, message: TaskiqMessage) -> str:
|
def _build_deduplication_key(self, message: TaskiqMessage) -> str | None:
|
||||||
explicit_key: str | None = message.labels.get(DEDUP_EXPLICIT_KEY_LABEL)
|
explicit_key: str | None = message.labels.get(DEDUP_EXPLICIT_KEY_LABEL)
|
||||||
if explicit_key is not None:
|
if explicit_key is not None:
|
||||||
return f"{self.key_prefix}:{explicit_key}"
|
return f"{self.key_prefix}:{explicit_key}"
|
||||||
@@ -67,10 +67,13 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
|
|||||||
if key_fields is not None
|
if key_fields is not None
|
||||||
else message.kwargs
|
else message.kwargs
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
payload = json.dumps(
|
payload = json.dumps(
|
||||||
{"task": message.task_name, "kwargs": kwargs},
|
{"task": message.task_name, "kwargs": kwargs},
|
||||||
sort_keys=True,
|
sort_keys=True,
|
||||||
)
|
)
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
fingerprint = hashlib.sha256(payload.encode()).hexdigest()[:16]
|
fingerprint = hashlib.sha256(payload.encode()).hexdigest()[:16]
|
||||||
return f"{self.key_prefix}:{fingerprint}"
|
return f"{self.key_prefix}:{fingerprint}"
|
||||||
|
|
||||||
@@ -81,7 +84,10 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
|
|||||||
return int(labels.get(DEDUP_TTL_LABEL, self.default_ttl))
|
return int(labels.get(DEDUP_TTL_LABEL, self.default_ttl))
|
||||||
|
|
||||||
async def _release_if_owned(self, key: str, task_id: str) -> None:
|
async def _release_if_owned(self, key: str, task_id: str) -> None:
|
||||||
assert self._redis is not None
|
if self._redis is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"RedisDeduplicationMiddleware.startup() was never called."
|
||||||
|
)
|
||||||
released = await check_and_delete(self._redis, key, task_id)
|
released = await check_and_delete(self._redis, key, task_id)
|
||||||
if released:
|
if released:
|
||||||
logger.debug("Released lock %s", key)
|
logger.debug("Released lock %s", key)
|
||||||
@@ -92,8 +98,18 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
|
|||||||
if not self._is_enabled(message.labels):
|
if not self._is_enabled(message.labels):
|
||||||
return message
|
return message
|
||||||
|
|
||||||
assert self._redis is not None
|
if self._redis is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"RedisDeduplicationMiddleware.startup() was never called."
|
||||||
|
)
|
||||||
key = self._build_deduplication_key(message)
|
key = self._build_deduplication_key(message)
|
||||||
|
if key is None:
|
||||||
|
logger.warning(
|
||||||
|
"Task %s has non-JSON-serializable kwargs; deduplication skipped."
|
||||||
|
" Use the deduplication_key label to deduplicate this task.",
|
||||||
|
message.task_name,
|
||||||
|
)
|
||||||
|
return message
|
||||||
ttl = self._get_ttl(message.labels)
|
ttl = self._get_ttl(message.labels)
|
||||||
|
|
||||||
logger.debug("Acquiring lock %s for task %s", key, message.task_name)
|
logger.debug("Acquiring lock %s for task %s", key, message.task_name)
|
||||||
@@ -118,9 +134,10 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
|
|||||||
) -> None:
|
) -> None:
|
||||||
if not self._is_enabled(message.labels):
|
if not self._is_enabled(message.labels):
|
||||||
return
|
return
|
||||||
await self._release_if_owned(
|
key = self._build_deduplication_key(message)
|
||||||
self._build_deduplication_key(message), message.task_id
|
if key is None:
|
||||||
)
|
return
|
||||||
|
await self._release_if_owned(key, message.task_id)
|
||||||
|
|
||||||
async def on_error(
|
async def on_error(
|
||||||
self,
|
self,
|
||||||
@@ -130,6 +147,7 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
|
|||||||
) -> None:
|
) -> None:
|
||||||
if not self._is_enabled(message.labels):
|
if not self._is_enabled(message.labels):
|
||||||
return
|
return
|
||||||
await self._release_if_owned(
|
key = self._build_deduplication_key(message)
|
||||||
self._build_deduplication_key(message), message.task_id
|
if key is None:
|
||||||
)
|
return
|
||||||
|
await self._release_if_owned(key, message.task_id)
|
||||||
|
|||||||
@@ -92,7 +92,45 @@ class TestDefaultBuildDeduplicationKey:
|
|||||||
mw._redis = None
|
mw._redis = None
|
||||||
m = make_message()
|
m = make_message()
|
||||||
key = mw._build_deduplication_key(m)
|
key = mw._build_deduplication_key(m)
|
||||||
assert key.startswith("myapp:locks:")
|
assert key is not None and key.startswith("myapp:locks:")
|
||||||
|
|
||||||
|
def test_empty_kwargs_produces_consistent_key(self, middleware, make_message):
|
||||||
|
m1 = make_message(kwargs={})
|
||||||
|
m2 = make_message(kwargs={})
|
||||||
|
assert middleware._build_deduplication_key(
|
||||||
|
m1
|
||||||
|
) == middleware._build_deduplication_key(m2)
|
||||||
|
|
||||||
|
def test_key_fields_empty_list_ignores_all_kwargs(self, middleware, make_message):
|
||||||
|
m1 = make_message(kwargs={"a": 1}, labels={DEDUP_KEY_FIELDS_LABEL: []})
|
||||||
|
m2 = make_message(kwargs={"a": 999}, labels={DEDUP_KEY_FIELDS_LABEL: []})
|
||||||
|
assert middleware._build_deduplication_key(
|
||||||
|
m1
|
||||||
|
) == middleware._build_deduplication_key(m2)
|
||||||
|
|
||||||
|
def test_key_fields_absent_from_kwargs_are_ignored(self, middleware, make_message):
|
||||||
|
m1 = make_message(
|
||||||
|
kwargs={"order_id": 1}, labels={DEDUP_KEY_FIELDS_LABEL: ["user_id"]}
|
||||||
|
)
|
||||||
|
m2 = make_message(
|
||||||
|
kwargs={"order_id": 999}, labels={DEDUP_KEY_FIELDS_LABEL: ["user_id"]}
|
||||||
|
)
|
||||||
|
assert middleware._build_deduplication_key(
|
||||||
|
m1
|
||||||
|
) == middleware._build_deduplication_key(m2)
|
||||||
|
|
||||||
|
def test_explicit_key_takes_precedence_over_key_fields(
|
||||||
|
self, middleware, make_message
|
||||||
|
):
|
||||||
|
m = make_message(
|
||||||
|
kwargs={"a": 1},
|
||||||
|
labels={DEDUP_EXPLICIT_KEY_LABEL: "my-lock", DEDUP_KEY_FIELDS_LABEL: ["a"]},
|
||||||
|
)
|
||||||
|
assert middleware._build_deduplication_key(m) == "taskiq:deduplication:my-lock"
|
||||||
|
|
||||||
|
def test_non_serializable_kwargs_returns_none(self, middleware, make_message):
|
||||||
|
m = make_message(kwargs={"dt": object()})
|
||||||
|
assert middleware._build_deduplication_key(m) is None
|
||||||
|
|
||||||
|
|
||||||
class TestPreSend:
|
class TestPreSend:
|
||||||
@@ -140,6 +178,25 @@ class TestPreSend:
|
|||||||
await middleware.pre_send(make_message(kwargs={"x": 1}))
|
await middleware.pre_send(make_message(kwargs={"x": 1}))
|
||||||
await middleware.pre_send(make_message(kwargs={"x": 2}))
|
await middleware.pre_send(make_message(kwargs={"x": 2}))
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_non_serializable_kwargs_skips_deduplication(
|
||||||
|
self, middleware, make_message
|
||||||
|
):
|
||||||
|
msg1 = make_message(kwargs={"dt": object()})
|
||||||
|
msg2 = make_message(kwargs={"dt": object()})
|
||||||
|
await middleware.pre_send(msg1)
|
||||||
|
await middleware.pre_send(msg2) # should not raise
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_default_ttl_applied(self, fake_redis, make_message):
|
||||||
|
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost", default_ttl=77)
|
||||||
|
mw._redis = fake_redis
|
||||||
|
msg = make_message()
|
||||||
|
await mw.pre_send(msg)
|
||||||
|
key = mw._build_deduplication_key(msg)
|
||||||
|
ttl = await fake_redis.ttl(key)
|
||||||
|
assert 0 < ttl <= 77
|
||||||
|
|
||||||
|
|
||||||
class TestPostExecute:
|
class TestPostExecute:
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -166,6 +223,15 @@ class TestPostExecute:
|
|||||||
await middleware.post_execute(disabled_msg, make_result())
|
await middleware.post_execute(disabled_msg, make_result())
|
||||||
assert await fake_redis.exists(key)
|
assert await fake_redis.exists(key)
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_post_execute_after_ttl_expiry_is_safe(
|
||||||
|
self, middleware, fake_redis, make_message, make_result
|
||||||
|
):
|
||||||
|
msg = make_message()
|
||||||
|
await middleware.pre_send(msg)
|
||||||
|
await fake_redis.delete(middleware._build_deduplication_key(msg))
|
||||||
|
await middleware.post_execute(msg, make_result())
|
||||||
|
|
||||||
|
|
||||||
class TestOnError:
|
class TestOnError:
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -195,6 +261,26 @@ class TestOnError:
|
|||||||
assert await fake_redis.exists(key)
|
assert await fake_redis.exists(key)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRedispatchAfterRelease:
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_redispatch_after_post_execute(
|
||||||
|
self, middleware, make_message, make_result
|
||||||
|
):
|
||||||
|
msg = make_message()
|
||||||
|
await middleware.pre_send(msg)
|
||||||
|
await middleware.post_execute(msg, make_result())
|
||||||
|
await middleware.pre_send(make_message())
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_redispatch_after_on_error(
|
||||||
|
self, middleware, make_message, make_result
|
||||||
|
):
|
||||||
|
msg = make_message()
|
||||||
|
await middleware.pre_send(msg)
|
||||||
|
await middleware.on_error(msg, make_result(is_err=True), RuntimeError("boom"))
|
||||||
|
await middleware.pre_send(make_message())
|
||||||
|
|
||||||
|
|
||||||
class TestAtomicRelease:
|
class TestAtomicRelease:
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_only_owner_can_release(self, middleware, fake_redis, make_message):
|
async def test_only_owner_can_release(self, middleware, fake_redis, make_message):
|
||||||
@@ -240,3 +326,9 @@ class TestLifecycle:
|
|||||||
async def test_shutdown_without_startup_is_safe(self):
|
async def test_shutdown_without_startup_is_safe(self):
|
||||||
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
|
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
|
||||||
await mw.shutdown()
|
await mw.shutdown()
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_pre_send_without_startup_raises_runtime_error(self, make_message):
|
||||||
|
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
|
||||||
|
with pytest.raises(RuntimeError, match="startup"):
|
||||||
|
await mw.pre_send(make_message())
|
||||||
|
|||||||
@@ -1468,7 +1468,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "taskiq-deduplication"
|
name = "taskiq-deduplication"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
@@ -1510,7 +1510,7 @@ requires-dist = [
|
|||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "coverage", specifier = ">=7.0.0" },
|
{ name = "coverage", specifier = ">=7.0.0" },
|
||||||
{ name = "fakeredis", extras = ["lua"], specifier = ">=2.0.0" },
|
{ name = "fakeredis", extras = ["lua"], specifier = ">=2.35.1" },
|
||||||
{ name = "mkdocstrings-python", specifier = ">=2.0.2" },
|
{ name = "mkdocstrings-python", specifier = ">=2.0.2" },
|
||||||
{ name = "prek", specifier = ">=0.3.8" },
|
{ name = "prek", specifier = ">=0.3.8" },
|
||||||
{ name = "pytest", specifier = ">=8.0.0" },
|
{ name = "pytest", specifier = ">=8.0.0" },
|
||||||
@@ -1518,7 +1518,7 @@ dev = [
|
|||||||
{ name = "pytest-cov", specifier = ">=4.0.0" },
|
{ name = "pytest-cov", specifier = ">=4.0.0" },
|
||||||
{ name = "ruff", specifier = ">=0.1.0" },
|
{ name = "ruff", specifier = ">=0.1.0" },
|
||||||
{ name = "taskiq-deduplication" },
|
{ name = "taskiq-deduplication" },
|
||||||
{ name = "ty", specifier = ">=0.0.1a0" },
|
{ name = "ty", specifier = ">=0.0.34" },
|
||||||
{ name = "zensical", specifier = ">=0.0.30" },
|
{ name = "zensical", specifier = ">=0.0.30" },
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
@@ -1527,7 +1527,7 @@ docs = [
|
|||||||
]
|
]
|
||||||
tests = [
|
tests = [
|
||||||
{ name = "coverage", specifier = ">=7.0.0" },
|
{ name = "coverage", specifier = ">=7.0.0" },
|
||||||
{ name = "fakeredis", extras = ["lua"], specifier = ">=2.0.0" },
|
{ name = "fakeredis", extras = ["lua"], specifier = ">=2.35.1" },
|
||||||
{ name = "pytest", specifier = ">=8.0.0" },
|
{ name = "pytest", specifier = ">=8.0.0" },
|
||||||
{ name = "pytest-anyio", specifier = ">=0.0.0" },
|
{ name = "pytest-anyio", specifier = ">=0.0.0" },
|
||||||
{ name = "pytest-cov", specifier = ">=4.0.0" },
|
{ name = "pytest-cov", specifier = ">=4.0.0" },
|
||||||
@@ -1598,26 +1598,26 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ty"
|
name = "ty"
|
||||||
version = "0.0.33"
|
version = "0.0.34"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/84/44/9478c50c266826c1bf30d1692e589755bffa8f1c0a3eb7af8a346c255991/ty-0.0.33.tar.gz", hash = "sha256:46d63bda07403322cb6c28ccfdd5536be916e13df725c29f7ccd0a21f06bd9e8", size = 5559373, upload-time = "2026-04-28T10:45:13.18Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/24/e287388c63a19191be26b32ff4dbd06029834068150ebe2532939bc4c851/ty-0.0.33-py3-none-linux_armv6l.whl", hash = "sha256:94d0a9d2234261a8911396d59e506b5923fe0971dbda43b9dcea287936887fcc", size = 11021308, upload-time = "2026-04-28T10:45:43.34Z" },
|
{ url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/ca/ba1eed819895bd239fba8ee35dfcd5fcb266c203b0914a17a59579096bb5/ty-0.0.33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4a2b5ba078f90de342f56b5f7979bb77c9b9b1d8625a041352ffc6ee93c4073", size = 10777272, upload-time = "2026-04-28T10:45:32.905Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/a8/c3131d37b44b3fea1d6654a1c929a0cd0873822f77a90482b8ec28f6fbbd/ty-0.0.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84ff5707825e9af9668d2bcf66975f93e520a63b524ab494e3a8265735be2563", size = 10201078, upload-time = "2026-04-28T10:45:23.374Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/db/d8e37ff0045810cc65e1ff36aa0da0a2253c05659787ac987df8a16c7897/ty-0.0.33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e375285736f57886868e7af0b11c7b0ec5b6543fa15e7ad2a714fed9f077d4e0", size = 10732347, upload-time = "2026-04-28T10:45:21.444Z" },
|
{ url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/1a/20e83a412506a918e4684fc67b567cf7cc13b105470b3428cb23c3d5aa13/ty-0.0.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5680f6350c3b4e46b8bff6d7bb132366ea239463d6cad4892725d06046e65464", size = 10808238, upload-time = "2026-04-28T10:45:38.565Z" },
|
{ url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/4b/d0a39f4464dc6cb4cc2c159473ce216bd1846bfb684c0323a3cb36dce5c6/ty-0.0.33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5535538bad8d0f7e62bcdff02197cdb30e41451d80b35d27e17d128f2e1dc5d", size = 11288348, upload-time = "2026-04-28T10:45:08.419Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/7e/f1745e0f9583363d7a83d9a4990fc244f76ecc30840ddad83dc16a33c52d/ty-0.0.33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da196c42bbbc069e1e21e3e52107c061aa9660352dae57a41930690b56e2c02d", size = 11789907, upload-time = "2026-04-28T10:45:19.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/71/25f39f46a12d662859d45bc648555d0661044eb43db6b5648c9947487da9/ty-0.0.33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9281672921ef6d4460e03146b5e6c18cb1a3e3a3b8a1a88f6f33226d05a469b7", size = 11500774, upload-time = "2026-04-28T10:45:48.012Z" },
|
{ url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/ec/136959ecbb7c71cb90537f5aea441c73f4ab24612868a6ecdc9d7444d32d/ty-0.0.33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1b8f303f82da64e878108e764be3ecbcd7c9903ac0a7f7031614ed00b97ab", size = 11360314, upload-time = "2026-04-28T10:45:05.402Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/95/32809575c222f00beed498cb728e9290a0f5009f930025381bb7253b2206/ty-0.0.33-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:efe3af412c9ff67bce5fa37d0a2b0d8555c24072b145a5bac6c79637f1c83abe", size = 10707785, upload-time = "2026-04-28T10:45:10.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/89/c8e9531f7aa4a093359e15fa32c8e1277fbbe90d16894d7c6032d29f4b34/ty-0.0.33-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aeec29c91ea768601747da546c3efc20b72c2fb1bd52bcc786a5c6eeff51d27b", size = 10834987, upload-time = "2026-04-28T10:45:40.738Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/16/9835fbcf5338af1a1917bd28fdb8a7193c210b83f243aa286fa9f79cb3ad/ty-0.0.33-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a535977c52bbb5f7e96b8b70a6ad375ad077f4a9ff2492508ea3816a2b403819", size = 10968968, upload-time = "2026-04-28T10:45:30.26Z" },
|
{ url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/69/64c76aabc1bc70c7f24b686cd93c3407f8ea430905e395f59bf9603ef571/ty-0.0.33-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1d732facf39fcb221ba279d469c5040d37883e964f123b1563888efd34818180", size = 11458077, upload-time = "2026-04-28T10:45:45.971Z" },
|
{ url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/84/fae27b0c4718776a298690d31ca4cc1995f2e3e1c63a7b59e84c41498e9a/ty-0.0.33-py3-none-win32.whl", hash = "sha256:d90960b574428dc252f85e8598ec5fcb7f619794196b2fc95a90da075ed4681c", size = 10345364, upload-time = "2026-04-28T10:45:16.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/a0/a2938b23ae3e1a09a2d7c189e2ac5f7113676bae4e0e23948b568e18e5f8/ty-0.0.33-py3-none-win_amd64.whl", hash = "sha256:c1c3aec62c44de610c6e95f0a4e97ac3dbc07934bfdbf1fd90d758c9ff72f48e", size = 11342470, upload-time = "2026-04-28T10:45:26.455Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/62/7fb948aace38d2f6329261bb33c035a8484549c74f1db28649c7a4c6fed9/ty-0.0.33-py3-none-win_arm64.whl", hash = "sha256:0d44f99ba1b441e55e2aa301b2ac0a21112784931b46a5f66f4ea9efe5620d97", size = 10742673, upload-time = "2026-04-28T10:45:35.555Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ copyright = "Copyright © 2026 d3vyce"
|
|||||||
repo_url = "https://github.com/d3vyce/taskiq-deduplication"
|
repo_url = "https://github.com/d3vyce/taskiq-deduplication"
|
||||||
|
|
||||||
[project.theme]
|
[project.theme]
|
||||||
|
custom_dir = "docs/overrides"
|
||||||
language = "en"
|
language = "en"
|
||||||
features = [
|
features = [
|
||||||
"announce.dismiss",
|
"announce.dismiss",
|
||||||
|
|||||||
Reference in New Issue
Block a user