mirror of
https://github.com/d3vyce/taskiq-deduplication.git
synced 2026-08-06 03:44:08 +00:00
Compare commits
18
Commits
v1.0.0
...
1cee2ad080
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cee2ad080 | ||
|
|
e98230276f | ||
|
|
2a02121a17 | ||
|
|
f586342811 | ||
|
|
a36600c3e5 | ||
|
|
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,12 +50,13 @@ 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.
|
||||||
- **Per-task opt-out** — disable deduplication for individual tasks with the `deduplication` label.
|
- **Per-task opt-out** — disable deduplication for individual tasks with the `deduplication` label.
|
||||||
|
- **Startup resilience** — automatic reconnection with exponential backoff if Redis is unavailable at broker startup.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -50,12 +50,13 @@ 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.
|
||||||
- **Per-task opt-out** — disable deduplication for individual tasks with the `deduplication` label.
|
- **Per-task opt-out** — disable deduplication for individual tasks with the `deduplication` label.
|
||||||
|
- **Startup resilience** — automatic reconnection with exponential backoff if Redis is unavailable at broker startup.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ broker = ListQueueBroker("redis://localhost:6379").with_middlewares(
|
|||||||
| `default_deduplication` | `bool` | `True` | Whether deduplication is enabled for all tasks by default. Set `False` to opt-in per task instead of opting out. |
|
| `default_deduplication` | `bool` | `True` | Whether deduplication is enabled for all tasks by default. Set `False` to opt-in per task instead of opting out. |
|
||||||
| `default_ttl` | `int` | `300` | Default lock TTL in seconds. Overridden per task with the `deduplication_ttl` label. |
|
| `default_ttl` | `int` | `300` | Default lock TTL in seconds. Overridden per task with the `deduplication_ttl` label. |
|
||||||
| `key_prefix` | `str` | `"taskiq:deduplication"` | Prefix for all Redis lock keys. |
|
| `key_prefix` | `str` | `"taskiq:deduplication"` | Prefix for all Redis lock keys. |
|
||||||
|
| `startup_retries` | `int` | `3` | Number of connection attempts during broker startup. |
|
||||||
|
| `startup_retry_delay` | `float` | `1.0` | Base delay in seconds between retries (exponential backoff: delay × 2^n). |
|
||||||
|
|
||||||
```python
|
```python
|
||||||
broker = ListQueueBroker("redis://localhost:6379").with_middlewares(
|
broker = ListQueueBroker("redis://localhost:6379").with_middlewares(
|
||||||
@@ -29,10 +31,29 @@ broker = ListQueueBroker("redis://localhost:6379").with_middlewares(
|
|||||||
default_deduplication=True,
|
default_deduplication=True,
|
||||||
default_ttl=60,
|
default_ttl=60,
|
||||||
key_prefix="myapp:dedup",
|
key_prefix="myapp:dedup",
|
||||||
|
startup_retries=5,
|
||||||
|
startup_retry_delay=0.5,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Startup resilience
|
||||||
|
|
||||||
|
On startup the middleware verifies the Redis connection with a `PING`. If Redis is
|
||||||
|
temporarily unavailable, it retries with exponential backoff.
|
||||||
|
After all attempts are exhausted a `ConnectionError` is raised and the broker
|
||||||
|
fails to start.
|
||||||
|
|
||||||
|
Adjust `startup_retries` and `startup_retry_delay` to suit your deployment:
|
||||||
|
|
||||||
|
```python
|
||||||
|
RedisDeduplicationMiddleware(
|
||||||
|
redis_url="redis://localhost:6379",
|
||||||
|
startup_retries=5,
|
||||||
|
startup_retry_delay=2.0,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
When a task is dispatched, the middleware acquires a Redis lock keyed on the task's
|
When a task is dispatched, the middleware acquires a Redis lock keyed on the task's
|
||||||
|
|||||||
+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",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, Awaitable, cast
|
||||||
|
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
from taskiq import TaskiqMessage, TaskiqResult
|
from taskiq import TaskiqMessage, TaskiqResult
|
||||||
@@ -42,21 +43,49 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
|
|||||||
default_deduplication: bool = True,
|
default_deduplication: bool = True,
|
||||||
default_ttl: int = 300,
|
default_ttl: int = 300,
|
||||||
key_prefix: str = "taskiq:deduplication",
|
key_prefix: str = "taskiq:deduplication",
|
||||||
|
startup_retries: int = 3,
|
||||||
|
startup_retry_delay: float = 1.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.redis_url = redis_url
|
self.redis_url = redis_url
|
||||||
self.default_deduplication = default_deduplication
|
self.default_deduplication = default_deduplication
|
||||||
self.default_ttl = default_ttl
|
self.default_ttl = default_ttl
|
||||||
self.key_prefix = key_prefix
|
self.key_prefix = key_prefix
|
||||||
|
self.startup_retries = startup_retries
|
||||||
|
self.startup_retry_delay = startup_retry_delay
|
||||||
self._redis: Redis | None = None
|
self._redis: Redis | None = None
|
||||||
|
|
||||||
async def startup(self) -> None:
|
async def startup(self) -> None:
|
||||||
self._redis = Redis.from_url(self.redis_url)
|
last_error: BaseException | None = None
|
||||||
|
for attempt in range(self.startup_retries):
|
||||||
|
try:
|
||||||
|
self._redis = Redis.from_url(self.redis_url)
|
||||||
|
await cast(Awaitable[bool], self._redis.ping())
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
if attempt < self.startup_retries - 1:
|
||||||
|
delay = self.startup_retry_delay * (2**attempt)
|
||||||
|
logger.warning(
|
||||||
|
"Failed to connect to Redis (attempt %d/%d): %s. "
|
||||||
|
"Retrying in %.1fs...",
|
||||||
|
attempt + 1,
|
||||||
|
self.startup_retries,
|
||||||
|
exc,
|
||||||
|
delay,
|
||||||
|
)
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
logger.error(
|
||||||
|
"Failed to connect to Redis after %d attempts.", self.startup_retries
|
||||||
|
)
|
||||||
|
raise ConnectionError(
|
||||||
|
f"Could not connect to Redis after {self.startup_retries} attempts"
|
||||||
|
) from last_error
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
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 +96,13 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
|
|||||||
if key_fields is not None
|
if key_fields is not None
|
||||||
else message.kwargs
|
else message.kwargs
|
||||||
)
|
)
|
||||||
payload = json.dumps(
|
try:
|
||||||
{"task": message.task_name, "kwargs": kwargs},
|
payload = json.dumps(
|
||||||
sort_keys=True,
|
{"task": message.task_name, "kwargs": kwargs},
|
||||||
)
|
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 +113,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 +127,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 +163,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 +176,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)
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
from collections.abc import Awaitable
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
|
from redis.commands.core import AsyncScript
|
||||||
|
|
||||||
|
RELEASE_LUA_SCRIPT = """
|
||||||
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call('del', KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
async def check_and_delete(redis: Redis, key: str, owner: str) -> bool:
|
async def check_and_delete(redis: Redis, key: str, owner: str) -> bool:
|
||||||
"""Delete *key* only if its value equals *owner*. Returns True if deleted."""
|
"""Delete *key* only if its value equals *owner*.
|
||||||
release_script = """
|
|
||||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
||||||
return redis.call('del', KEYS[1])
|
|
||||||
else
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
"""
|
|
||||||
|
|
||||||
released = await cast(Awaitable[int], redis.eval(release_script, 1, key, owner))
|
Args:
|
||||||
|
redis: Async Redis client.
|
||||||
|
key: Lock key to delete.
|
||||||
|
owner: Expected value of the key (task_id).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the key was deleted, False otherwise.
|
||||||
|
"""
|
||||||
|
script: AsyncScript = redis.register_script(RELEASE_LUA_SCRIPT)
|
||||||
|
released: int = await script(keys=[key], args=[owner])
|
||||||
return bool(released)
|
return bool(released)
|
||||||
|
|||||||
+165
-1
@@ -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,81 @@ 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())
|
||||||
|
|
||||||
|
|
||||||
|
class TestStartupRetry:
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_startup_succeeds_after_retries(self):
|
||||||
|
mw = RedisDeduplicationMiddleware(
|
||||||
|
redis_url="redis://localhost",
|
||||||
|
startup_retries=3,
|
||||||
|
startup_retry_delay=0.01,
|
||||||
|
)
|
||||||
|
with patch("redis.asyncio.Redis.from_url") as mock_from_url:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.ping.side_effect = [
|
||||||
|
ConnectionError("fail"),
|
||||||
|
ConnectionError("fail"),
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
mock_from_url.return_value = mock_client
|
||||||
|
await mw.startup()
|
||||||
|
assert mw._redis is mock_client
|
||||||
|
assert mock_client.ping.call_count == 3
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_startup_raises_after_all_retries_exhausted(self):
|
||||||
|
mw = RedisDeduplicationMiddleware(
|
||||||
|
redis_url="redis://localhost",
|
||||||
|
startup_retries=2,
|
||||||
|
startup_retry_delay=0.01,
|
||||||
|
)
|
||||||
|
with patch("redis.asyncio.Redis.from_url") as mock_from_url:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.ping.side_effect = ConnectionError("refused")
|
||||||
|
mock_from_url.return_value = mock_client
|
||||||
|
with pytest.raises(ConnectionError, match="2 attempts"):
|
||||||
|
await mw.startup()
|
||||||
|
assert mock_client.ping.call_count == 2
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_startup_no_retry_on_first_success(self):
|
||||||
|
mw = RedisDeduplicationMiddleware(
|
||||||
|
redis_url="redis://localhost",
|
||||||
|
startup_retries=3,
|
||||||
|
startup_retry_delay=0.01,
|
||||||
|
)
|
||||||
|
with patch("redis.asyncio.Redis.from_url") as mock_from_url:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_from_url.return_value = mock_client
|
||||||
|
await mw.startup()
|
||||||
|
mock_client.ping.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_startup_retry_delay_exponential(self):
|
||||||
|
mw = RedisDeduplicationMiddleware(
|
||||||
|
redis_url="redis://localhost",
|
||||||
|
startup_retries=3,
|
||||||
|
startup_retry_delay=0.01,
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch("redis.asyncio.Redis.from_url") as mock_from_url,
|
||||||
|
patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||||
|
):
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.ping.side_effect = [
|
||||||
|
ConnectionError("fail"),
|
||||||
|
ConnectionError("fail"),
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
mock_from_url.return_value = mock_client
|
||||||
|
await mw.startup()
|
||||||
|
assert mock_sleep.call_count == 2
|
||||||
|
assert mock_sleep.call_args_list[0].args[0] == 0.01
|
||||||
|
assert mock_sleep.call_args_list[1].args[0] == 0.02
|
||||||
|
|||||||
@@ -962,26 +962,26 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prek"
|
name = "prek"
|
||||||
version = "0.3.11"
|
version = "0.3.13"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/60/5b980c70525ca5f0d17942d8eae13b399051aa384413366fe5df229712ea/prek-0.3.11.tar.gz", hash = "sha256:c4cf77848009503c58d80ff216e32af45b63ea49652bb5546748c1ebfd4d9847", size = 433440, upload-time = "2026-04-27T04:22:59.923Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/3c/59/0a279983f96bd5d538b4975f0a23121082aa3b8560b6649fdf61f8011b07/prek-0.3.13.tar.gz", hash = "sha256:c48586ee3708bfbf3df80121f55583e9a7d0fa166b08172c091fe5971e92a0ac", size = 444848, upload-time = "2026-05-05T18:07:09.076Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/2a/3392fa7d1fd1ce538915baa7597e7203bbe888367a8b15bfd51ca74d4714/prek-0.3.11-py3-none-linux_armv6l.whl", hash = "sha256:787e605716cfdc86ec01e7c5cf62799f39c28d49de5e37d75595c8e6248cb0f3", size = 5423112, upload-time = "2026-04-27T04:22:52.659Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/6a/9baa2bda21dccc2927e952416f6cc23a75eb99c9ed18837164ac2e4a5640/prek-0.3.13-py3-none-linux_armv6l.whl", hash = "sha256:b00d38f01235073c35aa5f48df57fefef45a6cec2ae0884d750345a2c7220370", size = 5506622, upload-time = "2026-05-05T18:06:53.091Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/b0/3fc653b30b70d6c2714fc56bcfe1c2439437fc38f60b72bc300603ace4cd/prek-0.3.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef1f37187ca52d75ba9c46b53007476c4eab2c3f11bd23defd57a81c62d90442", size = 5801382, upload-time = "2026-04-27T04:23:04.464Z" },
|
{ url = "https://files.pythonhosted.org/packages/56/77/d44b5d9bdca0879b865f8e47bf84cf5dc9e8b358d029e6d9b83d8809c116/prek-0.3.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0d89ac712c60e34d1550a606ad5fdfb8ad71d44ced8afa2fa5cbc106be4abd9e", size = 5878743, upload-time = "2026-05-05T18:07:23.164Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/46/39aedc7843c3703f1f43b686622e4f8cd123e03b87a163e5c8f2fbd56cda/prek-0.3.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d0828a1b50447502ea1be3f5a84da474fdca558cd5d76a1a5205169bb808c7", size = 5370817, upload-time = "2026-04-27T04:22:49.277Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/cf/19e8525cde8b3aa12858aca434d1fa653ef3b152da5af11eafc857634dc2/prek-0.3.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f9b5265863d18b5be4ea094fdce4fd6ca61a8c89a70ee3d8ee153b3e0ed6b272", size = 5434909, upload-time = "2026-05-05T18:07:25.276Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/83/df5f3aeacbdea96a88c4f06c98d3932469711fed4e3bf5b703dd6507abe7/prek-0.3.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bf49464b526ee36d2130baf60ab9580560bfaa60efd2997328e6d6671e209014", size = 5621405, upload-time = "2026-04-27T04:23:10.58Z" },
|
{ url = "https://files.pythonhosted.org/packages/7a/9a/e5f97194782de4dab622ce09dafb3ebdd2ee4d354a83ac4def7ebeee236c/prek-0.3.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:64b59a1550780af2bba37297c704b17f81d8e9df6288af1fab4017938e33b1db", size = 5697536, upload-time = "2026-05-05T18:07:05.475Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/f2/e32c9720747a327669863a4f92d05b9e6fadb851e903b0d7310a97c956a4/prek-0.3.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac344529f0d34757c7c95f65e66b9f6440a691f826eaf43f503247bd22023558", size = 5339780, upload-time = "2026-04-27T04:22:47.614Z" },
|
{ url = "https://files.pythonhosted.org/packages/c4/8c/e1f548ffc4b227e4c2b5a9b30f5978a7e0e6dad51305b97a2ba5b2a923e7/prek-0.3.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ce6cd8f114ba9bbdbe97422103fd886101949b1c42e588a7543c4436ead2020", size = 5428160, upload-time = "2026-05-05T18:07:01.489Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/2e/0e2f71b63bc2e5372575d5c1574b0666d2f90d30da51ed706a32cbf465a0/prek-0.3.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83672d963f249e2f246d3bec97f8fd2e8032e70da0a7d9acb2fa38af76dd82d2", size = 5735277, upload-time = "2026-04-27T04:23:12.437Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/44/abd919b00905a32d21dca2cec32c707860cf217da2431b62dd52684b310e/prek-0.3.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc03e924a24d8d961f56195853c8b206cb196be6db4ad8312125dae847d718ac", size = 5827275, upload-time = "2026-05-05T18:07:17.437Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/46/88abf51ac88eeff1ad2fe7d1797ca1fea43eb1ac1ddb8331463ac5b27ed2/prek-0.3.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef70957195d2896a30dd849e64f88344df7bb51af9c950cf16bd11519e7424b0", size = 6622420, upload-time = "2026-04-27T04:23:05.982Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/ed/cafd2b80d58a83faf8371c6543bd1475a2224242a3294da7f8582f6aa551/prek-0.3.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ca8c526a23873177fb3b92013500b08ef5f8bedc7263f9f3a44dd2f49645a26", size = 6710293, upload-time = "2026-05-05T18:07:10.663Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/b6/592028a45b084a68b76c7edef909c789d1c96b26761388f63659beef7166/prek-0.3.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f0cc07d2cdb5fe2882015d5fdafc9af98b4c560d4caa1ae948caeab4341b79", size = 6020038, upload-time = "2026-04-27T04:22:54.367Z" },
|
{ url = "https://files.pythonhosted.org/packages/35/09/52a4a27596b764173a34d74db09356b30faaacb4a1075b75adbc036a0008/prek-0.3.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5bbb175478438a871e3281d2c3c3f067288af73ad81707a9bdebfd769766c7d", size = 6096556, upload-time = "2026-05-05T18:07:19.46Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/f7/e97f55a1645a2e9becffeee28892ad8bb66cd144dabfa4392ea8e2674bbe/prek-0.3.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d7554b436dae2ec97f4351a46817e3561657244307d1c0915f355b859f4fab71", size = 5622539, upload-time = "2026-04-27T04:23:01.314Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/60/80f61729ce6498815d46d5580cf76da2c157c9b6494046183682441a0ea3/prek-0.3.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a65327a014d838341af757dfc05a706d10e8e33f039bc32bb3dbe2fa21c440c0", size = 5693267, upload-time = "2026-05-05T18:07:03.66Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/e2/f3119eef6b621782ad216a86d449609858ea34c57cf4a40fc6dc80556d7e/prek-0.3.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:caba5d635a5b64b7ac64d903f29b043ca5b0d9d9693543a0ef331ade89e6ad3f", size = 5440681, upload-time = "2026-04-27T04:23:07.422Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/9d/c7a663fe70676ffab2e0c6c9a71997a3ccd002ed5bc60b7422a937911af0/prek-0.3.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b6a200843a36a5b0c41764ce7639ccb3471d48b097f1c5e3fc8f034219b42626", size = 5532865, upload-time = "2026-05-05T18:07:15.237Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/62/22dd4f59a47654faeebe74651182ecc48d436542646cc92723052dfd9a45/prek-0.3.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:c95a63f19dde48e84b70bd63a235670834af15fa4df8b85d8b7894dd5bc419a9", size = 5314773, upload-time = "2026-04-27T04:22:57.912Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/68/506ef5a235536030e16f61e7210474554f6e05f845f27df5877d2dbb1a06/prek-0.3.13-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bdacaad8f35f343e063d251211fe34db1de9e5cc591795361ad69a6485202258", size = 5395951, upload-time = "2026-05-05T18:06:55.183Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/94/a8361462acb8d8f5b8505255b95ffbfc2ee0872a79b4e066eb330692f7be/prek-0.3.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7353b45f44a386c676fe96ba72a5ee326b676f789339f405cf6f1d69a1707194", size = 5596208, upload-time = "2026-04-27T04:23:09.08Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/00/22d7c6db7f43b58f7d015913c12660c9bbc82751cff6cfd8c31993cf30eb/prek-0.3.13-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f00328f1c520d8fefb910ab0d3c6764ee330d227952baa19b7e3de7242bd8b3b", size = 5681195, upload-time = "2026-05-05T18:07:12.804Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/0c/5f065b86bbeb9977074a055d8a05e90c7201f6c4c7032dada61739b5f8cb/prek-0.3.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:39f4e86176ccbb70c098df6abbc8e36c1d86cb81281abe92fb79dcd572418214", size = 6132833, upload-time = "2026-04-27T04:22:56.054Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/e3/fdf9882238796914ddaf11381a9083b374980156200a953324f6c795f34d/prek-0.3.13-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e5530a867bcf5b172b7513a64e71b06a337d1d184696227ae953845867376b8d", size = 6212085, upload-time = "2026-05-05T18:07:07.213Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/0c/8ab0ae140201dcee505f58b60abbe56bd05ac96b821a6866f6f90c4d971f/prek-0.3.11-py3-none-win32.whl", hash = "sha256:35d2361049653a3dcf27227b7f1b340c5c42a12c0e0361c4b785921bfd125839", size = 5120856, upload-time = "2026-04-27T04:23:02.752Z" },
|
{ url = "https://files.pythonhosted.org/packages/ca/1d/528759931344b5c7103085798f5fa2e86d27d9410b753a6bcbe7726aa8ba/prek-0.3.13-py3-none-win32.whl", hash = "sha256:326fac2bdce00074ce6c5046b861d310638aee2b9de1ed241ba7eb32bdc83898", size = 5199566, upload-time = "2026-05-05T18:07:21.416Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/05/9844c1125d3714f6f6c7b475884128a4b0c6c3ee0cd208ead44ca8174687/prek-0.3.11-py3-none-win_amd64.whl", hash = "sha256:a387689cd2e182f92dbb681151ee5a04f494fe97e95d6d783875da90b950e6d5", size = 5510916, upload-time = "2026-04-27T04:22:45.704Z" },
|
{ url = "https://files.pythonhosted.org/packages/6b/d0/8715ee837c73314a02767d20652cc312d1b6ff6733fa00f52de2b648bc3a/prek-0.3.13-py3-none-win_amd64.whl", hash = "sha256:841049f89f5ec9f4035299283d11e566ac5a068e3742ead1055ea04f886831fc", size = 5589599, upload-time = "2026-05-05T18:06:57.28Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/13/24b0288c553dc8d61f44c4d0746fe9bb1e1bd29d1e70571658536e4c0f72/prek-0.3.11-py3-none-win_arm64.whl", hash = "sha256:e4a8f900378a6657c7eb2fc4b12fa5c934edf209d0a24544539842479ec16e0b", size = 5345988, upload-time = "2026-04-27T04:22:50.918Z" },
|
{ url = "https://files.pythonhosted.org/packages/ff/cf/0af0b15be0ebd82f7e50adee149b05a73533d78cb1b97cb889f0647ebffe/prek-0.3.13-py3-none-win_arm64.whl", hash = "sha256:a9fd74e0aec550c6b8d41076fdcdd6ff121cd7d94d743c1338bd794784e3c775", size = 5419029, upload-time = "2026-05-05T18:06:59.645Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1450,7 +1450,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "taskiq"
|
name = "taskiq"
|
||||||
version = "0.12.2"
|
version = "0.12.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
@@ -1461,14 +1461,14 @@ dependencies = [
|
|||||||
{ name = "taskiq-dependencies" },
|
{ name = "taskiq-dependencies" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/23/7c5b1ed6faaa6cbc7735b455e17af4ba53ca3deea1a6ef640386640253d8/taskiq-0.12.2.tar.gz", hash = "sha256:de0c375d9c68481dd0625ebaa332f8f582866a0499663a45cedf7407b00e03f0", size = 393931, upload-time = "2026-04-18T10:29:29.924Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/e1/9bbf459974168c6a763f984a1fd6dbb06f5d0f07754e1dcd7361ef679543/taskiq-0.12.3.tar.gz", hash = "sha256:f9a3ae1b21d494a331e79e617ad116f3088e8bc98b9dc8cb9441e7957168cb76", size = 397625, upload-time = "2026-05-04T14:09:27.053Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/5f/7800adec9853b9ab74e8ce91efe15f961afccd1e829c6b8a87d8462a1485/taskiq-0.12.2-py3-none-any.whl", hash = "sha256:491491ad255fb88bf4d3e04e356dd1e6507ca7d8197b68a2f89ce7c5b5c7cc51", size = 89885, upload-time = "2026-04-18T10:29:28.229Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/04/c9ccf3b1aeee67bf8a79e02522b0847940b3e3f2dae316e6c4ab50a3cb47/taskiq-0.12.3-py3-none-any.whl", hash = "sha256:113ba7f65c71076f58f017f540d17c6389df5e76516b19a1f91b087dcd7a5803", size = 91787, upload-time = "2026-05-04T14:09:28.42Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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]]
|
||||||
@@ -1815,29 +1815,30 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zensical"
|
name = "zensical"
|
||||||
version = "0.0.39"
|
version = "0.0.40"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "deepmerge" },
|
{ name = "deepmerge" },
|
||||||
|
{ name = "jinja2" },
|
||||||
{ name = "markdown" },
|
{ name = "markdown" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
{ name = "pymdown-extensions" },
|
{ name = "pymdown-extensions" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/b1/819add51b42b0aac618622582db631a71cc7c6d91ab4787d73a7bd9badb1/zensical-0.0.39.tar.gz", hash = "sha256:2a8713c54362adb0881e9b0514b5ad9a696324756699dedb55fa1cbf3ccc0eda", size = 3914611, upload-time = "2026-05-01T16:33:28.402Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/a6/88062f7e235f58a5f05d82005fc35d9dbaed27c024fe9ffae5bce7f33661/zensical-0.0.40.tar.gz", hash = "sha256:5c294751977a664614cb84e987186ad8e282af77ce0d0d800fe48ee57791279d", size = 3920555, upload-time = "2026-05-04T16:19:07.962Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/f0/f1103a861cd610ae6cf31c4132530b7c11b023301d58f9adcdcac52e62f9/zensical-0.0.39-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1eb9b79936f968c0746a534ac39ddd138b5e86126843f653afaa84cfa1e8add5", size = 12669448, upload-time = "2026-05-01T16:32:54.356Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/c4/3066f4442923ca1e49269147b70ca7c84467524e8f5228724693b9ac85c2/zensical-0.0.40-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b65a7143c9c6a460880bf3e65b777952bd2dcede9dd17a6c6bac9b4a0686ad9b", size = 12691533, upload-time = "2026-05-04T16:18:31.72Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/97/e5a79bb004f834b7618388df99e609e4cee4e4876c81ac1c48991fee4780/zensical-0.0.39-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:093a4763c62ead4fe9cf7bf05857ce37290e4fa1903795b3dcb3006767d6c818", size = 12534151, upload-time = "2026-05-01T16:32:57.279Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/cb/03e961cbd01620ea91aeb835b0b4e8848c7bcdf5a799a620fb3e57bfc277/zensical-0.0.40-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:045bdcb6d00a11ddcab7d379d0d986cdf78dba8e9287d8e628ef11958241507d", size = 12556486, upload-time = "2026-05-04T16:18:35.278Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/d4/a02fdf9a68a0229cc96d865816df5799322e8515188ffecb29eef40538d3/zensical-0.0.39-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d86ca51a69dcadd04cc664b13c76663be9067107564276a3acf8ab90c9e0b3aa", size = 12926235, upload-time = "2026-05-01T16:33:00.174Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/76/7dde50220808bdc5f5e63b97866a684418410b3cae9d00cdae1d449bcc20/zensical-0.0.40-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d48ec476c2e8ce3f8585a1278083aabc35ec80361f2c4fc4a53b9a525778f7fc", size = 12935602, upload-time = "2026-05-04T16:18:38.308Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/07/c36890d69273fc6bbe61d179641ce616e6e100308298b48a16019a5bd6fd/zensical-0.0.39-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:849426b5a41b58da8cef85f7d248be780b512a88953309b9a7b4879d71a37b9c", size = 12888428, upload-time = "2026-05-01T16:33:02.716Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/55/6c8ef951c390b42249738f4338498e7a1fd64ff09e44d7cc19f5c948c45b/zensical-0.0.40-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48c38e0ae314c25f2e5e64210bbad9be6e970f2d40fe9da106586ad90ce5e85e", size = 12904314, upload-time = "2026-05-04T16:18:41.007Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/42/beb8c456ef9926dc91b288d27674813078f75dce7b28fc17c2f97168ca5c/zensical-0.0.39-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba187d2d555da85a5cde635ef32f7bee1d3634522f25b21d58a14f41a128656c", size = 13251625, upload-time = "2026-05-01T16:33:05.429Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/ae/95008f5dc2ee441efcdc2fab36ff29ce24d7477e53390fc340c8add39342/zensical-0.0.40-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25f62dcd61f6306cab890dfa34c81d2709f5db290b4c3f2675343771db28c90", size = 13269946, upload-time = "2026-05-04T16:18:44.387Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/95/b31d67aa17d79048d16fe2da47176fee02eff9a0840df3a583865c0b5048/zensical-0.0.39-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:601c3ba0fc33a8fa5f14470ee99757f9946c370dd4c46c7aacf056dbdcab1df6", size = 12960908, upload-time = "2026-05-01T16:33:08.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/b9/96/cdbb2bf04255ccaaa07861bdda1ee8dd1630d2233fc2f09636abbd5e084c/zensical-0.0.40-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:168fe3489dd93ae92978b4db11d9300c63e10d382b81634232c2872ce9e746c2", size = 12974962, upload-time = "2026-05-04T16:18:47.462Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/8c/c01c850510931fb247a9d6b6bb0bc3a9c1e7185157551ad9baf31a640e51/zensical-0.0.39-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b275d099b53e179188897037667261c9b6c647d0829ef7dbbd40f0e9ea0a1c4b", size = 13102894, upload-time = "2026-05-01T16:33:11.747Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/ce/66e86f89fc15bbe667794ba67d7efc8fa72fe7a1be19e1efb4246ff55442/zensical-0.0.40-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8652ba203bd588ebf2d66bda4457a4a7d8e193c886960859c75081c0e3b946de", size = 13111599, upload-time = "2026-05-04T16:18:50.14Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/4e/d0d72beedd002986b2932f608a0390973fd50c79e0e74636766cabe4b985/zensical-0.0.39-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:8b670c6a6d8b6d484e83f9b80387d73213549f473cadb19a12026e0edbcafc7f", size = 13159755, upload-time = "2026-05-01T16:33:14.728Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/76/3d71ebdabb02d79a5c523b5e646141c362c9559947078c8d56a9f3bd7a30/zensical-0.0.40-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9ffa6cf208b7ab6b771703be827d4d8c7f07f173abeffb35a8015a0b832b2a40", size = 13175406, upload-time = "2026-05-04T16:18:53.209Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/f1/b0224a8307e806ac5887f7af3b4b5483f5656e952edadb0474ff72dc6373/zensical-0.0.39-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:1fe98c35748bd0b99dc45f843508a174362e77216dbea34321ec06aed2810816", size = 13310315, upload-time = "2026-05-01T16:33:17.356Z" },
|
{ url = "https://files.pythonhosted.org/packages/e2/6a/2bb5f730786d590f02cb0fef796c148d5ac0d5c1556f2d78c987ad4e1346/zensical-0.0.40-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:7101ba0c739c78bc3a57d22130b59b9e6fdf96c21c8a6b4244070de6b34527d4", size = 13324783, upload-time = "2026-05-04T16:18:56.41Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/c0/2c872d7e40030dd2f7b82d78f23ae2b259e5657d9712398968af4ffd542c/zensical-0.0.39-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aafe2650e2f2814e95f79500cd121e35bb2fdda9b63255bb4ac4e530a05d3524", size = 13245269, upload-time = "2026-05-01T16:33:20.426Z" },
|
{ url = "https://files.pythonhosted.org/packages/2f/8c/1d2ba1454360ee948dd0f0807b048c076d9578d0d9ebba2a438ecfa9f82f/zensical-0.0.40-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:39bf728a68a5418feeda8f3385cd1063fdb8d896a6812c3dede4267b2868df12", size = 13260045, upload-time = "2026-05-04T16:18:59.244Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/18/24cfab771f36576c8fa098c47dbc61fc19f35f6e456169dc18b5d7e6b024/zensical-0.0.39-cp310-abi3-win32.whl", hash = "sha256:c49f006444f356feb2b4067fcf1f0a2050c444c194e0e95bc5e1f53fe3f9655a", size = 12242128, upload-time = "2026-05-01T16:33:23.345Z" },
|
{ url = "https://files.pythonhosted.org/packages/6c/61/efd51c5c5e15cfd5498d59df250f60294cc44d36d8ce4dc2a76fa3669c2f/zensical-0.0.40-cp310-abi3-win32.whl", hash = "sha256:bc750c3ba8d11833d9b9ac8fc14adc3435225b6d17314a21a91eb60209511ca5", size = 12244913, upload-time = "2026-05-04T16:19:02.219Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/8b/39bb18c55aad6ca413c1a808693a835b19f4173b460865924d04189acac9/zensical-0.0.39-cp310-abi3-win_amd64.whl", hash = "sha256:c1c0befb8ba10aa16f0acdea9373da5eae8932d50bc996fdc54598e9483ab667", size = 12470551, upload-time = "2026-05-01T16:33:26.185Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/9e/f3f2118fbcfd1c2dc705491c8864c596b1a748b67ffe2a024e512b9201ab/zensical-0.0.40-cp310-abi3-win_amd64.whl", hash = "sha256:c5c86ac468df2dfe515ff54ffa97725c38226f1e5c970059b7e88078abab89ab", size = 12475762, upload-time = "2026-05-04T16:19:05.025Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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