10 Commits
Author SHA1 Message Date
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1cee2ad080 ⬆ bump taskiq from 0.12.2 to 0.12.3 (#23)
Bumps [taskiq](https://github.com/taskiq-python/taskiq) from 0.12.2 to 0.12.3.
- [Release notes](https://github.com/taskiq-python/taskiq/releases)
- [Commits](https://github.com/taskiq-python/taskiq/compare/0.12.2...0.12.3)

---
updated-dependencies:
- dependency-name: taskiq
  dependency-version: 0.12.3
  dependency-type: direct:production
  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-08 14:26:13 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e98230276f ⬆ bump prek from 0.3.11 to 0.3.13 (#24)
Bumps [prek](https://github.com/j178/prek) from 0.3.11 to 0.3.13.
- [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.3.11...v0.3.13)

---
updated-dependencies:
- dependency-name: prek
  dependency-version: 0.3.13
  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-08 14:26:05 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2a02121a17 ⬆ bump zensical from 0.0.39 to 0.0.40 (#25)
Bumps [zensical](https://github.com/zensical/zensical) from 0.0.39 to 0.0.40.
- [Release notes](https://github.com/zensical/zensical/releases)
- [Commits](https://github.com/zensical/zensical/compare/v0.0.39...v0.0.40)

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