diff --git a/.github/workflows/pynumaflow-lite.yml b/.github/workflows/pynumaflow-lite.yml index 2d239831..5de0cb25 100644 --- a/.github/workflows/pynumaflow-lite.yml +++ b/.github/workflows/pynumaflow-lite.yml @@ -42,15 +42,10 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.10" - - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v10.3.0 with: - python-version: "3.10" + python-version: "3.11" working-directory: packages/pynumaflow-lite - name: Set up Rust @@ -94,7 +89,7 @@ jobs: - uses: actions/checkout@v7 - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v10.3.0 with: working-directory: packages/pynumaflow-lite @@ -108,7 +103,7 @@ jobs: run: | set -euo pipefail - for python_version in 3.10 3.11 3.12 3.13 3.14; do + for python_version in 3.11 3.12 3.13 3.14 3.15; do echo "::group::Python ${python_version}" uv python install "${python_version}" export UV_PYTHON="${python_version}" @@ -150,7 +145,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] platform: - os: ubuntu-24.04 target: x86_64-unknown-linux-gnu @@ -166,10 +161,23 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + - name: Install uv + uses: astral-sh/setup-uv@v10.3.0 with: - python-version: ${{ matrix.python-version }} + working-directory: packages/pynumaflow-lite + + # On Linux, maturin builds in the manylinux container and uses the + # python3.x of that container. On macOS, it builds on the host, so it + # needs an interpreter from uv. + - name: Set up Python ${{ matrix.python-version }} + if: ${{ !matrix.platform.linux }} + env: + PYTHON_VERSION: ${{ matrix.python-version }} + run: | + set -euo pipefail + + uv python install "${PYTHON_VERSION}" + echo "PYTHON_BIN=$(uv python find --no-project "${PYTHON_VERSION}")" >> "${GITHUB_ENV}" - name: Set up Rust run: | @@ -182,7 +190,7 @@ jobs: set -euo pipefail tag_version="${GITHUB_REF_NAME#pynumaflow-lite-v}" - package_version="$(cargo metadata --locked --no-deps --format-version 1 | python -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" + package_version="$(cargo metadata --locked --no-deps --format-version 1 | uv run --no-project python -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" if [ "${package_version}" != "${tag_version}" ]; then echo "Tag version ${tag_version} does not match Cargo.toml version ${package_version}" @@ -194,7 +202,7 @@ jobs: run: | set -euo pipefail - base_version="$(cargo metadata --locked --no-deps --format-version 1 | python -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" + base_version="$(cargo metadata --locked --no-deps --format-version 1 | uv run --no-project python -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" if [[ "${base_version}" == *-* ]]; then manual_version="${base_version}.dev.${GITHUB_RUN_ID}${GITHUB_RUN_ATTEMPT}" else @@ -202,7 +210,7 @@ jobs: fi export MANUAL_VERSION="${manual_version}" - python - <<'PY' + uv run --no-project python - <<'PY' import os import re from pathlib import Path @@ -244,7 +252,7 @@ jobs: --release --locked --out dist - -i ${{ matrix.platform.linux && format('python{0}', matrix.python-version) || 'python' }} + -i ${{ matrix.platform.linux && format('python{0}', matrix.python-version) || env.PYTHON_BIN }} - name: Upload wheel artifact uses: actions/upload-artifact@v7 @@ -268,10 +276,10 @@ jobs: steps: - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v10.3.0 - name: Download wheel artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: pynumaflow-lite-* path: dist diff --git a/packages/pynumaflow-lite/manifests/accumulator/Dockerfile b/packages/pynumaflow-lite/manifests/accumulator/Dockerfile index 445f85dc..ac4563b0 100644 --- a/packages/pynumaflow-lite/manifests/accumulator/Dockerfile +++ b/packages/pynumaflow-lite/manifests/accumulator/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim-bullseye AS builder +FROM python:3.11-slim-trixie AS builder ENV PYTHONFAULTHANDLER=1 \ PYTHONUNBUFFERED=1 \ diff --git a/packages/pynumaflow-lite/manifests/accumulator/accumulator_stream_sorter.py b/packages/pynumaflow-lite/manifests/accumulator/accumulator_stream_sorter.py index 63f2d664..02a312e3 100644 --- a/packages/pynumaflow-lite/manifests/accumulator/accumulator_stream_sorter.py +++ b/packages/pynumaflow-lite/manifests/accumulator/accumulator_stream_sorter.py @@ -8,7 +8,7 @@ import asyncio import signal from collections.abc import AsyncIterator -from datetime import datetime +from datetime import UTC, datetime from pynumaflow_lite.accumulator import ( Accumulator, @@ -25,10 +25,8 @@ class StreamSorter(Accumulator): """ def __init__(self): - from datetime import timezone - # Initialize with a very old timestamp (timezone-aware) - self.latest_wm = datetime.fromtimestamp(-1, tz=timezone.utc) + self.latest_wm = datetime.fromtimestamp(-1, tz=UTC) self.sorted_buffer: list[Datum] = [] print("StreamSorter initialized") diff --git a/packages/pynumaflow-lite/manifests/reduce/Dockerfile b/packages/pynumaflow-lite/manifests/reduce/Dockerfile index fa1417eb..1024e5f2 100644 --- a/packages/pynumaflow-lite/manifests/reduce/Dockerfile +++ b/packages/pynumaflow-lite/manifests/reduce/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim-bullseye AS builder +FROM python:3.11-slim-trixie AS builder ENV PYTHONFAULTHANDLER=1 \ PYTHONUNBUFFERED=1 \ diff --git a/packages/pynumaflow-lite/manifests/reducestream/Dockerfile b/packages/pynumaflow-lite/manifests/reducestream/Dockerfile index 14d1b02c..60cd2234 100644 --- a/packages/pynumaflow-lite/manifests/reducestream/Dockerfile +++ b/packages/pynumaflow-lite/manifests/reducestream/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim-bullseye AS builder +FROM python:3.11-slim-trixie AS builder ENV PYTHONFAULTHANDLER=1 \ PYTHONUNBUFFERED=1 \ diff --git a/packages/pynumaflow-lite/manifests/session_reduce/Dockerfile b/packages/pynumaflow-lite/manifests/session_reduce/Dockerfile index a5f6bdda..cf1ef156 100644 --- a/packages/pynumaflow-lite/manifests/session_reduce/Dockerfile +++ b/packages/pynumaflow-lite/manifests/session_reduce/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim-bullseye AS builder +FROM python:3.11-slim-trixie AS builder ENV PYTHONFAULTHANDLER=1 \ PYTHONUNBUFFERED=1 \ diff --git a/packages/pynumaflow-lite/manifests/sideinput/Dockerfile b/packages/pynumaflow-lite/manifests/sideinput/Dockerfile index e3ae81a8..895cfc2c 100644 --- a/packages/pynumaflow-lite/manifests/sideinput/Dockerfile +++ b/packages/pynumaflow-lite/manifests/sideinput/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim-bullseye AS builder +FROM python:3.11-slim-trixie AS builder ENV PYTHONFAULTHANDLER=1 \ PYTHONUNBUFFERED=1 \ diff --git a/packages/pynumaflow-lite/manifests/source/Dockerfile b/packages/pynumaflow-lite/manifests/source/Dockerfile index 59b4578f..5bcbb6d1 100644 --- a/packages/pynumaflow-lite/manifests/source/Dockerfile +++ b/packages/pynumaflow-lite/manifests/source/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim-bullseye AS builder +FROM python:3.11-slim-trixie AS builder ENV PYTHONFAULTHANDLER=1 \ PYTHONUNBUFFERED=1 \ diff --git a/packages/pynumaflow-lite/manifests/source/simple_source.py b/packages/pynumaflow-lite/manifests/source/simple_source.py index a85ea051..9104bae6 100644 --- a/packages/pynumaflow-lite/manifests/source/simple_source.py +++ b/packages/pynumaflow-lite/manifests/source/simple_source.py @@ -2,7 +2,7 @@ import logging import signal from collections.abc import AsyncIterator -from datetime import datetime, timezone +from datetime import UTC, datetime from pynumaflow_lite import sourcer from pynumaflow_lite._source_dtypes import Sourcer @@ -43,7 +43,7 @@ async def read_handler(self, datum: sourcer.ReadRequest) -> AsyncIterator[source message = sourcer.Message( payload=payload, offset=offset, - event_time=datetime.now(timezone.utc), + event_time=datetime.now(UTC), keys=["key1"], headers={"source": "simple"}, ) diff --git a/packages/pynumaflow-lite/manifests/sourcetransform/sourcetransform_event_filter.py b/packages/pynumaflow-lite/manifests/sourcetransform/sourcetransform_event_filter.py index 332e15c3..854a0b6c 100644 --- a/packages/pynumaflow-lite/manifests/sourcetransform/sourcetransform_event_filter.py +++ b/packages/pynumaflow-lite/manifests/sourcetransform/sourcetransform_event_filter.py @@ -1,10 +1,10 @@ import asyncio -from datetime import datetime, timezone +from datetime import UTC, datetime from pynumaflow_lite.sourcetransformer import Datum, Message, SourceTransformAsyncServer, SourceTransformer -january_first_2022 = datetime(2022, 1, 1, tzinfo=timezone.utc) -january_first_2023 = datetime(2023, 1, 1, tzinfo=timezone.utc) +january_first_2022 = datetime(2022, 1, 1, tzinfo=UTC) +january_first_2023 = datetime(2023, 1, 1, tzinfo=UTC) class EventFilter(SourceTransformer): diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_async_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_async_server.py new file mode 100644 index 00000000..c479b0e6 --- /dev/null +++ b/packages/pynumaflow-lite/pynumaflow_lite/_async_server.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import asyncio +import contextlib +import signal +from collections.abc import Awaitable, Callable +from types import TracebackType +from typing import Protocol, Self + +_SHUTDOWN_SIGNALS = (signal.SIGINT, signal.SIGTERM) + + +class _ServerCore(Protocol): + """The Rust server object that a `_AsyncServer` drives.""" + + def start(self, handler: Callable[..., object]) -> Awaitable[None]: ... + def wait_ready(self) -> Awaitable[None]: ... + def stop(self) -> None: ... + + +class _AsyncServer: + """Lifecycle shared by the async UDF servers. + + A subclass sets `_name` for error messages, and gives its Rust core object + and the user handler to `__init__`. + """ + + _name: str + + def __init__( + self, + core: _ServerCore, + handler: Callable[..., object], + *, + install_signal_handlers: bool, + ) -> None: + self._core = core + self._handler = handler + self._install_signal_handlers = install_signal_handlers + self._task: asyncio.Task[None] | None = None + self._serving = False + self._installed_signals: list[signal.Signals] = [] + + async def serve(self) -> None: + """Run the server until it stops. + + This is the entrypoint for an application that already runs an event + loop. It returns when a shutdown signal arrives or when `stop()` runs. + """ + await self._serve(install_signal_handlers=self._install_signal_handlers) + + async def _serve(self, *, install_signal_handlers: bool) -> None: + if self._serving: + raise RuntimeError(f"{self._name} server is already serving") + self._serving = True + try: + if install_signal_handlers: + self._add_signal_handlers() + await self._core.start(self._handler) + finally: + self._remove_signal_handlers() + self._serving = False + + def stop(self) -> None: + self._core.stop() + + async def wait_ready(self) -> None: + """Wait until the server answers the Numaflow readiness probe. + + There is no time limit. Use `asyncio.timeout()` to set one. + """ + await self._core.wait_ready() + + async def wait_for_termination(self) -> None: + """Wait until the background server task ends. + + Use this inside an `async with` block. It raises the handler error if + the server task failed. + """ + if self._task is None: + raise RuntimeError(f"{self._name} server is not serving") + await asyncio.shield(self._task) + + def _add_signal_handlers(self) -> None: + loop = asyncio.get_running_loop() + for sig in _SHUTDOWN_SIGNALS: + try: + loop.add_signal_handler(sig, self.stop) + except (NotImplementedError, RuntimeError, OSError): + continue + self._installed_signals.append(sig) + + def _remove_signal_handlers(self) -> None: + if not self._installed_signals: + return + try: + loop = asyncio.get_running_loop() + except RuntimeError: + self._installed_signals.clear() + return + for sig in self._installed_signals: + with contextlib.suppress(NotImplementedError, OSError): + loop.remove_signal_handler(sig) + self._installed_signals.clear() + + async def __aenter__(self) -> Self: + """Start the server in a background task and wait until it is ready. + + This form is for tests and for code that must run other work next to + the server. It never installs signal handlers. There is no time limit: + use `asyncio.timeout()` to set one. If the server stops before it is + ready, this raises the server error at once. + """ + if self._task is not None and not self._task.done(): + raise RuntimeError(f"{self._name} server is already serving") + + task = self._task = asyncio.create_task(self._serve(install_signal_handlers=False)) + ready = asyncio.create_task(self.wait_ready()) + try: + await asyncio.wait((task, ready), return_when=asyncio.FIRST_COMPLETED) + if task.done(): + task.result() + raise RuntimeError(f"{self._name} server stopped before it was ready") + ready.result() + except BaseException: + ready.cancel() + await asyncio.wait((ready,)) + self.stop() + self._task = None + # Surface the server error, if there is one. It explains the + # failure better than a cancellation or a timeout does. + await task + raise + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.stop() + if self._task is not None: + try: + await self._task + finally: + self._task = None diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_batchmap_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_batchmap_server.py index 69f33237..da2566c2 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_batchmap_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_batchmap_server.py @@ -1,21 +1,18 @@ from __future__ import annotations -import asyncio -import contextlib -import signal from collections.abc import AsyncIterable, Awaitable, Callable -from types import TracebackType from typing import TypeAlias +from ._async_server import _AsyncServer from .pynumaflow_lite import batchmapper as _batchmapper Datum: TypeAlias = _batchmapper.Datum BatchResponse: TypeAlias = _batchmapper.BatchResponse -_SHUTDOWN_SIGNALS = (signal.SIGINT, signal.SIGTERM) +class BatchMapAsyncServer(_AsyncServer): + _name = "batchmap" -class BatchMapAsyncServer: def __init__( self, handler: Callable[[AsyncIterable[Datum]], Awaitable[list[BatchResponse]]], @@ -23,103 +20,9 @@ def __init__( sock_file: str | None = None, server_info_file: str | None = None, install_signal_handlers: bool = True, - ): - self._core = _batchmapper._BatchMapAsyncServer(sock_file, server_info_file) - self._handler = handler - self._install_signal_handlers = install_signal_handlers - self._task: asyncio.Task[None] | None = None - self._serving = False - self._installed_signals: list[signal.Signals] = [] - - async def serve(self) -> None: - """Run the batchmap server until it stops. - - This is the entrypoint for an application that already runs an event - loop. It returns when a shutdown signal arrives or when `stop()` runs. - """ - await self._serve(install_signal_handlers=self._install_signal_handlers) - - async def _serve(self, *, install_signal_handlers: bool) -> None: - if self._serving: - raise RuntimeError("batchmap server is already serving") - self._serving = True - try: - if install_signal_handlers: - self._add_signal_handlers() - await self._core.start(self._handler) - finally: - self._remove_signal_handlers() - self._serving = False - - def stop(self) -> None: - self._core.stop() - - async def wait_ready(self, timeout: float = 30.0) -> None: - await self._core.wait_ready(timeout) - - async def wait_for_termination(self) -> None: - """Wait until the background server task ends. - - Use this inside an `async with` block. It raises the handler error if - the server task failed. - """ - if self._task is None: - raise RuntimeError("batchmap server is not serving") - await asyncio.shield(self._task) - - def _add_signal_handlers(self) -> None: - loop = asyncio.get_running_loop() - for sig in _SHUTDOWN_SIGNALS: - try: - loop.add_signal_handler(sig, self.stop) - except (NotImplementedError, RuntimeError, OSError): - continue - self._installed_signals.append(sig) - - def _remove_signal_handlers(self) -> None: - if not self._installed_signals: - return - try: - loop = asyncio.get_running_loop() - except RuntimeError: - self._installed_signals.clear() - return - for sig in self._installed_signals: - with contextlib.suppress(NotImplementedError, OSError): - loop.remove_signal_handler(sig) - self._installed_signals.clear() - - async def __aenter__(self) -> BatchMapAsyncServer: - """Start the server in a background task and wait until it is ready. - - This form is for tests and for code that must run other work next to - the server. It never installs signal handlers. - """ - if self._task is not None and not self._task.done(): - raise RuntimeError("batchmap server is already serving") - - self._task = asyncio.create_task(self._serve(install_signal_handlers=False)) - try: - await self.wait_ready() - except BaseException: - self.stop() - task, self._task = self._task, None - if task is not None: - # Surface the server error, if there is one. It explains the - # failure better than the `wait_ready` error does. - await task - raise - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - tb: TracebackType | None, ) -> None: - self.stop() - if self._task is not None: - try: - await self._task - finally: - self._task = None + super().__init__( + _batchmapper._BatchMapAsyncServer(sock_file, server_info_file), + handler, + install_signal_handlers=install_signal_handlers, + ) diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_map_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_map_server.py index 04b5d97e..33b0a3d3 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_map_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_map_server.py @@ -1,21 +1,18 @@ from __future__ import annotations -import asyncio -import contextlib -import signal from collections.abc import Awaitable, Callable -from types import TracebackType from typing import TypeAlias +from ._async_server import _AsyncServer from .pynumaflow_lite import mapper as _mapper Datum: TypeAlias = _mapper.Datum Message: TypeAlias = _mapper.Message -_SHUTDOWN_SIGNALS = (signal.SIGINT, signal.SIGTERM) +class MapAsyncServer(_AsyncServer): + _name = "map" -class MapAsyncServer: def __init__( self, handler: Callable[[Datum], Awaitable[list[Message]]], @@ -24,102 +21,8 @@ def __init__( server_info_file: str | None = None, install_signal_handlers: bool = True, ) -> None: - self._core = _mapper._MapAsyncServer(sock_file, server_info_file) - self._handler = handler - self._install_signal_handlers = install_signal_handlers - self._task: asyncio.Task[None] | None = None - self._serving = False - self._installed_signals: list[signal.Signals] = [] - - async def serve(self) -> None: - """Run the map server until it stops. - - This is the entrypoint for an application that already runs an event - loop. It returns when a shutdown signal arrives or when `stop()` runs. - """ - await self._serve(install_signal_handlers=self._install_signal_handlers) - - async def _serve(self, *, install_signal_handlers: bool) -> None: - if self._serving: - raise RuntimeError("map server is already serving") - self._serving = True - try: - if install_signal_handlers: - self._add_signal_handlers() - await self._core.start(self._handler) - finally: - self._remove_signal_handlers() - self._serving = False - - def stop(self) -> None: - self._core.stop() - - async def wait_ready(self, timeout: float = 30.0) -> None: - await self._core.wait_ready(timeout) - - async def wait_for_termination(self) -> None: - """Wait until the background server task ends. - - Use this inside an `async with` block. It raises the handler error if - the server task failed. - """ - if self._task is None: - raise RuntimeError("map server is not serving") - await asyncio.shield(self._task) - - def _add_signal_handlers(self) -> None: - loop = asyncio.get_running_loop() - for sig in _SHUTDOWN_SIGNALS: - try: - loop.add_signal_handler(sig, self.stop) - except (NotImplementedError, RuntimeError, OSError): - continue - self._installed_signals.append(sig) - - def _remove_signal_handlers(self) -> None: - if not self._installed_signals: - return - try: - loop = asyncio.get_running_loop() - except RuntimeError: - self._installed_signals.clear() - return - for sig in self._installed_signals: - with contextlib.suppress(NotImplementedError, OSError): - loop.remove_signal_handler(sig) - self._installed_signals.clear() - - async def __aenter__(self) -> MapAsyncServer: - """Start the server in a background task and wait until it is ready. - - This form is for tests and for code that must run other work next to - the server. It never installs signal handlers. - """ - if self._task is not None and not self._task.done(): - raise RuntimeError("map server is already serving") - - self._task = asyncio.create_task(self._serve(install_signal_handlers=False)) - try: - await self.wait_ready() - except BaseException: - self.stop() - task, self._task = self._task, None - if task is not None: - # Surface the server error, if there is one. It explains the - # failure better than the `wait_ready` error does. - await task - raise - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.stop() - if self._task is not None: - try: - await self._task - finally: - self._task = None + super().__init__( + _mapper._MapAsyncServer(sock_file, server_info_file), + handler, + install_signal_handlers=install_signal_handlers, + ) diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_mapstream_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_mapstream_server.py index 31ac047a..1553f01b 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_mapstream_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_mapstream_server.py @@ -1,21 +1,18 @@ from __future__ import annotations -import asyncio -import contextlib -import signal from collections.abc import AsyncIterator, Callable -from types import TracebackType from typing import TypeAlias +from ._async_server import _AsyncServer from .pynumaflow_lite import mapstreamer as _mapstreamer Datum: TypeAlias = _mapstreamer.Datum Message: TypeAlias = _mapstreamer.Message -_SHUTDOWN_SIGNALS = (signal.SIGINT, signal.SIGTERM) +class MapStreamAsyncServer(_AsyncServer): + _name = "mapstream" -class MapStreamAsyncServer: def __init__( self, handler: Callable[[Datum], AsyncIterator[Message]], @@ -24,102 +21,8 @@ def __init__( server_info_file: str | None = None, install_signal_handlers: bool = True, ) -> None: - self._core = _mapstreamer._MapStreamAsyncServer(sock_file, server_info_file) - self._handler = handler - self._install_signal_handlers = install_signal_handlers - self._task: asyncio.Task[None] | None = None - self._serving = False - self._installed_signals: list[signal.Signals] = [] - - async def serve(self) -> None: - """Run the mapstream server until it stops. - - This is the entrypoint for an application that already runs an event - loop. It returns when a shutdown signal arrives or when `stop()` runs. - """ - await self._serve(install_signal_handlers=self._install_signal_handlers) - - async def _serve(self, *, install_signal_handlers: bool) -> None: - if self._serving: - raise RuntimeError("mapstream server is already serving") - self._serving = True - try: - if install_signal_handlers: - self._add_signal_handlers() - await self._core.start(self._handler) - finally: - self._remove_signal_handlers() - self._serving = False - - def stop(self) -> None: - self._core.stop() - - async def wait_ready(self, timeout: float = 30.0) -> None: - await self._core.wait_ready(timeout) - - async def wait_for_termination(self) -> None: - """Wait until the background server task ends. - - Use this inside an `async with` block. It raises the handler error if - the server task failed. - """ - if self._task is None: - raise RuntimeError("mapstream server is not serving") - await asyncio.shield(self._task) - - def _add_signal_handlers(self) -> None: - loop = asyncio.get_running_loop() - for sig in _SHUTDOWN_SIGNALS: - try: - loop.add_signal_handler(sig, self.stop) - except (NotImplementedError, RuntimeError, OSError): - continue - self._installed_signals.append(sig) - - def _remove_signal_handlers(self) -> None: - if not self._installed_signals: - return - try: - loop = asyncio.get_running_loop() - except RuntimeError: - self._installed_signals.clear() - return - for sig in self._installed_signals: - with contextlib.suppress(NotImplementedError, OSError): - loop.remove_signal_handler(sig) - self._installed_signals.clear() - - async def __aenter__(self) -> MapStreamAsyncServer: - """Start the server in a background task and wait until it is ready. - - This form is for tests and for code that must run other work next to - the server. It never installs signal handlers. - """ - if self._task is not None and not self._task.done(): - raise RuntimeError("mapstream server is already serving") - - self._task = asyncio.create_task(self._serve(install_signal_handlers=False)) - try: - await self.wait_ready() - except BaseException: - self.stop() - task, self._task = self._task, None - if task is not None: - # Surface the server error, if there is one. It explains the - # failure better than the `wait_ready` error does. - await task - raise - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.stop() - if self._task is not None: - try: - await self._task - finally: - self._task = None + super().__init__( + _mapstreamer._MapStreamAsyncServer(sock_file, server_info_file), + handler, + install_signal_handlers=install_signal_handlers, + ) diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_sink_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_sink_server.py index c8ce034a..6f11cd3d 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_sink_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_sink_server.py @@ -1,21 +1,18 @@ from __future__ import annotations -import asyncio -import contextlib -import signal from collections.abc import AsyncIterator, Awaitable, Callable -from types import TracebackType from typing import TypeAlias +from ._async_server import _AsyncServer from .pynumaflow_lite import sinker as _sinker Datum: TypeAlias = _sinker.Datum Response: TypeAlias = _sinker.Response -_SHUTDOWN_SIGNALS = (signal.SIGINT, signal.SIGTERM) +class SinkAsyncServer(_AsyncServer): + _name = "sink" -class SinkAsyncServer: def __init__( self, handler: Callable[[AsyncIterator[Datum]], Awaitable[list[Response]]], @@ -24,102 +21,8 @@ def __init__( server_info_file: str | None = None, install_signal_handlers: bool = True, ) -> None: - self._core = _sinker._SinkAsyncServer(sock_file, server_info_file) - self._handler = handler - self._install_signal_handlers = install_signal_handlers - self._task: asyncio.Task[None] | None = None - self._serving = False - self._installed_signals: list[signal.Signals] = [] - - async def serve(self) -> None: - """Run the sink server until it stops. - - This is the entrypoint for an application that already runs an event - loop. It returns when a shutdown signal arrives or when `stop()` runs. - """ - await self._serve(install_signal_handlers=self._install_signal_handlers) - - async def _serve(self, *, install_signal_handlers: bool) -> None: - if self._serving: - raise RuntimeError("sink server is already serving") - self._serving = True - try: - if install_signal_handlers: - self._add_signal_handlers() - await self._core.start(self._handler) - finally: - self._remove_signal_handlers() - self._serving = False - - def stop(self) -> None: - self._core.stop() - - async def wait_ready(self, timeout: float = 30.0) -> None: - await self._core.wait_ready(timeout) - - async def wait_for_termination(self) -> None: - """Wait until the background server task ends. - - Use this inside an `async with` block. It raises the handler error if - the server task failed. - """ - if self._task is None: - raise RuntimeError("sink server is not serving") - await asyncio.shield(self._task) - - def _add_signal_handlers(self) -> None: - loop = asyncio.get_running_loop() - for sig in _SHUTDOWN_SIGNALS: - try: - loop.add_signal_handler(sig, self.stop) - except (NotImplementedError, RuntimeError, OSError): - continue - self._installed_signals.append(sig) - - def _remove_signal_handlers(self) -> None: - if not self._installed_signals: - return - try: - loop = asyncio.get_running_loop() - except RuntimeError: - self._installed_signals.clear() - return - for sig in self._installed_signals: - with contextlib.suppress(NotImplementedError, OSError): - loop.remove_signal_handler(sig) - self._installed_signals.clear() - - async def __aenter__(self) -> SinkAsyncServer: - """Start the server in a background task and wait until it is ready. - - This form is for tests and for code that must run other work next to - the server. It never installs signal handlers. - """ - if self._task is not None and not self._task.done(): - raise RuntimeError("sink server is already serving") - - self._task = asyncio.create_task(self._serve(install_signal_handlers=False)) - try: - await self.wait_ready() - except BaseException: - self.stop() - task, self._task = self._task, None - if task is not None: - # Surface the server error, if there is one. It explains the - # failure better than the `wait_ready` error does. - await task - raise - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.stop() - if self._task is not None: - try: - await self._task - finally: - self._task = None + super().__init__( + _sinker._SinkAsyncServer(sock_file, server_info_file), + handler, + install_signal_handlers=install_signal_handlers, + ) diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_sourcetransform_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_sourcetransform_server.py index fb4c01b8..73b80a94 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_sourcetransform_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_sourcetransform_server.py @@ -1,21 +1,18 @@ from __future__ import annotations -import asyncio -import contextlib -import signal from collections.abc import Awaitable, Callable -from types import TracebackType from typing import TypeAlias +from ._async_server import _AsyncServer from .pynumaflow_lite import sourcetransformer as _sourcetransformer Datum: TypeAlias = _sourcetransformer.Datum Message: TypeAlias = _sourcetransformer.Message -_SHUTDOWN_SIGNALS = (signal.SIGINT, signal.SIGTERM) +class SourceTransformAsyncServer(_AsyncServer): + _name = "sourcetransform" -class SourceTransformAsyncServer: def __init__( self, handler: Callable[[Datum], Awaitable[list[Message]]], @@ -24,102 +21,8 @@ def __init__( server_info_file: str | None = None, install_signal_handlers: bool = True, ) -> None: - self._core = _sourcetransformer._SourceTransformAsyncServer(sock_file, server_info_file) - self._handler = handler - self._install_signal_handlers = install_signal_handlers - self._task: asyncio.Task[None] | None = None - self._serving = False - self._installed_signals: list[signal.Signals] = [] - - async def serve(self) -> None: - """Run the sourcetransform server until it stops. - - This is the entrypoint for an application that already runs an event - loop. It returns when a shutdown signal arrives or when `stop()` runs. - """ - await self._serve(install_signal_handlers=self._install_signal_handlers) - - async def _serve(self, *, install_signal_handlers: bool) -> None: - if self._serving: - raise RuntimeError("sourcetransform server is already serving") - self._serving = True - try: - if install_signal_handlers: - self._add_signal_handlers() - await self._core.start(self._handler) - finally: - self._remove_signal_handlers() - self._serving = False - - def stop(self) -> None: - self._core.stop() - - async def wait_ready(self, timeout: float = 30.0) -> None: - await self._core.wait_ready(timeout) - - async def wait_for_termination(self) -> None: - """Wait until the background server task ends. - - Use this inside an `async with` block. It raises the handler error if - the server task failed. - """ - if self._task is None: - raise RuntimeError("sourcetransform server is not serving") - await asyncio.shield(self._task) - - def _add_signal_handlers(self) -> None: - loop = asyncio.get_running_loop() - for sig in _SHUTDOWN_SIGNALS: - try: - loop.add_signal_handler(sig, self.stop) - except (NotImplementedError, RuntimeError, OSError): - continue - self._installed_signals.append(sig) - - def _remove_signal_handlers(self) -> None: - if not self._installed_signals: - return - try: - loop = asyncio.get_running_loop() - except RuntimeError: - self._installed_signals.clear() - return - for sig in self._installed_signals: - with contextlib.suppress(NotImplementedError, OSError): - loop.remove_signal_handler(sig) - self._installed_signals.clear() - - async def __aenter__(self) -> SourceTransformAsyncServer: - """Start the server in a background task and wait until it is ready. - - This form is for tests and for code that must run other work next to - the server. It never installs signal handlers. - """ - if self._task is not None and not self._task.done(): - raise RuntimeError("sourcetransform server is already serving") - - self._task = asyncio.create_task(self._serve(install_signal_handlers=False)) - try: - await self.wait_ready() - except BaseException: - self.stop() - task, self._task = self._task, None - if task is not None: - # Surface the server error, if there is one. It explains the - # failure better than the `wait_ready` error does. - await task - raise - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.stop() - if self._task is not None: - try: - await self._task - finally: - self._task = None + super().__init__( + _sourcetransformer._SourceTransformAsyncServer(sock_file, server_info_file), + handler, + install_signal_handlers=install_signal_handlers, + ) diff --git a/packages/pynumaflow-lite/pynumaflow_lite/batchmapper.pyi b/packages/pynumaflow-lite/pynumaflow_lite/batchmapper.pyi index e87630d8..fde5a076 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/batchmapper.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/batchmapper.pyi @@ -1,8 +1,7 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import AsyncIterable, Awaitable, Callable from types import TracebackType +from typing import Self from ._batchmapper_dtypes import BatchMapper as BatchMapper @@ -80,7 +79,7 @@ class _BatchMapAsyncServer: server_info_file: str | None = ..., ) -> None: ... def start(self, handler: Callable[[AsyncIterable[Datum]], Awaitable[list[BatchResponse]]]) -> Awaitable[None]: ... - def wait_ready(self, timeout: float = ...) -> Awaitable[None]: ... + def wait_ready(self) -> Awaitable[None]: ... def stop(self) -> None: ... class BatchMapAsyncServer: @@ -94,9 +93,9 @@ class BatchMapAsyncServer: ) -> None: ... async def serve(self) -> None: ... def stop(self) -> None: ... - async def wait_ready(self, timeout: float = ...) -> None: ... + async def wait_ready(self) -> None: ... async def wait_for_termination(self) -> None: ... - async def __aenter__(self) -> BatchMapAsyncServer: ... + async def __aenter__(self) -> Self: ... async def __aexit__( self, exc_type: type[BaseException] | None, diff --git a/packages/pynumaflow-lite/pynumaflow_lite/mapper.pyi b/packages/pynumaflow-lite/pynumaflow_lite/mapper.pyi index 2ccbfc27..3e0666e8 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/mapper.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/mapper.pyi @@ -1,8 +1,7 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import Awaitable, Callable from types import TracebackType +from typing import Self from ._map_dtypes import Mapper as Mapper @@ -79,7 +78,7 @@ class _MapAsyncServer: self, handler: Callable[[Datum], Awaitable[list[Message]]], ) -> Awaitable[None]: ... - def wait_ready(self, timeout: float = ...) -> Awaitable[None]: ... + def wait_ready(self) -> Awaitable[None]: ... def stop(self) -> None: ... class MapAsyncServer: @@ -93,9 +92,9 @@ class MapAsyncServer: ) -> None: ... async def serve(self) -> None: ... def stop(self) -> None: ... - async def wait_ready(self, timeout: float = ...) -> None: ... + async def wait_ready(self) -> None: ... async def wait_for_termination(self) -> None: ... - async def __aenter__(self) -> MapAsyncServer: ... + async def __aenter__(self) -> Self: ... async def __aexit__( self, exc_type: type[BaseException] | None, diff --git a/packages/pynumaflow-lite/pynumaflow_lite/mapstreamer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/mapstreamer.pyi index 91bf70bc..8b38cc4f 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/mapstreamer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/mapstreamer.pyi @@ -1,8 +1,7 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import AsyncIterator, Awaitable, Callable from types import TracebackType +from typing import Self from ._mapstream_dtypes import MapStreamer as MapStreamer @@ -70,7 +69,7 @@ class _MapStreamAsyncServer: server_info_file: str | None = ..., ) -> None: ... def start(self, handler: Callable[[Datum], AsyncIterator[Message]]) -> Awaitable[None]: ... - def wait_ready(self, timeout: float = ...) -> Awaitable[None]: ... + def wait_ready(self) -> Awaitable[None]: ... def stop(self) -> None: ... class MapStreamAsyncServer: @@ -84,9 +83,9 @@ class MapStreamAsyncServer: ) -> None: ... async def serve(self) -> None: ... def stop(self) -> None: ... - async def wait_ready(self, timeout: float = ...) -> None: ... + async def wait_ready(self) -> None: ... async def wait_for_termination(self) -> None: ... - async def __aenter__(self) -> MapStreamAsyncServer: ... + async def __aenter__(self) -> Self: ... async def __aexit__( self, exc_type: type[BaseException] | None, diff --git a/packages/pynumaflow-lite/pynumaflow_lite/reducer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/reducer.pyi index 2095e354..f12e0f96 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/reducer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/reducer.pyi @@ -1,5 +1,3 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import AsyncIterable, Awaitable, Callable diff --git a/packages/pynumaflow-lite/pynumaflow_lite/reducestreamer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/reducestreamer.pyi index b3530f95..2d28a7f9 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/reducestreamer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/reducestreamer.pyi @@ -1,5 +1,3 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable diff --git a/packages/pynumaflow-lite/pynumaflow_lite/session_reducer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/session_reducer.pyi index ead7a315..d18997f6 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/session_reducer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/session_reducer.pyi @@ -1,5 +1,3 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import Awaitable diff --git a/packages/pynumaflow-lite/pynumaflow_lite/sideinputer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/sideinputer.pyi index ca57e079..3d242174 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/sideinputer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/sideinputer.pyi @@ -1,5 +1,3 @@ -from __future__ import annotations - from collections.abc import Awaitable # Re-export the Python ABC for user convenience and typing diff --git a/packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi b/packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi index 1e80398c..762c19fa 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi @@ -1,8 +1,7 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import AsyncIterator, Awaitable, Callable from types import TracebackType +from typing import Self from ._sink_dtypes import Sinker as Sinker @@ -93,7 +92,7 @@ class _SinkAsyncServer: server_info_file: str | None = ..., ) -> None: ... def start(self, handler: _SinkHandler) -> Awaitable[None]: ... - def wait_ready(self, timeout: float = ...) -> Awaitable[None]: ... + def wait_ready(self) -> Awaitable[None]: ... def stop(self) -> None: ... class SinkAsyncServer: @@ -107,9 +106,9 @@ class SinkAsyncServer: ) -> None: ... async def serve(self) -> None: ... def stop(self) -> None: ... - async def wait_ready(self, timeout: float = ...) -> None: ... + async def wait_ready(self) -> None: ... async def wait_for_termination(self) -> None: ... - async def __aenter__(self) -> SinkAsyncServer: ... + async def __aenter__(self) -> Self: ... async def __aexit__( self, exc_type: type[BaseException] | None, diff --git a/packages/pynumaflow-lite/pynumaflow_lite/sourcer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/sourcer.pyi index 804438b0..46757472 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/sourcer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/sourcer.pyi @@ -1,5 +1,3 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import Awaitable diff --git a/packages/pynumaflow-lite/pynumaflow_lite/sourcetransformer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/sourcetransformer.pyi index 0dacc357..fd50d42a 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/sourcetransformer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/sourcetransformer.pyi @@ -1,8 +1,7 @@ -from __future__ import annotations - import datetime as _dt from collections.abc import Awaitable, Callable from types import TracebackType +from typing import Self from ._sourcetransformer_dtypes import SourceTransformer as SourceTransformer @@ -82,7 +81,7 @@ class _SourceTransformAsyncServer: self, handler: Callable[[Datum], Awaitable[list[Message]]], ) -> Awaitable[None]: ... - def wait_ready(self, timeout: float = ...) -> Awaitable[None]: ... + def wait_ready(self) -> Awaitable[None]: ... def stop(self) -> None: ... class SourceTransformAsyncServer: @@ -96,9 +95,9 @@ class SourceTransformAsyncServer: ) -> None: ... async def serve(self) -> None: ... def stop(self) -> None: ... - async def wait_ready(self, timeout: float = ...) -> None: ... + async def wait_ready(self) -> None: ... async def wait_for_termination(self) -> None: ... - async def __aenter__(self) -> SourceTransformAsyncServer: ... + async def __aenter__(self) -> Self: ... async def __aexit__( self, exc_type: type[BaseException] | None, diff --git a/packages/pynumaflow-lite/pyproject.toml b/packages/pynumaflow-lite/pyproject.toml index 668cc368..a2a524aa 100644 --- a/packages/pynumaflow-lite/pyproject.toml +++ b/packages/pynumaflow-lite/pyproject.toml @@ -4,10 +4,16 @@ build-backend = "maturin" [project] name = "pynumaflow-lite" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 3 - Alpha", "Programming Language :: Rust", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] @@ -18,7 +24,7 @@ features = ["pyo3/extension-module"] [tool.ruff] line-length = 119 src = ["pynumaflow_lite", "tests", "manifests"] -target-version = "py310" +target-version = "py311" show-fixes = true [tool.ruff.lint] @@ -59,6 +65,6 @@ docstring-code-format = true [dependency-groups] dev = [ "pytest>=9.1.1", - "ruff>=0.15.20", - "maturin>=1.14,<2.0", + "ruff>=0.17.0", + "maturin>=1.15,<2.0", ] diff --git a/packages/pynumaflow-lite/src/batchmap/mod.rs b/packages/pynumaflow-lite/src/batchmap/mod.rs index 11c2c13f..1c8e618d 100644 --- a/packages/pynumaflow-lite/src/batchmap/mod.rs +++ b/packages/pynumaflow-lite/src/batchmap/mod.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, time::Duration}; +use std::collections::HashMap; use numaflow::batchmap; @@ -309,21 +309,14 @@ impl BatchMapAsyncServer { } /// Wait until the Numaflow IsReady probe succeeds over the batchmap UDS. - #[pyo3(signature = (timeout: "float"=30.0) -> "None")] - pub fn wait_ready<'a>(&self, py: Python<'a>, timeout: f64) -> PyResult> { - if !timeout.is_finite() || timeout < 0.0 { - return Err(pyo3::PyErr::new::( - "timeout must be a non-negative finite float", - )); - } - + #[pyo3(signature = () -> "None")] + pub fn wait_ready<'a>(&self, py: Python<'a>) -> PyResult> { let sock_file = self.sock_file.clone(); - let timeout = Duration::from_secs_f64(timeout); - pyo3_async_runtimes::tokio::future_into_py( - py, - crate::map::wait_for_ready(sock_file, timeout, "batchmap"), - ) + pyo3_async_runtimes::tokio::future_into_py(py, async move { + crate::map::wait_for_ready(sock_file).await; + Ok(()) + }) } #[pyo3(signature = () -> "None")] diff --git a/packages/pynumaflow-lite/src/map/mod.rs b/packages/pynumaflow-lite/src/map/mod.rs index 6a2e4fc3..5b5e0fb0 100644 --- a/packages/pynumaflow-lite/src/map/mod.rs +++ b/packages/pynumaflow-lite/src/map/mod.rs @@ -12,7 +12,6 @@ use chrono::{DateTime, Utc}; pub mod server; use tokio::net::UnixStream; -use tokio::time::Instant; use tonic::transport::Uri; use tower::service_fn; @@ -281,33 +280,18 @@ pub(crate) async fn map_grpc_client( Ok(MapClient::new(channel)) } -pub(crate) async fn wait_for_ready( - sock_file: String, - timeout: Duration, - component: &'static str, -) -> PyResult<()> { - let deadline = Instant::now() + timeout; - +// Poll the IsReady probe until it succeeds. There is no deadline: Python sets +// one with asyncio.timeout(), and cancellation drops this future. +pub(crate) async fn wait_for_ready(sock_file: String) { loop { if let Ok(mut client) = map_grpc_client(sock_file.clone()).await && let Ok(response) = client.is_ready(()).await && response.into_inner().ready { - return Ok(()); - } - - let now = Instant::now(); - if now >= deadline { - return Err(pyo3::PyErr::new::( - format!("timed out waiting for {component} server readiness"), - )); + return; } - tokio::time::sleep(std::cmp::min( - Duration::from_millis(100), - deadline.saturating_duration_since(now), - )) - .await; + tokio::time::sleep(Duration::from_millis(100)).await; } } @@ -352,18 +336,14 @@ impl MapAsyncServer { } /// Wait until the Numaflow IsReady probe succeeds over the map UDS. - #[pyo3(signature = (timeout: "float"=30.0) -> "None")] - pub fn wait_ready<'a>(&self, py: Python<'a>, timeout: f64) -> PyResult> { - if !timeout.is_finite() || timeout < 0.0 { - return Err(pyo3::PyErr::new::( - "timeout must be a non-negative finite float", - )); - } - + #[pyo3(signature = () -> "None")] + pub fn wait_ready<'a>(&self, py: Python<'a>) -> PyResult> { let sock_file = self.sock_file.clone(); - let timeout = Duration::from_secs_f64(timeout); - pyo3_async_runtimes::tokio::future_into_py(py, wait_for_ready(sock_file, timeout, "map")) + pyo3_async_runtimes::tokio::future_into_py(py, async move { + wait_for_ready(sock_file).await; + Ok(()) + }) } /// Trigger server shutdown from Python (idempotent). diff --git a/packages/pynumaflow-lite/src/mapstream/mod.rs b/packages/pynumaflow-lite/src/mapstream/mod.rs index c5bda782..99b17a8c 100644 --- a/packages/pynumaflow-lite/src/mapstream/mod.rs +++ b/packages/pynumaflow-lite/src/mapstream/mod.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::sync::Mutex; -use std::time::Duration; use chrono::{DateTime, Utc}; use numaflow::mapstream; @@ -222,21 +221,14 @@ impl MapStreamAsyncServer { } /// Wait until the Numaflow IsReady probe succeeds over the mapstream UDS. - #[pyo3(signature = (timeout: "float"=30.0) -> "None")] - pub fn wait_ready<'a>(&self, py: Python<'a>, timeout: f64) -> PyResult> { - if !timeout.is_finite() || timeout < 0.0 { - return Err(pyo3::PyErr::new::( - "timeout must be a non-negative finite float", - )); - } - + #[pyo3(signature = () -> "None")] + pub fn wait_ready<'a>(&self, py: Python<'a>) -> PyResult> { let sock_file = self.sock_file.clone(); - let timeout = Duration::from_secs_f64(timeout); - pyo3_async_runtimes::tokio::future_into_py( - py, - wait_for_ready(sock_file, timeout, "mapstream"), - ) + pyo3_async_runtimes::tokio::future_into_py(py, async move { + wait_for_ready(sock_file).await; + Ok(()) + }) } /// Trigger server shutdown from Python (idempotent). diff --git a/packages/pynumaflow-lite/src/pyrs.rs b/packages/pynumaflow-lite/src/pyrs.rs index f44868ba..5413dc30 100644 --- a/packages/pynumaflow-lite/src/pyrs.rs +++ b/packages/pynumaflow-lite/src/pyrs.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use pyo3::exceptions::PyBaseExceptionGroup; use pyo3::prelude::*; use pyo3::{IntoPyObjectExt, Py, PyAny, PyErr, Python}; use tokio::sync::oneshot::{Receiver, Sender}; @@ -62,10 +63,8 @@ pub(crate) fn format_error(py: Python<'_>, error: &PyErr) -> String { } } -// Join every handler failure into one error for Python to raise. -// -// Python 3.11 and later have BaseExceptionGroup, which prints each traceback in -// turn. Older versions have no group type, so they get the first error only. +// Join every handler failure into one BaseExceptionGroup for Python to raise, +// so that Python prints each traceback in turn. pub(crate) fn combine_errors(py: Python<'_>, errors: Vec) -> PyErr { let first = || errors.first().expect("errors is never empty").clone_ref(py); @@ -73,17 +72,13 @@ pub(crate) fn combine_errors(py: Python<'_>, errors: Vec) -> PyErr { return first(); } - let Ok(group_type) = py - .import("builtins") - .and_then(|builtins| builtins.getattr("BaseExceptionGroup")) - else { - return first(); - }; - let values: Vec<_> = errors.iter().map(|error| error.value(py).clone()).collect(); let message = format!("{} handler calls failed", values.len()); - match group_type.call1((message, values)) { + match py + .get_type::() + .call1((message, values)) + { Ok(group) => PyErr::from_value(group), Err(_) => first(), } diff --git a/packages/pynumaflow-lite/src/sink/mod.rs b/packages/pynumaflow-lite/src/sink/mod.rs index e63e1a13..fab62c0e 100644 --- a/packages/pynumaflow-lite/src/sink/mod.rs +++ b/packages/pynumaflow-lite/src/sink/mod.rs @@ -13,7 +13,6 @@ pub mod server; use tokio::net::UnixStream; use tokio::sync::mpsc; -use tokio::time::Instant; use tonic::transport::Uri; use tower::service_fn; @@ -419,29 +418,18 @@ async fn sink_client(sock_file: String) -> PyResult PyResult<()> { - let deadline = Instant::now() + timeout; - +// Poll the IsReady probe until it succeeds. There is no deadline: Python sets +// one with asyncio.timeout(), and cancellation drops this future. +async fn wait_for_ready(sock_file: String) { loop { if let Ok(mut client) = sink_client(sock_file.clone()).await && let Ok(response) = client.is_ready(()).await && response.into_inner().ready { - return Ok(()); - } - - let now = Instant::now(); - if now >= deadline { - return Err(pyo3::PyErr::new::( - "timed out waiting for sink server readiness", - )); + return; } - tokio::time::sleep(std::cmp::min( - Duration::from_millis(100), - deadline.saturating_duration_since(now), - )) - .await; + tokio::time::sleep(Duration::from_millis(100)).await; } } @@ -487,19 +475,12 @@ impl SinkAsyncServer { } /// Wait until the Numaflow IsReady probe succeeds over the sink UDS. - #[pyo3(signature = (timeout: "float"=30.0) -> "None")] - pub fn wait_ready<'a>(&self, py: Python<'a>, timeout: f64) -> PyResult> { - if !timeout.is_finite() || timeout < 0.0 { - return Err(pyo3::PyErr::new::( - "timeout must be a non-negative finite float", - )); - } - + #[pyo3(signature = () -> "None")] + pub fn wait_ready<'a>(&self, py: Python<'a>) -> PyResult> { let sock_file = self.sock_file.clone(); - let timeout = Duration::from_secs_f64(timeout); pyo3_async_runtimes::tokio::future_into_py(py, async move { - wait_for_ready(sock_file, timeout).await?; + wait_for_ready(sock_file).await; Ok(()) }) } diff --git a/packages/pynumaflow-lite/src/sourcetransform/mod.rs b/packages/pynumaflow-lite/src/sourcetransform/mod.rs index c2ab11a6..4e94a9ea 100644 --- a/packages/pynumaflow-lite/src/sourcetransform/mod.rs +++ b/packages/pynumaflow-lite/src/sourcetransform/mod.rs @@ -8,7 +8,6 @@ use numaflow::proto::source_transformer::source_transform_client::SourceTransfor use numaflow::sourcetransform; use pyo3::prelude::*; use tokio::net::UnixStream; -use tokio::time::Instant; use tonic::transport::Uri; use tower::service_fn; @@ -294,29 +293,18 @@ async fn source_transform_client( Ok(SourceTransformClient::new(channel)) } -async fn wait_for_ready(sock_file: String, timeout: Duration) -> PyResult<()> { - let deadline = Instant::now() + timeout; - +// Poll the IsReady probe until it succeeds. There is no deadline: Python sets +// one with asyncio.timeout(), and cancellation drops this future. +async fn wait_for_ready(sock_file: String) { loop { if let Ok(mut client) = source_transform_client(sock_file.clone()).await && let Ok(response) = client.is_ready(()).await && response.into_inner().ready { - return Ok(()); - } - - let now = Instant::now(); - if now >= deadline { - return Err(pyo3::PyErr::new::( - "timed out waiting for sourcetransform server readiness", - )); + return; } - tokio::time::sleep(std::cmp::min( - Duration::from_millis(100), - deadline.saturating_duration_since(now), - )) - .await; + tokio::time::sleep(Duration::from_millis(100)).await; } } @@ -365,18 +353,14 @@ impl SourceTransformAsyncServer { } /// Wait until the Numaflow IsReady probe succeeds over the sourcetransform UDS. - #[pyo3(signature = (timeout: "float"=30.0) -> "None")] - pub fn wait_ready<'a>(&self, py: Python<'a>, timeout: f64) -> PyResult> { - if !timeout.is_finite() || timeout < 0.0 { - return Err(pyo3::PyErr::new::( - "timeout must be a non-negative finite float", - )); - } - + #[pyo3(signature = () -> "None")] + pub fn wait_ready<'a>(&self, py: Python<'a>) -> PyResult> { let sock_file = self.sock_file.clone(); - let timeout = Duration::from_secs_f64(timeout); - pyo3_async_runtimes::tokio::future_into_py(py, wait_for_ready(sock_file, timeout)) + pyo3_async_runtimes::tokio::future_into_py(py, async move { + wait_for_ready(sock_file).await; + Ok(()) + }) } /// Trigger server shutdown from Python (idempotent). diff --git a/packages/pynumaflow-lite/tests/_test_utils.py b/packages/pynumaflow-lite/tests/_test_utils.py index d86c4f3c..f448a6f4 100644 --- a/packages/pynumaflow-lite/tests/_test_utils.py +++ b/packages/pynumaflow-lite/tests/_test_utils.py @@ -34,7 +34,12 @@ async def _unused_handler(datums): return [] server = sinker.SinkAsyncServer(_unused_handler, sock_file=str(path)) - asyncio.run(server.wait_ready(timeout=timeout)) + + async def _wait() -> None: + async with asyncio.timeout(timeout): + await server.wait_ready() + + asyncio.run(_wait()) def _stop_server(server: subprocess.Popen, timeout: float) -> str: diff --git a/packages/pynumaflow-lite/tests/examples/accumulator_stream_sorter.py b/packages/pynumaflow-lite/tests/examples/accumulator_stream_sorter.py index 6c7abb8d..9fbe9fc4 100644 --- a/packages/pynumaflow-lite/tests/examples/accumulator_stream_sorter.py +++ b/packages/pynumaflow-lite/tests/examples/accumulator_stream_sorter.py @@ -8,7 +8,7 @@ import asyncio import signal from collections.abc import AsyncIterator -from datetime import datetime +from datetime import UTC, datetime from pynumaflow_lite.accumulator import ( Accumulator, @@ -25,10 +25,8 @@ class StreamSorter(Accumulator): """ def __init__(self): - from datetime import timezone - # Initialize with a very old timestamp (timezone-aware) - self.latest_wm = datetime.fromtimestamp(-1, tz=timezone.utc) + self.latest_wm = datetime.fromtimestamp(-1, tz=UTC) self.sorted_buffer: list[Datum] = [] print("StreamSorter initialized") diff --git a/packages/pynumaflow-lite/tests/examples/source_simple.py b/packages/pynumaflow-lite/tests/examples/source_simple.py index 036bb179..118aa246 100644 --- a/packages/pynumaflow-lite/tests/examples/source_simple.py +++ b/packages/pynumaflow-lite/tests/examples/source_simple.py @@ -2,7 +2,7 @@ import logging import signal from collections.abc import AsyncIterator -from datetime import datetime, timezone +from datetime import UTC, datetime from pynumaflow_lite import sourcer from pynumaflow_lite._source_dtypes import Sourcer @@ -51,7 +51,7 @@ async def read_handler(self, datum: sourcer.ReadRequest) -> AsyncIterator[source message = sourcer.Message( payload=payload, offset=offset, - event_time=datetime.now(timezone.utc), + event_time=datetime.now(UTC), keys=["key1"], headers={"source": "simple"}, user_metadata=user_metadata, diff --git a/packages/pynumaflow-lite/tests/examples/sourcetransform_event_filter.py b/packages/pynumaflow-lite/tests/examples/sourcetransform_event_filter.py index c19bb503..a02082e7 100644 --- a/packages/pynumaflow-lite/tests/examples/sourcetransform_event_filter.py +++ b/packages/pynumaflow-lite/tests/examples/sourcetransform_event_filter.py @@ -1,10 +1,10 @@ import asyncio -from datetime import datetime, timezone +from datetime import UTC, datetime from pynumaflow_lite.sourcetransformer import Datum, Message, SourceTransformAsyncServer, SourceTransformer -january_first_2022 = datetime(2022, 1, 1, tzinfo=timezone.utc) -january_first_2023 = datetime(2023, 1, 1, tzinfo=timezone.utc) +january_first_2022 = datetime(2022, 1, 1, tzinfo=UTC) +january_first_2023 = datetime(2023, 1, 1, tzinfo=UTC) class EventFilter(SourceTransformer): diff --git a/packages/pynumaflow-lite/tests/examples/sourcetransform_event_filter_func.py b/packages/pynumaflow-lite/tests/examples/sourcetransform_event_filter_func.py index 9c96d3c2..7924a6a7 100644 --- a/packages/pynumaflow-lite/tests/examples/sourcetransform_event_filter_func.py +++ b/packages/pynumaflow-lite/tests/examples/sourcetransform_event_filter_func.py @@ -1,10 +1,10 @@ import asyncio -from datetime import datetime, timezone +from datetime import UTC, datetime from pynumaflow_lite.sourcetransformer import Datum, Message, SourceTransformAsyncServer -january_first_2022 = datetime(2022, 1, 1, tzinfo=timezone.utc) -january_first_2023 = datetime(2023, 1, 1, tzinfo=timezone.utc) +january_first_2022 = datetime(2022, 1, 1, tzinfo=UTC) +january_first_2023 = datetime(2023, 1, 1, tzinfo=UTC) async def event_filter(datum: Datum) -> list[Message]: diff --git a/packages/pynumaflow-lite/tests/test_sink_unit.py b/packages/pynumaflow-lite/tests/test_sink_unit.py index 7805d9c5..9fea7b25 100644 --- a/packages/pynumaflow-lite/tests/test_sink_unit.py +++ b/packages/pynumaflow-lite/tests/test_sink_unit.py @@ -102,6 +102,33 @@ async def handler(datums: AsyncIterator[sinker.Datum]) -> str: asyncio.run(_exercise_server(tmp_path, handler)) +def test_sink_server_startup_failure_raises_at_once(): + server = sinker.SinkAsyncServer( + _unit_handler, + sock_file="/nonexistent-pnl-dir/sink.sock", + server_info_file="/nonexistent-pnl-dir/sink.info", + ) + + async def enter() -> None: + # The timeout is only a guard. The server error must arrive first. + async with asyncio.timeout(10), server: + pass + + with pytest.raises(Exception, match="binding Unix socket"): + asyncio.run(enter()) + + +def test_sink_server_wait_ready_obeys_asyncio_timeout(tmp_path: Path): + server = sinker.SinkAsyncServer(_unit_handler, sock_file=str(tmp_path / "nobody-listens.sock")) + + async def wait() -> None: + async with asyncio.timeout(0.2): + await server.wait_ready() + + with pytest.raises(TimeoutError): + asyncio.run(wait()) + + def test_sink_server_rejects_sync_handler(tmp_path: Path): def handler(datums: AsyncIterator[sinker.Datum]) -> list[sinker.Response]: return [] diff --git a/packages/pynumaflow-lite/tests/test_sourcetransform_unit.py b/packages/pynumaflow-lite/tests/test_sourcetransform_unit.py index 03e39d88..e5069069 100644 --- a/packages/pynumaflow-lite/tests/test_sourcetransform_unit.py +++ b/packages/pynumaflow-lite/tests/test_sourcetransform_unit.py @@ -3,7 +3,7 @@ import subprocess import uuid from contextlib import suppress -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path import pytest @@ -12,7 +12,7 @@ CARGO_ROOT = Path(__file__).resolve().parent.parent -EVENT_TIME = datetime(2023, 6, 1, tzinfo=timezone.utc) +EVENT_TIME = datetime(2023, 6, 1, tzinfo=UTC) async def _unit_handler(datum: sourcetransformer.Datum) -> list[sourcetransformer.Message]: diff --git a/packages/pynumaflow-lite/uv.lock b/packages/pynumaflow-lite/uv.lock index 3341e7bd..ad48d2bb 100644 --- a/packages/pynumaflow-lite/uv.lock +++ b/packages/pynumaflow-lite/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" [[package]] name = "colorama" @@ -11,18 +11,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - [[package]] name = "iniconfig" version = "2.1.0" @@ -34,26 +22,23 @@ wheels = [ [[package]] name = "maturin" -version = "1.14.1" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/c8/22e5e21b2679c9bce6415ca578034ca2cc9316be0642ae21e051a2d5198c/maturin-1.15.0.tar.gz", hash = "sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a", size = 385504, upload-time = "2026-08-24T12:11:22.665Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, - { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, - { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, - { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, - { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/14/69/5c01b461044eb1f45ddcce006706eb88110c793cdb11c7ae0b5e08492e94/maturin-1.15.0-py3-none-linux_armv6l.whl", hash = "sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82", size = 10206220, upload-time = "2026-08-24T12:10:53.327Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1f/2b431554e11687cdb1077e0cdadcc118c53f611086b3af00c8545a67c6a5/maturin-1.15.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7", size = 19416513, upload-time = "2026-08-24T12:10:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/51/36/e23a21cb34a648b711036b9b2fe1d4f3f4ee24f8db54215d73f1a9a3a3ec/maturin-1.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd", size = 10014962, upload-time = "2026-08-24T12:10:58.486Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/33b15cb2d8f30f12c807955e8f2fd775027692904e30ec0784744ce8cd83/maturin-1.15.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55", size = 10196223, upload-time = "2026-08-24T12:11:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/b495e19e2f5c503b540452b2039115e7b2363867e8c5ad4179eb752fa92c/maturin-1.15.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e", size = 10541186, upload-time = "2026-08-24T12:11:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2b/2abff58037188d852b124871b1f0d720e1c2bfb3d4f1b03d87c52cd66488/maturin-1.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136", size = 10083468, upload-time = "2026-08-24T12:11:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/b7e9f8be99a6627849e81ac7b6694876bce8f50a92995fe17e3cf2610f0a/maturin-1.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1", size = 10047786, upload-time = "2026-08-24T12:11:07.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/167e3cb7accee11b507dbe53e0e87aeccb376d44ae66284c96ee4df3a9fd/maturin-1.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d", size = 13315332, upload-time = "2026-08-24T12:11:09.433Z" }, + { url = "https://files.pythonhosted.org/packages/14/4d/801379f646cbc6b00998e5289b0630a886be3a4ee4c75b6bc9b87478a7f1/maturin-1.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15", size = 10807183, upload-time = "2026-08-24T12:11:11.73Z" }, + { url = "https://files.pythonhosted.org/packages/89/27/2e612e1cbd1580e9e94d4722c227b5180dca27b32b955a34b79918aa1292/maturin-1.15.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176", size = 10413274, upload-time = "2026-08-24T12:11:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/202a7b4d75a51f20f84ec9ce3b7345b12b822207072164e2b1c6ef665125/maturin-1.15.0-py3-none-win32.whl", hash = "sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca", size = 8928744, upload-time = "2026-08-24T12:11:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/40/dc/4e90da594986ba78dd3bc8a5921ecdcdb11085b22b02a412caab3b225601/maturin-1.15.0-py3-none-win_amd64.whl", hash = "sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1", size = 10335085, upload-time = "2026-08-24T12:11:18.326Z" }, + { url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" }, ] [[package]] @@ -98,9 +83,9 @@ dev = [ [package.metadata.requires-dev] dev = [ - { name = "maturin", specifier = ">=1.14,<2.0" }, + { name = "maturin", specifier = ">=1.15,<2.0" }, { name = "pytest", specifier = ">=9.1.1" }, - { name = "ruff", specifier = ">=0.15.20" }, + { name = "ruff", specifier = ">=0.17.0" }, ] [[package]] @@ -109,12 +94,10 @@ version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ @@ -123,73 +106,25 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, - { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, - { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, - { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, - { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, -] - -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" }, + { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" }, ]