Cookbook
Scenario-oriented recipes that combine the tools — downloader, schema-drift, normalize, and loader, orchestrated end-to-end by taxi-run — into real workflows. Each recipe is self-contained and copy-pasteable, aimed at data engineers running the stack in a real environment. Where a recipe overlaps with a per-tool guide, this page focuses on the glue between tools and the operational details (cron, proxies, dev bootstraps).
Nightly refresh via cron
Goal: keep the local TLC mirror caught up on new months, refresh normalized parquet, and log any schema drift.
Recipe — a shell script plus a scheduler entry.
-
Create
/srv/taxi/bin/nightly-refresh.sh. The orchestrator (taxi-run) already chains download → normalize per type and honors each stage's exit code, so the whole refresh is one command:If you'd rather run each stage yourself instead of going through
taxi-run(for finer-grained logging, say), the equivalent per-type chain is: -
Wire it into cron (
crontab -e): -
Or use a systemd timer + service instead.
/etc/systemd/system/taxi-refresh.service:/etc/systemd/system/taxi-refresh.timer:Enable:
Notes:
--recent's stop-on-local semantic makes this fully idempotent — running it twice in the same day just skips everything.taxi-runexits 0 for a clean run, 1 if any type's normalize amended its mapping and needs a human to review the new drift, or 2 for an operational failure (download failure, a real normalize config error). Consider paging on exit=2 vs. logging-only on exit=1 — see the orchestrator guide's exit codes. Noteset -euo pipefailmeans the script stops at thetaxi-runline on any non-zero exit, so the schema-drift snapshot won't run that day either; drop-e(or run schema-drift unconditionally with|| truebefore it) if you want the snapshot regardless.raw/grows monotonically — plan capacity via the Downloader guide's sizing table.- Prefer systemd timers over cron on modern hosts:
Persistent=truecatches up missed runs after downtime, and logs land in the journal automatically.
DuckDB httpfs — no local mirror
Goal: run one-off analytics queries against TLC parquet without downloading anything.
Recipe:
-
Start DuckDB:
-
Load the
httpfsextension and query directly:
Notes:
- First query downloads the file; subsequent queries in the same session hit DuckDB's in-memory cache. Across sessions, use the on-disk cache:
SET enable_external_file_cache=true; PRAGMA cache_directory='/tmp/duckdb-cache'; - Costs: TLC's CloudFront is unmetered from the requester side, but repeated full-scans of a 50 MB file are wasteful. Use the downloader for anything you'll query more than a couple of times.
- Schema drift is on you here — DuckDB will happily union files with different schemas via
union_by_name=true, but drops columns silently. Cross-reference withschema-driftoutput. - The glob pattern hits CloudFront's directory listing, which requires HTTP range requests — some corporate proxies mangle these. If globs fail, list the URLs explicitly.
Side-by-side comparison of multiple TLC years
Goal: compute trips-per-year and average fare across a decade of yellow taxi data using the normalized mirror.
Recipe: with a full history mirror at raw-normalized/yellow/:
-
Start DuckDB in the taxi directory:
-
Aggregate across every year:
Notes:
- Uses
raw-normalized/(post-normalize output), notraw/— schemas are uniform, no need to reason about pre-2015pickup_latitudevs post-2015PULocationID. - Pre-2015 rows have NULL for
PULocationID(they were represented as lat/lon in the raw and dropped during normalize per the mapping'sacknowledged_data_loss:entry). TheWHERE PULocationID IS NOT NULLhandles that. - ~30 GB of parquet queried in one DuckDB command takes seconds — parquet + DuckDB scale surprisingly well on a laptop with plenty of RAM.
- If you're memory-bound, add
SET memory_limit='8GB'; SET threads=4;before running — DuckDB will spill to disk rather than OOM.
Loading the normalizer's output into SQL Server
Goal: take raw-normalized/ and get it into SQL Server as one table per type per year, re-runnable without re-loading data that's already there.
Recipe:
-
Point the loader at a reachable SQL Server and set the password (there is no
--passwordflag — it only ever comes from the environment): -
See the plan before writing anything:
This prints, per year, one of
skip,append month(s) NN, NN, ortruncate + reload (N month file(s))— see the loader guide for how that decision is made. -
Run it for real:
Omit the type to load all four (
yellow,green,fhv,fhvhv) in turn:
Notes:
- The loader reads
raw-normalized/<type>/<year>/*.parquetby default; point it elsewhere with--data-dir DIR(readsDIR/raw-normalized) or--input-dir DIR(readsDIR/<type>/<year>/*.parquetdirectly, bypassing theraw-normalizedassumption). - Re-running
taxi-loadafter a freshnormalizepass is the common case, not a special one: new months on disk get appended; years where nothing changed are skipped; anything that looks inconsistent (a prior interrupted run, a row-count mismatch against the manifest) gets truncated and reloaded rather than silently left half-updated. Pass--full-refreshto force every year onto the reload path regardless of the manifest. - Table DDL is generated straight from the parquet schema via
taxi_shared's type mapper — there's no separate DDL file to hand-maintain or apply. - One command does download → normalize → load end to end:
uv run taxi-run yellow --load(see the orchestrator guide).
Running behind a corporate proxy
Goal: get the downloader, uv, and gh to work behind a corporate HTTP proxy at http://proxy.corp.internal:3128.
Recipe:
-
Export the standard proxy env vars once per shell:
-
Then everything just works:
-
Persist across sessions by adding to
~/.bashrc/~/.zshrc:
Notes:
- If your proxy requires authentication:
http://user:pass@proxy.corp.internal:3128. Prefer a keychain-backed helper over hardcoding. - Some corporate proxies MITM HTTPS with a self-signed CA. Point Python (and therefore
taxi-download'shttpxclient) at the CA bundle viaSSL_CERT_FILE=/path/to/corp-ca.pem; uv readsUV_NATIVE_TLS=truefor OS trust store on Windows/macOS. NO_PROXY=localhost,127.0.0.1is still worth setting even thoughtaxi-load's connection to SQL Server goes through DuckDB'smssqlextension over the native TDS protocol, not HTTP, so it isn't itself proxy-aware — the exclusion matters for every other localhost-bound tool in the same shell (e.g.sqlcmd, a local dev server) that would otherwise try to route through the proxy and fail.- Docker's daemon has its own proxy configuration (
/etc/systemd/system/docker.service.d/http-proxy.conf) — env vars in your shell don't affect image pulls.
Populating a fresh dev SQL Server
Goal: go from an empty Docker-hosted SQL Server to a populated taxi database in well under 30 minutes.
Recipe:
-
Start SQL Server:
-
Fetch three months of yellow (small enough to complete in a couple of minutes):
-
Normalize (first-run bootstrap + human ack pass omitted for brevity — on a stable-schema
--recent 3window it's typically a no-op): -
Load into SQL Server. The loader creates the
taxidatabase anddboschema automatically, derives table DDL from the parquet, and writes one table per year (e.g.yellow_2026):Steps 2–4 collapse into a single command if you'd rather not run each stage by hand:
-
Verify the load:
Notes:
- The loader's defaults (
--host localhost,--port 1433,--database taxi,--schema dbo,--user sa) match this docker-compose setup, so no flags are needed beyondMSSQL_PASSWORDfor a local dev target. - Total time is dominated by network to CloudFront (parquet download); the SQL Server load itself is a bulk
COPYvia DuckDB'smssqlextension and is fast even for a full year. On a residential gigabit link,--recent 3 yellowend to end is a few minutes, not 30. - Re-running
taxi-load yellowafter loading is a no-op (skip) unless new months landed on disk, in which case it appends just those months — see the loader guide for the full reconcile model. - If you're iterating on schema and want to reset a year without losing the container:
uv run taxi-load yellow --full-refreshforces a drop-and-reload of every year processed, or drop the table by hand (sqlcmd ... -Q "DROP TABLE yellow_2026;") and re-runtaxi-load.