Pipeline to Dashboardnihalsulaiman.com

Week 1 · Ingest anything › Day 2

Day 2. Engineering setup that survives a real job

⏱ 4h · learn 60m · lab 135m · ship 30m · review 15m🏷 allBefore: Day 1

By the end of today you can

  • Set up a reproducible Python project with uv, ruff and pytest
  • Keep secrets out of git with .env and config objects
  • Write structured logs a pipeline can be debugged from
  • Use the bi-skills-kit CLAUDE.md rules on your own project

1. Why this matters

Most "pipeline broke" incidents aren't clever bugs. They're a laptop-only dependency, a password in code, a path that only exists on one machine, or a log that says Error and nothing else. Five-year engineers set these up on day one, so you will too.

2. A project layout you'll reuse every week

retail-ingest/
├── pyproject.toml        # dependencies + tool config (one file)
├── uv.lock               # exact versions, committed
├── .env.example          # names of settings, no values
├── .gitignore            # .env, data/, .venv/
├── README.md
├── src/retail_ingest/
│   ├── __init__.py
│   ├── config.py         # reads settings once, typed
│   ├── log.py            # structured logging
│   └── sources/          # one module per source
├── tests/
└── data/                 # git-ignored: raw/, staging/

uv (from Astral) creates the venv, resolves dependencies and writes a lock file in seconds. pip + requirements.txt still works; the lock file is the part that matters.

pip install uv           # or see docs.astral.sh/uv for the installer
uv init --package retail-ingest && cd retail-ingest
uv add pandas polars duckdb openpyxl pydantic-settings python-dotenv
uv add --dev pytest ruff
uv run pytest

3. Config and secrets

Rule: code reads settings; settings live outside code. Locally that's a git-ignored .env. In production it's environment variables injected by the orchestrator or a vault (Azure Key Vault, Week 4).

# src/retail_ingest/config.py
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_prefix="P2D_")
    data_dir: Path = Path("./data")
    landing_dir: Path = Path("./data/landing")
    imap_host: str = "imap.example.com"
    imap_user: str = ""
    imap_password: str = ""          # never logged, never committed

settings = Settings()

Watch out

If a secret is ever committed, deleting the line is not enough: it stays in git history. Rotate the secret first, then clean history. GitHub's push protection and gitleaks catch most leaks before they happen; turn both on.

4. Logs you can debug from

A good log line answers: what step, on which input, what happened, how many rows, how long. Use key=value or JSON so you can grep and parse it.

# src/retail_ingest/log.py
import json, logging, sys, time

class JsonFormatter(logging.Formatter):
    def format(self, r):
        base = {"ts": time.strftime("%Y-%m-%dT%H:%M:%S"), "level": r.levelname, "msg": r.getMessage()}
        base.update(getattr(r, "ctx", {}))
        return json.dumps(base)

def get_logger(name="p2d"):
    log = logging.getLogger(name)
    if not log.handlers:
        h = logging.StreamHandler(sys.stdout); h.setFormatter(JsonFormatter())
        log.addHandler(h); log.setLevel(logging.INFO)
    return log

# usage
log = get_logger()
log.info("file landed", extra={"ctx": {"source": "email", "file": "sales_R104.csv", "rows": 812, "sha256": "ab12…"}})
Maths and principle

Principle: reproducibility means the output is a function of (code version, dependency versions, input data, config). Pin all four and you can reproduce any run.

Equation: \(\text{output} = f(\text{commit}, \text{lockfile}, \text{inputs}, \text{config})\). Remove any argument from your records and the function is no longer determined.

Analogy: a lab notebook records apparatus, calibration and conditions, not just the result. Where it breaks: external clocks and APIs ("now()", "yesterday's data") are hidden inputs; make them explicit parameters.

Try it: a job uses date.today() - 1 to choose which file to load. Name the hidden input and the fix. (The wall clock. Pass the business date as a parameter; Week 3 partitions do exactly this.)

5. Claude Code, safely, from day two

You'll use Claude Code throughout the course as a pair programmer. Copy CLAUDE.md from your bi-skills-kit/claude-code/ into each project and trim it. The rules that matter most on a personal project: never print secrets, propose before applying on anything shared, and save evidence (queries and outputs) for every claim. Ask it to explain code it writes, and reject anything you can't explain back.

The 5-year lens

On a team, "works on my machine" costs hours. The lock file, .env.example, a make/just file with setup, test, run, and a README "first run in 5 minutes" section are what make you the person others trust with production. Interviewers who've run teams notice these files in your repos before they read your code.

Lab

  1. Create the retail-ingest project with uv as above. Add ruff config to pyproject.toml (line-length = 100, rules E,F,I,B,UP).
  2. Write config.py and log.py. Create .env and .env.example. Confirm .env is ignored: git check-ignore .env.
  3. Write tests/test_config.py that sets P2D_DATA_DIR with monkeypatch.setenv and asserts Settings().data_dir changes.
  4. Add a justfile or Makefile with setup, lint (ruff check . && ruff format --check .), test.
  5. Install pre-commit with ruff and gitleaks hooks. Try committing a fake key AKIA… and watch it get blocked.
  6. Copy and adapt CLAUDE.md. Ask Claude Code: "Read this repo and list anything that would fail on a fresh machine." Fix what it finds.

Ship

Push nothing public yet. Commit locally: chore: project skeleton with config, logging, tests, pre-commit.

Check yourself

1. Where should the IMAP password for production live?

2. Why commit uv.lock?

3. Which log line is most useful at 6 a.m.?

My notes