13 Commits
Author SHA1 Message Date
d3vyce 4fb99e358e Version 1.0.2 2026-05-04 13:22:22 -04:00
d3vyceandGitHub 66b418bff1 fix: replace assert with RuntimeError for missing startup and document lifecycle requirement (#16) 2026-05-04 19:09:32 +02:00
d3vyceandGitHub 84fff031df fix: correct stale metadata and remove outdated worker-side detection feature bullet (#14) 2026-05-04 19:04:12 +02:00
d3vyceandGitHub bede6f8a40 fix: skip deduplication for non-JSON-serializable kwargs instead of raising TypeError (#12) 2026-05-04 18:53:11 +02:00
d3vyceandGitHub 446071924e test: add edge case coverage for key fields, default TTL, and task lifecycle (#10) 2026-05-03 17:17:49 +02:00
d3vyce 3af968581d Version 1.0.1 2026-05-02 11:49:00 -04:00
d3vyce 1d15a36140 docs: add analytics + fix reference 2026-05-02 09:06:56 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
82a777ba14 ⬆ update fakeredis[lua] requirement from >=2.0.0 to >=2.35.1 (#6)
Updates the requirements on [fakeredis[lua]](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.0.0...v2.35.1)

---
updated-dependencies:
- dependency-name: fakeredis[lua]
  dependency-version: 2.35.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 14:59:49 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
de0d612f2c ⬆ bump ty from 0.0.33 to 0.0.34 (#7)
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.33 to 0.0.34.
- [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.33...0.0.34)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.34
  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-05-02 14:59:31 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11cea69f44 ⬆ update uv-build requirement from <0.12.0,>=0.10 to >=0.11.8,<0.12.0 (#8)
Updates the requirements on [uv-build](https://github.com/astral-sh/uv) to permit the latest version.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.10.0...0.11.8)

---
updated-dependencies:
- dependency-name: uv-build
  dependency-version: 0.11.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 14:59:20 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11515ec72d ⬆ bump actions/deploy-pages from 4 to 5 (#3)
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 14:58:56 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1c31cc2633 ⬆ bump actions/configure-pages from 5 to 6 (#4)
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 5 to 6.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 14:58:48 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f343ebe6a9 ⬆ bump actions/upload-pages-artifact from 4 to 5 (#5)
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 14:58:40 +02:00
11 changed files with 178 additions and 59 deletions
+3 -3
View File
@@ -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
+2 -2
View File
@@ -50,8 +50,8 @@ except DuplicateTaskError:
## Features ## Features
- **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis queue lock, before they reach the broker. - **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis lock, before they reach the broker.
- **Worker-side detection** — logs concurrent duplicate executions without raising, keeping `SmartRetryMiddleware` safe from retry storms. - **Atomic lock release** — lock is released on completion or error via a Lua check-and-delete; only the owning task can release its lock.
- **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label. - **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label.
- **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely. - **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely.
- **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments. - **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments.
+2 -2
View File
@@ -50,8 +50,8 @@ except DuplicateTaskError:
## Features ## Features
- **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis queue lock, before they reach the broker. - **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis lock, before they reach the broker.
- **Worker-side detection** — logs concurrent duplicate executions without raising, keeping `SmartRetryMiddleware` safe from retry storms. - **Atomic lock release** — lock is released on completion or error via a Lua check-and-delete; only the owning task can release its lock.
- **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label. - **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label.
- **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely. - **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely.
- **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments. - **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments.
+7
View File
@@ -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
View File
@@ -1,13 +1,14 @@
# API Reference # API Reference
## Middleware You can import them directly from `taskiq_deduplication`:
::: taskiq_deduplication.RedisDeduplicationMiddleware ```python
options: from taskiq_deduplication import (
show_source: false DuplicateTaskError,
RedisDeduplicationMiddleware,
)
```
## Exceptions ## ::: taskiq_deduplication.RedisDeduplicationMiddleware
::: taskiq_deduplication.DuplicateTaskError ## ::: taskiq_deduplication.DuplicateTaskError
options:
show_source: false
+6 -6
View File
@@ -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]
+2 -2
View File
@@ -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",
+27 -9
View File
@@ -56,7 +56,7 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
if self._redis is not None: if self._redis is not None:
await self._redis.aclose() await self._redis.aclose()
def _build_deduplication_key(self, message: TaskiqMessage) -> str: def _build_deduplication_key(self, message: TaskiqMessage) -> str | None:
explicit_key: str | None = message.labels.get(DEDUP_EXPLICIT_KEY_LABEL) explicit_key: str | None = message.labels.get(DEDUP_EXPLICIT_KEY_LABEL)
if explicit_key is not None: if explicit_key is not None:
return f"{self.key_prefix}:{explicit_key}" return f"{self.key_prefix}:{explicit_key}"
@@ -67,10 +67,13 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
if key_fields is not None if key_fields is not None
else message.kwargs else message.kwargs
) )
try:
payload = json.dumps( payload = json.dumps(
{"task": message.task_name, "kwargs": kwargs}, {"task": message.task_name, "kwargs": kwargs},
sort_keys=True, sort_keys=True,
) )
except TypeError:
return None
fingerprint = hashlib.sha256(payload.encode()).hexdigest()[:16] fingerprint = hashlib.sha256(payload.encode()).hexdigest()[:16]
return f"{self.key_prefix}:{fingerprint}" return f"{self.key_prefix}:{fingerprint}"
@@ -81,7 +84,10 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
return int(labels.get(DEDUP_TTL_LABEL, self.default_ttl)) return int(labels.get(DEDUP_TTL_LABEL, self.default_ttl))
async def _release_if_owned(self, key: str, task_id: str) -> None: async def _release_if_owned(self, key: str, task_id: str) -> None:
assert self._redis is not None if self._redis is None:
raise RuntimeError(
"RedisDeduplicationMiddleware.startup() was never called."
)
released = await check_and_delete(self._redis, key, task_id) released = await check_and_delete(self._redis, key, task_id)
if released: if released:
logger.debug("Released lock %s", key) logger.debug("Released lock %s", key)
@@ -92,8 +98,18 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
if not self._is_enabled(message.labels): if not self._is_enabled(message.labels):
return message return message
assert self._redis is not None if self._redis is None:
raise RuntimeError(
"RedisDeduplicationMiddleware.startup() was never called."
)
key = self._build_deduplication_key(message) key = self._build_deduplication_key(message)
if key is None:
logger.warning(
"Task %s has non-JSON-serializable kwargs; deduplication skipped."
" Use the deduplication_key label to deduplicate this task.",
message.task_name,
)
return message
ttl = self._get_ttl(message.labels) ttl = self._get_ttl(message.labels)
logger.debug("Acquiring lock %s for task %s", key, message.task_name) logger.debug("Acquiring lock %s for task %s", key, message.task_name)
@@ -118,9 +134,10 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
) -> None: ) -> None:
if not self._is_enabled(message.labels): if not self._is_enabled(message.labels):
return return
await self._release_if_owned( key = self._build_deduplication_key(message)
self._build_deduplication_key(message), message.task_id if key is None:
) return
await self._release_if_owned(key, message.task_id)
async def on_error( async def on_error(
self, self,
@@ -130,6 +147,7 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware):
) -> None: ) -> None:
if not self._is_enabled(message.labels): if not self._is_enabled(message.labels):
return return
await self._release_if_owned( key = self._build_deduplication_key(message)
self._build_deduplication_key(message), message.task_id if key is None:
) return
await self._release_if_owned(key, message.task_id)
+93 -1
View File
@@ -92,7 +92,45 @@ class TestDefaultBuildDeduplicationKey:
mw._redis = None mw._redis = None
m = make_message() m = make_message()
key = mw._build_deduplication_key(m) key = mw._build_deduplication_key(m)
assert key.startswith("myapp:locks:") assert key is not None and key.startswith("myapp:locks:")
def test_empty_kwargs_produces_consistent_key(self, middleware, make_message):
m1 = make_message(kwargs={})
m2 = make_message(kwargs={})
assert middleware._build_deduplication_key(
m1
) == middleware._build_deduplication_key(m2)
def test_key_fields_empty_list_ignores_all_kwargs(self, middleware, make_message):
m1 = make_message(kwargs={"a": 1}, labels={DEDUP_KEY_FIELDS_LABEL: []})
m2 = make_message(kwargs={"a": 999}, labels={DEDUP_KEY_FIELDS_LABEL: []})
assert middleware._build_deduplication_key(
m1
) == middleware._build_deduplication_key(m2)
def test_key_fields_absent_from_kwargs_are_ignored(self, middleware, make_message):
m1 = make_message(
kwargs={"order_id": 1}, labels={DEDUP_KEY_FIELDS_LABEL: ["user_id"]}
)
m2 = make_message(
kwargs={"order_id": 999}, labels={DEDUP_KEY_FIELDS_LABEL: ["user_id"]}
)
assert middleware._build_deduplication_key(
m1
) == middleware._build_deduplication_key(m2)
def test_explicit_key_takes_precedence_over_key_fields(
self, middleware, make_message
):
m = make_message(
kwargs={"a": 1},
labels={DEDUP_EXPLICIT_KEY_LABEL: "my-lock", DEDUP_KEY_FIELDS_LABEL: ["a"]},
)
assert middleware._build_deduplication_key(m) == "taskiq:deduplication:my-lock"
def test_non_serializable_kwargs_returns_none(self, middleware, make_message):
m = make_message(kwargs={"dt": object()})
assert middleware._build_deduplication_key(m) is None
class TestPreSend: class TestPreSend:
@@ -140,6 +178,25 @@ class TestPreSend:
await middleware.pre_send(make_message(kwargs={"x": 1})) await middleware.pre_send(make_message(kwargs={"x": 1}))
await middleware.pre_send(make_message(kwargs={"x": 2})) await middleware.pre_send(make_message(kwargs={"x": 2}))
@pytest.mark.anyio
async def test_non_serializable_kwargs_skips_deduplication(
self, middleware, make_message
):
msg1 = make_message(kwargs={"dt": object()})
msg2 = make_message(kwargs={"dt": object()})
await middleware.pre_send(msg1)
await middleware.pre_send(msg2) # should not raise
@pytest.mark.anyio
async def test_default_ttl_applied(self, fake_redis, make_message):
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost", default_ttl=77)
mw._redis = fake_redis
msg = make_message()
await mw.pre_send(msg)
key = mw._build_deduplication_key(msg)
ttl = await fake_redis.ttl(key)
assert 0 < ttl <= 77
class TestPostExecute: class TestPostExecute:
@pytest.mark.anyio @pytest.mark.anyio
@@ -166,6 +223,15 @@ class TestPostExecute:
await middleware.post_execute(disabled_msg, make_result()) await middleware.post_execute(disabled_msg, make_result())
assert await fake_redis.exists(key) assert await fake_redis.exists(key)
@pytest.mark.anyio
async def test_post_execute_after_ttl_expiry_is_safe(
self, middleware, fake_redis, make_message, make_result
):
msg = make_message()
await middleware.pre_send(msg)
await fake_redis.delete(middleware._build_deduplication_key(msg))
await middleware.post_execute(msg, make_result())
class TestOnError: class TestOnError:
@pytest.mark.anyio @pytest.mark.anyio
@@ -195,6 +261,26 @@ class TestOnError:
assert await fake_redis.exists(key) assert await fake_redis.exists(key)
class TestRedispatchAfterRelease:
@pytest.mark.anyio
async def test_redispatch_after_post_execute(
self, middleware, make_message, make_result
):
msg = make_message()
await middleware.pre_send(msg)
await middleware.post_execute(msg, make_result())
await middleware.pre_send(make_message())
@pytest.mark.anyio
async def test_redispatch_after_on_error(
self, middleware, make_message, make_result
):
msg = make_message()
await middleware.pre_send(msg)
await middleware.on_error(msg, make_result(is_err=True), RuntimeError("boom"))
await middleware.pre_send(make_message())
class TestAtomicRelease: class TestAtomicRelease:
@pytest.mark.anyio @pytest.mark.anyio
async def test_only_owner_can_release(self, middleware, fake_redis, make_message): async def test_only_owner_can_release(self, middleware, fake_redis, make_message):
@@ -240,3 +326,9 @@ class TestLifecycle:
async def test_shutdown_without_startup_is_safe(self): async def test_shutdown_without_startup_is_safe(self):
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost") mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
await mw.shutdown() await mw.shutdown()
@pytest.mark.anyio
async def test_pre_send_without_startup_raises_runtime_error(self, make_message):
mw = RedisDeduplicationMiddleware(redis_url="redis://localhost")
with pytest.raises(RuntimeError, match="startup"):
await mw.pre_send(make_message())
Generated
+22 -22
View File
@@ -1468,7 +1468,7 @@ wheels = [
[[package]] [[package]]
name = "taskiq-deduplication" name = "taskiq-deduplication"
version = "1.0.0" version = "1.0.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "redis" }, { name = "redis" },
@@ -1510,7 +1510,7 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "coverage", specifier = ">=7.0.0" }, { name = "coverage", specifier = ">=7.0.0" },
{ name = "fakeredis", extras = ["lua"], specifier = ">=2.0.0" }, { name = "fakeredis", extras = ["lua"], specifier = ">=2.35.1" },
{ name = "mkdocstrings-python", specifier = ">=2.0.2" }, { name = "mkdocstrings-python", specifier = ">=2.0.2" },
{ name = "prek", specifier = ">=0.3.8" }, { name = "prek", specifier = ">=0.3.8" },
{ name = "pytest", specifier = ">=8.0.0" }, { name = "pytest", specifier = ">=8.0.0" },
@@ -1518,7 +1518,7 @@ dev = [
{ name = "pytest-cov", specifier = ">=4.0.0" }, { name = "pytest-cov", specifier = ">=4.0.0" },
{ name = "ruff", specifier = ">=0.1.0" }, { name = "ruff", specifier = ">=0.1.0" },
{ name = "taskiq-deduplication" }, { name = "taskiq-deduplication" },
{ name = "ty", specifier = ">=0.0.1a0" }, { name = "ty", specifier = ">=0.0.34" },
{ name = "zensical", specifier = ">=0.0.30" }, { name = "zensical", specifier = ">=0.0.30" },
] ]
docs = [ docs = [
@@ -1527,7 +1527,7 @@ docs = [
] ]
tests = [ tests = [
{ name = "coverage", specifier = ">=7.0.0" }, { name = "coverage", specifier = ">=7.0.0" },
{ name = "fakeredis", extras = ["lua"], specifier = ">=2.0.0" }, { name = "fakeredis", extras = ["lua"], specifier = ">=2.35.1" },
{ name = "pytest", specifier = ">=8.0.0" }, { name = "pytest", specifier = ">=8.0.0" },
{ name = "pytest-anyio", specifier = ">=0.0.0" }, { name = "pytest-anyio", specifier = ">=0.0.0" },
{ name = "pytest-cov", specifier = ">=4.0.0" }, { name = "pytest-cov", specifier = ">=4.0.0" },
@@ -1598,26 +1598,26 @@ wheels = [
[[package]] [[package]]
name = "ty" name = "ty"
version = "0.0.33" version = "0.0.34"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/84/44/9478c50c266826c1bf30d1692e589755bffa8f1c0a3eb7af8a346c255991/ty-0.0.33.tar.gz", hash = "sha256:46d63bda07403322cb6c28ccfdd5536be916e13df725c29f7ccd0a21f06bd9e8", size = 5559373, upload-time = "2026-04-28T10:45:13.18Z" } sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/24/e287388c63a19191be26b32ff4dbd06029834068150ebe2532939bc4c851/ty-0.0.33-py3-none-linux_armv6l.whl", hash = "sha256:94d0a9d2234261a8911396d59e506b5923fe0971dbda43b9dcea287936887fcc", size = 11021308, upload-time = "2026-04-28T10:45:43.34Z" }, { url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" },
{ url = "https://files.pythonhosted.org/packages/00/ca/ba1eed819895bd239fba8ee35dfcd5fcb266c203b0914a17a59579096bb5/ty-0.0.33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4a2b5ba078f90de342f56b5f7979bb77c9b9b1d8625a041352ffc6ee93c4073", size = 10777272, upload-time = "2026-04-28T10:45:32.905Z" }, { url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" },
{ url = "https://files.pythonhosted.org/packages/25/a8/c3131d37b44b3fea1d6654a1c929a0cd0873822f77a90482b8ec28f6fbbd/ty-0.0.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84ff5707825e9af9668d2bcf66975f93e520a63b524ab494e3a8265735be2563", size = 10201078, upload-time = "2026-04-28T10:45:23.374Z" }, { url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" },
{ url = "https://files.pythonhosted.org/packages/7b/db/d8e37ff0045810cc65e1ff36aa0da0a2253c05659787ac987df8a16c7897/ty-0.0.33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e375285736f57886868e7af0b11c7b0ec5b6543fa15e7ad2a714fed9f077d4e0", size = 10732347, upload-time = "2026-04-28T10:45:21.444Z" }, { url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/e0/1a/20e83a412506a918e4684fc67b567cf7cc13b105470b3428cb23c3d5aa13/ty-0.0.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5680f6350c3b4e46b8bff6d7bb132366ea239463d6cad4892725d06046e65464", size = 10808238, upload-time = "2026-04-28T10:45:38.565Z" }, { url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" },
{ url = "https://files.pythonhosted.org/packages/5d/4b/d0a39f4464dc6cb4cc2c159473ce216bd1846bfb684c0323a3cb36dce5c6/ty-0.0.33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5535538bad8d0f7e62bcdff02197cdb30e41451d80b35d27e17d128f2e1dc5d", size = 11288348, upload-time = "2026-04-28T10:45:08.419Z" }, { url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" },
{ url = "https://files.pythonhosted.org/packages/35/7e/f1745e0f9583363d7a83d9a4990fc244f76ecc30840ddad83dc16a33c52d/ty-0.0.33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da196c42bbbc069e1e21e3e52107c061aa9660352dae57a41930690b56e2c02d", size = 11789907, upload-time = "2026-04-28T10:45:19.064Z" }, { url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" },
{ url = "https://files.pythonhosted.org/packages/a5/71/25f39f46a12d662859d45bc648555d0661044eb43db6b5648c9947487da9/ty-0.0.33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9281672921ef6d4460e03146b5e6c18cb1a3e3a3b8a1a88f6f33226d05a469b7", size = 11500774, upload-time = "2026-04-28T10:45:48.012Z" }, { url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" },
{ url = "https://files.pythonhosted.org/packages/94/ec/136959ecbb7c71cb90537f5aea441c73f4ab24612868a6ecdc9d7444d32d/ty-0.0.33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1b8f303f82da64e878108e764be3ecbcd7c9903ac0a7f7031614ed00b97ab", size = 11360314, upload-time = "2026-04-28T10:45:05.402Z" }, { url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" },
{ url = "https://files.pythonhosted.org/packages/cf/95/32809575c222f00beed498cb728e9290a0f5009f930025381bb7253b2206/ty-0.0.33-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:efe3af412c9ff67bce5fa37d0a2b0d8555c24072b145a5bac6c79637f1c83abe", size = 10707785, upload-time = "2026-04-28T10:45:10.836Z" }, { url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" },
{ url = "https://files.pythonhosted.org/packages/13/89/c8e9531f7aa4a093359e15fa32c8e1277fbbe90d16894d7c6032d29f4b34/ty-0.0.33-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aeec29c91ea768601747da546c3efc20b72c2fb1bd52bcc786a5c6eeff51d27b", size = 10834987, upload-time = "2026-04-28T10:45:40.738Z" }, { url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" },
{ url = "https://files.pythonhosted.org/packages/31/16/9835fbcf5338af1a1917bd28fdb8a7193c210b83f243aa286fa9f79cb3ad/ty-0.0.33-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a535977c52bbb5f7e96b8b70a6ad375ad077f4a9ff2492508ea3816a2b403819", size = 10968968, upload-time = "2026-04-28T10:45:30.26Z" }, { url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" },
{ url = "https://files.pythonhosted.org/packages/36/69/64c76aabc1bc70c7f24b686cd93c3407f8ea430905e395f59bf9603ef571/ty-0.0.33-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1d732facf39fcb221ba279d469c5040d37883e964f123b1563888efd34818180", size = 11458077, upload-time = "2026-04-28T10:45:45.971Z" }, { url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" },
{ url = "https://files.pythonhosted.org/packages/91/84/fae27b0c4718776a298690d31ca4cc1995f2e3e1c63a7b59e84c41498e9a/ty-0.0.33-py3-none-win32.whl", hash = "sha256:d90960b574428dc252f85e8598ec5fcb7f619794196b2fc95a90da075ed4681c", size = 10345364, upload-time = "2026-04-28T10:45:16.836Z" }, { url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" },
{ url = "https://files.pythonhosted.org/packages/3c/a0/a2938b23ae3e1a09a2d7c189e2ac5f7113676bae4e0e23948b568e18e5f8/ty-0.0.33-py3-none-win_amd64.whl", hash = "sha256:c1c3aec62c44de610c6e95f0a4e97ac3dbc07934bfdbf1fd90d758c9ff72f48e", size = 11342470, upload-time = "2026-04-28T10:45:26.455Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" },
{ url = "https://files.pythonhosted.org/packages/ab/62/7fb948aace38d2f6329261bb33c035a8484549c74f1db28649c7a4c6fed9/ty-0.0.33-py3-none-win_arm64.whl", hash = "sha256:0d44f99ba1b441e55e2aa301b2ac0a21112784931b46a5f66f4ea9efe5620d97", size = 10742673, upload-time = "2026-04-28T10:45:35.555Z" }, { url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" },
] ]
[[package]] [[package]]
+1
View File
@@ -7,6 +7,7 @@ copyright = "Copyright &copy; 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",