10 Commits
Author SHA1 Message Date
d3vyce 62cf4fb3da Version 1.0.4 2026-06-02 13:35:35 -04:00
d3vyceandGitHub 56be33d5b5 test: add real-Redis integration tests (#45) 2026-06-02 19:18:32 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f9d326ba02 ⬆ bump prek from 0.4.0 to 0.4.1 (#35)
Bumps [prek](https://github.com/j178/prek) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/j178/prek/releases)
- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)
- [Commits](https://github.com/j178/prek/compare/v0.4.0...v0.4.1)

---
updated-dependencies:
- dependency-name: prek
  dependency-version: 0.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 19:00:26 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
475e97d9ac ⬆ bump ty from 0.0.35 to 0.0.38 (#36)
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.35 to 0.0.38.
- [Release notes](https://github.com/astral-sh/ty/releases)
- [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ty/compare/0.0.35...0.0.38)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.38
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 19:00:03 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
acc0599eab ⬆ bump zensical from 0.0.41 to 0.0.43 (#37)
Bumps [zensical](https://github.com/zensical/zensical) from 0.0.41 to 0.0.43.
- [Release notes](https://github.com/zensical/zensical/releases)
- [Commits](https://github.com/zensical/zensical/compare/v0.0.41...v0.0.43)

---
updated-dependencies:
- dependency-name: zensical
  dependency-version: 0.0.43
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 18:59:48 +02:00
d3vyceandGitHub db6e2b9715 fix: close leaked Redis clients on failed startup retries (#43) 2026-06-02 18:56:57 +02:00
d3vyceandGitHub e253096236 fix: warn on invalid label values instead of crashing or silently ignoring (#41) 2026-06-02 18:52:41 +02:00
d3vyceandGitHub c651459dd8 fix: lock leaks when sender and worker middlewares have different default_deduplication (#39) 2026-06-02 18:44:37 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
787b8a7640 ⬆ bump pymdown-extensions from 10.21.2 to 10.21.3 (#34)
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.21.2 to 10.21.3.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.21.2...10.21.3)

---
updated-dependencies:
- dependency-name: pymdown-extensions
  dependency-version: 10.21.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 21:46:38 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8b4fd9c167 ⬆ bump idna from 3.13 to 3.15 (#33)
Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.13...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 21:46:25 +02:00
8 changed files with 406 additions and 107 deletions
+38 -2
View File
@@ -73,8 +73,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: uv sync --group dev run: uv sync --group dev
- name: Run tests with coverage - name: Run unit tests with coverage
run: uv run pytest --cov --cov-report=xml --cov-report=term-missing --junitxml=junit.xml -o junit_family=legacy run: uv run pytest -m "not integration" --cov --cov-report=xml --cov-report=term-missing --junitxml=junit.xml -o junit_family=legacy
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: matrix.python-version == '3.14' if: matrix.python-version == '3.14'
@@ -91,3 +91,39 @@ jobs:
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
report_type: test_results report_type: test_results
test-integration:
name: Integration (${{ matrix.backend.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
backend:
- { name: redis, image: "redis:7" }
- { name: valkey, image: "valkey/valkey:8" }
services:
store:
image: ${{ matrix.backend.image }}
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 5
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
run: uv python install 3.13
- name: Install dependencies
run: uv sync --group dev
- name: Run integration tests
run: uv run pytest -m integration -v
+5 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "taskiq-deduplication" name = "taskiq-deduplication"
version = "1.0.3" version = "1.0.4"
description = "Redis-backed deduplication middleware for Taskiq" description = "Redis-backed deduplication middleware for Taskiq"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
@@ -64,9 +64,13 @@ build-backend = "uv_build"
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
anyio_mode = "auto"
filterwarnings = [ filterwarnings = [
"ignore::DeprecationWarning", "ignore::DeprecationWarning",
] ]
markers = [
"integration: requires a live Redis at localhost:6379",
]
[tool.coverage.run] [tool.coverage.run]
source = ["src/taskiq_deduplication"] source = ["src/taskiq_deduplication"]
+1 -1
View File
@@ -2,7 +2,7 @@
from .middleware import DuplicateTaskError, RedisDeduplicationMiddleware from .middleware import DuplicateTaskError, RedisDeduplicationMiddleware
__version__ = "1.0.3" __version__ = "1.0.4"
__all__ = [ __all__ = [
"DuplicateTaskError", "DuplicateTaskError",
+42 -9
View File
@@ -60,12 +60,14 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
async def startup(self) -> None: async def startup(self) -> None:
last_error: BaseException | None = None last_error: BaseException | None = None
for attempt in range(self.startup_retries): for attempt in range(self.startup_retries):
client = Redis.from_url(self.redis_url)
try: try:
self._redis = Redis.from_url(self.redis_url) await cast(Awaitable[bool], client.ping())
await cast(Awaitable[bool], self._redis.ping()) self._redis = client
self._release_script = self._redis.register_script(RELEASE_LUA_SCRIPT) self._release_script = self._redis.register_script(RELEASE_LUA_SCRIPT)
return return
except Exception as exc: except Exception as exc:
await client.aclose()
last_error = exc last_error = exc
if attempt < self.startup_retries - 1: if attempt < self.startup_retries - 1:
delay = self.startup_retry_delay * (2**attempt) delay = self.startup_retry_delay * (2**attempt)
@@ -90,23 +92,51 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
await self._redis.aclose() await self._redis.aclose()
@staticmethod @staticmethod
def _parse_bool_label(value: Any, default: bool) -> bool: def _parse_bool_label(value: Any, default: bool, label_name: str = "") -> bool:
if isinstance(value, bool): if isinstance(value, bool):
return value return value
if value is not None:
logger.warning(
"Invalid %r value %r (expected bool); falling back to default (%r).",
label_name,
value,
default,
)
return default return default
@staticmethod @staticmethod
def _parse_list_label(value: Any) -> list[str] | None: def _parse_list_label(value: Any, label_name: str = "") -> list[str] | None:
if isinstance(value, list): if isinstance(value, list):
return value return value
if value is not None:
logger.warning(
"Invalid %r value %r (expected list[str]); ignoring.",
label_name,
value,
)
return None return None
@staticmethod
def _parse_int_label(value: Any, default: int, label_name: str = "") -> int:
try:
return int(value)
except (TypeError, ValueError):
logger.warning(
"Invalid %r value %r (expected int); falling back to default (%d).",
label_name,
value,
default,
)
return default
def _build_deduplication_key(self, message: TaskiqMessage) -> str | None: 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}"
key_fields = self._parse_list_label(message.labels.get(DEDUP_KEY_FIELDS_LABEL)) key_fields = self._parse_list_label(
message.labels.get(DEDUP_KEY_FIELDS_LABEL), DEDUP_KEY_FIELDS_LABEL
)
kwargs = ( kwargs = (
{k: v for k, v in message.kwargs.items() if k in key_fields} {k: v for k, v in message.kwargs.items() if k in key_fields}
if key_fields is not None if key_fields is not None
@@ -124,11 +154,15 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
def _is_enabled(self, labels: dict[str, Any]) -> bool: def _is_enabled(self, labels: dict[str, Any]) -> bool:
return self._parse_bool_label( return self._parse_bool_label(
labels.get(DEDUP_LABEL), self.default_deduplication labels.get(DEDUP_LABEL), self.default_deduplication, DEDUP_LABEL
) )
def _get_ttl(self, labels: dict[str, Any]) -> int: def _get_ttl(self, labels: dict[str, Any]) -> int:
return int(labels.get(DEDUP_TTL_LABEL, self.default_ttl)) return self._parse_int_label(
labels.get(DEDUP_TTL_LABEL, self.default_ttl),
self.default_ttl,
DEDUP_TTL_LABEL,
)
async def _release_if_owned(self, key: str, task_id: str) -> None: async def _release_if_owned(self, key: str, task_id: str) -> None:
if self._redis is None: if self._redis is None:
@@ -186,8 +220,7 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
return message return message
async def _release_lock(self, message: TaskiqMessage) -> None: async def _release_lock(self, message: TaskiqMessage) -> None:
if not self._is_enabled(message.labels): # The cached key is set by pre_send() only when deduplication is enabled.
return
key = self._get_cached_key(message) key = self._get_cached_key(message)
if key is None: if key is None:
return return
+18
View File
@@ -1,5 +1,8 @@
from typing import Awaitable, cast
import pytest import pytest
import fakeredis.aioredis import fakeredis.aioredis
from redis.asyncio import Redis
from taskiq import TaskiqMessage, TaskiqResult from taskiq import TaskiqMessage, TaskiqResult
@@ -15,6 +18,21 @@ async def fake_redis():
await client.aclose() await client.aclose()
@pytest.fixture
async def real_redis():
client = Redis.from_url("redis://localhost:6379/15")
try:
await cast(Awaitable[bool], client.ping())
except Exception:
await client.aclose()
pytest.skip("Redis not available at localhost:6379")
return
await client.flushdb()
yield client
await client.flushdb()
await client.aclose()
@pytest.fixture @pytest.fixture
def make_message(): def make_message():
def _make(task_name="my_task", task_id="task-1", labels=None, kwargs=None): def _make(task_name="my_task", task_id="task-1", labels=None, kwargs=None):
+88
View File
@@ -0,0 +1,88 @@
"""Integration tests against a live Redis instance (localhost:6379/15).
These tests are skipped automatically when Redis is not reachable.
In CI a Redis service is started before the test step.
"""
import pytest
from taskiq_deduplication import DuplicateTaskError, RedisDeduplicationMiddleware
from taskiq_deduplication.utils import RELEASE_LUA_SCRIPT
@pytest.fixture
def mw(real_redis):
middleware = RedisDeduplicationMiddleware(redis_url="redis://localhost:6379/15")
middleware._redis = real_redis
middleware._release_script = real_redis.register_script(RELEASE_LUA_SCRIPT)
return middleware
@pytest.mark.integration
async def test_full_lifecycle(mw, real_redis, make_message, make_result):
msg = make_message()
await mw.pre_send(msg)
key = mw._build_deduplication_key(msg)
assert await real_redis.exists(key)
await mw.post_execute(msg, make_result())
assert not await real_redis.exists(key)
await mw.pre_send(make_message())
@pytest.mark.integration
async def test_duplicate_rejected(mw, make_message):
await mw.pre_send(make_message())
with pytest.raises(DuplicateTaskError):
await mw.pre_send(make_message())
@pytest.mark.integration
async def test_redispatch_after_on_error(mw, make_message, make_result):
msg = make_message()
await mw.pre_send(msg)
await mw.on_error(msg, make_result(is_err=True), RuntimeError("boom"))
await mw.pre_send(make_message())
@pytest.mark.integration
async def test_lua_only_owner_can_release(mw, real_redis, make_message):
owner_msg = make_message(task_id="owner")
key = mw._build_deduplication_key(owner_msg)
await real_redis.set(key, "owner", ex=60)
await mw._release_if_owned(key, "intruder")
assert await real_redis.exists(key)
await mw._release_if_owned(key, "owner")
assert not await real_redis.exists(key)
@pytest.mark.integration
async def test_ttl_is_applied(mw, real_redis, make_message):
msg = make_message()
await mw.pre_send(msg)
key = mw._build_deduplication_key(msg)
ttl = await real_redis.ttl(key)
assert 0 < ttl <= mw.default_ttl
@pytest.mark.integration
async def test_explicit_key_end_to_end(mw, real_redis, make_message, make_result):
from taskiq_deduplication.middleware import DEDUP_EXPLICIT_KEY_LABEL
msg = make_message(labels={DEDUP_EXPLICIT_KEY_LABEL: "my-lock"})
await mw.pre_send(msg)
assert await real_redis.exists("taskiq:deduplication:my-lock")
with pytest.raises(DuplicateTaskError):
await mw.pre_send(
make_message(
kwargs={"different": "kwargs"},
labels={DEDUP_EXPLICIT_KEY_LABEL: "my-lock"},
)
)
await mw.post_execute(msg, make_result())
assert not await real_redis.exists("taskiq:deduplication:my-lock")
+156 -36
View File
@@ -134,27 +134,23 @@ class TestDefaultBuildDeduplicationKey:
class TestPreSend: class TestPreSend:
@pytest.mark.anyio
async def test_first_send_passes(self, middleware, make_message): async def test_first_send_passes(self, middleware, make_message):
msg = make_message() msg = make_message()
result = await middleware.pre_send(msg) result = await middleware.pre_send(msg)
assert result is msg assert result is msg
@pytest.mark.anyio
async def test_duplicate_raises(self, middleware, make_message): async def test_duplicate_raises(self, middleware, make_message):
msg = make_message() msg = make_message()
await middleware.pre_send(msg) await middleware.pre_send(msg)
with pytest.raises(DuplicateTaskError): with pytest.raises(DuplicateTaskError):
await middleware.pre_send(make_message()) await middleware.pre_send(make_message())
@pytest.mark.anyio
async def test_deduplication_disabled_label(self, middleware, make_message): async def test_deduplication_disabled_label(self, middleware, make_message):
msg1 = make_message(labels={DEDUP_LABEL: False}) msg1 = make_message(labels={DEDUP_LABEL: False})
msg2 = make_message(labels={DEDUP_LABEL: False}) msg2 = make_message(labels={DEDUP_LABEL: False})
await middleware.pre_send(msg1) await middleware.pre_send(msg1)
await middleware.pre_send(msg2) # should not raise await middleware.pre_send(msg2) # should not raise
@pytest.mark.anyio
async def test_deduplication_disabled_by_default_init( async def test_deduplication_disabled_by_default_init(
self, fake_redis, make_message self, fake_redis, make_message
): ):
@@ -165,7 +161,6 @@ class TestPreSend:
await mw.pre_send(make_message()) await mw.pre_send(make_message())
await mw.pre_send(make_message()) # should not raise await mw.pre_send(make_message()) # should not raise
@pytest.mark.anyio
async def test_ttl_applied(self, middleware, fake_redis, make_message): async def test_ttl_applied(self, middleware, fake_redis, make_message):
msg = make_message(labels={DEDUP_TTL_LABEL: 42}) msg = make_message(labels={DEDUP_TTL_LABEL: 42})
await middleware.pre_send(msg) await middleware.pre_send(msg)
@@ -173,12 +168,10 @@ class TestPreSend:
ttl = await fake_redis.ttl(key) ttl = await fake_redis.ttl(key)
assert 0 < ttl <= 42 assert 0 < ttl <= 42
@pytest.mark.anyio
async def test_different_kwargs_both_pass(self, middleware, make_message): async def test_different_kwargs_both_pass(self, middleware, make_message):
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( async def test_non_serializable_kwargs_skips_deduplication(
self, middleware, make_message self, middleware, make_message
): ):
@@ -187,7 +180,6 @@ class TestPreSend:
await middleware.pre_send(msg1) await middleware.pre_send(msg1)
await middleware.pre_send(msg2) # should not raise await middleware.pre_send(msg2) # should not raise
@pytest.mark.anyio
async def test_default_ttl_applied(self, fake_redis, make_message): async def test_default_ttl_applied(self, fake_redis, make_message):
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost", default_ttl=77) mw = RedisDeduplicationMiddleware(redis_url="redis://localhost", default_ttl=77)
mw._redis = fake_redis mw._redis = fake_redis
@@ -199,7 +191,6 @@ class TestPreSend:
class TestPostExecute: class TestPostExecute:
@pytest.mark.anyio
async def test_releases_lock( async def test_releases_lock(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -211,7 +202,6 @@ class TestPostExecute:
await middleware.post_execute(msg, make_result()) await middleware.post_execute(msg, make_result())
assert not await fake_redis.exists(key) assert not await fake_redis.exists(key)
@pytest.mark.anyio
async def test_deduplication_disabled_noop( async def test_deduplication_disabled_noop(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -223,7 +213,6 @@ 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( async def test_post_execute_after_ttl_expiry_is_safe(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -234,7 +223,6 @@ class TestPostExecute:
class TestOnError: class TestOnError:
@pytest.mark.anyio
async def test_releases_lock_on_error( async def test_releases_lock_on_error(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -246,7 +234,6 @@ class TestOnError:
await middleware.on_error(msg, make_result(is_err=True), RuntimeError("boom")) await middleware.on_error(msg, make_result(is_err=True), RuntimeError("boom"))
assert not await fake_redis.exists(key) assert not await fake_redis.exists(key)
@pytest.mark.anyio
async def test_deduplication_disabled_noop( async def test_deduplication_disabled_noop(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -261,8 +248,51 @@ class TestOnError:
assert await fake_redis.exists(key) assert await fake_redis.exists(key)
class TestSenderWorkerConfigMismatch:
async def test_post_execute_releases_lock_despite_worker_disabled_default(
self, fake_redis, make_message, make_result
):
sender_mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost", default_deduplication=True
)
sender_mw._redis = fake_redis
worker_mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost", default_deduplication=False
)
worker_mw._redis = fake_redis
msg = make_message()
await sender_mw.pre_send(msg)
key = sender_mw._build_deduplication_key(msg)
assert await fake_redis.exists(key)
await worker_mw.post_execute(msg, make_result())
assert not await fake_redis.exists(key)
async def test_on_error_releases_lock_despite_worker_disabled_default(
self, fake_redis, make_message, make_result
):
sender_mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost", default_deduplication=True
)
sender_mw._redis = fake_redis
worker_mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost", default_deduplication=False
)
worker_mw._redis = fake_redis
msg = make_message()
await sender_mw.pre_send(msg)
key = sender_mw._build_deduplication_key(msg)
assert await fake_redis.exists(key)
await worker_mw.on_error(msg, make_result(is_err=True), RuntimeError("boom"))
assert not await fake_redis.exists(key)
class TestRedispatchAfterRelease: class TestRedispatchAfterRelease:
@pytest.mark.anyio
async def test_redispatch_after_post_execute( async def test_redispatch_after_post_execute(
self, middleware, make_message, make_result self, middleware, make_message, make_result
): ):
@@ -271,7 +301,6 @@ class TestRedispatchAfterRelease:
await middleware.post_execute(msg, make_result()) await middleware.post_execute(msg, make_result())
await middleware.pre_send(make_message()) await middleware.pre_send(make_message())
@pytest.mark.anyio
async def test_redispatch_after_on_error( async def test_redispatch_after_on_error(
self, middleware, make_message, make_result self, middleware, make_message, make_result
): ):
@@ -282,7 +311,6 @@ class TestRedispatchAfterRelease:
class TestAtomicRelease: class TestAtomicRelease:
@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):
owner_msg = make_message(task_id="owner-task") owner_msg = make_message(task_id="owner-task")
key = middleware._build_deduplication_key(owner_msg) key = middleware._build_deduplication_key(owner_msg)
@@ -295,7 +323,6 @@ class TestAtomicRelease:
await middleware._release_if_owned(key, "owner-task") await middleware._release_if_owned(key, "owner-task")
assert not await fake_redis.exists(key) assert not await fake_redis.exists(key)
@pytest.mark.anyio
async def test_release_missing_key_is_noop(self, middleware, fake_redis): async def test_release_missing_key_is_noop(self, middleware, fake_redis):
await middleware._release_if_owned( await middleware._release_if_owned(
"taskiq:deduplication:nonexistent", "some-task" "taskiq:deduplication:nonexistent", "some-task"
@@ -303,7 +330,6 @@ class TestAtomicRelease:
class TestLifecycle: class TestLifecycle:
@pytest.mark.anyio
async def test_startup_creates_redis_client(self): async def test_startup_creates_redis_client(self):
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost") mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
assert mw._redis is None assert mw._redis is None
@@ -315,7 +341,6 @@ class TestLifecycle:
mock_from_url.assert_called_once_with("redis://localhost") mock_from_url.assert_called_once_with("redis://localhost")
assert mw._redis is mock_client assert mw._redis is mock_client
@pytest.mark.anyio
async def test_shutdown_closes_redis_client(self): async def test_shutdown_closes_redis_client(self):
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost") mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
mock_client = AsyncMock() mock_client = AsyncMock()
@@ -323,12 +348,10 @@ class TestLifecycle:
await mw.shutdown() await mw.shutdown()
mock_client.aclose.assert_called_once() mock_client.aclose.assert_called_once()
@pytest.mark.anyio
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): async def test_pre_send_without_startup_raises_runtime_error(self, make_message):
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost") mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
with pytest.raises(RuntimeError, match="startup"): with pytest.raises(RuntimeError, match="startup"):
@@ -336,7 +359,6 @@ class TestLifecycle:
class TestStartupRetry: class TestStartupRetry:
@pytest.mark.anyio
async def test_startup_succeeds_after_retries(self): async def test_startup_succeeds_after_retries(self):
mw = RedisDeduplicationMiddleware( mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost", redis_url="redis://localhost",
@@ -356,7 +378,6 @@ class TestStartupRetry:
assert mw._redis is mock_client assert mw._redis is mock_client
assert mock_client.ping.call_count == 3 assert mock_client.ping.call_count == 3
@pytest.mark.anyio
async def test_startup_raises_after_all_retries_exhausted(self): async def test_startup_raises_after_all_retries_exhausted(self):
mw = RedisDeduplicationMiddleware( mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost", redis_url="redis://localhost",
@@ -371,7 +392,6 @@ class TestStartupRetry:
await mw.startup() await mw.startup()
assert mock_client.ping.call_count == 2 assert mock_client.ping.call_count == 2
@pytest.mark.anyio
async def test_startup_no_retry_on_first_success(self): async def test_startup_no_retry_on_first_success(self):
mw = RedisDeduplicationMiddleware( mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost", redis_url="redis://localhost",
@@ -385,7 +405,6 @@ class TestStartupRetry:
await mw.startup() await mw.startup()
mock_client.ping.assert_called_once() mock_client.ping.assert_called_once()
@pytest.mark.anyio
async def test_startup_retry_delay_exponential(self): async def test_startup_retry_delay_exponential(self):
mw = RedisDeduplicationMiddleware( mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost", redis_url="redis://localhost",
@@ -409,23 +428,58 @@ class TestStartupRetry:
assert mock_sleep.call_args_list[0].args[0] == 0.01 assert mock_sleep.call_args_list[0].args[0] == 0.01
assert mock_sleep.call_args_list[1].args[0] == 0.02 assert mock_sleep.call_args_list[1].args[0] == 0.02
async def test_startup_closes_failed_clients(self):
mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost",
startup_retries=3,
startup_retry_delay=0.01,
)
failed1, failed2, good = AsyncMock(), AsyncMock(), AsyncMock()
failed1.ping.side_effect = ConnectionError("fail")
failed2.ping.side_effect = ConnectionError("fail")
good.register_script = MagicMock()
with patch(
"redis.asyncio.Redis.from_url", side_effect=[failed1, failed2, good]
):
await mw.startup()
failed1.aclose.assert_called_once()
failed2.aclose.assert_called_once()
good.aclose.assert_not_called()
assert mw._redis is good
async def test_startup_closes_client_when_all_retries_exhausted(self):
mw = RedisDeduplicationMiddleware(
redis_url="redis://localhost",
startup_retries=2,
startup_retry_delay=0.01,
)
failed1, failed2 = AsyncMock(), AsyncMock()
failed1.ping.side_effect = ConnectionError("fail")
failed2.ping.side_effect = ConnectionError("fail")
with patch("redis.asyncio.Redis.from_url", side_effect=[failed1, failed2]):
with pytest.raises(ConnectionError):
await mw.startup()
failed1.aclose.assert_called_once()
failed2.aclose.assert_called_once()
class TestLabelTypeParsing: class TestLabelTypeParsing:
@pytest.mark.anyio
async def test_bool_label_false_disables_dedup(self, fake_redis, make_message): async def test_bool_label_false_disables_dedup(self, fake_redis, make_message):
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost") mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
mw._redis = fake_redis mw._redis = fake_redis
await mw.pre_send(make_message(labels={DEDUP_LABEL: False})) await mw.pre_send(make_message(labels={DEDUP_LABEL: False}))
await mw.pre_send(make_message(labels={DEDUP_LABEL: False})) await mw.pre_send(make_message(labels={DEDUP_LABEL: False}))
@pytest.mark.anyio
async def test_bool_label_true_enables_dedup(self, middleware, make_message): async def test_bool_label_true_enables_dedup(self, middleware, make_message):
msg = make_message(labels={DEDUP_LABEL: True}) msg = make_message(labels={DEDUP_LABEL: True})
await middleware.pre_send(msg) await middleware.pre_send(msg)
with pytest.raises(DuplicateTaskError): with pytest.raises(DuplicateTaskError):
await middleware.pre_send(make_message(labels={DEDUP_LABEL: True})) await middleware.pre_send(make_message(labels={DEDUP_LABEL: True}))
@pytest.mark.anyio
async def test_key_fields_list_parsed_correctly(self, middleware, make_message): async def test_key_fields_list_parsed_correctly(self, middleware, make_message):
m1 = make_message( m1 = make_message(
kwargs={"a": 1, "b": 2, "c": 3}, kwargs={"a": 1, "b": 2, "c": 3},
@@ -439,7 +493,6 @@ class TestLabelTypeParsing:
m1 m1
) == middleware._build_deduplication_key(m2) ) == middleware._build_deduplication_key(m2)
@pytest.mark.anyio
async def test_key_fields_non_list_ignored(self, middleware, make_message): async def test_key_fields_non_list_ignored(self, middleware, make_message):
m = make_message( m = make_message(
kwargs={"a": 1}, kwargs={"a": 1},
@@ -448,9 +501,56 @@ class TestLabelTypeParsing:
key = middleware._build_deduplication_key(m) key = middleware._build_deduplication_key(m)
assert key is not None assert key is not None
async def test_invalid_bool_label_warns_and_uses_default(
self, middleware, make_message, caplog
):
import logging
msg = make_message(labels={DEDUP_LABEL: "yes"})
with caplog.at_level(logging.WARNING, logger="taskiq_deduplication.middleware"):
await middleware.pre_send(msg)
assert any("yes" in r.message for r in caplog.records)
async def test_invalid_key_fields_label_warns_and_falls_back_to_all_kwargs(
self, middleware, make_message, caplog
):
import logging
msg = make_message(kwargs={"a": 1}, labels={DEDUP_KEY_FIELDS_LABEL: "user_id"})
with caplog.at_level(logging.WARNING, logger="taskiq_deduplication.middleware"):
key = middleware._build_deduplication_key(msg)
assert any("user_id" in r.message for r in caplog.records)
# falls back to full-kwargs fingerprint — key must still be produced
assert key is not None
async def test_invalid_ttl_string_warns_and_uses_default(
self, middleware, fake_redis, make_message, caplog
):
import logging
msg = make_message(labels={DEDUP_TTL_LABEL: "oops"})
with caplog.at_level(logging.WARNING, logger="taskiq_deduplication.middleware"):
await middleware.pre_send(msg)
assert any("oops" in r.message for r in caplog.records)
key = middleware._build_deduplication_key(msg)
ttl = await fake_redis.ttl(key)
assert 0 < ttl <= middleware.default_ttl
async def test_invalid_ttl_none_warns_and_uses_default(
self, middleware, fake_redis, make_message, caplog
):
import logging
msg = make_message(labels={DEDUP_TTL_LABEL: None})
with caplog.at_level(logging.WARNING, logger="taskiq_deduplication.middleware"):
await middleware.pre_send(msg)
assert any("None" in r.message for r in caplog.records)
key = middleware._build_deduplication_key(msg)
ttl = await fake_redis.ttl(key)
assert 0 < ttl <= middleware.default_ttl
class TestKeyCaching: class TestKeyCaching:
@pytest.mark.anyio
async def test_key_cached_during_pre_send(self, middleware, make_message): async def test_key_cached_during_pre_send(self, middleware, make_message):
from taskiq_deduplication.middleware import _CACHED_KEY_LABEL from taskiq_deduplication.middleware import _CACHED_KEY_LABEL
@@ -458,7 +558,6 @@ class TestKeyCaching:
await middleware.pre_send(msg) await middleware.pre_send(msg)
assert _CACHED_KEY_LABEL in msg.labels assert _CACHED_KEY_LABEL in msg.labels
@pytest.mark.anyio
async def test_post_execute_uses_cached_key( async def test_post_execute_uses_cached_key(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -470,7 +569,6 @@ class TestKeyCaching:
await middleware.post_execute(msg, make_result()) await middleware.post_execute(msg, make_result())
assert not await fake_redis.exists(cached_key) assert not await fake_redis.exists(cached_key)
@pytest.mark.anyio
async def test_on_error_uses_cached_key( async def test_on_error_uses_cached_key(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -482,7 +580,6 @@ class TestKeyCaching:
await middleware.on_error(msg, make_result(is_err=True), RuntimeError("boom")) await middleware.on_error(msg, make_result(is_err=True), RuntimeError("boom"))
assert not await fake_redis.exists(cached_key) assert not await fake_redis.exists(cached_key)
@pytest.mark.anyio
async def test_cached_key_none_when_key_build_fails(self, middleware, make_message): async def test_cached_key_none_when_key_build_fails(self, middleware, make_message):
from taskiq_deduplication.middleware import _CACHED_KEY_LABEL from taskiq_deduplication.middleware import _CACHED_KEY_LABEL
@@ -490,7 +587,6 @@ class TestKeyCaching:
await middleware.pre_send(msg) await middleware.pre_send(msg)
assert msg.labels[_CACHED_KEY_LABEL] is None assert msg.labels[_CACHED_KEY_LABEL] is None
@pytest.mark.anyio
async def test_post_execute_noop_when_cached_key_is_none( async def test_post_execute_noop_when_cached_key_is_none(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -498,7 +594,6 @@ class TestKeyCaching:
await middleware.pre_send(msg) await middleware.pre_send(msg)
await middleware.post_execute(msg, make_result()) await middleware.post_execute(msg, make_result())
@pytest.mark.anyio
async def test_on_error_noop_when_cached_key_is_none( async def test_on_error_noop_when_cached_key_is_none(
self, middleware, fake_redis, make_message, make_result self, middleware, fake_redis, make_message, make_result
): ):
@@ -506,10 +601,35 @@ class TestKeyCaching:
await middleware.pre_send(msg) await middleware.pre_send(msg)
await middleware.on_error(msg, make_result(is_err=True), RuntimeError("boom")) await middleware.on_error(msg, make_result(is_err=True), RuntimeError("boom"))
@pytest.mark.anyio
async def test_release_if_owned_raises_without_redis( async def test_release_if_owned_raises_without_redis(
self, middleware, make_message self, middleware, make_message
): ):
middleware._redis = None middleware._redis = None
with pytest.raises(RuntimeError, match="startup"): with pytest.raises(RuntimeError, match="startup"):
await middleware._release_if_owned("some-key", "some-task") await middleware._release_if_owned("some-key", "some-task")
class TestTTLExpiry:
async def test_lock_expiry_admits_duplicate(
self, middleware, fake_redis, make_message
):
msg = make_message()
await middleware.pre_send(msg)
# simulate TTL expiry by deleting the key
await fake_redis.delete(middleware._build_deduplication_key(msg))
# same fingerprint should now pass since lock is gone
await middleware.pre_send(make_message())
class TestExplicitKeyEdgeCases:
def test_empty_string_key_produces_prefix_only_key(self, middleware, make_message):
m = make_message(labels={DEDUP_EXPLICIT_KEY_LABEL: ""})
key = middleware._build_deduplication_key(m)
assert key == "taskiq:deduplication:"
async def test_empty_string_key_acquires_lock(
self, middleware, fake_redis, make_message
):
msg = make_message(labels={DEDUP_EXPLICIT_KEY_LABEL: ""})
await middleware.pre_send(msg)
assert await fake_redis.exists("taskiq:deduplication:")
Generated
+58 -58
View File
@@ -508,11 +508,11 @@ wheels = [
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.13" version = "3.15"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
] ]
[[package]] [[package]]
@@ -962,26 +962,26 @@ wheels = [
[[package]] [[package]]
name = "prek" name = "prek"
version = "0.4.0" version = "0.4.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/68/00050a4184f038a622855b1989b013d0ac5bfc0a29bf3cdbd1ed823595d8/prek-0.4.0.tar.gz", hash = "sha256:47f42477c8453c7440e4e656e5ab0c2a1e4c25daa5ed441a9ac1a2b7634abc12", size = 446399, upload-time = "2026-05-14T10:50:35.194Z" } sdist = { url = "https://files.pythonhosted.org/packages/1c/01/1d2c238c6f226d75881cd7a5532e980f4d524babc3c034d16ad89e88b6e1/prek-0.4.1.tar.gz", hash = "sha256:622a8812bda87cf4ddcae2dab5ccecc55b88d70c677129dbe25e975d923179f0", size = 452606, upload-time = "2026-05-20T04:27:19.259Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/83/e4f5f574b8b3a80305a638e0cb2d46e3aa9596ac2b1e2dd6c910fedea376/prek-0.4.0-py3-none-linux_armv6l.whl", hash = "sha256:f4cba2132e038349b4b0a00a73b300e4192bae9f78fff8df0365c00bd19140e7", size = 5509604, upload-time = "2026-05-14T10:50:39.306Z" }, { url = "https://files.pythonhosted.org/packages/d1/ca/0274343faf2672d649b1e648053d3cb48fdfef7a390b43713d95880ebb67/prek-0.4.1-py3-none-linux_armv6l.whl", hash = "sha256:10e7e78ffe65dfba7d687a8c71b2f473554d1ba60f43c742105da4c0030feed9", size = 5515584, upload-time = "2026-05-20T04:27:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/05/de/ee9b8648944d44a8403ca49172d60936f5476196a7baf38c86df4aec7243/prek-0.4.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:944670565dfb3800465355f299effa31572822566d10c6210e6f76bf399ddf53", size = 5877244, upload-time = "2026-05-14T10:50:54.213Z" }, { url = "https://files.pythonhosted.org/packages/37/4e/6a067f530194a6e4141c36463eece92356dfd7f924ffe0cbf456bdca723b/prek-0.4.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b25807e0aa57d2118747e127b58e7a1bf41d5d7b3323f5f3f1f3cb10031245cc", size = 5878925, upload-time = "2026-05-20T04:27:31.71Z" },
{ url = "https://files.pythonhosted.org/packages/33/f8/2e6021993332a249667102de0960160aa942880b0634d3e8920d062ebdb4/prek-0.4.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8272f32e698eae514556086ad73d02026ec00bbd4d26c420f761ea857cfd795", size = 5435063, upload-time = "2026-05-14T10:50:31.75Z" }, { url = "https://files.pythonhosted.org/packages/e3/3d/a334c0f5b88fadca888eadfc1fb3d7f1dc8358b1a534d0987339ecb8eb92/prek-0.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:efa95331c4c171a867c0064c19d8a4abc94a1c1c920c8b8092f2d7d87f4b90a8", size = 5440994, upload-time = "2026-05-20T04:27:40.578Z" },
{ url = "https://files.pythonhosted.org/packages/2a/8b/98fef34684f52c7f5b7b56bce14aec2b24b7abbcd7b6523cc83a63f74c58/prek-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a860fda0f27f872622689358a583e5f2a5771241331848233274f4cfeb8ae9bb", size = 5690323, upload-time = "2026-05-14T10:50:42.583Z" }, { url = "https://files.pythonhosted.org/packages/0c/3b/fa6eb635495c3576e65d7f42a48b9fdf4926dd052010df506ed98e9f9680/prek-0.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2d1805123ab5d730629de588bf319ea39e7078b589b3288c95740f1b4780a1d4", size = 5692369, upload-time = "2026-05-20T04:27:23.184Z" },
{ url = "https://files.pythonhosted.org/packages/00/18/bf18cf0d3ab5b5eddadc6ade754ba6269ae857b32a4605036397ec56ccc1/prek-0.4.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:473ce0262e36b8e77b327941ad6d5747ac451b4c441cdc86dab95640b68fff72", size = 5423877, upload-time = "2026-05-14T10:50:36.434Z" }, { url = "https://files.pythonhosted.org/packages/70/cb/9d9078723b3facb40289444332ca82bf38c0e1db3b5a907af461aba12324/prek-0.4.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:051c442b570b53756225410240577bee1aeace6be52955dfacf45a9783223b36", size = 5430031, upload-time = "2026-05-20T04:27:27.475Z" },
{ url = "https://files.pythonhosted.org/packages/ce/1e/881f18b3cbf6fdca4ec8eb62d2c2e0cb9458fd4cebd136f5e0f76c7aa8d2/prek-0.4.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e46277b577991ccdc7b13e2cfa7d260ae13e77f5af543e50e188799f649f0ad8", size = 5830461, upload-time = "2026-05-14T10:50:45.885Z" }, { url = "https://files.pythonhosted.org/packages/ff/96/2d8cc6b5425215cd0b610f1dcef3f6f0f23db2a2b85f1a6fca43b7e7fe24/prek-0.4.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76663998827a2cbc94f5e209319809655489b5bd1f8e70568a623372e80253f0", size = 5834244, upload-time = "2026-05-20T04:27:44.229Z" },
{ url = "https://files.pythonhosted.org/packages/91/ea/1623391bdf1e103d7959cc7d41caedcd1ab982412c7db9f9f9ee5c0e09f4/prek-0.4.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b60bd4fdc323896a5bada2708734d16ff59ee5e32b8c390ae2ebe328426964a3", size = 6717949, upload-time = "2026-05-14T10:50:51.133Z" }, { url = "https://files.pythonhosted.org/packages/59/e0/cce02f3ade48a6d4bffb25e5f0ac28d10928263b0a4f53ecc72954957f4e/prek-0.4.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ab3460641762edf128b1ec8e833ce7e9ae015d1268a894560cb90d3393a7527", size = 6711903, upload-time = "2026-05-20T04:27:34.128Z" },
{ url = "https://files.pythonhosted.org/packages/da/56/59d73f20d3bf5ec1d1e88f24473cf77fb485acefd285c1eb79d2f102e8dc/prek-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7203bff21e622a5482e956384c43990b0092faab02f72118275839a77a45e939", size = 6104294, upload-time = "2026-05-14T10:50:57.251Z" }, { url = "https://files.pythonhosted.org/packages/c9/2a/ccd581b6222277a2aa095530844d5bb76db4547042f05a9cb649476bf904/prek-0.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e69a9c02ead38706a5d2a4ae209dccba08ccb5d0026e1d08e723c66ab964750", size = 6084138, upload-time = "2026-05-20T04:27:46.549Z" },
{ url = "https://files.pythonhosted.org/packages/b7/7f/c03e17c06069b96805c3579c00f830bad0e700f30aa3fed08c78958be77a/prek-0.4.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:44e3716a3e2983add2c094775d564536342011e84f770132389819c965513d6d", size = 5703788, upload-time = "2026-05-14T10:50:55.717Z" }, { url = "https://files.pythonhosted.org/packages/0a/b7/6164a7dc6bb4796cfc19445be798302cc7625b62e2bec89ffb4272d7f983/prek-0.4.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:dc744fedf98df8a00a9e3bcd629b163fee5e9f9e22bce66029d9945241586165", size = 5698950, upload-time = "2026-05-20T04:27:36.165Z" },
{ url = "https://files.pythonhosted.org/packages/93/6f/f5664a1712ba214f67a0e93411dc62893a02165b5f04f3182e2c188d0623/prek-0.4.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6e3a0e43d7f345a5e32962736c335b5fc466d499f2556fb6f24028dd08108618", size = 5538311, upload-time = "2026-05-14T10:50:33.859Z" }, { url = "https://files.pythonhosted.org/packages/96/40/8151d6445a0f41ad60e979db39d8b0c6b074aad919cf5c73233281f0dff1/prek-0.4.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c0877e82c52359d655fe1072b3a5228639184d1d5f03c6803b6530cd6da1ef20", size = 5538662, upload-time = "2026-05-20T04:27:15.045Z" },
{ url = "https://files.pythonhosted.org/packages/d9/af/8a2aa0e02363e96d05a21ca73b4ea862459535bf6cf293710594396df450/prek-0.4.0-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0d19efff4537e4a68b5bd61dcbf5ccb2dd8343df3ea9482aabe37d241ff18ddf", size = 5398617, upload-time = "2026-05-14T10:50:37.828Z" }, { url = "https://files.pythonhosted.org/packages/96/d7/1f9892a45bb2dc8a3b4b89eb08f5de1cf745fcd7df9e535463ba4d41cebe/prek-0.4.1-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:60928d1dad45ff3e491d3083a50643cc213aa2d54f1dbd8d702d7193773c020e", size = 5406581, upload-time = "2026-05-20T04:27:21.101Z" },
{ url = "https://files.pythonhosted.org/packages/6e/06/2b175e2ef812a3ab6ebab2dcd7aa0d03aea24852b73c6c4a30882800a01c/prek-0.4.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0d9483404e5b8bf65111ee6b821e6cf7a5ba9c3a2065e7a0b7a39a17daaffcc9", size = 5685656, upload-time = "2026-05-14T10:50:40.781Z" }, { url = "https://files.pythonhosted.org/packages/9f/b8/94ddac155b502859e4dc7943db99fa7fffecfa3878a2ef11726a8e72fad0/prek-0.4.1-py3-none-musllinux_1_1_i686.whl", hash = "sha256:17ffa9d8dd40791b9b99cafe558c5cc28e78e5be57607b280b15f0dab90264e9", size = 5688880, upload-time = "2026-05-20T04:27:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/39/00/aee25867a6e88bb5cadee41dd83745876a273d3c149612424cf068239a8e/prek-0.4.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:01d69684306c67917ed69497d0f523216f13f2306723b62e1ace454209477b45", size = 6217097, upload-time = "2026-05-14T10:50:52.903Z" }, { url = "https://files.pythonhosted.org/packages/d0/fd/e93d3853d1bdc06b281fff2aaf4106e19610fe5187c67c9ff13195f2df59/prek-0.4.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cdf4503a240369f66321213d9c4bc6f925014b64ff7121de9e9920c9b9838ce2", size = 6203536, upload-time = "2026-05-20T04:27:42.366Z" },
{ url = "https://files.pythonhosted.org/packages/f2/d2/e0cca68af94b7639badd3967fc37ae6159979baf6cc3c7f1d68f0a966bdf/prek-0.4.0-py3-none-win32.whl", hash = "sha256:ce2a8feb5dc1f1e748879d0e14c2145353059b830ac5e3f64d92127ab16efc78", size = 5204721, upload-time = "2026-05-14T10:50:49.269Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/760969d6bfc77e3eba04f6c3801c81076e96a908a6c277c142a4b0f31f4e/prek-0.4.1-py3-none-win32.whl", hash = "sha256:7c515492ef3585e6bcd7b83f1bb1cb131038abc88ed2c843de1e4c3ceb865b19", size = 5208995, upload-time = "2026-05-20T04:27:38.331Z" },
{ url = "https://files.pythonhosted.org/packages/2d/5b/462c907502e7b9f634c73de4e6d36fa44b5d652a52a9ddd51811b2030ca7/prek-0.4.0-py3-none-win_amd64.whl", hash = "sha256:b218d92ad5d2ff0b59240d7d837fa9edc61849894958acb3a225efff7a2faa65", size = 5595119, upload-time = "2026-05-14T10:50:47.482Z" }, { url = "https://files.pythonhosted.org/packages/89/12/d43daf290a73dbc3e1a3eabb9077e45df661923949bee045de67cbe82524/prek-0.4.1-py3-none-win_amd64.whl", hash = "sha256:8fa707971465d8ad021c907e43691aad7bb98942943e61e294ece5f95d9fbc78", size = 5591734, upload-time = "2026-05-20T04:27:12.744Z" },
{ url = "https://files.pythonhosted.org/packages/0d/a0/a0be2f73cbc8fd65a5e1de576aa5a5324339c7cc9f7dd328009a5e1a3ae1/prek-0.4.0-py3-none-win_arm64.whl", hash = "sha256:ff1f1fa311023418d40a443bca6928a9f5ce073d0b9130b641954183aa31736d", size = 5423774, upload-time = "2026-05-14T10:50:44.19Z" }, { url = "https://files.pythonhosted.org/packages/ab/36/2ab7647fe1e84bba2baae7f04de241197eed62683fb3085e164de266d111/prek-0.4.1-py3-none-win_arm64.whl", hash = "sha256:5b4a348537924b20e208cbd87ef58e96ec37d691c5bec2969209c40de0ecf72e", size = 5423147, upload-time = "2026-05-20T04:27:17.023Z" },
] ]
[[package]] [[package]]
@@ -1249,15 +1249,15 @@ wheels = [
[[package]] [[package]]
name = "pymdown-extensions" name = "pymdown-extensions"
version = "10.21.2" version = "10.21.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "markdown" }, { name = "markdown" },
{ name = "pyyaml" }, { name = "pyyaml" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" },
] ]
[[package]] [[package]]
@@ -1468,7 +1468,7 @@ wheels = [
[[package]] [[package]]
name = "taskiq-deduplication" name = "taskiq-deduplication"
version = "1.0.3" version = "1.0.4"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "redis" }, { name = "redis" },
@@ -1598,27 +1598,27 @@ wheels = [
[[package]] [[package]]
name = "ty" name = "ty"
version = "0.0.35" version = "0.0.38"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/53/440e7b1212c4b0abbd4adb7aed93f4971aa1f8dca386ac5515930afa9172/ty-0.0.35.tar.gz", hash = "sha256:8375c240ab38138a19db07996c9808fb7a92047c1492e1ce587c2ef5112ad3a9", size = 5629237, upload-time = "2026-05-10T18:25:17.105Z" } sdist = { url = "https://files.pythonhosted.org/packages/33/3b/45be6b37d5060d6917bf7f1f234c00d360fc5f8b7486f8a96af640e25661/ty-0.0.38.tar.gz", hash = "sha256:fbc8d47f7630457669ab41e333dc093897fdb7ead1ffc94dcf8f30b5d39aa56d", size = 5681218, upload-time = "2026-05-20T00:15:32.781Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/84/19662ee881675815b7fafff940a365be1985730465afd9b75cb2edd5f8b3/ty-0.0.35-py3-none-linux_armv6l.whl", hash = "sha256:85ae1e59b9fb0b40e9d84fe61b29653c5f2f5e78b487ece371a7a38c20c781cf", size = 11198741, upload-time = "2026-05-10T18:24:49.378Z" }, { url = "https://files.pythonhosted.org/packages/54/43/ea9b4e57d6a266670dbe34858e92f6093ca054ad1b48f1c82580a72340fb/ty-0.0.38-py3-none-linux_armv6l.whl", hash = "sha256:3501dcf44ca03f813f9cb4fabfdf601adc0ac1337c411405b470530679e37a45", size = 11289326, upload-time = "2026-05-20T00:14:52.371Z" },
{ url = "https://files.pythonhosted.org/packages/62/df/7e5b6f83d85b4d2e5b72b5dceb388f440acc10679417bd46f829b9200fab/ty-0.0.35-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:709dbb7af4fcadb1196863c00b8791bbbbcc9dacbe15a0ff17f0af82b35d415b", size = 10948304, upload-time = "2026-05-10T18:24:58.246Z" }, { url = "https://files.pythonhosted.org/packages/5d/ff/24e2f623a1c6b5f5ccf8bf82fccd937033c6a7dba57a4028c7f41270fa4a/ty-0.0.38-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b34b4094b76252c3e8c90762cdd5e8a9f1101534484745ff4b480f71eb38ac2e", size = 11063047, upload-time = "2026-05-20T00:14:42.832Z" },
{ url = "https://files.pythonhosted.org/packages/59/94/72d7263aca055cde427f0ebcf08d6a74e5a5fee1d1e7fdd553696089cecb/ty-0.0.35-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2cb0877419ab0c8708b6925cb0c2800b263842bd3c425113f200538772f3a0cc", size = 10407413, upload-time = "2026-05-10T18:24:37.422Z" }, { url = "https://files.pythonhosted.org/packages/e9/41/4f0d910f0acbd20b358eda80a5cd6a8361d27ff5b8e87ab559d3f69f125e/ty-0.0.38-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c518ad33a877677365baab2e21d82cf59ffee789203a15a143f5179ee5a1d3f8", size = 10494436, upload-time = "2026-05-20T00:15:24.425Z" },
{ url = "https://files.pythonhosted.org/packages/b6/23/fda6fae8a81ce0cb5f24cdfe63260e110c7af8844e31fa07d1e6e8ef0232/ty-0.0.35-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7afbcfc61904b7e82e7fe1a1db832a40d8f01e69dee1775f6594e552980536c", size = 10932614, upload-time = "2026-05-10T18:24:47.401Z" }, { url = "https://files.pythonhosted.org/packages/69/d8/da06833422082aa98b169a391f9197e2d73865e96c90b6979ac886b890a2/ty-0.0.38-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9238494722303eccddc6a27eb647948b694eecd6b974910d13b9e6cd46bbeb6a", size = 11000992, upload-time = "2026-05-20T00:14:58.368Z" },
{ url = "https://files.pythonhosted.org/packages/72/3d/b98d8d4aa1a5ed6daaf15864e838f605ca7b1e8b93b7e17b96ed4bc4dfed/ty-0.0.35-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b61498cc3e4178031c079951257fbdb209a891b4feb10ad6c40f615a51846f41", size = 10962982, upload-time = "2026-05-10T18:24:44.88Z" }, { url = "https://files.pythonhosted.org/packages/16/f7/e1172197fb827e6410ca3eb0dc68ef2789f3c70683696f2a0ce5c90764fd/ty-0.0.38-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d91d7336c5d51bf822ac0df512f300584ca4dcca041fc6a6d7df03a8ddbb31", size = 11058583, upload-time = "2026-05-20T00:15:11.314Z" },
{ url = "https://files.pythonhosted.org/packages/18/c4/2881aad71bf6fb2f8df17fc8e4bc89e904e54490a3ee747b5ef73f98ac85/ty-0.0.35-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573b1eacda349fc8dba0d767b41631c3a6f66412363127c5bf2b1b40a1d898d2", size = 11476274, upload-time = "2026-05-10T18:24:42.4Z" }, { url = "https://files.pythonhosted.org/packages/5b/61/7fbaf0c05981e006a8804287819c574dff90a6bf8e96efad7226be0700aa/ty-0.0.38-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65165879814993450710b9349791e4898c65e36b1e14eec554884c06a2f20ff1", size = 11531036, upload-time = "2026-05-20T00:15:14.62Z" },
{ url = "https://files.pythonhosted.org/packages/34/0f/7717650adaeaddd23eea70470e2c26d3f0b9b18fdc7f26ec9552d6001f17/ty-0.0.35-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7209746158d6393c1040aa64b3ca29622e212ea7d8bae22ba50dbcbb4f96f0a", size = 12012027, upload-time = "2026-05-10T18:25:00.752Z" }, { url = "https://files.pythonhosted.org/packages/49/e3/47c0c64e401d50f925df3e52479d4e7626754b2a9e38201d142fdacd6252/ty-0.0.38-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d61868b8d1c4033bf8088191de953fed245c2f9e1bb9d2d53e5699170b0924c", size = 12129991, upload-time = "2026-05-20T00:14:39.475Z" },
{ url = "https://files.pythonhosted.org/packages/22/c9/1a16cb4aab6f4707d8f550772e91abc26d1c8870f19b5e2453ad10bb8209/ty-0.0.35-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4466a1470aa4418d49a9aa45d9da7de42033addd0a2837c5b2b0eb71d3c2bcd3", size = 11648894, upload-time = "2026-05-10T18:25:12.44Z" }, { url = "https://files.pythonhosted.org/packages/90/99/2f452d02901bcd7f1b109cf5b848727ce37f372c3406143aa52d1305d40e/ty-0.0.38-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f9a9175548c98dbff7707865738c07c2b1f8e07a09b8c68101baebb5dac59a4", size = 11756167, upload-time = "2026-05-20T00:15:27.526Z" },
{ url = "https://files.pythonhosted.org/packages/18/a1/a977c0e07e9f88db9c67f90c6342a4dc4422c8091fa07bf26521870687c5/ty-0.0.35-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb44bb742d52c309dcaa6598bcf4d82eb4bf1241b9e4940461e522e30093fe8b", size = 11560482, upload-time = "2026-05-10T18:25:05.172Z" }, { url = "https://files.pythonhosted.org/packages/dd/0c/c7e14d111c813e1a20b82e944f1c997c4631a2bb710eaa64fb6b26835e13/ty-0.0.38-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375d3a964c6b4aea2e9237fdb5eb9ed03dc43088986a94209a28a4ea3b62001c", size = 11637099, upload-time = "2026-05-20T00:15:21.261Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c1/a5fb11227d5cc4ac3f29a115d8c8bc817578e8ef6907d1e4c914ddbf45ee/ty-0.0.35-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:34b219250736c989b2670a03782c61315f523f3a2be37f1f90b1207e2212c188", size = 11718495, upload-time = "2026-05-10T18:24:54.12Z" }, { url = "https://files.pythonhosted.org/packages/37/de/ab02659dd1ed62898db7db4d37f9937c80854dd45e95093fa0fe10328d82/ty-0.0.38-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:cdfd547782c45267aa0b52abad31bd406bf4768c264532ef9e2360cd3c6ce048", size = 11813583, upload-time = "2026-05-20T00:14:45.875Z" },
{ url = "https://files.pythonhosted.org/packages/3c/cb/e92e4317388b6d1fd821a46941b448a8a1ff0bf13e22147c5167d8fa1b00/ty-0.0.35-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88e2ac497decc0940ef1a07571dee8a746112a93a09cdc7f8bca0099752e2e05", size = 10900815, upload-time = "2026-05-10T18:25:02.941Z" }, { url = "https://files.pythonhosted.org/packages/7e/57/bd1b5ebf4e71a4295484afac0202df1740b0807762b86744b1bef4534984/ty-0.0.38-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:858bc675b75626470abe4e6c3b3934b853642b04f2ac4d7139fcefea3b48b213", size = 10975405, upload-time = "2026-05-20T00:15:30.354Z" },
{ url = "https://files.pythonhosted.org/packages/e9/4f/03bd87388a92567f262f35ac64e10d2be047d258f2dfcf1405f500fa2b90/ty-0.0.35-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:02cae51b53e6ec17d5d827ff1a3a76fd119705b56a92156e04399eda6e911596", size = 10998051, upload-time = "2026-05-10T18:25:14.68Z" }, { url = "https://files.pythonhosted.org/packages/e7/55/0305c78711bbd23922cf291996a08ef9544f4179da98e9a75c14e608f379/ty-0.0.38-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54be4f00432870da42cd74fe145a3362fd248e22d032c74bd807cb45bf068f94", size = 11097551, upload-time = "2026-05-20T00:14:55.179Z" },
{ url = "https://files.pythonhosted.org/packages/b4/60/6edbc375ee6073973200096168f644e1081e5e55a7d42596826465b275de/ty-0.0.35-py3-none-musllinux_1_2_i686.whl", hash = "sha256:11871d730c9400d899ac0b9f3d660ed2e7e433377c8725549f8250a36a7f2620", size = 11148910, upload-time = "2026-05-10T18:24:51.842Z" }, { url = "https://files.pythonhosted.org/packages/7c/4f/7effe7f9a6ac9719eb7234172c01739c5f888bb47f9acc2ea8da1f4afed3/ty-0.0.38-py3-none-musllinux_1_2_i686.whl", hash = "sha256:494af66a76a86dbf16a3003d3b63b03484aa4c7489dfe11f3ee5413b98b22d60", size = 11214391, upload-time = "2026-05-20T00:15:18.094Z" },
{ url = "https://files.pythonhosted.org/packages/4d/b1/a845d2066ed521c477450f436d4bd353d107e7c02dd6536a485944aaf892/ty-0.0.35-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ad0a2f0530d0933dcc99ad36ac556c63e384ea72ab9a18d23ad2e2c9fd61c73", size = 11671005, upload-time = "2026-05-10T18:24:56.223Z" }, { url = "https://files.pythonhosted.org/packages/75/cd/d9fdfec3a74a6ad0209fa5e7113ae29d4f457d0651cfbb813b4c6563e0d4/ty-0.0.38-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3d92527c4be78a5ce6d32e8bb0aa2a6988d4076eddf1294e56fdaf06d1a98e7e", size = 11730871, upload-time = "2026-05-20T00:14:49.219Z" },
{ url = "https://files.pythonhosted.org/packages/73/81/1d5912a54fb66b2f95ac828ae61d422ef5afeae1263e4d231e40796c229f/ty-0.0.35-py3-none-win32.whl", hash = "sha256:0e25d63ec4ab116e7f6757e44d16ca9216bca679d19ecc36d119cf80faada61a", size = 10481096, upload-time = "2026-05-10T18:24:39.976Z" }, { url = "https://files.pythonhosted.org/packages/0e/4a/beefade12d109b4f7793d61b04b4478b1ad4d1465a719e7ff55b2d42461a/ty-0.0.38-py3-none-win32.whl", hash = "sha256:36fc5dd5dc09207ff3004b1560a79a3fb8d12456daeec914a7b802a918da654c", size = 10548583, upload-time = "2026-05-20T00:15:07.892Z" },
{ url = "https://files.pythonhosted.org/packages/3b/36/1c7f8632bfec1c321f01581d4c940a3617b24bd3e8b37c8a7363d33fbfc4/ty-0.0.35-py3-none-win_amd64.whl", hash = "sha256:6a0a6d259f6f2f8f2f954c6f013d4e0b5eba68af6b353bf19a47d59ec254a3d5", size = 11555691, upload-time = "2026-05-10T18:25:07.792Z" }, { url = "https://files.pythonhosted.org/packages/15/64/941b205e2e46cc2297c245c64aa7691410b7454fa4d07a6cb3cf59487833/ty-0.0.38-py3-none-win_amd64.whl", hash = "sha256:eef0a8956ba14514076b1a963d13eb32986d9ebad7f0527b3cc01cb68bf35147", size = 11650542, upload-time = "2026-05-20T00:15:01.441Z" },
{ url = "https://files.pythonhosted.org/packages/7a/fb/59325221bce52f6e833d6865ce8360ef7d5e1e21151b38df6dc77c4327a7/ty-0.0.35-py3-none-win_arm64.whl", hash = "sha256:619c52c0fb2aa21961a848a1995135ad3b6d0a9aa54da0194e60f679cc200e13", size = 10925457, upload-time = "2026-05-10T18:25:10.352Z" }, { url = "https://files.pythonhosted.org/packages/59/02/c1c4f9ec4b94d95190636fa13f79c32f65165fbe3a0503882d4df164d2ac/ty-0.0.38-py3-none-win_arm64.whl", hash = "sha256:79abfc8658a026c30b1c955613437dab3ef4b12feca56a3e6df50903cc39e07f", size = 11010307, upload-time = "2026-05-20T00:15:04.567Z" },
] ]
[[package]] [[package]]
@@ -1816,7 +1816,7 @@ wheels = [
[[package]] [[package]]
name = "zensical" name = "zensical"
version = "0.0.41" version = "0.0.43"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
@@ -1828,18 +1828,18 @@ dependencies = [
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "tomli" }, { name = "tomli" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/89/d6/b3e931233e53a2377ef5915cc6e786845c3263306874a469af8fb569ef9c/zensical-0.0.41.tar.gz", hash = "sha256:6c3c90301123749dfc26a210d6c080f0691253c7c765ad308a10b4518369a6fe", size = 3927788, upload-time = "2026-05-09T14:35:29.005Z" } sdist = { url = "https://files.pythonhosted.org/packages/d4/85/ec45162e7824a8f879d887ef0774ee65926bf7d1064e2eebccc7eaee3378/zensical-0.0.43.tar.gz", hash = "sha256:dc2d3804ff562795c1024130e0c3ce79736467930729dda314f096d0e35b98c8", size = 3932396, upload-time = "2026-05-19T09:44:07.418Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/72/08/ee18207c9b4e3ada74a0f4adf253bea90da39ae43772761cd91072e3a1fc/zensical-0.0.41-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f06a0015dcfdf7aeca73f4998a401db65db0ae2dd72da9629a7be8f9a4d0b7b6", size = 12701539, upload-time = "2026-05-09T14:34:48.6Z" }, { url = "https://files.pythonhosted.org/packages/55/c2/55e0709607ae41c266987c3b91a1a9702b37fbbef0d07eddfe5e25c2d823/zensical-0.0.43-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:17c335362b6bac3a50178181694a964f6d9f0c516fc532129ba5a0a5c4103fb6", size = 12706531, upload-time = "2026-05-19T09:43:32.729Z" },
{ url = "https://files.pythonhosted.org/packages/4c/93/d4635fbbce8171cf71dd64285d9f6d5773a2b624b928f1dd8acaf1ee9f9f/zensical-0.0.41-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:4e524ce68c9ff082ffaded9f742407097cf51bab692b7bc18d3c174b966174fe", size = 12560038, upload-time = "2026-05-09T14:34:51.666Z" }, { url = "https://files.pythonhosted.org/packages/2c/64/ce8627bc5ea30556162b29b041fe97d6a6aef2a87b51f12def628e4fa608/zensical-0.0.43-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8fe97f185194215f6193af45a17d2b30ebd72c8113e3650f2d7d6767b9c2206", size = 12563012, upload-time = "2026-05-19T09:43:35.962Z" },
{ url = "https://files.pythonhosted.org/packages/f2/4a/1730a30377bbb0914ed740e0e289d379b0552673b6cf912aefe7a205440c/zensical-0.0.41-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4afe35331cd2394c408cd362458936479cc0ed4fb272478498e4794aafc7414", size = 12942926, upload-time = "2026-05-09T14:34:54.393Z" }, { url = "https://files.pythonhosted.org/packages/66/d1/533bc9454f0e06b3d9d8bd2e7ac405308c3d4dee6572acab98f0ed6d1c07/zensical-0.0.43-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c4c85978c765b3e7f347e8102dfe1373d4bbe4229d7008b6bdbf352f1fbcd7f", size = 12947599, upload-time = "2026-05-19T09:43:38.754Z" },
{ url = "https://files.pythonhosted.org/packages/32/e3/d9a0416ef4edc043ce9f404a66f1934f102bcb645b103abb26b180ba5680/zensical-0.0.41-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a850285050f03aeb3b67ce7d99943093059fe8d32fc7731fa9f27be45c64cc", size = 12912711, upload-time = "2026-05-09T14:34:57.174Z" }, { url = "https://files.pythonhosted.org/packages/75/a0/94f47d6fb592997be7ab9526938c929f0199adf2637c3c2b2b9b2101b28e/zensical-0.0.43-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90d7c06ffd07b2bdf78bef041d541baba8a3ea51fd2dd84dbdbc5b0229076524", size = 12904911, upload-time = "2026-05-19T09:43:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/68/d0/775852783bef835425306a2fcd8236ef14fd19160e1b4261e192bf2d9f54/zensical-0.0.41-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35052e9dbefabe3a71c4836cfc4afa6c9469e5eeddc2a3ee750803ae3fe777dc", size = 13275869, upload-time = "2026-05-09T14:34:59.93Z" }, { url = "https://files.pythonhosted.org/packages/96/fb/1db3ad9a86ff772f74a8bc60ad5b447aa02a158e70f94adacf50bdd5c40f/zensical-0.0.43-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60022f4a6b95e46ec0023f51052fcd491743b3ebd08c0066b22a5cf1e741fecd", size = 13269386, upload-time = "2026-05-19T09:43:45.387Z" },
{ url = "https://files.pythonhosted.org/packages/c3/95/554273cc09a270ced0213d3e0aac8b3fc2b472fc2b26771d56fc8fd55047/zensical-0.0.41-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47f459205fb55f64dcb6c65e9f3c2fa00a2b4306c5ef1b71b9a50c45007071d", size = 12980177, upload-time = "2026-05-09T14:35:02.81Z" }, { url = "https://files.pythonhosted.org/packages/31/ee/b24fd0f94885519d851c35615b086d069a1077b0198021a56755395a4633/zensical-0.0.43-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e278eb948a0b7545d50609d713c7c27e366dade4523ff73a311a5d5f136518a", size = 12999364, upload-time = "2026-05-19T09:43:48.549Z" },
{ url = "https://files.pythonhosted.org/packages/ec/b5/d74d5040b3121db5c72b0134f0455641b90b1277fb1330a8e5e0029ca8d3/zensical-0.0.41-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:aa3b3b3a4e6f75f6bb3c1aca1fad7a96cebf54cbd4e31122f6876503b8801666", size = 13119629, upload-time = "2026-05-09T14:35:07.105Z" }, { url = "https://files.pythonhosted.org/packages/28/78/401ccd7afd9d2690f81b5319b7f1eed05108154ce20e4207053914518c1c/zensical-0.0.43-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b85e5ab99fbda13823e67c43a4be6e5ebda6600602969c6575e143f20ac203fd", size = 13124392, upload-time = "2026-05-19T09:43:50.965Z" },
{ url = "https://files.pythonhosted.org/packages/62/9a/93527acd7750092d7fca2e6c43fe2b8f1e85e1c96a1002baf6a08201c6f7/zensical-0.0.41-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:565133fd48b2ce939698c174c0c1c6470407a8fb6a90a2bb0eeec97cd4344444", size = 13182183, upload-time = "2026-05-09T14:35:10.105Z" }, { url = "https://files.pythonhosted.org/packages/98/b3/9af6eba5826b0ef143fc8308bd1e219e221441e307a958e39f824ba9ab53/zensical-0.0.43-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:751385accc92cccfd4560dabed7c423870686ef6ede244a67e5c96286af25e8f", size = 13177538, upload-time = "2026-05-19T09:43:53.964Z" },
{ url = "https://files.pythonhosted.org/packages/b2/7e/d77e4c809bfcbad40db85a6a7beeda2ee5c964232e0186783c3a837a7d0b/zensical-0.0.41-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:cec0a2b05eaaace0c7424bab3f2884da03ade212cac4ba4487c58691ec13ec65", size = 13330444, upload-time = "2026-05-09T14:35:13.245Z" }, { url = "https://files.pythonhosted.org/packages/be/6b/cd090bd6659d32692487206469988ee84d41aa6de4cdf9e380f847da90e2/zensical-0.0.43-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:dd3ff5bfa6e65cf3d2550dc639c3da2a3bfa11087b83d57e06623c4c1607d583", size = 13327086, upload-time = "2026-05-19T09:43:56.8Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e8/ecbb7e34bff88aa892c676b8b2e2ddf425f94d66cbb84b80016095191b77/zensical-0.0.41-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1736f0cb7686628cc6f53952d208423f20b542f0c16b0c2ddd7e702bf6e41fdd", size = 13263093, upload-time = "2026-05-09T14:35:20.826Z" }, { url = "https://files.pythonhosted.org/packages/79/5b/ac2555354b5a53cb9c2c942811905c47be0b9f5603d3c1328ee8564333eb/zensical-0.0.43-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85055a115b12f49c6ab194dcf04f966fc06b690ed6a8ddddd819929fc5f340e6", size = 13284645, upload-time = "2026-05-19T09:43:59.329Z" },
{ url = "https://files.pythonhosted.org/packages/c1/6f/48b2f81ce708d19bb807d94716f2772ec4b74389b6d29024669fc470df08/zensical-0.0.41-cp310-abi3-win32.whl", hash = "sha256:34a78645c68fba152faacb66516c895283166154f8b15b61440a6c21c84f0974", size = 12253644, upload-time = "2026-05-09T14:35:23.598Z" }, { url = "https://files.pythonhosted.org/packages/d0/c6/1688ec6e5be15e3ab367d7804753291bfbdff3109b06e20c19ce30a7129c/zensical-0.0.43-cp310-abi3-win32.whl", hash = "sha256:8a75ddd4bb3cd3c4a8e71d2ebae44c5611fd636c1d355c6124dd96e2f9c52838", size = 12256740, upload-time = "2026-05-19T09:44:02.102Z" },
{ url = "https://files.pythonhosted.org/packages/a0/92/5cf943133f61b996965743deeaff467f278135521f58d83ca68d2601ded3/zensical-0.0.41-cp310-abi3-win_amd64.whl", hash = "sha256:00d80cd573152e0efb655143bbdfe8788eb4b33167a802639fdb1b1800b724ac", size = 12483190, upload-time = "2026-05-09T14:35:26.43Z" }, { url = "https://files.pythonhosted.org/packages/ca/a8/d967e70eac810a7e9eb8c5150d6d02848a1f42260f42977c71debed3cb02/zensical-0.0.43-cp310-abi3-win_amd64.whl", hash = "sha256:03a9d1744a6394ad66c355d6f1de04cfd92efa525b0b94bf6dbf6971c5cd2c6b", size = 12496166, upload-time = "2026-05-19T09:44:04.915Z" },
] ]