DE Mastery GitHub

Build your own mini stream processor · Stage 1 of 8

Operator graph + push-based execution

You are building a mini stream processor in pure Python — the engine behind Flink's DataStream API. You grow a single file, processor.py, across 8 stages. This stage lays the dataflow foundation: a chain of operators that a record is pushed through, one element at a time.

The public API (stable for every stage)

The dataclasses, operator classes and function signatures are defined in the starter. Do not rename them — the tests call them directly. The surface grows each stage; this stage uses:

# records / markers that flow downstream
Event(key=None, value=None, ts=None)   # a data record
Watermark(t)                           # event-time marker (stage 3)
Barrier(id)                            # checkpoint marker  (stage 6)

# build + run a job
Environment().source(records)          # -> Stream
stream.map(fn).filter(fn).sink()       # fluent chain of operators
collect(stream) -> list                # run it; return the Sink's data events

This stage's task

Make the operator chain actually execute, push-based (like Flink): the Source emits each record to a downstream callback; each operator's process(elem, out) transforms and forwards; the Sink collects.

  • Source.run(out) pushes every record downstream in order.
  • MapOp applies fn to data events and forwards Watermark/Barrier markers unchanged. FilterOp keeps data events for which fn is true, and always forwards markers. SinkOp collects data events and drops markers.
  • _drive_chain(ops) wires the chain (compose each operator's callback into the next) and runs the Source through it; it returns (sink_or_None, terminal_out).
  • collect(stream) returns the Sink's collected data events; with no Sink, the terminal data events (markers excluded). An empty source yields [].

Constraints

  • Push model, element-at-a-time: downstream order equals source order.
  • Markers pass through transforms but never reach a Sink.
  • Pure stdlib, deterministic, no sleeps. The suite runs in well under 60 s.

Run it

de start FLK-BYO/01       # copies the cumulative starter into your workspace
de test                  # runs this stage (later: earlier stages too)
de hint                  # progressive hints

Hints

Hint 1

A job is a linear chain of operators starting at a Source. Build it with the Stream builder (map/filter/sink already append operator objects to self.ops). Your job is to make the operators actually run.

Hint 2

Push model: the Source calls a downstream callback once per record; each operator's process(elem, out) does its work and calls out(...) 0+ times. Markers (Watermark, Barrier) must be FORWARDED by map/filter and DROPPED by the sink. Wire the chain in _drive_chain by composing per-operator callbacks from the last operator backwards.

Hint 3

collect() runs _drive_chain and returns the Sink's collected list (data events only). With no Sink, return the terminal data events (filter out Watermark/Barrier). An empty source yields an empty list.

Run it locally
uv run de start FLK-BYO/01-operator-graph
uv run de test