[Python Series 20] Building a Study-Report Automation Tool

한국어 버전

Building the Automation Tool

In this final chapter, we stitch every concept into one flow. The goal is to build an automation tool that collects student study reports, summarizes them, and sends the result. The logic stays simple, but you will touch comprehensions, decorators, generators, context managers, type hints, async code, and packaging. Do not combine everything at once—move stage by stage and reopen the relevant chapter (decorators in part 14, context managers in 16, type hints in 17, async in 18, packaging in 19) whenever you need a refresher.

Key terms

  1. Pipeline: a data flow that connects input → processing → output in order
  2. Asynchronous notification: dispatching HTTP or other external sends concurrently via async functions

Core ideas

Study notes

  • Time required: 90–120 minutes (with per-module practice)
  • Prerequisites: hands-on experience from parts 02–19, Typer CLI builds, basic pytest usage
  • Goal: connect loader → summary → notifier → CLI into a single pipeline and log testing ideas
  • JSONL (JSON Lines) stores one JSON object per line.
  • Generators process files line by line to save memory.
  • TypedDict documents the summary data shape for collaborators.
  • Protocol expresses the async notification contract in code.
  • Each stage title includes a reference to earlier chapters so you know where to review.

Code examples

Project overview

  • Input: a JSONL file (reports.jsonl) with per-student progress data
  • Processing: use a generator to read each line, filter with comprehensions, and log via decorators
  • Output: send summaries through an async HTTP API

Project layout:

  __init__.py
  loader.py
  summary.py
  notifier.py
  cli.py

stage0: "Stage 0
prep data"
stage1: "Stage 1 (Ch16)
loader.py
context + generator"
stage2: "Stage 2 (Ch13·14·17)
summary.py
comprehension + type hints"
stage3: "Stage 3 (Ch18)
notifier.py
async/await"
stage4: "Stage 4 (Ch12·15·19)
cli.py
Typer + packaging"

Listing both the data types and the chapter references next to each module helps you jump back to the right lesson whenever you get stuck.

Stage-by-stage checkpoints

Stage 0. Create JSONL sample data (Core)

reports.jsonl contains one study record per line.

{"student": "민지", "module": "functions", "score": 95, "done": true}
{"student": "준호", "module": "async", "score": 81, "done": false}
  • Input prep: 5–10 lines are enough. Generate it with Path.write_text from part 06 if you like.
  • Checkpoint: remove blank lines or malformed JSON now because Stage 1 will fail fast when json.loads hits them.

Stage 1. Data loader (generator + context manager, Core)

# loader.py
from collections.abc import Iterator
from pathlib import Path


    with path.open("r", encoding="utf-8") as f:
        for line in f:
            if line.strip():
                yield json.loads(line)
  • Use with to open the file and close it automatically.
  • A generator processes each line without loading everything into memory.

Input → Output

  • Input: JSONL file path
  • Output: Iterator[dict]
Line 1 → {'student': '민지', 'module': 'functions', ...}
Line 2 → {'student': '준호', 'module': 'async', ...}

If anything breaks, revisit part 16 (context managers) and part 06 (file I/O) to double-check cleanup.

Stage 2. Summaries (comprehensions + type hints, Core)

# summary.py
from typing import Iterable, TypedDict


class Summary(TypedDict):
    student: str
    avg_score: float
    completed: list[str]


    summaries = []
    for student, items in group_by_student(records).items():
        scores = [item["score"] for item in items]
        completed = [item["module"] for item in items if item["done"]]
        summaries.append(
            {
                "student": student,
                "avg_score": sum(scores) / len(scores),
                "completed": completed,
            }
        )
    return summaries


    grouped: dict[str, list[dict]] = {}
    for record in records:
        grouped.setdefault(record["student"], []).append(record)
    return grouped
  • Use list comprehensions to pluck conditional values.
  • TypedDict documents the output.

Input → Output

  • Input: Iterator[dict] from Stage 1
  • Output: list[Summary]
[
    {
        "student": "민지",
        "avg_score": 94.5,
        "completed": ["functions", "decorators"],
    },
    ...
]

If comprehensions feel fuzzy, revisit part 13; if decorators or type hints are rusty, jump back to parts 14 and 17 before comparing with this code.

Stage 2 extension: decorator-based logging (Optional)

# utils.py


    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            logging.info(message)
            return func(*args, **kwargs)

        return wrapper

    return decorator
# summary.py
from .utils import log_action


@log_action("Generating summaries")
def summarize(...):
    ...

Inject a shared log entry before Stage 2 does any work. The pattern mirrors the decorator practice from part 14, so both your CLI and tests emit the same messages.

Stage 3. Async notification module (Core → Plus)

# notifier.py
from typing import Protocol


class Notifier(Protocol):
    async def send(self, payload: dict) -> None:
        ...


class WebhookNotifier:
    def __init__(self, endpoint: str):
        self.endpoint = endpoint

    async def send(self, payload: dict) -> None:
        async with httpx.AsyncClient(timeout=5) as client:
            await client.post(self.endpoint, json=payload)


async def dispatch_all(notifier: Notifier, summaries):
    tasks = [asyncio.create_task(notifier.send(summary)) for summary in summaries]
    await asyncio.gather(*tasks)
  • Use Protocol to document the interface.
  • async with serves as an async context manager to close the HTTP session safely.

Input → Output

  • Input: list[Summary], webhook URL, event loop
  • Output: None on success, exception on failure (with logs)
Summary 1 → POST /webhook (payload 1)
Summary 2 → POST /webhook (payload 2)

If this feels unfamiliar, revisit parts 18 (async), 12 (logging), and 16 (context managers) to remember why async with httpx.AsyncClient matters.

Stage 4. CLI glue (Core)

# cli.py
from pathlib import Path

from .loader import load_reports
from .summary import summarize
from .notifier import WebhookNotifier, dispatch_all

app = typer.Typer(help="Study report summarizer")


@app.command()
def run(path: str, webhook: str):
    records = load_reports(Path(path))
    summaries = summarize(records)
    notifier = WebhookNotifier(webhook)
    asyncio.run(dispatch_all(notifier, summaries))

Input → Output

  • Input: terminal command uv run studybot run reports.jsonl https://hooks.slack.com/...
  • Output: success logs plus Slack messages
$ uv run studybot run reports.jsonl $WEBHOOK
INFO Generating summaries (Stage 2)
INFO Slack dispatch complete (Stage 3)

The CLI reuses part 12’s Typer structure, part 15’s iterator/generator emphasis, and part 19’s packaging flow. Add [project.scripts] studybot = "studybot.cli:app" to your pyproject.toml to ship it.

Testing ideas (Optional)

  • loader: create a temporary file and confirm the generator yields line by line.
  • summarize: feed fixed input and assert averages plus completion lists.
  • dispatch_all: use httpx.MockTransport or a fake notifier to ensure the expected number of calls.

Why it matters

  • Applying the entire series to one project locks in practical intuition.
  • You experience how generators, type hints, async code, and CLIs fit together.
  • Writing down test ideas keeps maintenance plans realistic.
  • Completing Stages 1–4 reenacts the lessons from chapters 12–19 in order.
  • Note the inputs and outputs for each stage so reviews stay lightweight.

Practice

  • Follow along: Wire up load_reports, summarize, and dispatch_all, then run the CLI with local JSONL samples.
  • Extend: Add the log_action decorator for stage-level logs and accept extra options via typer.Option, such as an output path.
  • Debug: Point the webhook to an invalid endpoint, observe the failure, then fix it using exception logs and test doubles.
  • Definition of done: Keep CLI output logs plus testing notes, and capture future expansion ideas in your backlog.

Wrap-up

This mini project proves how much you can accomplish with pure Python. Whenever you feel stuck, follow the stage label plus the chapter number to revisit the right lesson. Because it reuses the Typer CLI from part 12 and the packaging checklist from part 19, you naturally review earlier content. Consider extending it in these directions:

  1. Add data validation with pydantic.
  2. Integrate with a FastAPI or Django backend.
  3. Polish CLI output via rich and schedule recurring runs with schedule.

The fundamentals you learned hold up regardless of framework. Revisit parts 01–11 for syntax basics and 12–19 for focused features whenever you need to expand this capstone. Instead of forcing every feature in at once, finish each stage, verify the output, and only then move forward.

💬 댓글

이 글에 대한 의견을 남겨주세요