Configurable data directory (--data-dir) — design spec (2026-07-26)
Goal: Let every pipeline stage write/read its data under a caller-chosen base directory instead of
the hardcoded repo-relative raw / raw-normalized. A single --data-dir <BASE> flag on each tool
derives the fixed subfolders (BASE/raw, BASE/raw-normalized); the orchestrator threads one
--data-dir through all stages so taxi-run --data-dir /taxi/data downloads to /taxi/data/raw,
normalizes to /taxi/data/raw-normalized, and loads from there.
Decisions locked (from brainstorming)
- One base
--data-dir, derived subfolders — not per-part input/output flags. Keep it simple. - Keep the subfolder names
rawandraw-normalized(do not rename tonormalized). --data-diron all four tools (downloader, normalizer, loader, orchestrator) for a uniform interface; the loader keeps--input-diras an explicit lower-level override.- Fully backward-compatible: every new flag defaults to today's behavior.
- Mappings are code, not data:
normalize/mappings/<type>.yamlstays resolved from the repo/CWD, NOT under--data-dir.
Current state (facts, from code)
- Downloader
downloader/download_taxi_data.sh: output dir =$OUTPUT_DIRif set, else<script_dir>/../raw(lines 70-77). No CLI flag for location. Layout<output_dir>/<type>/<year>/<file>. Args today: positionalTYPE,--recent [N] [TYPE]. - Normalizer
normalize/src/taxi_normalize/cli.py:main(argv=None); argparse has onlydata_type+--sample._normalize_onehardcodes (relative to CWD):raw_dir = Path("raw")/data_type,mapping_path = Path("normalize")/"mappings"/f"{data_type}.yaml",out_dir = Path("raw-normalized")/data_type(lines 55-58). - Loader
loader/src/taxi_loader/cli.py:--input-dir(default"raw-normalized", lines 41-42);discover_month_filesglobsPath(input_dir)/data_typerglob*.parquet(lines 52-65). - Orchestrator
orchestrator/src/taxi_orchestrate/{cli,stages}.py:--data-dir(default None) →root = data_dir or find_repo_root(cwd)(cli.py:85); every stage runs viastages.run(cmd, cwd=root)(stages.py:58-62).build_download_cmd(root, …)locates the script atroot/downloader/…but passes no output location — the script writes to<script_dir>/../raw, so--data-dirdoes NOT redirect downloads today (the gap this feature fixes).build_normalize_cmdpasses onlydata_type/--sample(relies on cwd).build_load_cmd(t, INPUT_DIR="raw-normalized", conn)passes--input-dir raw-normalized. - No shared path constant —
"raw"/"raw-normalized"literals are repeated per component. (The separatecurate.pyandschema-drifttools already have their own--raw-dirflags — precedent, but out of scope here.)
Design
Common rule
--data-dir DIR names a base directory. Derived, fixed subfolders under it:
DIR/raw/<type>/<year>/… (downloaded + raw input) and DIR/raw-normalized/<type>/… (normalized
output/loader input). Default DIR reproduces today's behavior per tool (see each).
A. Downloader (download_taxi_data.sh)
- Add a
--data-dir DIRoption to the arg parser (a flag taking a value). - Output-dir precedence:
$OUTPUT_DIR(explicit full path, if set) →$DATA_DIR/raw(if--data-dirgiven) →<script_dir>/../raw(default, unchanged). Update the "Files saved to" echoes to reflect the resolved dir. - Layout under the resolved output dir is unchanged.
B. Normalizer (taxi_normalize/cli.py)
- Add
--data-dir(default"."). raw_dir = Path(data_dir)/"raw"/data_type;out_dir = Path(data_dir)/"raw-normalized"/data_type.mapping_pathunchanged (Path("normalize")/"mappings"/f"{data_type}.yaml"relative to CWD).- Default
"."= today's behavior (raw/raw-normalizedin CWD).
C. Loader (taxi_loader/cli.py)
- Add
--data-dir(defaultNone); change--input-dirdefault toNone. - Resolve the effective input dir (precedence: explicit
--input-dir>--data-dir-derived > default):Put this in a small helper (e.g.resolve_input_dir(args)) so it is unit-testable. Discovery logic unchanged. Default (neither flag) ="raw-normalized", today's behavior.
D. Orchestrator (cli.py + stages.py)
- Compute both roots explicitly:
repo_root = find_repo_root(Path.cwd())— where the downloader script +normalize/mappings/live.data_dir = Path(args.data_dir).resolve() if args.data_dir else repo_root.- Run every stage with
cwd = repo_root(so the downloader script path and mappings resolve), and pass--data-dir <data_dir>explicitly to every stage: build_download_cmd: stillbash <repo_root>/downloader/download_taxi_data.sh …, now appended with--data-dir <data_dir>.build_normalize_cmd: append--data-dir <data_dir>.build_load_cmd: pass--data-dir <data_dir>(loader derives<data_dir>/raw-normalized); theINPUT_DIR="raw-normalized"constant + explicit--input-dirare dropped from the load cmd.- Update the
--data-dirhelp text to describe the derived-subfolder behavior. - Default (no
--data-dir):data_dir == repo_root,cwd == repo_root→ download/normalize/load all operate onrepo_root/rawandrepo_root/raw-normalized, identical to today.
Backward compatibility & test impact
- All tool defaults reproduce current behavior; standalone runs and most tests are unaffected.
- Tests that change (expected, part of this feature):
tests/taxi_loader/test_cli.py— the--input-dirdefault is nowNone; assert the resolved input dir (viaresolve_input_dir) instead ofns.input_dir == "raw-normalized". Add cases for--data-dirderiving<dir>/raw-normalizedand--input-diroverriding it.tests/taxi_orchestrate/test_stages.py—build_load_cmdnow emits--data-dir <dir>instead of--input-dir raw-normalized;build_normalize_cmd/build_download_cmdnow include--data-dir. Update assertions accordingly.tests/e2e/test_pipeline_e2e.py— still passes (mappings now read from the repo viacwd=repo_rootrather than the workroot copy; the generatedworkroot/rawis read via--data-dir). Theshutil.copytree(REPO_MAPPINGS, …)becomes redundant; verify the e2e test still passes against SQL Server, and optionally drop the now-unneeded copy.- Normalizer tests (which
chdirand use CWD-relativeraw) are unaffected —--data-dirdefault"."keeps CWD-relative behavior.
Testing strategy
- Downloader: it has no tests today; add a focused test (bash invoked via subprocess, or assert the
resolved
output_dirlogic) proving--data-dir DIRwrites underDIR/rawand that the default andOUTPUT_DIRpaths are unchanged. Keep it small (do not actually download — e.g.--recent 0/dry path, or test only the directory-resolution branch). - Normalizer: a test that generates raw under a temp
--data-dirand asserts output lands in<data-dir>/raw-normalizedwhile mappings are still read from the repo. Reuse existing fixtures. - Loader: unit tests for
resolve_input_dircovering all three precedence branches. - Orchestrator:
build_*_cmdunit tests asserting each stage receives--data-dir <data_dir>andcwd == repo_root; a--dry-runend-to-end that prints the planned commands with a custom--data-dir. - Integration: the SQL-Server e2e (
taxi-run --skip-download --load --data-dir <workroot>) still loads the expected row counts. - Full suite stays green (203 passed / 11 skipped baseline, adjusted for the updated assertions).
Out of scope
- Renaming
raw/raw-normalized. - Per-part / per-stage separate input+output overrides (explicitly rejected — one base dir only).
- Adding
--data-dirto the separatecurate.py/schema-drifttools (they already have--raw-dir; unifying them is a possible later cleanup, not this feature). - A shared path-constant module / centralizing the
raw/raw-normalizedliterals (nice-to-have; the derivation lives in each tool for now). - Making the mappings directory configurable (mappings stay repo-relative).
Notes for the plan
- The downloader bash arg loop must handle
--data-dir <value>alongside the existing positionalTYPEand--recent [N] [TYPE]parsing — get the ordering right so--data-dircan appear in any position the orchestrator emits it. - Keep
resolve_input_dirpure (takes the parsed args / the two values) for easy unit testing. - Confirm the orchestrator
--dry-runpath prints the new--data-dirin each command.