Skip to content

Commit d9dd427

Browse files
committed
Merge branch '2026-generators'
2 parents 66c57bf + 36bb271 commit d9dd427

9 files changed

Lines changed: 466 additions & 0 deletions

File tree

2026/generators/00_basic.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Generator
2+
3+
4+
def read_logs() -> Generator[str, None, None]:
5+
lines = [
6+
"info User logged in",
7+
"warning Slow database query",
8+
"error Payment failed",
9+
]
10+
for line in lines:
11+
print(f"producing: {line}")
12+
yield line
13+
14+
15+
def main():
16+
for line in read_logs():
17+
print(f"consuming: {line}")
18+
19+
20+
if __name__ == "__main__":
21+
main()

2026/generators/01_structured.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from dataclasses import dataclass
2+
from enum import StrEnum, auto
3+
from typing import Generator, Iterable
4+
5+
6+
# --- Domain model ---
7+
class LogLevel(StrEnum):
8+
INFO = auto()
9+
WARNING = auto()
10+
ERROR = auto()
11+
12+
13+
@dataclass(slots=True)
14+
class LogRecord:
15+
level: LogLevel
16+
message: str
17+
18+
19+
# --- Source generator ---
20+
def read_logs() -> Generator[str, None, None]:
21+
lines = [
22+
"info User logged in",
23+
"warning Slow database query",
24+
"error Payment failed",
25+
]
26+
for line in lines:
27+
print(f"producing: {line}")
28+
yield line
29+
30+
31+
# --- Transformation step ---
32+
def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, None]:
33+
for line in lines:
34+
level_text, message = line.split(" ", maxsplit=1)
35+
level = LogLevel(level_text) # validated conversion
36+
yield LogRecord(level=level, message=message)
37+
38+
39+
def handle_records(records: Iterable[LogRecord]) -> None:
40+
for record in records:
41+
print(f"handling: {record}")
42+
43+
44+
# --- Application entry point ---
45+
def main() -> None:
46+
records = parse_logs(read_logs())
47+
handle_records(records)
48+
49+
50+
if __name__ == "__main__":
51+
main()

2026/generators/02_composition.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from dataclasses import dataclass
2+
from enum import StrEnum, auto
3+
from functools import reduce
4+
from typing import Any, Callable, Generator, Iterable
5+
6+
7+
# --- Domain model ---
8+
class LogLevel(StrEnum):
9+
INFO = auto()
10+
WARNING = auto()
11+
ERROR = auto()
12+
13+
14+
@dataclass(slots=True)
15+
class LogRecord:
16+
level: LogLevel
17+
message: str
18+
19+
20+
# --- Source generator ---
21+
def read_logs() -> Generator[str, None, None]:
22+
lines = [
23+
"info User logged in",
24+
"warning Slow database query",
25+
"error Payment failed",
26+
]
27+
for line in lines:
28+
print(f"producing: {line}")
29+
yield line
30+
31+
32+
# --- Pipeline stages ---
33+
def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, None]:
34+
for line in lines:
35+
level_text, message = line.split(" ", maxsplit=1)
36+
level = LogLevel(level_text)
37+
yield LogRecord(level=level, message=message)
38+
39+
40+
def filter_important(
41+
records: Iterable[LogRecord],
42+
) -> Generator[LogRecord, None, None]:
43+
for record in records:
44+
if record.level in {LogLevel.WARNING, LogLevel.ERROR}:
45+
yield record
46+
47+
48+
def normalize_messages(
49+
records: Iterable[LogRecord],
50+
) -> Generator[LogRecord, None, None]:
51+
for record in records:
52+
yield LogRecord(
53+
level=record.level,
54+
message=record.message.lower(),
55+
)
56+
57+
58+
# --- Composition helper ---
59+
type PipelineStage = Callable[[Iterable[Any]], Iterable[Any]]
60+
61+
62+
def compose(*stages: PipelineStage) -> PipelineStage:
63+
def apply(data: Iterable[Any]) -> Iterable[Any]:
64+
return reduce(lambda acc, stage: stage(acc), stages, data)
65+
66+
return apply
67+
68+
69+
def handle_records(records: Iterable[LogRecord]) -> None:
70+
for record in records:
71+
print(f"handling: {record}")
72+
73+
74+
# --- Application entry point ---
75+
def main() -> None:
76+
pipeline = compose(
77+
parse_logs,
78+
filter_important,
79+
normalize_messages,
80+
)
81+
82+
handle_records(pipeline(read_logs()))
83+
84+
85+
if __name__ == "__main__":
86+
main()

2026/generators/03_backpressure.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import time
2+
from dataclasses import dataclass
3+
from enum import StrEnum, auto
4+
from functools import reduce
5+
from typing import Any, Callable, Generator, Iterable
6+
7+
8+
# --- Domain model ---
9+
class LogLevel(StrEnum):
10+
INFO = auto()
11+
WARNING = auto()
12+
ERROR = auto()
13+
14+
15+
@dataclass(slots=True)
16+
class LogRecord:
17+
level: LogLevel
18+
message: str
19+
20+
21+
# --- Source generator ---
22+
def read_logs() -> Generator[str, None, None]:
23+
lines = [
24+
"info User logged in",
25+
"warning Slow database query",
26+
"error Payment failed",
27+
]
28+
for line in lines:
29+
print(f"producing: {line}")
30+
yield line
31+
32+
33+
# --- Pipeline stages ---
34+
def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, None]:
35+
for line in lines:
36+
print(f"parsing: {line}")
37+
level_text, message = line.split(" ", maxsplit=1)
38+
level = LogLevel(level_text)
39+
yield LogRecord(level=level, message=message)
40+
41+
42+
def filter_important(
43+
records: Iterable[LogRecord],
44+
) -> Generator[LogRecord, None, None]:
45+
for record in records:
46+
print(f"filtering: {record}")
47+
if record.level in {LogLevel.WARNING, LogLevel.ERROR}:
48+
yield record
49+
50+
51+
def normalize_messages(
52+
records: Iterable[LogRecord],
53+
) -> Generator[LogRecord, None, None]:
54+
for record in records:
55+
print(f"normalizing: {record}")
56+
yield LogRecord(
57+
level=record.level,
58+
message=record.message.lower(),
59+
)
60+
61+
62+
# --- Composition helper ---
63+
type PipelineStage = Callable[[Iterable[Any]], Iterable[Any]]
64+
65+
66+
def compose(*stages: PipelineStage) -> PipelineStage:
67+
def apply(data: Iterable[Any]) -> Iterable[Any]:
68+
return reduce(lambda acc, stage: stage(acc), stages, data)
69+
70+
return apply
71+
72+
73+
# --- Slow consumer ---
74+
def handle_records(records: Iterable[LogRecord]) -> None:
75+
for record in records:
76+
print(f"handling: {record}")
77+
time.sleep(1)
78+
79+
80+
# --- Application entry point ---
81+
def main() -> None:
82+
pipeline = compose(
83+
parse_logs,
84+
filter_important,
85+
normalize_messages,
86+
)
87+
88+
handle_records(pipeline(read_logs()))
89+
90+
91+
if __name__ == "__main__":
92+
main()

2026/generators/04_send.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from dataclasses import dataclass
2+
from enum import StrEnum, auto
3+
from typing import Generator, Iterable
4+
5+
6+
# --- Domain model ---
7+
class LogLevel(StrEnum):
8+
INFO = auto()
9+
WARNING = auto()
10+
ERROR = auto()
11+
12+
13+
@dataclass(slots=True)
14+
class LogRecord:
15+
level: LogLevel
16+
message: str
17+
18+
19+
# --- Source generator ---
20+
def read_logs() -> Generator[str, None, None]:
21+
lines = [
22+
"info User logged in",
23+
"warning Slow database query",
24+
"error Payment failed",
25+
"warning Disk space low",
26+
]
27+
for line in lines:
28+
print(f"producing: {line}")
29+
yield line
30+
31+
32+
def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, None]:
33+
for line in lines:
34+
level_text, message = line.split(" ", maxsplit=1)
35+
yield LogRecord(level=LogLevel(level_text), message=message)
36+
37+
38+
def should_emit(record: LogRecord, threshold: LogLevel) -> bool:
39+
if threshold is LogLevel.WARNING:
40+
return record.level in {LogLevel.WARNING, LogLevel.ERROR}
41+
return record.level is LogLevel.ERROR
42+
43+
44+
# --- Simpler send() example ---
45+
def threshold_filter() -> Generator[LogLevel, LogLevel | None, None]:
46+
threshold = LogLevel.ERROR
47+
while True:
48+
new_threshold = yield threshold
49+
if new_threshold is not None:
50+
threshold = new_threshold
51+
52+
53+
def main() -> None:
54+
records = parse_logs(read_logs())
55+
56+
filter_settings = threshold_filter()
57+
threshold = next(filter_settings) # prime the generator
58+
59+
for index, record in enumerate(records):
60+
if index == 2:
61+
threshold = filter_settings.send(LogLevel.WARNING)
62+
else:
63+
threshold = next(filter_settings)
64+
65+
if should_emit(record, threshold):
66+
print(f"handling: {record}")
67+
68+
69+
if __name__ == "__main__":
70+
main()

2026/generators/04b_return.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from collections.abc import Generator, Iterable
2+
from dataclasses import dataclass
3+
from enum import StrEnum, auto
4+
5+
6+
class LogLevel(StrEnum):
7+
INFO = auto()
8+
WARNING = auto()
9+
ERROR = auto()
10+
11+
12+
@dataclass(slots=True)
13+
class LogRecord:
14+
level: LogLevel
15+
message: str
16+
17+
18+
def read_logs() -> Generator[str, None, None]:
19+
lines = [
20+
"info User logged in",
21+
"warning Slow database query",
22+
"error Payment failed",
23+
]
24+
for line in lines:
25+
yield line
26+
27+
28+
def parse_logs(lines: Iterable[str]) -> Generator[LogRecord, None, int]:
29+
count = 0
30+
for line in lines:
31+
level_text, message = line.split(" ", maxsplit=1)
32+
yield LogRecord(level=LogLevel(level_text), message=message)
33+
count += 1
34+
35+
return count # 👈 final result
36+
37+
38+
def main() -> None:
39+
parser = parse_logs(read_logs())
40+
41+
try:
42+
while True:
43+
record = next(parser)
44+
print(f"handling: {record}")
45+
except StopIteration as e:
46+
print(f"Total records processed: {e.value}")
47+
48+
49+
if __name__ == "__main__":
50+
main()

0 commit comments

Comments
 (0)