From 2ef3bfb1bc0a64256839904e5fe5012df0cb4efd Mon Sep 17 00:00:00 2001 From: Daniel van Flymen Date: Sat, 23 May 2026 14:10:53 +0200 Subject: [PATCH] Implement periodic task scheduling with cron and interval support --- README.md | 55 ++++++++++ example_node.py | 4 +- pyproject.toml | 2 + synapse_p2p/__init__.py | 7 ++ synapse_p2p/background.py | 63 ------------ synapse_p2p/node.py | 42 ++++---- synapse_p2p/periodic.py | 72 +++++++++++++ synapse_p2p/schedules.py | 126 +++++++++++++++++++++++ synapse_p2p/tests/test_background.py | 67 ------------ synapse_p2p/tests/test_node.py | 7 +- synapse_p2p/tests/test_periodic.py | 101 ++++++++++++++++++ synapse_p2p/tests/test_schedules.py | 37 +++++++ synapse_p2p/types.py | 10 +- uv.lock | 147 +++++++++++++++++++-------- 14 files changed, 544 insertions(+), 196 deletions(-) delete mode 100644 synapse_p2p/background.py create mode 100644 synapse_p2p/periodic.py create mode 100644 synapse_p2p/schedules.py delete mode 100644 synapse_p2p/tests/test_background.py create mode 100644 synapse_p2p/tests/test_periodic.py create mode 100644 synapse_p2p/tests/test_schedules.py diff --git a/README.md b/README.md index 10107cf..7ef85f5 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ It also ships with a CLI tool to monitor your swarms: - [Advertised artifacts and agent cards](#advertised-artifacts-and-agent-cards) - [Ask](#ask) - [Broadcast conversations](#broadcast) +- [Periodic tasks](#periodic-tasks) - [Heartbeats and liveness](#heartbeats) - [CLI](#cli) - [`sn watch`](#sn-watch) @@ -387,6 +388,60 @@ Why this matters: --- +## Periodic tasks + +Use `@node.periodic(...)` to run an async function on an interval, cron expression, or solar event while the node is running. Periodic tasks start with `await node.start()` or `node.run()`, and `await node.stop()` cancels scheduled and in-flight runs. + +```python +from synapse_p2p import Node, cron, every, solar + +node = Node(name="worker") + + +@node.periodic(every(seconds=30)) +async def refresh_cache() -> None: + print("refreshing cache") + + +@node.periodic(cron("0 9 * * mon-fri", tz="Europe/London")) +async def weekday_digest() -> None: + print("weekday digest") + + +@node.periodic(solar("civil_twilight_begin", latitude=51.5, longitude=-0.1, tz="Europe/London")) +async def dawn_check() -> None: + print("civil twilight has begun") + + +node.run() +``` + +For simple intervals, a number is shorthand for seconds: + +```python +@node.periodic(30) # equivalent to every(seconds=30) +async def refresh_cache() -> None: + ... +``` + +Built-in schedules: + +- `every(seconds=..., minutes=..., hours=..., days=...)` +- `cron("*/15 * * * *", tz="UTC")` +- `solar("sunrise", latitude=..., longitude=..., tz="UTC")` + +Solar events include `sunrise`, `sunset`, `solar_noon`, `civil_twilight_begin`, `civil_twilight_end`, `nautical_twilight_begin`, `nautical_twilight_end`, `astronomical_twilight_begin`, and `astronomical_twilight_end`. + +Notes: + +- The decorated function must be `async def` and take no arguments. +- The first run starts immediately when the node starts; later runs follow the schedule. +- Long-running tasks can overlap if a previous run is still active when the next scheduled time arrives. +- Exceptions are logged and do not stop future runs. +- Tasks added after `await node.start()` are scheduled immediately. + +--- + ## Heartbeats Nodes heartbeat known peers and mark stale peers offline. diff --git a/example_node.py b/example_node.py index f3217f3..78c7030 100644 --- a/example_node.py +++ b/example_node.py @@ -3,9 +3,9 @@ app = Node(port=9999) -@app.background(3) +@app.periodic(3) async def heartbeat(): - print("Running background task every 3 seconds") + print("Running periodic task every 3 seconds") @app.endpoint("sum") diff --git a/pyproject.toml b/pyproject.toml index 3df583a..b9403bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,8 @@ keywords = ["agents", "agent-to-agent", "multi-agent", "rpc", "p2p", "msgpack", authors = [{ name = "Daniel van Flymen", email = "vanflymen@gmail.com" }] requires-python = ">=3.10" dependencies = [ + "astral>=3.2", + "croniter>=6.0", "loguru", "msgpack", "rich>=13", diff --git a/synapse_p2p/__init__.py b/synapse_p2p/__init__.py index 5b936c8..454de7c 100644 --- a/synapse_p2p/__init__.py +++ b/synapse_p2p/__init__.py @@ -22,6 +22,7 @@ from synapse_p2p.client import Client from synapse_p2p.messages import RemoteProcedureCall, RPCError, RPCRequest, RPCResponse from synapse_p2p.node import Capability, Node +from synapse_p2p.schedules import CronSchedule, IntervalSchedule, SolarSchedule, cron, every, solar from synapse_p2p.serializers import BaseRPCSerializer, MessagePackRPCSerializer from synapse_p2p.types import ( AdvertisedArtifact, @@ -43,6 +44,7 @@ "BroadcastReply", "Client", "Connection", + "CronSchedule", "MessagePackRPCSerializer", "Node", "NodeKind", @@ -52,6 +54,11 @@ "RPCResponse", "RemoteProcedureCall", "ServedArtifact", + "IntervalSchedule", + "SolarSchedule", + "cron", + "every", + "solar", "__logo__", "__version__", ] diff --git a/synapse_p2p/background.py b/synapse_p2p/background.py deleted file mode 100644 index 1c5c878..0000000 --- a/synapse_p2p/background.py +++ /dev/null @@ -1,63 +0,0 @@ -import asyncio - -from loguru import logger - -from synapse_p2p.types import BackgroundTask - - -class BackgroundTaskHandler: - def __init__(self) -> None: - self.tasks: list[BackgroundTask] = [] - self._running: set[asyncio.Task] = set() - self._scheduled: set[asyncio.TimerHandle] = set() - self._started = False - - def add_task(self, task: BackgroundTask) -> None: - self.tasks.append(task) - - async def _run(self, task: BackgroundTask) -> None: - try: - await task.callable() - except asyncio.CancelledError: - raise - except Exception: - logger.exception("Background task {!r} raised", task.name) - - def _schedule(self, task: BackgroundTask) -> None: - if not self._started: - return - - # Keep a strong reference so the task isn't garbage-collected mid-run - # (https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task). - running = asyncio.create_task(self._run(task), name=task.name) - self._running.add(running) - running.add_done_callback(self._running.discard) - - handle: asyncio.TimerHandle | None = None - - def schedule_next() -> None: - if handle is not None: - self._scheduled.discard(handle) - self._schedule(task) - - handle = asyncio.get_running_loop().call_later(task.period, schedule_next) - self._scheduled.add(handle) - - def start(self) -> None: - if self._started: - return - self._started = True - for task in self.tasks: - self._schedule(task) - - async def stop(self) -> None: - self._started = False - for handle in self._scheduled: - handle.cancel() - self._scheduled.clear() - - for task in self._running: - task.cancel() - if self._running: - await asyncio.gather(*self._running, return_exceptions=True) - self._running.clear() diff --git a/synapse_p2p/node.py b/synapse_p2p/node.py index ec4ce20..093c9d8 100644 --- a/synapse_p2p/node.py +++ b/synapse_p2p/node.py @@ -13,21 +13,22 @@ from loguru import logger from synapse_p2p import __logo__ -from synapse_p2p.background import BackgroundTaskHandler from synapse_p2p.client import Client from synapse_p2p.exceptions import InvalidMessageError from synapse_p2p.framing import read_frame, write_frame from synapse_p2p.mdns import MdnsDiscovery from synapse_p2p.messages import RPCError, RPCRequest, RPCResponse from synapse_p2p.network import advertised_address +from synapse_p2p.periodic import PeriodicTaskHandler +from synapse_p2p.schedules import Schedule, every from synapse_p2p.serializers import BaseRPCSerializer, MessagePackRPCSerializer from synapse_p2p.types import ( - BackgroundTask, Broadcast, BroadcastReply, Connection, NodeKind, Peer, + PeriodicTask, ServedArtifact, build_connection_from_peer_name, ) @@ -107,7 +108,7 @@ def __init__( self.endpoint_directory: dict[str, Callable] = {} self.endpoint_metadata: dict[str, EndpointMetadata] = {} self.max_upload_size = max_upload_size - self.background_executor = BackgroundTaskHandler() + self.periodic_executor = PeriodicTaskHandler() self.serializer_class = serializer_class self._listener: asyncio.Server | None = None self._register_system_endpoints() @@ -309,9 +310,9 @@ def _print_startup(self) -> None: for endpoint in self.endpoint_directory: print(f"- {endpoint}") - if self.background_executor.tasks: - print("\nBackground Tasks:") - for task in self.background_executor.tasks: + if self.periodic_executor.tasks: + print("\nPeriodic Tasks:") + for task in self.periodic_executor.tasks: print(f"- {task.name} ({task.period}s)") print() @@ -325,14 +326,14 @@ async def start(self) -> asyncio.Server: if socket is not None: _bound_address, self.port = socket.getsockname()[:2] self.address = advertised_address(self.bind, self.advertise) - self.background_executor.start() + self.periodic_executor.start() if self.mdns is not None: await self.mdns.start() return self._listener async def stop(self) -> None: - """Stop accepting connections and cancel background tasks.""" - await self.background_executor.stop() + """Stop accepting connections and cancel periodic tasks.""" + await self.periodic_executor.stop() if self.mdns is not None: await self.mdns.stop() if self._listener is None: @@ -440,18 +441,18 @@ async def _reap_stale_peers(self) -> None: def _register_lifecycle_tasks(self) -> None: if self.heartbeat_interval is None: return - self.background_executor.add_task( - BackgroundTask( + self.periodic_executor.add_task( + PeriodicTask( name="_synapse.heartbeat", callable=self._send_heartbeats, - period=self.heartbeat_interval, + schedule=every(seconds=self.heartbeat_interval), ) ) - self.background_executor.add_task( - BackgroundTask( + self.periodic_executor.add_task( + PeriodicTask( name="_synapse.reap_stale_peers", callable=self._reap_stale_peers, - period=self.heartbeat_interval, + schedule=every(seconds=self.heartbeat_interval), ) ) @@ -647,12 +648,15 @@ def decorator(wrapped: Callable) -> Callable: return decorator - def background(self, period: float) -> Callable: - """Decorator to schedule a coroutine as a periodic background task.""" + def periodic(self, schedule: float | Schedule) -> Callable: + """Decorator to schedule a coroutine as a periodic task.""" + task_schedule = every(seconds=schedule) if isinstance(schedule, int | float) else schedule def decorator(wrapped: Callable) -> Callable: - self.background_executor.add_task( - BackgroundTask(name=wrapped.__name__, callable=wrapped, period=period) + if not inspect.iscoroutinefunction(wrapped): + raise TypeError("periodic task must be an async function") + self.periodic_executor.add_task( + PeriodicTask(name=wrapped.__name__, callable=wrapped, schedule=task_schedule) ) return wrapped diff --git a/synapse_p2p/periodic.py b/synapse_p2p/periodic.py new file mode 100644 index 0000000..43c9e27 --- /dev/null +++ b/synapse_p2p/periodic.py @@ -0,0 +1,72 @@ +import asyncio +from datetime import UTC, datetime + +from loguru import logger + +from synapse_p2p.types import PeriodicTask + + +class PeriodicTaskHandler: + def __init__(self) -> None: + self.tasks: list[PeriodicTask] = [] + self._running: set[asyncio.Task] = set() + self._scheduled: set[asyncio.TimerHandle] = set() + self._started = False + + def add_task(self, task: PeriodicTask) -> None: + self.tasks.append(task) + if self._started: + self._schedule(task, immediate=True) + + async def _run(self, task: PeriodicTask) -> None: + try: + await task.callable() + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Periodic task {!r} raised", task.name) + + def _schedule(self, task: PeriodicTask, *, immediate: bool = False) -> None: + if not self._started: + return + + delay = 0.0 + if not immediate: + now = datetime.now(UTC) + task.next_run = task.schedule.next_after(task.next_run or now) + delay = max(0.0, (task.next_run - now).total_seconds()) + + handle: asyncio.TimerHandle | None = None + + def run_and_schedule_next() -> None: + if handle is not None: + self._scheduled.discard(handle) + + # Keep a strong reference so the task isn't garbage-collected mid-run + # (https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task). + running = asyncio.create_task(self._run(task), name=task.name) + self._running.add(running) + running.add_done_callback(self._running.discard) + self._schedule(task) + + handle = asyncio.get_running_loop().call_later(delay, run_and_schedule_next) + self._scheduled.add(handle) + + def start(self) -> None: + if self._started: + return + self._started = True + for task in self.tasks: + self._schedule(task, immediate=True) + + async def stop(self) -> None: + self._started = False + for handle in self._scheduled: + handle.cancel() + self._scheduled.clear() + + for task in self._running: + task.cancel() + if self._running: + await asyncio.gather(*self._running, return_exceptions=True) + self._running.clear() diff --git a/synapse_p2p/schedules.py b/synapse_p2p/schedules.py new file mode 100644 index 0000000..e499804 --- /dev/null +++ b/synapse_p2p/schedules.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import UTC, datetime, timedelta, tzinfo +from typing import Protocol +from zoneinfo import ZoneInfo + +from astral import Depression, Observer +from astral.sun import dawn, dusk, noon, sunrise, sunset +from croniter import croniter + + +class Schedule(Protocol): + def next_after(self, now: datetime) -> datetime: + """Return the next scheduled run time after ``now``.""" + + +def _coerce_tz(tz: str | tzinfo | None) -> tzinfo: + if tz is None: + return UTC + if isinstance(tz, str): + return ZoneInfo(tz) + return tz + + +def _aware(now: datetime, tz: tzinfo) -> datetime: + if now.tzinfo is None: + return now.replace(tzinfo=tz) + return now.astimezone(tz) + + +@dataclass(frozen=True, slots=True) +class IntervalSchedule: + seconds: float + + def __post_init__(self) -> None: + if self.seconds <= 0: + raise ValueError("interval schedule must be greater than 0 seconds") + + def next_after(self, now: datetime) -> datetime: + return now + timedelta(seconds=self.seconds) + + +@dataclass(frozen=True, slots=True) +class CronSchedule: + expression: str + tz: tzinfo = UTC + day_or: bool = True + + def next_after(self, now: datetime) -> datetime: + base = _aware(now, self.tz) + return croniter(self.expression, base, day_or=self.day_or).get_next(datetime) + + +@dataclass(frozen=True, slots=True) +class SolarSchedule: + event: str + latitude: float + longitude: float + tz: tzinfo = UTC + + def next_after(self, now: datetime) -> datetime: + base = _aware(now, self.tz) + observer = Observer(latitude=self.latitude, longitude=self.longitude) + day = base.date() + + for _ in range(370): + candidate = self._event_time(observer, day) + if candidate > base: + return candidate + day += timedelta(days=1) + + raise RuntimeError(f"could not find next solar event {self.event!r}") + + def _event_time(self, observer: Observer, day) -> datetime: + match self.event: + case "sunrise": + return sunrise(observer, date=day, tzinfo=self.tz) + case "sunset": + return sunset(observer, date=day, tzinfo=self.tz) + case "solar_noon" | "noon": + return noon(observer, date=day, tzinfo=self.tz) + case "civil_twilight_begin" | "civil_dawn" | "dawn": + return dawn(observer, date=day, depression=Depression.CIVIL, tzinfo=self.tz) + case "civil_twilight_end" | "civil_dusk" | "dusk": + return dusk(observer, date=day, depression=Depression.CIVIL, tzinfo=self.tz) + case "nautical_twilight_begin" | "nautical_dawn": + return dawn(observer, date=day, depression=Depression.NAUTICAL, tzinfo=self.tz) + case "nautical_twilight_end" | "nautical_dusk": + return dusk(observer, date=day, depression=Depression.NAUTICAL, tzinfo=self.tz) + case "astronomical_twilight_begin" | "astronomical_dawn": + return dawn(observer, date=day, depression=Depression.ASTRONOMICAL, tzinfo=self.tz) + case "astronomical_twilight_end" | "astronomical_dusk": + return dusk(observer, date=day, depression=Depression.ASTRONOMICAL, tzinfo=self.tz) + case _: + raise ValueError(f"unknown solar event: {self.event}") + + +def every( + *, + seconds: float = 0, + minutes: float = 0, + hours: float = 0, + days: float = 0, +) -> IntervalSchedule: + total = seconds + minutes * 60 + hours * 3600 + days * 86400 + return IntervalSchedule(total) + + +def cron(expression: str, *, tz: str | tzinfo | None = None, day_or: bool = True) -> CronSchedule: + return CronSchedule(expression=expression, tz=_coerce_tz(tz), day_or=day_or) + + +def solar( + event: str, + *, + latitude: float, + longitude: float, + tz: str | tzinfo | None = None, +) -> SolarSchedule: + return SolarSchedule( + event=event, + latitude=latitude, + longitude=longitude, + tz=_coerce_tz(tz), + ) diff --git a/synapse_p2p/tests/test_background.py b/synapse_p2p/tests/test_background.py deleted file mode 100644 index 5114a3d..0000000 --- a/synapse_p2p/tests/test_background.py +++ /dev/null @@ -1,67 +0,0 @@ -import asyncio - -import pytest - -from synapse_p2p.background import BackgroundTaskHandler -from synapse_p2p.types import BackgroundTask - - -@pytest.mark.asyncio -async def test_background_task_exception_does_not_stop_scheduling(): - calls = 0 - - async def flaky(): - nonlocal calls - calls += 1 - raise RuntimeError("boom") - - handler = BackgroundTaskHandler() - handler.add_task(BackgroundTask(name="flaky", callable=flaky, period=0.01)) - handler.start() - - await asyncio.sleep(0.05) - - assert calls >= 2, "scheduler should keep firing despite the task raising" - - -@pytest.mark.asyncio -async def test_background_task_holds_strong_reference_until_done(): - started = asyncio.Event() - finish = asyncio.Event() - - async def gated(): - started.set() - await finish.wait() - - handler = BackgroundTaskHandler() - handler.add_task(BackgroundTask(name="gated", callable=gated, period=999)) - handler.start() - - await started.wait() - assert len(handler._running) == 1 - - finish.set() - await asyncio.sleep(0) - await asyncio.sleep(0) - assert handler._running == set() - - -@pytest.mark.asyncio -async def test_background_task_stop_cancels_future_runs(): - calls = 0 - - async def tick(): - nonlocal calls - calls += 1 - - handler = BackgroundTaskHandler() - handler.add_task(BackgroundTask(name="tick", callable=tick, period=0.01)) - handler.start() - await asyncio.sleep(0.02) - await handler.stop() - - calls_after_stop = calls - await asyncio.sleep(0.03) - - assert calls_after_stop >= 1 - assert calls == calls_after_stop diff --git a/synapse_p2p/tests/test_node.py b/synapse_p2p/tests/test_node.py index 41b3ebd..4bf45cb 100644 --- a/synapse_p2p/tests/test_node.py +++ b/synapse_p2p/tests/test_node.py @@ -25,12 +25,12 @@ async def ping(**kwargs): assert "ping" in node.endpoint_directory -def test_background_decorator_registers_task(node): - @node.background(5) +def test_periodic_decorator_registers_task(node): + @node.periodic(5) async def heartbeat(): pass - task = next(task for task in node.background_executor.tasks if task.name == "heartbeat") + task = next(task for task in node.periodic_executor.tasks if task.name == "heartbeat") assert task.period == 5 @@ -245,6 +245,7 @@ async def test_synapse_artifacts_endpoints(): RPCRequest(id="get", endpoint="_synapse.artifact.get", args=["agent-card"]), ) assert fetched.ok is True + assert isinstance(fetched.result, dict) assert fetched.result["content"] == {"name": "reviewer", "capabilities": ["code-review"]} diff --git a/synapse_p2p/tests/test_periodic.py b/synapse_p2p/tests/test_periodic.py new file mode 100644 index 0000000..d9c3f92 --- /dev/null +++ b/synapse_p2p/tests/test_periodic.py @@ -0,0 +1,101 @@ +import asyncio + +import pytest + +from synapse_p2p.node import Node +from synapse_p2p.periodic import PeriodicTaskHandler +from synapse_p2p.schedules import every +from synapse_p2p.types import PeriodicTask + + +@pytest.mark.asyncio +async def test_periodic_task_exception_does_not_stop_scheduling(): + calls = 0 + + async def flaky(): + nonlocal calls + calls += 1 + raise RuntimeError("boom") + + handler = PeriodicTaskHandler() + handler.add_task(PeriodicTask(name="flaky", callable=flaky, schedule=every(seconds=0.01))) + handler.start() + + await asyncio.sleep(0.05) + + assert calls >= 2, "scheduler should keep firing despite the task raising" + + +@pytest.mark.asyncio +async def test_periodic_task_holds_strong_reference_until_done(): + started = asyncio.Event() + finish = asyncio.Event() + + async def gated(): + started.set() + await finish.wait() + + handler = PeriodicTaskHandler() + handler.add_task(PeriodicTask(name="gated", callable=gated, schedule=every(seconds=999))) + handler.start() + + await started.wait() + assert len(handler._running) == 1 + + finish.set() + await asyncio.sleep(0) + await asyncio.sleep(0) + assert handler._running == set() + + +@pytest.mark.asyncio +async def test_periodic_task_stop_cancels_future_runs(): + calls = 0 + + async def tick(): + nonlocal calls + calls += 1 + + handler = PeriodicTaskHandler() + handler.add_task(PeriodicTask(name="tick", callable=tick, schedule=every(seconds=0.01))) + handler.start() + await asyncio.sleep(0.02) + await handler.stop() + + calls_after_stop = calls + await asyncio.sleep(0.03) + + assert calls_after_stop >= 1 + assert calls == calls_after_stop + + +@pytest.mark.asyncio +async def test_periodic_task_added_after_start_is_scheduled(): + called = asyncio.Event() + + async def late_task(): + called.set() + + handler = PeriodicTaskHandler() + handler.start() + handler.add_task( + PeriodicTask(name="late_task", callable=late_task, schedule=every(seconds=999)) + ) + + await asyncio.wait_for(called.wait(), timeout=0.1) + await handler.stop() + + +def test_periodic_task_rejects_non_positive_interval(): + with pytest.raises(ValueError, match="interval schedule"): + every(seconds=0) + + +def test_periodic_decorator_rejects_sync_functions(): + node = Node(bind="127.0.0.1") + + with pytest.raises(TypeError, match="async function"): + + @node.periodic(1) + def tick(): + pass diff --git a/synapse_p2p/tests/test_schedules.py b/synapse_p2p/tests/test_schedules.py new file mode 100644 index 0000000..562a0ea --- /dev/null +++ b/synapse_p2p/tests/test_schedules.py @@ -0,0 +1,37 @@ +from datetime import UTC, datetime, timedelta + +import pytest + +from synapse_p2p.schedules import cron, every, solar + + +def test_every_returns_next_interval(): + schedule = every(minutes=5) + now = datetime(2026, 1, 1, 12, 0, tzinfo=UTC) + + assert schedule.next_after(now) == now + timedelta(minutes=5) + + +def test_cron_returns_next_matching_datetime(): + schedule = cron("*/15 * * * *", tz="UTC") + now = datetime(2026, 1, 1, 12, 1, tzinfo=UTC) + + assert schedule.next_after(now) == datetime(2026, 1, 1, 12, 15, tzinfo=UTC) + + +def test_solar_supports_civil_twilight(): + schedule = solar("civil_twilight_begin", latitude=51.5, longitude=-0.1, tz="UTC") + now = datetime(2026, 6, 1, 0, 0, tzinfo=UTC) + + next_run = schedule.next_after(now) + + assert next_run.tzinfo is not None + assert next_run.date() == now.date() + assert 2 <= next_run.hour <= 4 + + +def test_solar_rejects_unknown_event(): + schedule = solar("not_real", latitude=51.5, longitude=-0.1, tz="UTC") + + with pytest.raises(ValueError, match="unknown solar event"): + schedule.next_after(datetime(2026, 1, 1, tzinfo=UTC)) diff --git a/synapse_p2p/types.py b/synapse_p2p/types.py index 766f682..99d7d0b 100644 --- a/synapse_p2p/types.py +++ b/synapse_p2p/types.py @@ -3,6 +3,7 @@ import time from collections.abc import Awaitable, Callable from dataclasses import asdict, dataclass, field +from datetime import datetime from enum import StrEnum from hashlib import sha256 from typing import Any @@ -27,10 +28,15 @@ class Connection: @dataclass(slots=True) -class BackgroundTask: +class PeriodicTask: name: str callable: Callable[..., Awaitable[Any]] - period: float + schedule: Any + next_run: datetime | None = None + + @property + def period(self) -> float | None: + return getattr(self.schedule, "seconds", None) @dataclass(slots=True) diff --git a/uv.lock b/uv.lock index 07344d8..74cc022 100644 --- a/uv.lock +++ b/uv.lock @@ -250,6 +250,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] +[[package]] +name = "astral" +version = "3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/d1/1adbf06a38dc339e41a1666f6c7135924594c20fd46e060fb263248c564d/astral-3.2.tar.gz", hash = "sha256:9b7c3b412e9e69d172cfb24be0e6addcc9f1bd01a28db8bebe66d75ccc533d88", size = 48075, upload-time = "2022-11-05T18:12:02.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/80/d6edd9c3259913cfe39aff2bea4da65de5ad0235a578405e37aabace5f2c/astral-3.2-py3-none-any.whl", hash = "sha256:cb7b49a3f0d4c64ae666be131276d2a3226134c598db10e672028cf8ff855f83", size = 38325, upload-time = "2022-11-05T18:12:01.164Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -728,6 +740,18 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "croniter" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/de/5832661ed55107b8a09af3f0a2e71e0957226a59eb1dcf0a445cce6daf20/croniter-6.2.2.tar.gz", hash = "sha256:ba60832a5ec8e12e51b8691c3309a113d1cf6526bdf1a48150ce8ec7a532d0ab", size = 113762, upload-time = "2026-03-15T08:43:48.112Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/39/783980e78cb92c2d7bdb1fc7dbc86e94ccc6d58224d76a7f1f51b6c51e30/croniter-6.2.2-py3-none-any.whl", hash = "sha256:a5d17b1060974d36251ea4faf388233eca8acf0d09cbd92d35f4c4ac8f279960", size = 45422, upload-time = "2026-03-15T08:43:46.626Z" }, +] + [[package]] name = "cryptography" version = "48.0.0" @@ -3328,6 +3352,8 @@ wheels = [ name = "synapse-p2p" source = { editable = "." } dependencies = [ + { name = "astral" }, + { name = "croniter" }, { name = "loguru" }, { name = "msgpack" }, { name = "rich" }, @@ -3353,6 +3379,8 @@ dev = [ [package.metadata] requires-dist = [ + { name = "astral", specifier = ">=3.2" }, + { name = "croniter", specifier = ">=6.0" }, { name = "loguru" }, { name = "msgpack" }, { name = "pydantic-ai", marker = "extra == 'examples'", specifier = ">=1.32" }, @@ -3623,6 +3651,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + [[package]] name = "uncalled-for" version = "0.3.2" @@ -4064,50 +4101,80 @@ wheels = [ [[package]] name = "zeroconf" -version = "0.149.13" +version = "0.149.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ifaddr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/f3/d34bbbdf6568951649562b1ed1ef2a1f748e9bfe1e0c16a5adb504576cfd/zeroconf-0.149.13.tar.gz", hash = "sha256:a48d7f7b857d383c89cb89ab4530801049dc43a75f6f5c91d4f411f5a0fce62e", size = 191989, upload-time = "2026-05-20T19:05:57.858Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/5b/e7343482ecc40d776f0f81a9a5e0fa5fb737017eb3eabaae29aae1613d34/zeroconf-0.149.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e506c52f49a9c01a29f2f015363bff1e471a258e996ba1496d308ad7848d0394", size = 1671669, upload-time = "2026-05-20T19:32:51.75Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/d2656352be1a2acd091a09b213318a1769093be02020e3e441e0a6833cfc/zeroconf-0.149.13-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb07e56e92700f2a338eb6a9b32624d5b8d44579fd2f7055a9224363b5550e3c", size = 2151582, upload-time = "2026-05-20T19:32:55.231Z" }, - { url = "https://files.pythonhosted.org/packages/01/b0/2ed24283687329eb8ad143d83419137c71811357a2de41b221f0b5212a2a/zeroconf-0.149.13-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0823e95d2acec5d4cc447b024334b637dd81bf4a2dcffc09d188ee7025c0e250", size = 1924168, upload-time = "2026-05-20T19:32:57.287Z" }, - { url = "https://files.pythonhosted.org/packages/be/8b/62a65f2377a98ca236cc0728ec6263e095a96c0b0c38d444a19a34139b84/zeroconf-0.149.13-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f12765527067a653cf2843d14a85ca005634a1f844d92e96c5af1de1f7999d", size = 2236288, upload-time = "2026-05-20T19:32:59.594Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f3/42333dfcabe24602b4a91e9fddc221ad18e401d7ad7f27f48b561b5ba8b8/zeroconf-0.149.13-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b84f165cbd20cc0fa2562e3494117b0beaea8453af39e084fa46e64f7edb71f", size = 2190825, upload-time = "2026-05-20T19:33:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/83/28/33d016e8d4653f3cdbffaba22122f5e74836e3f51d0bba85ea1c8ceacd02/zeroconf-0.149.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6275407140b4d435c1738438edd43a95040880479555b9c09ec628bf612ad281", size = 2178342, upload-time = "2026-05-20T19:33:03.941Z" }, - { url = "https://files.pythonhosted.org/packages/fa/af/15a6745c55c3713428a0478c48f37eb90394bf1fe3d3c98dbd275bcc5e4f/zeroconf-0.149.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8ccbd582671d47c0aaecdc80635a4ccf28ace0c7d2578dba90fed445fdb5b296", size = 1982840, upload-time = "2026-05-20T19:33:05.866Z" }, - { url = "https://files.pythonhosted.org/packages/f7/50/df0157864f627d651f90c3b69f9fb155d6f5d6649593890f20d5ce96c35e/zeroconf-0.149.13-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ec1ed00e9fdb44d231a751036134b1895bcc9a9eecace3521ab82c8331a62277", size = 2196607, upload-time = "2026-05-20T19:33:08.032Z" }, - { url = "https://files.pythonhosted.org/packages/06/85/1b2ee5325a0bc32ca90e27773302c687b86db003654920851bc753dc9f18/zeroconf-0.149.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc0e285c9873f078ca86723f5a3b002d5fb6655598f434f91902e8f5cd40d1e6", size = 2257358, upload-time = "2026-05-20T19:33:09.878Z" }, - { url = "https://files.pythonhosted.org/packages/9c/36/ca0c6b2766a4160efc68dcf3c8e19ac0f329d22001b435fedfc0740256bb/zeroconf-0.149.13-cp310-cp310-win32.whl", hash = "sha256:e51d9c49c85975ec79fbeb6c9bc0f8aeaad25d31f1c827cb9188ba3dd295d1b6", size = 1279392, upload-time = "2026-05-20T19:33:12.078Z" }, - { url = "https://files.pythonhosted.org/packages/08/2c/0e4f4aeab0297448efbd92324aecac3ddf28eb93826d311b0ecbd2b60e6f/zeroconf-0.149.13-cp310-cp310-win_amd64.whl", hash = "sha256:cc9566759dc2d55480e825ade40c94cac2c27d9c830a6249b440c69c567a36a6", size = 1480950, upload-time = "2026-05-20T19:33:14.417Z" }, - { url = "https://files.pythonhosted.org/packages/b3/36/47ee97bd5c133a795f98f68bb05fef6e441071fc4a25fcde4f4afa67ae55/zeroconf-0.149.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93d4c07edf8d6b39eb9da32b3e97250375c6dd9e1e8da55653dd9361e505ccab", size = 1666972, upload-time = "2026-05-20T19:33:16.737Z" }, - { url = "https://files.pythonhosted.org/packages/65/02/8c1adf05b86738cba15b3301792832a9b950641f854fc9d864085e115ee5/zeroconf-0.149.13-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3d69a3a1f1dc23dc6cacb4e7ee728bc1270dd2e8512309c1a5c4c3f6ef9b269", size = 2147317, upload-time = "2026-05-20T19:33:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/47/1b/b90f6ffc8a3afc6885416f2b41688c6b44b91d1df8eb40d80108256d4e7d/zeroconf-0.149.13-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:196341a9845bf58290450d93a5ce631bd08fb2486389ec7beef06ca19d31d2d8", size = 1917182, upload-time = "2026-05-20T19:33:21.379Z" }, - { url = "https://files.pythonhosted.org/packages/59/88/6a752b97061780254abd812a26154c882a741047e09c1cf353d5cd97f06a/zeroconf-0.149.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93a9b7b2c2f09139d6df0c80a682f937cc1b5108998608594ea604335e78bb5c", size = 2228986, upload-time = "2026-05-20T19:33:23.413Z" }, - { url = "https://files.pythonhosted.org/packages/86/2b/76243bff9e7344a3d7ae2e3bb210ef77a853c40480df3d4e560274626dc5/zeroconf-0.149.13-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c3a7afb349312f024e039409260b676d0982141cc713de4a17281f98c075297", size = 2188940, upload-time = "2026-05-20T19:33:25.834Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ed/4c243ebb8030713e38f140d437917c2453a96c09542147d42bf6d24ce601/zeroconf-0.149.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8bc357ff1a542f9b82501fdae1c6ae04a688a3c9f221f4fd892587ce5719cd6", size = 2172924, upload-time = "2026-05-20T19:33:28.015Z" }, - { url = "https://files.pythonhosted.org/packages/29/dc/f4cb3ec373e941231f0d9d7fee12de0aea08e7ec2878b7252c59fff80f89/zeroconf-0.149.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fe6a8c7a333801bd02af2a9c0ba31ee4d40e0047c43038e582ac980dded09991", size = 1971157, upload-time = "2026-05-20T19:33:30.41Z" }, - { url = "https://files.pythonhosted.org/packages/25/a0/67e65af2eedb04009454ac13aabe7d2983f472e074acfbe52b9e0ce08d8f/zeroconf-0.149.13-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ed136f1202d0b3da4914acb383b9c96e13d430f6ffabef3ec5d35d1fc2fa3c0d", size = 2194186, upload-time = "2026-05-20T19:33:32.667Z" }, - { url = "https://files.pythonhosted.org/packages/54/c5/0f191f32281dac7982ec0c067763bb4330addf1141d74ff070e75a50fffc/zeroconf-0.149.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15dea892189cf49a3268e5034c15677c3090700b93a7c37861a7d922da3bb26a", size = 2251266, upload-time = "2026-05-20T19:33:35.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/69/bf94f028bf562c87ee64f7b2a2a8993fd50257b1c51aa687126662eddda2/zeroconf-0.149.13-cp311-cp311-win32.whl", hash = "sha256:33e467710bf75d525cc743e2ae96b2a797b6d6bfc2c08dabb7f42a6267c28eaf", size = 1270707, upload-time = "2026-05-20T19:33:36.946Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4a/0182cb5c5a2c1d891ab862ca79aeb775fcec2eab9d0340c207241a845a52/zeroconf-0.149.13-cp311-cp311-win_amd64.whl", hash = "sha256:332071b69414f808bef057610cc7b7056818168263ed64761f6a704529284ad7", size = 1482687, upload-time = "2026-05-20T19:33:39.053Z" }, - { url = "https://files.pythonhosted.org/packages/fd/f4/aa41c9cfc9bef42c9d79926edb873c4dc0c186ca9d7dc835e02cbd82af91/zeroconf-0.149.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db520e278a0e14dd60b474dd98c4c3a62f772ab106548c0140971eb057b06df3", size = 1670550, upload-time = "2026-05-20T19:33:41.4Z" }, - { url = "https://files.pythonhosted.org/packages/6b/6e/0c1e45a591a524e57e781891a3423ab0eef74c6be4631b374070e324e4b7/zeroconf-0.149.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a265652131367ac10049e4788969d00262d831ac6d6d1f805f1c754663b7f0d", size = 2069774, upload-time = "2026-05-20T19:33:44.137Z" }, - { url = "https://files.pythonhosted.org/packages/74/9f/83d4dcfc7d887c2839c1551b98e5ddaeca4285b316dae7a9321a1540c6ad/zeroconf-0.149.13-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d5339a86dc25a37cc682ff09c6319ffab5f35b4a3282f5350f7028a8a46ddbef", size = 1873429, upload-time = "2026-05-20T19:33:46.22Z" }, - { url = "https://files.pythonhosted.org/packages/1f/cc/e7a5b53932ec999456e99df9b0faf569a77a38c0d4a2507c1b1648629a4d/zeroconf-0.149.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:478a94422fb8868102544d152f64eb2399b45a1e6432b08de825af30f16948b2", size = 2167113, upload-time = "2026-05-20T19:33:48.791Z" }, - { url = "https://files.pythonhosted.org/packages/de/8e/25f469e5090b5f7422af4cbcb195f4bd326ce1d2cdb96176d39242d482de/zeroconf-0.149.13-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8c6644ad04c657eb6a612fdd10fcae21302f1bcafaeda6a27a8acce09e23ad56", size = 2102615, upload-time = "2026-05-20T19:33:51.191Z" }, - { url = "https://files.pythonhosted.org/packages/36/fc/e4e76b55ff06f4b6020ba5feb7b0c2099e2ff23117d9da7c9fe150c1173b/zeroconf-0.149.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dd9cff86d1f6b733abeac75e0b08311d2007ef0fd0fdf611420d2dafa1ac6291", size = 2093594, upload-time = "2026-05-20T19:33:53.59Z" }, - { url = "https://files.pythonhosted.org/packages/28/6c/b3c76e76bf08325f96409d4851fe0d1b303abaf11c733db6c5db173d08a1/zeroconf-0.149.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:59e76e9f882023f67085732b2947042d179c8f1f1b92d5561a37070092393afc", size = 1961191, upload-time = "2026-05-20T19:33:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/ee/97/1cc353632c45e2e30270146cebd7429e2ad75145a82111de40c2f7ec3613/zeroconf-0.149.13-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ae639b026bdecb95b0f9a129c5565a6e04a1995ebece070486feca650c006ebb", size = 2107259, upload-time = "2026-05-20T19:33:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/70/75/865584002b301d9baa835a293efb1c4332edb29b86851a0c96db1148f89e/zeroconf-0.149.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f3dde0ba425307dac9a2514cb788ef057903dd529db88b405e0d565b3b5d4d47", size = 2188560, upload-time = "2026-05-20T19:34:01.231Z" }, - { url = "https://files.pythonhosted.org/packages/2a/80/fb4a87e67bbe192c5cd9826c95ddd83f52585da209c56fc64e57a3f23bce/zeroconf-0.149.13-cp312-cp312-win32.whl", hash = "sha256:ebb6cd451ccb7b7b24fc4196826d5da7eeec7f6fcadda6a103f65d1dc090155c", size = 1263581, upload-time = "2026-05-20T19:34:03.338Z" }, - { url = "https://files.pythonhosted.org/packages/ad/02/52d68af86eddf7dbe6904ed278b62a0da815e6c43a6cf18bd9deca8bc650/zeroconf-0.149.13-cp312-cp312-win_amd64.whl", hash = "sha256:834a3db6fd5878ed9b20124f55bdc45a7088b687e701bd05f31f1dec9e147de1", size = 1480834, upload-time = "2026-05-20T19:34:05.494Z" }, - { url = "https://files.pythonhosted.org/packages/16/e7/ea35e8753d6213b6e842896930a0f58b374065285fbbdcdc3f70662c7459/zeroconf-0.149.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1b4965d812f48e5759ea636291e0cfe0b93cfeb7556450e1fccca5b48286a3a3", size = 1657172, upload-time = "2026-05-20T19:34:08.042Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4f/557d00dcfaac455e9073a296f3fd7700c476e84dffb4530a20cac118ed5a/zeroconf-0.149.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7cc4e28dc082507037127eaf52607aae00d939808d5f3a5517c0922e47100b", size = 2064219, upload-time = "2026-05-20T19:34:10.086Z" }, - { url = "https://files.pythonhosted.org/packages/74/8b/1115f6413234b5dc268466382efd4ebb9d864583d1dc1921a58f98de2243/zeroconf-0.149.13-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e56a541db6a14ea2fe56c2386d1d424e31050768c892eaff320be40508604a1b", size = 1870278, upload-time = "2026-05-20T19:34:12.43Z" }, - { url = "https://files.pythonhosted.org/packages/11/de/8635ab823ed5952c9090d01fa69dfa3ffbd5c3756c77ae6a0f73b9658fb4/zeroconf-0.149.13-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:a8d4ea06e9c0b46dde7e9ef6a256f1c6a29bad70bd22872c07ac9f23ca43db12", size = 2170935, upload-time = "2026-05-20T19:05:54.818Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/83/34/c981e760690f7b7dc91532d4d4ad21e3922887aaa425a0e7bff8067152da/zeroconf-0.149.16.tar.gz", hash = "sha256:5e6b5a3b153c2cc2a8d9e6f6f189ec5638f7d9c86fc3e88a6c53eb6863761a5e", size = 196586, upload-time = "2026-05-21T14:04:17.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/d2/1d69613f0a05a13cf95e11ed49c018ec300a2929ac1adf650082a6dca716/zeroconf-0.149.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d42d6726c9d010e51d57fce8c5d4627a1b3d0589dfedfc55277a8fe0d127f83f", size = 1676520, upload-time = "2026-05-21T14:31:14.144Z" }, + { url = "https://files.pythonhosted.org/packages/09/84/59f4d11c2a91db810dc8c757860e06c9b0b20e6988a904faa1086ac09ff0/zeroconf-0.149.16-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e53b2754dd1a56c388a3f60a51000f658b690198ca7dd70ff6f6e7971f7ed9b", size = 2159375, upload-time = "2026-05-21T14:31:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/9d/36/409e54a4aaeec2a2667ff1a3c82b1ee6d42e4b925b3249ed1a6af650b783/zeroconf-0.149.16-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b306b239abcfde7467b8b667e50c626bb2c9be85ecfd5dbeea033ed7f76eb759", size = 1933094, upload-time = "2026-05-21T14:31:19.02Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5b/8991121f410f74215ccfac1d0e8b423361f089e29c7ab34fa96efeda1f94/zeroconf-0.149.16-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a99d2d4f9a0a58bd226b935ff0cec6fc854ea59d48c83890acd95d6aef5e634", size = 2243868, upload-time = "2026-05-21T14:31:20.872Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7a/2bd7d271bb817d5e4a20e089cdab97bb8d1298315604e519a223a1639385/zeroconf-0.149.16-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:848840d21c772987f6f25877bff34c1530b128a636d80a733018581eb0354430", size = 2197721, upload-time = "2026-05-21T14:31:22.744Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d1/a5df1f2406856f3150b17238fc758baf7849595978e794f59fcd224873b6/zeroconf-0.149.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b950eb95dc5b6aa92d9f626f9e7aff9c01137a54a1e453b52a68555fcaae3800", size = 2185241, upload-time = "2026-05-21T14:31:24.443Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7f/9efe38cf8d435835ec723c159645986b81a104c6396fe469750befcca0e6/zeroconf-0.149.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7c024873a6bf5b6f2db680480f1c4406c3369f032d1759dd2a26cca5f4e02c09", size = 1990225, upload-time = "2026-05-21T14:31:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/16/f7/06a6a7142e3870f07ca7698a88f004b945f2d243c99a2cc76ad13f5113d4/zeroconf-0.149.16-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9927cae46232685c903dfab028d0711151ca3cb56756795a268607eda4d1ec9f", size = 2204703, upload-time = "2026-05-21T14:31:28.157Z" }, + { url = "https://files.pythonhosted.org/packages/3e/50/12a8a80cc2ed065148abd042ecb2113901b9a1ecab2147ea260c516a7dc2/zeroconf-0.149.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a7de0b296ca902a92edfd513436e3ca8fa446c85d34482b6cd2b281c632b8cb9", size = 2264959, upload-time = "2026-05-21T14:31:29.972Z" }, + { url = "https://files.pythonhosted.org/packages/1c/84/ee6ad05d33333dad0ae10da7d9b299ee2528471b15fd564afb3e6f3b5ff7/zeroconf-0.149.16-cp310-cp310-win32.whl", hash = "sha256:0c410379b035d773082d09812e5517accf70eb604f816bbdd38c54f6549b3679", size = 1284784, upload-time = "2026-05-21T14:31:31.552Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e9/1ee0864eb8b9cbfa927e5f3bd3045dae35656817f81f32160e2892b362a9/zeroconf-0.149.16-cp310-cp310-win_amd64.whl", hash = "sha256:85c19ef841feab8f16be29f57550595f05ed123dcbf82afcc8a45c38f683864e", size = 1513734, upload-time = "2026-05-21T14:31:33.363Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4e/e0c1a9a0b5b80ab286c8269a653068960e3ec280bd54d5fd137bbf8960bd/zeroconf-0.149.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d04ed04cda62f787c0378270724333f5707f560b1fa51489ac4677a2fa99cb90", size = 1672025, upload-time = "2026-05-21T14:31:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/c4/76/db455e0663e2043f368f81b78387f678ff68beb0a18d3acd29b4fa1e5af4/zeroconf-0.149.16-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8b18624311eb11aeeea2ac85fd0ff2763d22f25a2d282110efb91b66c8e43b0", size = 2154441, upload-time = "2026-05-21T14:31:37.142Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f0/296c719ea8829f3f4ef457a80b3ebd40c7b75283482d3b18cabaf28552cb/zeroconf-0.149.16-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8457aee89e1e561835e1abd32816eb2cdf4888af54f4c15e2d94326c1a467bb7", size = 1924672, upload-time = "2026-05-21T14:31:38.907Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6a/1e8a987a2b2805a59d355cd2e1d5fce03bc1da8859a0efcad4edb26892af/zeroconf-0.149.16-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:414bf6758e4e389162a6d00c179c0eafd8932a36e58bb3791570eafbb194c28a", size = 2236440, upload-time = "2026-05-21T14:31:42.846Z" }, + { url = "https://files.pythonhosted.org/packages/c6/24/11364b8db6a5db39e8fdaf0d89923c5ac1f33478f5bf53cc76ba5ef05da6/zeroconf-0.149.16-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f9aaf85163be00178aeee4d02644d9ccbe8273ab52fa43e12d7d9f78a4d0cde", size = 2195767, upload-time = "2026-05-21T14:31:44.401Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3d/735eb897ba82213c307a04c4910b3924faf08669e4221f6f54744acc9aff/zeroconf-0.149.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f117a9b49d735cfa101514df6c1ca52d01ad98be55896d4b9cc63eed64840bea", size = 2179382, upload-time = "2026-05-21T14:31:46.324Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0a/7befcb3646d25051762218d911c554bbfd6492f3076c6f17ac3d02ca8e09/zeroconf-0.149.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:63c4f279d18ecb6bcd8d3b6c34c4ae3bfb34a57321b0303b35683e13a2048fd9", size = 1977895, upload-time = "2026-05-21T14:31:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/f6/89/378dcd9b3567597d560e2dbe92931653b6cf0e6f258492615aed3d5c6d8b/zeroconf-0.149.16-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8d2545501c6d4711fa9cfbe2f731f409156c40ddf5e822b5b461445e1b1aafdb", size = 2201422, upload-time = "2026-05-21T14:31:50.083Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/82e4902a95ca8c22094f8fbf9cec4712ef22ff7fc171bb4aa4bf0d51d6f1/zeroconf-0.149.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c96231f5f5fbd101c55257bb5e57c9a0bde36a6259104637c40afa941db0b367", size = 2258970, upload-time = "2026-05-21T14:31:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/42/a7/70b6e6e999dd9594d6205f88e1aeb50d9173a2648ef15f313f2e9b1902b9/zeroconf-0.149.16-cp311-cp311-win32.whl", hash = "sha256:dad44dfcf58b7919247e0d12bde93a05296dedcccf00bae772a98f9be5a854c0", size = 1275248, upload-time = "2026-05-21T14:31:54.836Z" }, + { url = "https://files.pythonhosted.org/packages/de/f4/678d0d04e27bc25db1cbe252d3055f2bb35df0e7e7b2428d4a84be8e6cbe/zeroconf-0.149.16-cp311-cp311-win_amd64.whl", hash = "sha256:7a1ad0c328fe79172ef654dd5fa79e819c757cde06e38e520a40b7cbbbfe0753", size = 1517265, upload-time = "2026-05-21T14:31:56.55Z" }, + { url = "https://files.pythonhosted.org/packages/62/3c/a34998e51b9923ce7446c06016df1dca4dce3fe9074d7673678dfb939fb7/zeroconf-0.149.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e1d403ca6145268664af9d87a7522f9ad7e5e388482db0d4b055553616564087", size = 1676206, upload-time = "2026-05-21T14:31:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/24/07eaf020c2f206dfd35378db721e81e9f48792d8f6297663763ca9739157/zeroconf-0.149.16-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50c9ec1fb46e336ca7b098d44cae5fc21baf9d44cd758b776c06b453b2d3d73e", size = 2076988, upload-time = "2026-05-21T14:32:00.114Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/3ea6bbd4151e2afc9249fa6c345c68f5fffafcb608a0147b3a4e49df75d3/zeroconf-0.149.16-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95b8681ab0a3209d04debc96818998bcc86642be32a17b17f6ecda5d7ff560f9", size = 1879896, upload-time = "2026-05-21T14:32:02.172Z" }, + { url = "https://files.pythonhosted.org/packages/ad/21/9dd0d3a9587155b7efd10d5f5484df836c2ec2b54ae719ef1c972a9a90a9/zeroconf-0.149.16-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:65b312685ef3e12284f907c07217df3c6873438313ce0e6cf53e01a182aaf0c4", size = 2175116, upload-time = "2026-05-21T14:32:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/452e418d4fadb10118ea381056099714eec5cec42d6fbee7f28eeebb41d2/zeroconf-0.149.16-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ca881fbf6edd21651b247710cdaa7b72593982bb65433bcf80c509df9550d12", size = 2110395, upload-time = "2026-05-21T14:32:06.524Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0f/1883b7bed80fd33627c802593b49377e2aa0a458c430ebc3710cfce062ac/zeroconf-0.149.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ea9acdb58407bb73c11cad6d4965bdc01bec3c2284727c4a482d444f943092c", size = 2101220, upload-time = "2026-05-21T14:32:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d6/460c6a4ebbd350935073fb7501b38d143791e8478d737934cae296fffc4a/zeroconf-0.149.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cf3e109ec0181bab93ac954b17f2921eebac28a97c81ebf3559aeaad7a3b7a67", size = 1969601, upload-time = "2026-05-21T14:32:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/4e/7e/f4bdad2ba7623e131907db61ce436bdb3bbef7e08917448170b87aa11446/zeroconf-0.149.16-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4fd08c930004ffaad2dc192762f9a2cd7f1db34b5fb1a36f38993dde2c4d55d9", size = 2114226, upload-time = "2026-05-21T14:32:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fd/7b5c0244c8db96e79c355ebb48865663ada68de096d02d09e743861b1662/zeroconf-0.149.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a46c8fc8a4133574484b24b6bef3ae46e7e0958167d2c44ee84cc2edff2b4c9f", size = 2196367, upload-time = "2026-05-21T14:32:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/3b/42/ba94573fa62df4e623d7e7a0ca68e2aab644c729e83c64117da39ef7f9a3/zeroconf-0.149.16-cp312-cp312-win32.whl", hash = "sha256:48e0847568b35d3ccce0eaf0546313d3f35541f794f244097e6c8e80e75ec78c", size = 1272101, upload-time = "2026-05-21T14:32:16.168Z" }, + { url = "https://files.pythonhosted.org/packages/03/bb/f829895ed725d58e5891632e7b2c507b4f88508d2d301b57452ce57190ab/zeroconf-0.149.16-cp312-cp312-win_amd64.whl", hash = "sha256:813c9f0223c97d67970b4013cedeea072a28c0809962d58750d00d502066d2d0", size = 1508797, upload-time = "2026-05-21T14:32:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/ebaa279ecdb453c7b09d4ee20fef74d8684e2d9b76ea5b6fca7bb39e0d73/zeroconf-0.149.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:575a548bdc820223e2360fdcf56fb1eea4d8345f286c441c3ef1cd8f68d853f1", size = 1662398, upload-time = "2026-05-21T14:32:19.878Z" }, + { url = "https://files.pythonhosted.org/packages/17/db/18b63b074b966f6374d03d8cd62a5276ab8796021dc798bf79c4723a04ab/zeroconf-0.149.16-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58efbb338f1ac44b62cfe92999e4476c2051288729de2f6ba8939c977acdd0c5", size = 2070137, upload-time = "2026-05-21T14:32:21.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/55/3f0803f6c6c006761b236117e440e00815c366568ffead49a99a83b10295/zeroconf-0.149.16-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a14610cc2eb90e1530e042183c9ea6981260cdcb97c4ecc7b11ca3749a433664", size = 1875696, upload-time = "2026-05-21T14:32:23.686Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9e/8a7eef9c9c4af9f5a90e95703db1adbfc932453d01679f719f84323bcf0a/zeroconf-0.149.16-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6cb2873ae74265e28756f83454d1fde53eaaed9bc380bb43ccb2ac42b4b5a5a", size = 2169690, upload-time = "2026-05-21T14:32:25.558Z" }, + { url = "https://files.pythonhosted.org/packages/6d/dc/0bd920ece38ca90053d330dee74a1f01089cb0eb7bc7632cf7e87f412311/zeroconf-0.149.16-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:74fedb7c4a073cdc410770e54c27e8c87072c5a4e68eb8d680c0111c84364c0d", size = 2106805, upload-time = "2026-05-21T14:32:27.821Z" }, + { url = "https://files.pythonhosted.org/packages/b5/14/26ce98ef1420aa46a5b02ef24349cf41218313726481949d67807213e218/zeroconf-0.149.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b76b7228db26895090ff0e156ef8e193039e971c443d3c14fa8589f08bae2c6e", size = 2100414, upload-time = "2026-05-21T14:32:29.67Z" }, + { url = "https://files.pythonhosted.org/packages/1e/aa/3a2a09a4f2699ddb7506ef0b802d41b6858d09224c42a9b672ea9db8a086/zeroconf-0.149.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cd4b56d4951d62a6e7629286877c123703ac27b2a35e7744a63cc860ee3aaf39", size = 1963115, upload-time = "2026-05-21T14:32:31.448Z" }, + { url = "https://files.pythonhosted.org/packages/24/d9/74f5e6f4530dfb615f3ecc1d23c4ac72b6c86b1bdd4a6fe9dab15466f609/zeroconf-0.149.16-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7c1003a992ef65a10a29194630a58c2bfd4a87b9117a161c2ad9a1773d8191e1", size = 2117608, upload-time = "2026-05-21T14:32:33.41Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cb4ed1b97ab3a4f4d8a328439c2050211f6826cefec2d0bd6e0bcd59b4bd/zeroconf-0.149.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:63d48d9ed65f2bf05d237fc42773dab74d8ddf6d241c88e65297769c5ba87931", size = 2193839, upload-time = "2026-05-21T14:32:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/be/d9/064323d0b9ba42e93c9afd9db3aa6d14c7ddbfe07dc242d897c5f9abf7b5/zeroconf-0.149.16-cp313-cp313-win32.whl", hash = "sha256:9dbff444474354460f19b592b1f57b72688f23455d8f208f616ca49e8ecbc4ef", size = 1269059, upload-time = "2026-05-21T14:32:37.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/77/c15f97a2e3e373bd6e1431238028700f5537667e502d7fa0b218619ac6ad/zeroconf-0.149.16-cp313-cp313-win_amd64.whl", hash = "sha256:8f669e6aed52feaf73316a6205eff09696bfddce3e4f39fbc2c6e28500366dda", size = 1505791, upload-time = "2026-05-21T14:32:39.903Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d8/6524389c593f6963224cc0604bdb0424f97a7baa0417690120a652bfbba5/zeroconf-0.149.16-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:379ee7aadefffa2be227a501025356ef9685397a38b471df42032c30eff9e54d", size = 1678965, upload-time = "2026-05-21T14:32:41.797Z" }, + { url = "https://files.pythonhosted.org/packages/09/17/a7c58cb389ee7f344a30014ba16c2badbf85eb4cc0ae5c89bedd74070561/zeroconf-0.149.16-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb7fb399b475ec1d18cfef70ffce89bd168a37f3f5db9387ce37d7a180baa678", size = 2095711, upload-time = "2026-05-21T14:32:43.718Z" }, + { url = "https://files.pythonhosted.org/packages/92/3a/de0fff96e52cc04441f6194aa3db2b8e4e886b93b5c5ece370db5e921f14/zeroconf-0.149.16-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:57029dca8e6c9be83bd94130d41f6e1c2ce8b3f4678480b27bf24aa2053ade24", size = 1852716, upload-time = "2026-05-21T14:32:45.675Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/d067e60f39ceee21e3c12b1ed7181743001b46c331b3657717d7b0f8eab2/zeroconf-0.149.16-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa4576c6d9b029ea425c2fffa586dd5d25295ec78d28b219f35e9a2d6786cb7f", size = 2182010, upload-time = "2026-05-21T14:32:47.845Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1e/6e7d8b87d59b6cb81127377aa8a8008c3dabe3f12d7fc0b709ef29bfcc86/zeroconf-0.149.16-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d58bd30d137402f101b1b7e5c7ab508632e101007fea38f3b3907e983cd32789", size = 2117823, upload-time = "2026-05-21T14:32:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/22/53/d5ca316708c3fa1c81423b88aa778f9d19e8bd48ca8dc3d035e53f687844/zeroconf-0.149.16-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:1559b089d58e47a057a68cfe36e9910acc01d500b2d51ae410a38dbda5d66230", size = 2178304, upload-time = "2026-05-21T14:04:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/1d139ddf2c9e8484c14a5cf5dd08033513a894e061843fe64ffee9c30f64/zeroconf-0.149.16-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ddf6db171e2a44dda0cb68956c50e165a6f106b0f4ec480575ff2ef91134dfef", size = 2126907, upload-time = "2026-05-21T14:32:52.089Z" }, + { url = "https://files.pythonhosted.org/packages/39/a6/e85aae306295b7bdd66e53f01fe62c18da6fd9a93cb89033950c52b536b9/zeroconf-0.149.16-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5d8308cad249e52fc7466c2a9315438d42e9a142c5c33f622b60601d9f0e7ef7", size = 1934204, upload-time = "2026-05-21T14:32:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/07/f1/26b033222d669d7e96a3a1b2cdb252905686288d9071ea96716b8899d548/zeroconf-0.149.16-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:56a14eb52d65c683a180f91d007c87ff7922a5f6e839dd1fb74d81675d783111", size = 2127389, upload-time = "2026-05-21T14:32:56.182Z" }, + { url = "https://files.pythonhosted.org/packages/70/92/b57d3569ad362f57ab9dd1f5a48c22c2a81fe0dedf5e31c92b7853b109d4/zeroconf-0.149.16-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f817bf1b0319885882a208511249e5e2fe5ceb02fe3a9060ce295f064d13c3bd", size = 2204573, upload-time = "2026-05-21T14:32:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/35d5ac05eaed8de2f39af47ea9501244a98ffe2a70b1ddfac04d06a1b070/zeroconf-0.149.16-cp314-cp314-win32.whl", hash = "sha256:aad4b1941354bf7d0b4005b22495e6fc99218e37c9ebf0f556da9cf1ac94060f", size = 1301124, upload-time = "2026-05-21T14:33:00.147Z" }, + { url = "https://files.pythonhosted.org/packages/09/64/ee0c34a953c474a2c512252cd2bfd5932882afbc9519adceb3102d1c4261/zeroconf-0.149.16-cp314-cp314-win_amd64.whl", hash = "sha256:f0814028202457aac116f4315c8eff407b77aa0b441b0f27a60ac83699a04641", size = 1544411, upload-time = "2026-05-21T14:33:02.193Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4b/e8e2208c874b381264b21bac29aef50212c5e357988ff009200c7ed32074/zeroconf-0.149.16-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:35301c63f8dd0328e1e78fc8444cdc39bd472e6cda2570dd185b78126f576c41", size = 3354872, upload-time = "2026-05-21T14:33:04.485Z" }, + { url = "https://files.pythonhosted.org/packages/7a/73/b6b017f67d745c37c1ebc4cde3c51ff10edcd88adf7a6924c21105b175bd/zeroconf-0.149.16-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2bdd9052521a7795d6386021b1346a8f87ec66da6a917d904a327e947a6c87ce", size = 4010851, upload-time = "2026-05-21T14:33:06.692Z" }, + { url = "https://files.pythonhosted.org/packages/69/5b/9e5e15e5b0428a25b768348cdec870d533b0a54fe6f0e773630329fbd159/zeroconf-0.149.16-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e77d9235a33a79ebe57fa711fe5a1f9a225e64b2964ab329d320d22cdcf1486", size = 3556413, upload-time = "2026-05-21T14:33:09.547Z" }, + { url = "https://files.pythonhosted.org/packages/ef/24/8d8a89e2775a86e4bd1501ff91db689320bcfc7b93f725d8530f42211cfa/zeroconf-0.149.16-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79cb12a3a189cf20a5903af3a76fc0a12d32b44a1e75391c0c4875d82d959843", size = 4166790, upload-time = "2026-05-21T14:33:12.107Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/7c8f1d999d37d8c6f186cad6b5eacfc270fadb3412bf6dcd354e1696e4fc/zeroconf-0.149.16-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:752942a1a803cc4aa1dc83227d0ce408912b8c5f7b22db0d9ee9b6541cb1f32b", size = 4039796, upload-time = "2026-05-21T14:33:14.288Z" }, + { url = "https://files.pythonhosted.org/packages/78/0c/f0d563de36cb4c6b7c68cbe588a0ffe129a12f1b59d880b6216dde967f05/zeroconf-0.149.16-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:37b93d889804f029d782059f77d22073b7fcff055a3e48f414164e2a79dd758b", size = 4075206, upload-time = "2026-05-21T14:33:16.623Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/df2cb77eefde839cd095f8262a41044be851130dce5ed84125896dd5e9b4/zeroconf-0.149.16-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:9aa9ba48ceec11f5b768a81335409672eff294d7ac9c6ac5dcbcc1b0d3538a49", size = 3707566, upload-time = "2026-05-21T14:33:18.79Z" }, + { url = "https://files.pythonhosted.org/packages/34/59/d76eabba71dca1cb17ad806ed79ad9c96f5150304cf4cfc532a9db30edcf/zeroconf-0.149.16-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2f5a409d2df93e02137d4cd86c6bcab5636ee4b80bc40963eb1ece32a631c848", size = 4065727, upload-time = "2026-05-21T14:33:21.374Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9f/a830a10b69844cc3f3e5bb82a1622b9757e627c5d68236f58ebbfe6c16b1/zeroconf-0.149.16-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25ace9939767ed7bda7da6d08be50ba3ad90a9055ddbc28288d5c8d080a7c7c3", size = 4215403, upload-time = "2026-05-21T14:33:23.571Z" }, + { url = "https://files.pythonhosted.org/packages/0e/74/0f52fd22323e7142dc9a72b5f46ba6e8170835a5d7fc15325ab98f574569/zeroconf-0.149.16-cp314-cp314t-win32.whl", hash = "sha256:f8445c04cdb544a3aa58e5e30cb54dfd4c1cb82ab6389e7155872fa1c7ba7c0f", size = 2595104, upload-time = "2026-05-21T14:33:25.958Z" }, + { url = "https://files.pythonhosted.org/packages/8e/fd/f44626de06585216091baef6da5c56ea834f36de4ec754e2a2a79b659987/zeroconf-0.149.16-cp314-cp314t-win_amd64.whl", hash = "sha256:fc77736d1b8c22c1ad2c13d48aab0374a75f8aa1a21bae075478eb1c54a2c1bf", size = 3103203, upload-time = "2026-05-21T14:33:28.109Z" }, ] [[package]]