Skip to content

Commit 41f6c0f

Browse files
authored
chore: source transformer example using pynumaflow-lite (#294)
Signed-off-by: Vigith Maurice <vigith@gmail.com>
1 parent 24ce158 commit 41f6c0f

6 files changed

Lines changed: 204 additions & 1 deletion

File tree

packages/pynumaflow-lite/manifests/session_reduce/pipeline.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ spec:
1111
vertices:
1212
- name: in
1313
source:
14-
# A self data generating source
1514
http: { }
1615
- name: session-counter
1716
udf:
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
FROM python:3.11-slim-bullseye AS builder
2+
3+
ENV PYTHONFAULTHANDLER=1 \
4+
PYTHONUNBUFFERED=1 \
5+
PYTHONHASHSEED=random \
6+
PIP_NO_CACHE_DIR=on \
7+
PIP_DISABLE_PIP_VERSION_CHECK=on \
8+
PIP_DEFAULT_TIMEOUT=100 \
9+
POETRY_HOME="/opt/poetry" \
10+
POETRY_VIRTUALENVS_IN_PROJECT=true \
11+
POETRY_NO_INTERACTION=1 \
12+
PYSETUP_PATH="/opt/pysetup"
13+
14+
ENV PATH="$POETRY_HOME/bin:$PATH"
15+
16+
RUN apt-get update \
17+
&& apt-get install --no-install-recommends -y \
18+
curl \
19+
wget \
20+
# deps for building python deps
21+
build-essential \
22+
&& apt-get install -y git \
23+
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
24+
&& curl -sSL https://install.python-poetry.org | python3 -
25+
26+
FROM builder AS udf
27+
28+
WORKDIR $PYSETUP_PATH
29+
COPY ./ ./
30+
31+
RUN pip install $PYSETUP_PATH/pynumaflow_lite-0.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
32+
33+
RUN poetry lock
34+
RUN poetry install --no-cache --no-root && \
35+
rm -rf ~/.cache/pypoetry/
36+
37+
CMD ["python", "sourcetransform_event_filter.py"]
38+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
To create the `wheel` file, refer [root](../../README.md)
2+
3+
## HOWTO build Image
4+
5+
```bash
6+
docker build . -t quay.io/numaio/numaflow/pynumaflow-lite-sourcetransform-event-filter:v1 --load
7+
```
8+
9+
Load it now to `k3d`
10+
11+
```bash
12+
k3d image import quay.io/numaio/numaflow/pynumaflow-lite-sourcetransform-event-filter:v1
13+
```
14+
15+
## Run the pipeline
16+
17+
```bash
18+
kubectl apply -f pipeline.yaml
19+
```
20+
21+
## About this example
22+
23+
This source transformer filters and routes messages based on their event time:
24+
25+
- **Messages before 2022**: Dropped
26+
- **Messages within 2022**: Tagged with `within_year_2022` and event time set to Jan 1, 2022
27+
- **Messages after 2022**: Tagged with `after_year_2022` and event time set to Jan 1, 2023
28+
29+
This demonstrates how source transformers can be used to:
30+
1. Filter out old/stale data
31+
2. Normalize event times
32+
3. Route messages to different downstream vertices based on conditions
33+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
apiVersion: numaflow.numaproj.io/v1alpha1
2+
kind: Pipeline
3+
metadata:
4+
name: sourcetransform-event-filter
5+
spec:
6+
vertices:
7+
- name: in
8+
source:
9+
# HTTP Source to control the event time
10+
http: { }
11+
transformer:
12+
container:
13+
image: quay.io/numaio/numaflow/pynumaflow-lite-sourcetransform-event-filter:v1
14+
imagePullPolicy: Never
15+
- name: sink
16+
scale:
17+
min: 1
18+
sink:
19+
log: { }
20+
edges:
21+
- from: in
22+
to: sink
23+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[project]
2+
name = "sourcetransform-event-filter"
3+
version = "0.1.0"
4+
description = "Source Transformer Event Filter Example"
5+
authors = [
6+
{ name = "Vigith Maurice", email = "vigith@gmail.com" }
7+
]
8+
readme = "README.md"
9+
requires-python = ">=3.11"
10+
dependencies = [
11+
]
12+
13+
14+
[build-system]
15+
requires = ["poetry-core>=2.0.0,<3.0.0"]
16+
build-backend = "poetry.core.masonry.api"
17+
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import asyncio
2+
import signal
3+
from datetime import datetime, timezone
4+
from pynumaflow_lite import sourcetransformer
5+
6+
# Define epoch timestamps for filtering
7+
january_first_2022 = datetime(2022, 1, 1, tzinfo=timezone.utc)
8+
january_first_2023 = datetime(2023, 1, 1, tzinfo=timezone.utc)
9+
10+
11+
class EventFilter(sourcetransformer.SourceTransformer):
12+
"""
13+
A source transformer that filters and routes messages based on event time.
14+
15+
- Messages before 2022 are dropped
16+
- Messages within 2022 are tagged with "within_year_2022"
17+
- Messages after 2022 are tagged with "after_year_2022"
18+
"""
19+
20+
async def handler(
21+
self, keys: list[str], datum: sourcetransformer.Datum
22+
) -> sourcetransformer.Messages:
23+
val = datum.value
24+
event_time = datum.event_time
25+
messages = sourcetransformer.Messages()
26+
27+
if event_time < january_first_2022:
28+
print(f"Got event time: {event_time}, it is before 2022, so dropping")
29+
messages.append(sourcetransformer.Message.message_to_drop(event_time))
30+
elif event_time < january_first_2023:
31+
print(f"Got event time: {event_time}, it is within year 2022, so forwarding to within_year_2022")
32+
messages.append(
33+
sourcetransformer.Message(
34+
value=val,
35+
event_time=january_first_2022,
36+
keys=keys,
37+
tags=["within_year_2022"]
38+
)
39+
)
40+
else:
41+
print(f"Got event time: {event_time}, it is after year 2022, so forwarding to after_year_2022")
42+
messages.append(
43+
sourcetransformer.Message(
44+
value=val,
45+
event_time=january_first_2023,
46+
keys=keys,
47+
tags=["after_year_2022"]
48+
)
49+
)
50+
51+
return messages
52+
53+
54+
# Optional: ensure default signal handlers are in place so asyncio.run can handle them cleanly.
55+
signal.signal(signal.SIGINT, signal.default_int_handler)
56+
try:
57+
signal.signal(signal.SIGTERM, signal.SIG_DFL)
58+
except AttributeError:
59+
pass
60+
61+
62+
async def start(f: callable):
63+
server = sourcetransformer.SourceTransformAsyncServer()
64+
65+
# Register loop-level signal handlers so we control shutdown and avoid asyncio.run
66+
# converting it into KeyboardInterrupt/CancelledError traces.
67+
loop = asyncio.get_running_loop()
68+
loop.set_debug(True)
69+
print("Registering signal handlers", loop)
70+
try:
71+
loop.add_signal_handler(signal.SIGINT, lambda: server.stop())
72+
loop.add_signal_handler(signal.SIGTERM, lambda: server.stop())
73+
except (NotImplementedError, RuntimeError):
74+
print("Failed to register signal handlers")
75+
# add_signal_handler may not be available on some platforms/contexts; fallback below.
76+
pass
77+
78+
try:
79+
await server.start(f)
80+
print("Shutting down gracefully...")
81+
except asyncio.CancelledError:
82+
# Fallback in case the task was cancelled by the runner
83+
try:
84+
server.stop()
85+
except Exception:
86+
pass
87+
return
88+
89+
90+
if __name__ == "__main__":
91+
async_handler = EventFilter()
92+
asyncio.run(start(async_handler))
93+

0 commit comments

Comments
 (0)