Skip to content

Commit 9c5886f

Browse files
committed
Make sinker more Pythonic
Signed-off-by: Sreekanth <prsreekanth920@gmail.com>
1 parent 4ea045f commit 9c5886f

13 files changed

Lines changed: 631 additions & 453 deletions

File tree

.github/workflows/pynumaflow-lite.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
rustup default stable
6060
6161
- name: Install dependencies
62-
run: uv sync --group dev
62+
run: uv sync --locked --group dev
6363

6464
- name: Check Rust formatting
6565
run: cargo fmt --all --check
@@ -112,7 +112,7 @@ jobs:
112112
echo "::group::Python ${python_version}"
113113
uv python install "${python_version}"
114114
export UV_PYTHON="${python_version}"
115-
uv sync --group dev --python "${python_version}"
115+
uv sync --locked --group dev --python "${python_version}"
116116
uv run --python "${python_version}" maturin develop --locked
117117
118118
uv run --python "${python_version}" pytest -v
Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import asyncio
21
import logging
3-
import signal
4-
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable
2+
from collections.abc import AsyncIterable
53

64
from pynumaflow_lite import sinker
7-
from pynumaflow_lite._sink_dtypes import Sinker
5+
from pynumaflow_lite.sinker import Sinker
86

97
# Configure logging
108
logging.basicConfig(level=logging.INFO)
@@ -16,37 +14,15 @@ class SimpleLogSink(Sinker):
1614
Simple log sink that logs each message and returns success responses.
1715
"""
1816

19-
async def handler(self, datums: AsyncIterable[sinker.Datum]) -> sinker.Responses:
20-
responses = sinker.Responses()
17+
async def handler(self, datums: AsyncIterable[sinker.Datum]) -> list[sinker.Response]:
18+
responses = []
2119
async for msg in datums:
2220
_LOGGER.info("User Defined Sink: %s", msg.value.decode("utf-8"))
23-
responses.append(sinker.Response.as_success(msg.id))
21+
responses.append(sinker.Response.success(msg.id))
2422
# if we are not able to write to sink and if we have a fallback sink configured
25-
# we can use Response.as_fallback(msg.id) to write the message to fallback sink
23+
# we can use Response.fallback(msg.id) to write the message to fallback sink
2624
return responses
2725

2826

29-
async def start(
30-
f: Callable[[AsyncIterator[sinker.Datum]], Awaitable[sinker.Responses]],
31-
):
32-
server = sinker.SinkAsyncServer()
33-
34-
# Register loop-level signal handlers so we control shutdown and avoid asyncio.run
35-
loop = asyncio.get_running_loop()
36-
try:
37-
loop.add_signal_handler(signal.SIGINT, lambda: server.stop())
38-
loop.add_signal_handler(signal.SIGTERM, lambda: server.stop())
39-
except (NotImplementedError, RuntimeError):
40-
pass
41-
42-
try:
43-
await server.start(f)
44-
print("Shutting down gracefully...")
45-
except asyncio.CancelledError:
46-
server.stop()
47-
return
48-
49-
5027
if __name__ == "__main__":
51-
async_handler = SimpleLogSink()
52-
asyncio.run(start(async_handler))
28+
sinker.SinkAsyncServer(SimpleLogSink()).run()

packages/pynumaflow-lite/pynumaflow_lite/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
from ._session_reduce_dtypes import SessionReducer
7373
from ._sideinput_dtypes import SideInput
7474
from ._sink_dtypes import Sinker
75+
from ._sink_server import SinkAsyncServer
7576
from ._source_dtypes import Sourcer
7677
from ._sourcetransformer_dtypes import SourceTransformer
7778

@@ -98,6 +99,7 @@
9899

99100
if sinker is not None:
100101
sinker.Sinker = Sinker
102+
sinker.SinkAsyncServer = SinkAsyncServer
101103

102104
if sourcer is not None:
103105
sourcer.Sourcer = Sourcer

packages/pynumaflow-lite/pynumaflow_lite/_sink_dtypes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABCMeta, abstractmethod
22
from collections.abc import AsyncIterable
33

4-
from pynumaflow_lite.sinker import Datum, Responses
4+
from pynumaflow_lite.sinker import Datum, Response
55

66

77
class Sinker(metaclass=ABCMeta):
@@ -13,7 +13,7 @@ def __call__(self, *args, **kwargs):
1313
return self.handler(*args, **kwargs)
1414

1515
@abstractmethod
16-
async def handler(self, datums: AsyncIterable[Datum]) -> Responses:
16+
async def handler(self, datums: AsyncIterable[Datum]) -> list[Response]:
1717
"""
1818
Implement this handler function for sink.
1919
Process the stream of datums and return responses.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import signal
5+
from types import TracebackType
6+
from typing import Any
7+
8+
from .pynumaflow_lite import sinker as _sinker
9+
10+
11+
class SinkAsyncServer:
12+
def __init__(
13+
self,
14+
handler: Any,
15+
*,
16+
sock_file: str | None = None,
17+
server_info_file: str | None = None,
18+
) -> None:
19+
self._core = _sinker._SinkAsyncServer(sock_file, server_info_file)
20+
self._handler = handler
21+
self._task: asyncio.Task[None] | None = None
22+
23+
async def serve(self) -> None:
24+
await self._core.start(self._handler)
25+
26+
def stop(self) -> None:
27+
self._core.stop()
28+
29+
async def wait_ready(self, timeout: float = 30.0) -> None:
30+
await self._core.wait_ready(timeout)
31+
32+
async def __aenter__(self) -> SinkAsyncServer:
33+
if self._task is not None and not self._task.done():
34+
raise RuntimeError("sink server is already serving")
35+
36+
self._task = asyncio.create_task(self.serve())
37+
try:
38+
await self.wait_ready()
39+
except asyncio.CancelledError:
40+
self.stop()
41+
if self._task is not None:
42+
await self._task
43+
raise
44+
except Exception:
45+
self.stop()
46+
if self._task is not None:
47+
await self._task
48+
raise
49+
return self
50+
51+
async def __aexit__(
52+
self,
53+
exc_type: type[BaseException] | None,
54+
exc: BaseException | None,
55+
tb: TracebackType | None,
56+
) -> None:
57+
self.stop()
58+
if self._task is not None:
59+
try:
60+
await self._task
61+
finally:
62+
self._task = None
63+
64+
async def _main(self) -> None:
65+
loop = asyncio.get_running_loop()
66+
try:
67+
loop.add_signal_handler(signal.SIGINT, self.stop)
68+
loop.add_signal_handler(signal.SIGTERM, self.stop)
69+
except (NotImplementedError, RuntimeError):
70+
pass
71+
72+
await self.serve()
73+
74+
def run(self) -> None:
75+
try:
76+
asyncio.run(self._main())
77+
except KeyboardInterrupt:
78+
self.stop()

packages/pynumaflow-lite/pynumaflow_lite/sinker.pyi

Lines changed: 55 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,113 +2,103 @@ from __future__ import annotations
22

33
import datetime as _dt
44
from collections.abc import AsyncIterator, Awaitable, Callable
5-
6-
class SystemMetadata:
7-
"""System-generated metadata groups per message (read-only for sink)."""
8-
9-
def __init__(self) -> None: ...
10-
def groups(self) -> list[str]:
11-
"""Returns the groups of the system metadata."""
12-
...
13-
14-
def keys(self, group: str) -> list[str]:
15-
"""Returns the keys of the system metadata for the given group."""
16-
...
17-
18-
def value(self, group: str, key: str) -> bytes:
19-
"""Returns the value of the system metadata for the given group and key."""
20-
...
21-
22-
def __repr__(self) -> str: ...
23-
24-
class UserMetadata:
25-
"""User-defined metadata groups per message (read-only for sink)."""
26-
27-
def __init__(self) -> None: ...
28-
def groups(self) -> list[str]:
29-
"""Returns the groups of the user metadata."""
30-
...
31-
32-
def keys(self, group: str) -> list[str]:
33-
"""Returns the keys of the user metadata for the given group."""
34-
...
35-
36-
def value(self, group: str, key: str) -> bytes:
37-
"""Returns the value of the user metadata for the given group and key."""
38-
...
39-
40-
def __repr__(self) -> str: ...
41-
42-
class KeyValueGroup:
43-
key_value: dict[str, bytes]
44-
45-
def __init__(self, key_value: dict[str, bytes] | None = ...) -> None: ...
46-
@staticmethod
47-
def from_dict(key_value: dict[str, bytes]) -> KeyValueGroup: ...
5+
from types import TracebackType
486

497
class Message:
508
keys: list[str] | None
519
value: bytes
52-
user_metadata: dict[str, KeyValueGroup] | None
10+
user_metadata: dict[str, dict[str, bytes]] | None
5311

5412
def __init__(
5513
self,
5614
value: bytes,
5715
keys: list[str] | None = ...,
58-
user_metadata: dict[str, KeyValueGroup] | None = ...,
16+
user_metadata: dict[str, dict[str, bytes]] | None = ...,
5917
) -> None: ...
18+
def __repr__(self) -> str: ...
19+
def __eq__(self, other: object) -> bool: ...
6020

6121
class Response:
6222
id: str
23+
error: str | None
6324

6425
@staticmethod
65-
def as_success(id: str) -> Response: ...
26+
def success(id: str) -> Response: ...
6627
@staticmethod
67-
def as_failure(id: str, err_msg: str) -> Response: ...
28+
def failure(id: str, error: str) -> Response: ...
6829
@staticmethod
69-
def as_fallback(id: str) -> Response: ...
30+
def fallback(id: str) -> Response: ...
7031
@staticmethod
71-
def as_serve(id: str, payload: bytes) -> Response: ...
32+
def serve(id: str, payload: bytes) -> Response: ...
7233
@staticmethod
73-
def as_on_success(id: str, message: Message | None = ...) -> Response: ...
74-
75-
class Responses:
76-
def __init__(self) -> None: ...
77-
def append(self, response: Response) -> None: ...
34+
def on_success(id: str, message: Message | None = ...) -> Response: ...
35+
def __repr__(self) -> str: ...
36+
def __eq__(self, other: object) -> bool: ...
7837

7938
class Datum:
8039
keys: list[str]
8140
value: bytes
8241
watermark: _dt.datetime
83-
eventtime: _dt.datetime
42+
event_time: _dt.datetime
8443
id: str
8544
headers: dict[str, str]
86-
user_metadata: UserMetadata
87-
system_metadata: SystemMetadata
45+
user_metadata: dict[str, dict[str, bytes]]
46+
system_metadata: dict[str, dict[str, bytes]]
8847

48+
def __init__(
49+
self,
50+
*,
51+
keys: list[str] = ...,
52+
value: bytes = ...,
53+
id: str = ...,
54+
event_time: _dt.datetime | None = ...,
55+
watermark: _dt.datetime | None = ...,
56+
headers: dict[str, str] = ...,
57+
user_metadata: dict[str, dict[str, bytes]] = ...,
58+
system_metadata: dict[str, dict[str, bytes]] = ...,
59+
) -> None: ...
8960
def __repr__(self) -> str: ...
9061
def __str__(self) -> str: ...
9162

92-
class SinkAsyncServer:
63+
_SinkHandler = Callable[[AsyncIterator[Datum]], Awaitable[list[Response]]]
64+
65+
class _SinkAsyncServer:
9366
def __init__(
9467
self,
9568
sock_file: str | None = ...,
96-
info_file: str | None = ...,
69+
server_info_file: str | None = ...,
9770
) -> None: ...
98-
def start(self, py_func: Callable[[AsyncIterator[Datum]], Awaitable[Responses]]) -> Awaitable[None]: ...
71+
def start(self, handler: _SinkHandler) -> Awaitable[None]: ...
72+
def wait_ready(self, timeout: float = ...) -> Awaitable[None]: ...
9973
def stop(self) -> None: ...
10074

10175
class Sinker:
102-
async def handler(self, datums: AsyncIterator[Datum]) -> Responses: ...
76+
async def handler(self, datums: AsyncIterator[Datum]) -> list[Response]: ...
77+
78+
class SinkAsyncServer:
79+
def __init__(
80+
self,
81+
handler: _SinkHandler | Sinker,
82+
*,
83+
sock_file: str | None = ...,
84+
server_info_file: str | None = ...,
85+
) -> None: ...
86+
def run(self) -> None: ...
87+
async def serve(self) -> None: ...
88+
def stop(self) -> None: ...
89+
async def wait_ready(self, timeout: float = ...) -> None: ...
90+
async def __aenter__(self) -> SinkAsyncServer: ...
91+
async def __aexit__(
92+
self,
93+
exc_type: type[BaseException] | None,
94+
exc: BaseException | None,
95+
tb: TracebackType | None,
96+
) -> None: ...
10397

10498
__all__ = [
10599
"Datum",
106-
"KeyValueGroup",
107100
"Message",
108101
"Response",
109-
"Responses",
110102
"SinkAsyncServer",
111103
"Sinker",
112-
"SystemMetadata",
113-
"UserMetadata",
114104
]

0 commit comments

Comments
 (0)