From 271a036e300eed5f131d09627c21dde5160809c4 Mon Sep 17 00:00:00 2001 From: Sreekanth Date: Sat, 10 Oct 2026 15:06:50 +0530 Subject: [PATCH 1/2] pynumaflow-lite: Require Python 3.11 or later Drop Python 3.10 and add Python 3.15 to the supported versions. - Set requires-python to >=3.11 and ruff target-version to py311. - Use typing.Self for __aenter__ return types and drop `from __future__ import annotations` in the stubs. - Use PyBaseExceptionGroup directly in combine_errors. The fallback for versions without BaseExceptionGroup is no longer necessary. - Use uv to install Python in CI and test on 3.11 to 3.15. - Move Dockerfile base images to slim-trixie. - Update ruff, maturin, and GitHub Actions versions. Signed-off-by: Sreekanth --- .github/workflows/pynumaflow-lite.yml | 46 +++--- .../manifests/accumulator/Dockerfile | 2 +- .../accumulator/accumulator_stream_sorter.py | 6 +- .../manifests/reduce/Dockerfile | 2 +- .../manifests/reducestream/Dockerfile | 2 +- .../manifests/session_reduce/Dockerfile | 2 +- .../manifests/sideinput/Dockerfile | 2 +- .../manifests/source/Dockerfile | 2 +- .../manifests/source/simple_source.py | 4 +- .../sourcetransform_event_filter.py | 6 +- .../pynumaflow_lite/_batchmap_server.py | 4 +- .../pynumaflow_lite/_map_server.py | 4 +- .../pynumaflow_lite/_mapstream_server.py | 4 +- .../pynumaflow_lite/_sink_server.py | 4 +- .../_sourcetransform_server.py | 4 +- .../pynumaflow_lite/batchmapper.pyi | 5 +- .../pynumaflow_lite/mapper.pyi | 5 +- .../pynumaflow_lite/mapstreamer.pyi | 5 +- .../pynumaflow_lite/reducer.pyi | 2 - .../pynumaflow_lite/reducestreamer.pyi | 2 - .../pynumaflow_lite/session_reducer.pyi | 2 - .../pynumaflow_lite/sideinputer.pyi | 2 - .../pynumaflow_lite/sinker.pyi | 5 +- .../pynumaflow_lite/sourcer.pyi | 2 - .../pynumaflow_lite/sourcetransformer.pyi | 5 +- packages/pynumaflow-lite/pyproject.toml | 15 +- packages/pynumaflow-lite/src/pyrs.rs | 19 +-- .../examples/accumulator_stream_sorter.py | 6 +- .../tests/examples/source_simple.py | 4 +- .../examples/sourcetransform_event_filter.py | 6 +- .../sourcetransform_event_filter_func.py | 6 +- .../tests/test_sourcetransform_unit.py | 4 +- packages/pynumaflow-lite/uv.lock | 139 +++++------------- 33 files changed, 127 insertions(+), 201 deletions(-) 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/_batchmap_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_batchmap_server.py index 69f33237..6e1adc93 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_batchmap_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_batchmap_server.py @@ -5,7 +5,7 @@ import signal from collections.abc import AsyncIterable, Awaitable, Callable from types import TracebackType -from typing import TypeAlias +from typing import Self, TypeAlias from .pynumaflow_lite import batchmapper as _batchmapper @@ -89,7 +89,7 @@ def _remove_signal_handlers(self) -> None: loop.remove_signal_handler(sig) self._installed_signals.clear() - async def __aenter__(self) -> BatchMapAsyncServer: + 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 diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_map_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_map_server.py index 04b5d97e..a9732068 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_map_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_map_server.py @@ -5,7 +5,7 @@ import signal from collections.abc import Awaitable, Callable from types import TracebackType -from typing import TypeAlias +from typing import Self, TypeAlias from .pynumaflow_lite import mapper as _mapper @@ -89,7 +89,7 @@ def _remove_signal_handlers(self) -> None: loop.remove_signal_handler(sig) self._installed_signals.clear() - async def __aenter__(self) -> MapAsyncServer: + 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 diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_mapstream_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_mapstream_server.py index 31ac047a..14b1ec0e 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_mapstream_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_mapstream_server.py @@ -5,7 +5,7 @@ import signal from collections.abc import AsyncIterator, Callable from types import TracebackType -from typing import TypeAlias +from typing import Self, TypeAlias from .pynumaflow_lite import mapstreamer as _mapstreamer @@ -89,7 +89,7 @@ def _remove_signal_handlers(self) -> None: loop.remove_signal_handler(sig) self._installed_signals.clear() - async def __aenter__(self) -> MapStreamAsyncServer: + 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 diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_sink_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_sink_server.py index c8ce034a..5b1c285d 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_sink_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_sink_server.py @@ -5,7 +5,7 @@ import signal from collections.abc import AsyncIterator, Awaitable, Callable from types import TracebackType -from typing import TypeAlias +from typing import Self, TypeAlias from .pynumaflow_lite import sinker as _sinker @@ -89,7 +89,7 @@ def _remove_signal_handlers(self) -> None: loop.remove_signal_handler(sig) self._installed_signals.clear() - async def __aenter__(self) -> SinkAsyncServer: + 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 diff --git a/packages/pynumaflow-lite/pynumaflow_lite/_sourcetransform_server.py b/packages/pynumaflow-lite/pynumaflow_lite/_sourcetransform_server.py index fb4c01b8..a1630b2e 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/_sourcetransform_server.py +++ b/packages/pynumaflow-lite/pynumaflow_lite/_sourcetransform_server.py @@ -5,7 +5,7 @@ import signal from collections.abc import Awaitable, Callable from types import TracebackType -from typing import TypeAlias +from typing import Self, TypeAlias from .pynumaflow_lite import sourcetransformer as _sourcetransformer @@ -89,7 +89,7 @@ def _remove_signal_handlers(self) -> None: loop.remove_signal_handler(sig) self._installed_signals.clear() - async def __aenter__(self) -> SourceTransformAsyncServer: + 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 diff --git a/packages/pynumaflow-lite/pynumaflow_lite/batchmapper.pyi b/packages/pynumaflow-lite/pynumaflow_lite/batchmapper.pyi index e87630d8..e6deaaa8 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 @@ -96,7 +95,7 @@ class BatchMapAsyncServer: def stop(self) -> None: ... async def wait_ready(self, timeout: float = ...) -> 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..1e0079de 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 @@ -95,7 +94,7 @@ class MapAsyncServer: def stop(self) -> None: ... async def wait_ready(self, timeout: float = ...) -> 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..6c8a9c02 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 @@ -86,7 +85,7 @@ class MapStreamAsyncServer: def stop(self) -> None: ... async def wait_ready(self, timeout: float = ...) -> 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..66c95264 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 @@ -109,7 +108,7 @@ class SinkAsyncServer: def stop(self) -> None: ... async def wait_ready(self, timeout: float = ...) -> 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..ef867957 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 @@ -98,7 +97,7 @@ class SourceTransformAsyncServer: def stop(self) -> None: ... async def wait_ready(self, timeout: float = ...) -> 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..372ed9be 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] @@ -42,6 +48,7 @@ select = [ ] ignore = [ "E501", # line too long + "ASYNC109", # wait_ready(timeout=...) is public API; the timeout runs in Rust ] [tool.ruff.lint.isort] @@ -59,6 +66,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/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/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_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" }, ] From 10aaec26b5b06471f22a2a770fad320ea5d717b0 Mon Sep 17 00:00:00 2001 From: Sreekanth Date: Sat, 10 Oct 2026 18:10:30 +0530 Subject: [PATCH 2/2] pynumaflow-lite: require Python 3.11, drop timeouts from server APIs Python 3.10 is EOL, so this raises the floor to 3.11 and cleans up the things we were carrying only for 3.10. The biggest change is how readiness works. Previously `wait_ready()` took a `timeout` argument that was enforced in Rust, and `async with server:` quietly used a 30 second default. That isn't how asyncio APIs work: the caller owns cancellation, and the idiomatic way to bound an await is `asyncio.timeout()`, which is available now that 3.11 is our minimum. So `wait_ready()` no longer takes any arguments and polls until the server is ready. pyo3-async-runtimes drops the Rust future when the Python future is cancelled, so an expired `asyncio.timeout()` really stops the polling loop instead of leaving it running behind us. Removing the fixed 30s from `__aenter__` meant it needed another way to avoid hanging forever when the server can't start. It now waits for whichever comes first: the server becoming ready, or the server task exiting. A startup failure (a bad socket path, for example) is raised immediately, instead of after 30 seconds of polling a server that is already dead. The five async server wrappers (map, batchmap, mapstream, sink, sourcetransform) were copies of each other that differed only in their Rust core type, their handler annotation, and the name in error messages. Rather than write the new `__aenter__` logic five times, they now share a private `_AsyncServer` base class. This is a breaking change to `wait_ready()`, but the package hasn't been published yet, so now is the time to do it. Smaller 3.11 cleanups: - `combine_errors` uses `PyBaseExceptionGroup` directly. The first-error-only fallback for interpreters without exception groups is gone. - `__aenter__` is annotated as returning `Self`. - `datetime.UTC` instead of `timezone.utc`. - Drop `from __future__ import annotations` from the stubs, where it has no effect. - CI no longer tests or builds wheels for 3.10. - Example images move from Debian bullseye (EOL) to trixie. Signed-off-by: Sreekanth --- .../pynumaflow_lite/_async_server.py | 147 ++++++++++++++++++ .../pynumaflow_lite/_batchmap_server.py | 115 ++------------ .../pynumaflow_lite/_map_server.py | 115 ++------------ .../pynumaflow_lite/_mapstream_server.py | 115 ++------------ .../pynumaflow_lite/_sink_server.py | 115 ++------------ .../_sourcetransform_server.py | 115 ++------------ .../pynumaflow_lite/batchmapper.pyi | 4 +- .../pynumaflow_lite/mapper.pyi | 4 +- .../pynumaflow_lite/mapstreamer.pyi | 4 +- .../pynumaflow_lite/sinker.pyi | 4 +- .../pynumaflow_lite/sourcetransformer.pyi | 4 +- packages/pynumaflow-lite/pyproject.toml | 1 - packages/pynumaflow-lite/src/batchmap/mod.rs | 21 +-- packages/pynumaflow-lite/src/map/mod.rs | 42 ++--- packages/pynumaflow-lite/src/mapstream/mod.rs | 20 +-- packages/pynumaflow-lite/src/sink/mod.rs | 35 +---- .../src/sourcetransform/mod.rs | 38 ++--- packages/pynumaflow-lite/tests/_test_utils.py | 7 +- .../pynumaflow-lite/tests/test_sink_unit.py | 27 ++++ 19 files changed, 278 insertions(+), 655 deletions(-) create mode 100644 packages/pynumaflow-lite/pynumaflow_lite/_async_server.py 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 6e1adc93..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 Self, TypeAlias +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) -> 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. - """ - 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 a9732068..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 Self, TypeAlias +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) -> 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. - """ - 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 14b1ec0e..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 Self, TypeAlias +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) -> 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. - """ - 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 5b1c285d..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 Self, TypeAlias +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) -> 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. - """ - 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 a1630b2e..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 Self, TypeAlias +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) -> 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. - """ - 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 e6deaaa8..fde5a076 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/batchmapper.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/batchmapper.pyi @@ -79,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: @@ -93,7 +93,7 @@ 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) -> Self: ... async def __aexit__( diff --git a/packages/pynumaflow-lite/pynumaflow_lite/mapper.pyi b/packages/pynumaflow-lite/pynumaflow_lite/mapper.pyi index 1e0079de..3e0666e8 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/mapper.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/mapper.pyi @@ -78,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: @@ -92,7 +92,7 @@ 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) -> Self: ... async def __aexit__( diff --git a/packages/pynumaflow-lite/pynumaflow_lite/mapstreamer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/mapstreamer.pyi index 6c8a9c02..8b38cc4f 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/mapstreamer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/mapstreamer.pyi @@ -69,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: @@ -83,7 +83,7 @@ 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) -> Self: ... async def __aexit__( diff --git a/packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi b/packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi index 66c95264..762c19fa 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi @@ -92,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: @@ -106,7 +106,7 @@ 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) -> Self: ... async def __aexit__( diff --git a/packages/pynumaflow-lite/pynumaflow_lite/sourcetransformer.pyi b/packages/pynumaflow-lite/pynumaflow_lite/sourcetransformer.pyi index ef867957..fd50d42a 100644 --- a/packages/pynumaflow-lite/pynumaflow_lite/sourcetransformer.pyi +++ b/packages/pynumaflow-lite/pynumaflow_lite/sourcetransformer.pyi @@ -81,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: @@ -95,7 +95,7 @@ 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) -> Self: ... async def __aexit__( diff --git a/packages/pynumaflow-lite/pyproject.toml b/packages/pynumaflow-lite/pyproject.toml index 372ed9be..a2a524aa 100644 --- a/packages/pynumaflow-lite/pyproject.toml +++ b/packages/pynumaflow-lite/pyproject.toml @@ -48,7 +48,6 @@ select = [ ] ignore = [ "E501", # line too long - "ASYNC109", # wait_ready(timeout=...) is public API; the timeout runs in Rust ] [tool.ruff.lint.isort] 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/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/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 []