EPIC: usda-data-crush-stats
Maps a multi-year USDA grape crush data pipeline from raw downloads through bronze, silver, and gold layers to final CSV fact and dimension tables.
What this file does
Maps a multi-year USDA grape crush data pipeline from raw downloads through bronze, silver, and gold layers to final CSV fact and dimension tables.
When to use it
- Building a repeatable ETL for government agricultural datasets
- Designing a medallion-architecture data pipeline with GitHub Actions scheduling
- Creating a dimensional model that joins crush tonnage to wine production statistics
- Planning a data pipeline that handles PDF, CSV, ZIP, and API sources
Assumes this stack
EPIC: usda-data-crush-stats
Repo: crtjer/usda-data-crush-stats
Goal: Repeatable data pipeline that ingests USDA NASS California Grape Crush statistics + related beverage alcohol datasets into raw and normalized/analytical layers.
Stack: Python 3.11+, pandas, requests, openpyxl, pdfplumber, duckdb (optional), GitHub Actions for scheduling
Git Identity: crtjer / crtjer@gmail.com
GitHub Token: /home/home-assistant/.openclaw/credentials/github.json → token field
Data Discovery Summary
Primary Dataset: USDA NASS California Grape Crush Reports
Available years: 1960–2024
- 1960–2015: PDF only
- 2016–2024: ZIP (10 XLSX tables) + standalone TB08 CSV (machine-readable)
- Reports come in 3 types: Final, Preliminary, Errata
TB08 CSV Schema (machine-readable — PRIMARY source for pipeline):
district INTEGER -- 1-17 (California Grape Pricing Districts) + 18=State Total
grape_type_code INTEGER -- 5=Raisin, 6=Table, 7=Wine White, 8=Wine Red/Black
grape_type_name VARCHAR -- e.g. "Wine Grapes (White)", "Raisin Grapes"
variety_code INTEGER -- e.g. 7004=Chardonnay, 7062=Albarino
variety_name VARCHAR -- e.g. "Chardonnay *", "Cabernet Sauvignon"
wt_price DECIMAL -- Weighted average price per ton (USD)
brix_code VARCHAR -- Contract type / Brix range code (e.g. "024500" = 24.5 Brix)
tons DECIMAL -- Tons crushed in this price bucket
row_type_code INTEGER -- 2=data_row, 3=summary_row
row_type_name VARCHAR -- "data_row" or "summary_row"
10 XLSX Tables per year (in ZIP):
| Table | Description |
|---|---|
| TB01 | State totals: Tons, Avg Brix, Purchased Tons, Avg Price — by Type & Variety (current vs prior year) |
| TB02 | Tons crushed by Variety & District (pivot: columns = 17 districts + state) |
| TB03 | Weighted Avg Brix by Variety & District (wine grapes) |
| TB04 | Tons by Variety & District (table grapes used for wine/concentrate/juice) |
| TB05 | Avg Brix by Variety & District (raisin grapes) |
| TB06 | Weighted Avg Price ($/ton) by Variety & District (all types) |
| TB07 | Historical — Distilling material: Tons, Avg Brix, Avg Price by year |
| TB08 | Machine-readable flat file (PRIMARY — same as standalone CSV) |
| TB09 | Tons by Variety, Price Range bucket & District |
| TB10 | Avg Price by Variety & District (10 largest processors) |
Related Dataset 2: USDA NASS Grape Acreage Reports (California)
URL: https://www.nass.usda.gov/Statistics_by_State/California/Publications/Specialty_and_Other_Releases/Grapes/Acreage/index.php Format: Annual PDF (1999–2024) + summary PDFs Data: Bearing/non-bearing acreage by variety, county, grape type
Related Dataset 3: USDA NASS QuickStats API (National)
URL: https://quickstats.nass.usda.gov/api Format: JSON/CSV via REST API (requires free API key registration) Relevant queries:
- commodity_desc=GRAPES, statisticcat_desc=CRUSHED, state_alpha=CA
- commodity_desc=WINE, sector_desc=CROPS Note: Returns up to 50,000 rows. Good for national comparisons.
Related Dataset 4: TTB Wine Statistics
URL: https://www.ttb.gov/statistics (navigate to Wine section) Format: CSV/Excel annual files Data: Wine production volumes, removals, stocks by state & wine type (still/sparkling/dessert)
Related Dataset 5: TTB Beer Statistics
URL: https://www.ttb.gov/statistics (Beer section) Data: Barrels produced, removed by brewery size & state
Related Dataset 6: TTB Distilled Spirits Statistics
URL: https://www.ttb.gov/statistics (Distilled Spirits section) Data: Spirits production by type (brandy made from grapes is highly relevant — links directly to crush tonnage)
Data Relationship Diagram (ERD)
erDiagram
DIM_CROP_YEAR {
int crop_year PK
varchar report_type
date report_date
varchar source_url
date ingested_at
}
DIM_DISTRICT {
int district_id PK
varchar district_name
varchar region
varchar description
}
DIM_GRAPE_VARIETY {
int variety_code PK
varchar variety_name
int grape_type_code
varchar grape_type_name
varchar grape_category
}
DIM_GRAPE_TYPE {
int grape_type_code PK
varchar grape_type_name
varchar category
}
FACT_CRUSH_STATS {
int id PK
int crop_year FK
int district_id FK
int variety_code FK
int grape_type_code FK
decimal wt_price_per_ton
varchar brix_code
decimal brix_value
decimal tons_crushed
varchar row_type
}
FACT_CRUSH_SUMMARY {
int id PK
int crop_year FK
int district_id FK
int variety_code FK
decimal total_tons
decimal avg_price_per_ton
decimal avg_brix
decimal purchased_tons
}
FACT_ACREAGE {
int id PK
int crop_year FK
int district_id FK
int variety_code FK
decimal bearing_acres
decimal non_bearing_acres
decimal total_acres
}
SOURCE_TTB_WINE {
int id PK
int year FK
varchar state
varchar wine_type
decimal gallons_produced
decimal gallons_removed
decimal gallons_on_hand
}
SOURCE_TTB_SPIRITS {
int id PK
int year FK
varchar spirits_type
decimal proof_gallons_produced
varchar source_material
}
DIM_CROP_YEAR ||--o{ FACT_CRUSH_STATS : "year"
DIM_DISTRICT ||--o{ FACT_CRUSH_STATS : "district"
DIM_GRAPE_VARIETY ||--o{ FACT_CRUSH_STATS : "variety"
DIM_GRAPE_TYPE ||--o{ FACT_CRUSH_STATS : "type"
DIM_CROP_YEAR ||--o{ FACT_CRUSH_SUMMARY : "year"
DIM_DISTRICT ||--o{ FACT_CRUSH_SUMMARY : "district"
DIM_GRAPE_VARIETY ||--o{ FACT_CRUSH_SUMMARY : "variety"
DIM_CROP_YEAR ||--o{ FACT_ACREAGE : "year"
DIM_DISTRICT ||--o{ FACT_ACREAGE : "district"
DIM_GRAPE_VARIETY ||--o{ FACT_ACREAGE : "variety"
DIM_CROP_YEAR ||--o{ SOURCE_TTB_WINE : "year"
DIM_CROP_YEAR ||--o{ SOURCE_TTB_SPIRITS : "year"
Information Architecture Diagram
flowchart TD
subgraph SOURCES["🌐 Source Systems"]
S1["USDA NASS\nGrape Crush Reports\n1960–2024\nCSV + XLSX + PDF"]
S2["USDA NASS\nGrape Acreage Reports\n1999–2024\nPDF"]
S3["USDA NASS\nQuickStats API\nNational aggregate"]
S4["TTB Wine Statistics\nCSV/Excel"]
S5["TTB Beer Statistics\nCSV/Excel"]
S6["TTB Distilled Spirits\nCSV/Excel"]
end
subgraph BRONZE["🥉 Bronze Layer — data/raw/"]
B1["crush_reports/\n{year}_final_gcbtb08.csv\n{year}_final_grape_crush.zip"]
B2["acreage_reports/\n{year}_grpac.pdf"]
B3["quickstats/\ngrapes_CA_{year}.csv"]
B4["ttb/\nwine_{year}.csv"]
B5["ttb/\nbeer_{year}.csv"]
B6["ttb/\nspirits_{year}.csv"]
B7["manifest.json\n(all downloaded files + checksums)"]
end
subgraph SILVER["🥈 Silver Layer — data/silver/"]
SV1["crush_stats_raw.parquet\n(all years TB08 parsed + year added)"]
SV2["acreage_raw.parquet\n(PDF-extracted acreage)"]
SV3["quickstats_grapes.parquet"]
SV4["ttb_wine_raw.parquet"]
SV5["ttb_spirits_raw.parquet"]
end
subgraph GOLD["🥇 Gold Layer — data/final/"]
G1["dim_district.csv\n17 districts + lookup"]
G2["dim_grape_variety.csv\nvariety_code, name, type, category"]
G3["dim_crop_year.csv\nyear, report_type, date"]
G4["fact_crush_stats.csv\nGrain: year/district/variety/brix_bucket"]
G5["fact_crush_summary.csv\nGrain: year/district/variety (aggregated)"]
G6["fact_acreage.csv\nGrain: year/district/variety"]
G7["fact_ttb_wine.csv\nGrain: year/state/wine_type"]
G8["fact_ttb_spirits.csv\nGrain: year/spirits_type"]
G9["bridge_crush_to_wine.csv\nCross-reference: crush tons → wine production"]
end
S1 --> B1
S2 --> B2
S3 --> B3
S4 --> B4
S5 --> B5
S6 --> B6
B1 --> SV1
B2 --> SV2
B3 --> SV3
B4 --> SV4
B6 --> SV5
SV1 --> G4 & G5
SV2 --> G6
SV3 --> G5
SV4 --> G7
SV5 --> G8
G4 & G7 --> G9
G1 & G2 & G3 --> G4
G1 & G2 & G3 --> G5
G1 & G2 & G3 --> G6
Pipeline Architecture
flowchart LR
subgraph EXTRACT
E1[scrape_manifest.py\nDiscover all URLs]
E2[download_crush.py\nDownload CSV/ZIP]
E3[download_acreage.py\nDownload PDFs]
E4[download_ttb.py\nDownload TTB CSVs]
E5[download_quickstats.py\nCall NASS API]
end
subgraph TRANSFORM
T1[parse_crush_tb08.py\nParse all TB08 CSVs\nadd year + report_type]
T2[parse_acreage_pdf.py\nExtract PDF tables\nwith pdfplumber]
T3[parse_ttb.py\nNormalize TTB files]
T4[parse_quickstats.py\nClean API response]
end
subgraph LOAD
L1[build_dimensions.py\nBuild dim tables]
L2[build_facts.py\nBuild fact tables]
L3[build_bridge.py\nCross-ref crush → TTB wine]
L4[validate.py\nRow counts, nulls, ranges]
end
E1 --> E2 & E3 & E4 & E5
E2 --> T1
E3 --> T2
E4 --> T3
E5 --> T4
T1 & T2 & T3 & T4 --> L1
L1 --> L2 --> L3 --> L4
Run command: make pipeline or python pipeline/run.py --year 2024
Scheduling: GitHub Actions on cron 0 8 1 3 * (March 1st annually, when final report drops)
Repo Structure
usda-data-crush-stats/
├── README.md
├── Makefile
├── requirements.txt
├── .github/
│ └── workflows/
│ └── pipeline.yml # Annual cron + manual dispatch
├── pipeline/
│ ├── run.py # Entrypoint: python pipeline/run.py
│ ├── config.py # URLs, paths, constants
│ ├── extract/
│ │ ├── scrape_manifest.py # Crawl NASS page, build file manifest
│ │ ├── download_crush.py # Download crush CSVs + ZIPs
│ │ ├── download_acreage.py # Download acreage PDFs
│ │ ├── download_ttb.py # Download TTB wine/spirits CSVs
│ │ └── download_quickstats.py # Call NASS QuickStats API
│ ├── transform/
│ │ ├── parse_crush_tb08.py # Parse TB08 CSV all years → silver
│ │ ├── parse_acreage_pdf.py # pdfplumber extract → silver
│ │ ├── parse_ttb.py # Normalize TTB files → silver
│ │ └── parse_quickstats.py # Clean NASS API data → silver
│ └── load/
│ ├── build_dimensions.py # dim_district, dim_variety, dim_year
│ ├── build_facts.py # fact_crush_stats, fact_crush_summary, fact_acreage
│ ├── build_bridge.py # crush → TTB wine cross-reference
│ └── validate.py # Data quality checks
├── data/
│ ├── raw/ # Downloaded files (gitignored if large)
│ │ ├── crush_reports/
│ │ ├── acreage_reports/
│ │ └── ttb/
│ ├── silver/ # Parsed intermediate parquet files
│ └── final/ # Final CSVs (committed to repo)
│ ├── dim_district.csv
│ ├── dim_grape_variety.csv
│ ├── dim_crop_year.csv
│ ├── fact_crush_stats.csv
│ ├── fact_crush_summary.csv
│ ├── fact_acreage.csv
│ └── fact_ttb_wine.csv
├── docs/
│ ├── data_dictionary.md
│ ├── erd.md # Mermaid ERD
│ └── architecture.md # Mermaid architecture diagrams
└── tests/
├── test_extract.py
├── test_transform.py
└── test_load.py
EPIC 1: Bronze Layer — Raw Data Ingestion
Summary: Build the extract layer that crawls, discovers, and downloads all raw source files to data/raw/. Produces a manifest.json tracking every file, its URL, checksum, and download date. Idempotent — re-running skips files already downloaded unless --force flag.
Story 1.1 — Scrape NASS Crush Reports Manifest
As a data engineer I want a script that crawls the NASS Grape Crush Reports page and builds a manifest of all downloadable files So that the pipeline knows exactly what to download without hardcoding URLs
Acceptance Criteria:
pipeline/extract/scrape_manifest.pycrawls https://www.nass.usda.gov/.../Crush/Reports/index.php- Extracts all links matching patterns:
*.csv,*.zip,*.pdfin Final/, Prelim/, Errata/ subdirectories - Parses year from URL path (e.g.
/2024/→ crop_year=2024) - Parses report_type from path (Final/Prelim/Errata)
- Outputs
data/raw/manifest.json:[{"year": 2024, "report_type": "Final", "table": "tb08", "url": "...", "filename": "...", "format": "csv"}] - Also crawls Acreage report page for PDF links
- Script is idempotent — re-running updates manifest without duplicating entries
- All relative NASS URLs resolved to absolute URLs (base: https://www.nass.usda.gov/)
- Includes 17+ years of CSV/XLSX data (2016–2024 minimum)
Story 1.2 — Download Grape Crush CSVs and ZIPs
As a data engineer I want a download script that fetches all crush report files from the manifest So that raw data is locally available for transformation
Acceptance Criteria:
pipeline/extract/download_crush.pyreads manifest.json- Downloads files to
data/raw/crush_reports/{year}/{filename} - Skips already-downloaded files (checksum match via MD5)
- Supports
--year 2024flag to download a single year - Supports
--forceflag to re-download regardless of cache - Prints progress:
[2024] Downloading tb08.csv... done (14.2 KB) - On failure: logs warning, continues to next file (does not halt pipeline)
- For ZIP files: also unzips into same directory
data/raw/crush_reports/added to.gitignore(raw files not committed)
Story 1.3 — Download TTB Wine & Spirits Statistics
As a data engineer I want TTB wine production and distilled spirits (brandy) stats downloaded So that we can join grape crush volume to downstream beverage production
Acceptance Criteria:
pipeline/extract/download_ttb.pydownloads TTB statistics files- Targets:
- TTB Wine Statistics: annual CSV/Excel files (wine production by state)
- TTB Distilled Spirits: brandy-specific tables where possible
- Files saved to
data/raw/ttb/wine_{year}.csvanddata/raw/ttb/spirits_{year}.csv - Handles TTB URL structure (may require browser-fetch or direct CSV links)
- Fallback: if TTB API/CSV not available, document the manual download path in README
- Minimum viable: successfully downloads at least 2022–2024 data
Story 1.4 — NASS QuickStats API Integration (National Context)
As a data engineer I want national grape/wine statistics from NASS QuickStats API So that California crush data can be contextualized against US totals
Acceptance Criteria:
pipeline/extract/download_quickstats.pycalls https://quickstats.nass.usda.gov/api- Query parameters:
commodity_desc=GRAPES,state_alpha=CA,statisticcat_desc=PRODUCTIONcommodity_desc=GRAPES,state_alpha=CA,statisticcat_desc=PRICE RECEIVED
- API key loaded from env var
NASS_API_KEY(documented in README — free to register at https://quickstats.nass.usda.gov/api) - Response saved as
data/raw/quickstats/grapes_CA.csv - Handles API rate limits gracefully (retry with backoff)
- If
NASS_API_KEYnot set: script logs warning and skips (pipeline still runs)
EPIC 2: Silver Layer — Parse & Normalize
Summary: Transform all raw files into clean, typed, consistent intermediate format. Each source gets its own parser. All silver outputs are parquet files in data/silver/. This layer handles schema inconsistencies across years (column renaming, encoding issues, merged cells in XLSX).
Story 2.1 — Parse TB08 CSV Across All Years
As a data engineer I want a parser that reads all TB08 CSVs and produces a unified normalized dataset So that all years of crush data are in a single queryable file
Acceptance Criteria:
pipeline/transform/parse_crush_tb08.pyreads alldata/raw/crush_reports/*/TB08 CSV files- Adds columns:
crop_year(from filename/path),report_type(Final/Prelim/Errata) - Normalizes column names to snake_case (already consistent in TB08)
- Casts types:
district→ int,tons→ float,wt_price→ float,variety_code→ int - Handles nulls:
wt_priceandbrix_codeare null on summary rows (expected) - Filters
row_type_code=2for data rows only intocrush_data.parquet - Keeps
row_type_code=3(summary rows) intocrush_summary.parquet - Deduplicates: if both Final and Errata exist for same year, use Errata (most current)
- Output:
data/silver/crush_data.parquetanddata/silver/crush_summary.parquet - Schema validation: assert expected columns present, assert no year gaps 2016–2024
- Row count logged:
Loaded 2024 Final: 48,312 rows
Story 2.2 — Parse Grape Acreage PDFs
As a data engineer I want bearing acreage data extracted from annual NASS PDFs So that we can analyze price and crush volume relative to planted acreage
Acceptance Criteria:
pipeline/transform/parse_acreage_pdf.pyusespdfplumberto extract tables- Processes
data/raw/acreage_reports/*.pdf(2010–2024 minimum) - Extracts:
crop_year,variety_name,bearing_acres,non_bearing_acres,total_acres,county_or_district - Handles PDF table layout variations across years (multi-line headers, merged cells)
- Outputs
data/silver/acreage_raw.parquet - If PDF parsing fails for a year: log warning + skip (do not crash)
- At least 10 years of acreage data successfully extracted (definition of done)
Story 2.3 — Parse TTB Wine & Spirits Files
As a data engineer I want TTB wine and spirits data parsed into normalized silver tables So that production volumes join cleanly to crush data
Acceptance Criteria:
pipeline/transform/parse_ttb.pyreads TTB source files- Wine output schema:
year,state,wine_type(table/dessert/sparkling/other),gallons_produced,gallons_removed,gallons_on_hand - Spirits output schema:
year,spirits_type,proof_gallons_produced,source_material(where available) - Filters wine table to CA records for CA-specific analysis
- Outputs
data/silver/ttb_wine.parquetanddata/silver/ttb_spirits.parquet - If TTB source file missing: outputs empty parquet with correct schema (pipeline doesn't break)
EPIC 3: Gold Layer — Dimensional Model
Summary: Build the final analytical layer — dimension tables and fact tables — from silver data. All gold outputs saved as CSV to data/final/ and committed to the repo. These are the files consumers query.
Story 3.1 — Build Dimension Tables
As a data analyst I want clean dimension lookup tables for district, variety, and year So that fact tables are normalized and self-documenting
Acceptance Criteria:
pipeline/load/build_dimensions.pybuilds 3 dimension tables:
dim_district.csv (static, hardcoded from USDA spec):
district_id, district_name, region, notes
1, North Coast, Northern CA, "Napa, Sonoma, Mendocino"
2, Sacramento Valley, Central Valley, ...
... (all 17 districts + district 0 = State Total)
dim_grape_variety.csv (derived from crush data):
variety_code, variety_name, grape_type_code, grape_type_name, grape_category
7004, Chardonnay, 7, Wine Grapes (White), wine
7062, Albarino, 7, Wine Grapes (White), wine
...
grape_category= wine / table / raisin (derived from grape_type_code)
dim_crop_year.csv:
crop_year, report_type, report_date, source_url, first_ingested_at
2024, Final, 2025-03-10, https://..., 2026-03-08
- All dim tables have surrogate PKs where appropriate
- dim_district has all 17 districts hardcoded (not derived) — they are stable per USDA spec
Story 3.2 — Build Fact Tables
As a data analyst I want grain-level fact tables for crush statistics and acreage So that I can slice by year, district, variety, and analyze pricing trends
Acceptance Criteria:
pipeline/load/build_facts.pybuilds:
fact_crush_stats.csv (grain: crop_year + district + variety_code + brix_code):
id, crop_year, district_id, variety_code, brix_code, brix_value,
wt_price_per_ton, tons_crushed, report_type
brix_value= parsed float from brix_code (e.g. "024500" → 24.5)- Only includes
data_rows(row_type=2)
fact_crush_summary.csv (grain: crop_year + district + variety_code):
id, crop_year, district_id, variety_code,
total_tons, avg_price_per_ton, avg_brix, purchased_tons
- Aggregated from fact_crush_stats
- All districts + state total row (district_id=0)
fact_acreage.csv (grain: crop_year + variety_code):
id, crop_year, variety_code, bearing_acres, non_bearing_acres, total_acres
Acceptance:
- FK integrity: all FKs in fact tables exist in dim tables
- No duplicate rows on grain
total_tonsin fact_crush_summary matches state-level total in original reports ±0.5%- CSV outputs committed to
data/final/
Story 3.3 — Build TTB Bridge Table
As a data analyst I want a cross-reference table linking grape crush tonnage to TTB wine production So that I can estimate conversion efficiency (tons crushed → gallons produced)
Acceptance Criteria:
pipeline/load/build_bridge.pyproducesdata/final/bridge_crush_to_wine.csv:
crop_year, ca_wine_tons_crushed, ca_gallons_wine_produced,
tons_per_gallon_ratio, source_crush, source_ttb
- Filters crush data to wine grapes only (grape_type_code IN (7, 8))
- Joins to TTB CA wine production by year
- Computes
tons_per_gallon_ratiowhere both datasets present - Where TTB data is missing: fills with NULL, notes in
source_ttbcolumn - Output is a time-series suitable for trend analysis
Story 3.4 — Data Validation Layer
As a data engineer I want automated validation of the final layer before any consumer uses it So that data quality issues are caught before they cause downstream problems
Acceptance Criteria:
pipeline/load/validate.pyruns after gold layer build- Checks:
- All expected crop years 2016–2024 present in fact_crush_stats
- No NULL tons_crushed in data rows
- fact_crush_summary totals match TB01 state totals ±1%
- dim_district has exactly 18 rows (17 districts + state total)
- dim_grape_variety has 100+ varieties
- No negative prices or tonnage values
- Most recent year's data present (within 14 months of today)
- Outputs
data/final/validation_report.json:{"run_at": "2026-03-08", "checks_passed": 8, "checks_failed": 0, "warnings": []} - Pipeline exits with code 1 if any check fails (blocks CI)
EPIC 4: Orchestration & Repeatability
Summary: Wire everything together into a single runnable pipeline with a Makefile, CLI entrypoint, and GitHub Actions workflow for automated annual runs.
Story 4.1 — Pipeline Entrypoint & Makefile
As a developer I want a single command to run the entire pipeline So that anyone can reproduce the full dataset with one command
Acceptance Criteria:
python pipeline/run.pyruns full pipeline: extract → transform → load → validate- Supports flags:
--year 2024— run for specific year only--skip-extract— skip download, use existing raw files--skip-ttb— skip TTB sources (if no internet or TTB down)--force— re-download all files
Makefiletargets:make pipeline— full runmake extract— extract onlymake transform— transform onlymake final— build gold layer onlymake validate— run validation onlymake clean— removedata/raw/anddata/silver/
requirements.txtpinned with all dependencies (requests, pandas, openpyxl, pdfplumber, pyarrow)README.mdincludes: quickstart, data dictionary links, run instructions, output file descriptions
Story 4.2 — GitHub Actions Annual Pipeline
As a data engineer I want the pipeline to run automatically each year when the NASS Final Grape Crush Report is published So that the dataset stays current without manual intervention
Acceptance Criteria:
.github/workflows/pipeline.ymlcreated- Triggers:
- Scheduled:
cron: '0 10 15 3 *'(March 15 annually — after final report typically publishes) - Manual:
workflow_dispatchwith optionalyearinput
- Scheduled:
- Steps:
- Checkout repo
- Setup Python 3.11
- Install dependencies from requirements.txt
- Run
python pipeline/run.py --year $YEAR - If validation passes: commit
data/final/changes +data/raw/manifest.jsonto main - Create a GitHub Release with the new year's data as an asset
- On failure: open a GitHub Issue titled "Pipeline failed for {year}" with error log
NASS_API_KEYstored as GitHub Actions secret (optional — pipeline runs without it)- Commit message:
chore: update data/final/ for {year} crop year [skip ci]
Story 4.3 — Data Dictionary & Documentation
As a data consumer I want clear documentation of every table, column, and data source So that I can use the data without needing to read the source code
Acceptance Criteria:
docs/data_dictionary.mddocuments:- Every table in
data/final/with column name, type, description, example value - Source mapping: which NASS table feeds which gold table
- Known data quirks (e.g. wt_price is NULL on summary rows, brix_code "000100" = no Brix contract)
- Grape type code reference: 5=Raisin, 6=Table, 7=Wine White, 8=Wine Red/Black
- District ID reference: 1=North Coast, 2=Sacramento Valley, etc.
- Every table in
README.mdincludes:- One-sentence description of each output file
- Link to NASS source
- Link to TTB source
- How to run
- Example query (pandas snippet to load fact_crush_summary and show top varieties by 2024 price)
- Both diagrams (ERD + architecture) embedded in README as Mermaid
Implementation Notes for Claude Code
- Repo creation: Create
crtjer/usda-data-crush-statsvia GitHub API using token from/home/home-assistant/.openclaw/credentials/github.json - Start with TB08 CSV — it's already machine-readable, clean, consistent. Proves the pipeline end-to-end before tackling XLSX/PDF complexity.
- Use pandas + openpyxl for XLSX parsing (already installed in test). Use
pdfplumberfor acreage PDFs. - Silver layer in parquet (pyarrow). Gold layer as CSV (committed, human-readable).
- TTB sources: If TTB CSV download is brittle, document manual download path and build the transform assuming the file exists. Do not block pipeline on TTB.
- brix_code parsing: Format "024500" = 24.5 Brix (divide by 1000). "000100" = unspecified/no-brix contract.
- District 0 or 18 = State Total — keep it but flag it in dim_district.
- Errata > Final > Prelim for dedup logic when multiple report types exist for same year.
- Test with 2022, 2023, 2024 first (all have TB08 CSV). Then extend to 2016–2021 (XLSX only, no standalone CSV — parse from ZIP).
- Notify when done:
openclaw system event --text "usda-data-crush-stats pipeline built — PR ready for review" --mode now
What's inside
4 epics, 10 stories, 3 architecture diagrams, full repo tree, and 2 schema definitions
Change this for your project
- Replace
crtjer/usda-data-crush-statswith your own repository name - Replace
/home/home-assistant/.openclaw/credentials/github.jsonwith your own GitHub token path - Replace
crtjer@gmail.comwith your own Git identity email - Replace
NASS_API_KEYenv var references with your own API key variable name
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Idempotent download scripts that skip files with matching checksums
- Separate bronze/silver/gold layers with explicit schema definitions at each stage
- Validation step that blocks CI if row counts or FK integrity checks fail
Related Documents
Zig 0.16.0 Context Document for LLMs
Documents breaking changes from Zig 0.13/0.14 to 0.16.0, covering syntax, I/O, build system, and standard library updates.
绘制对比图片 KITTI
Documents the topic remapping and data flow needed to connect Fast-LIO2 frontend with SC-PGO backend for KITTI evaluation.
Beads Workflow Context
Guides when to use bd graph-based issue tracker versus TodoWrite for session vs multi-session work, with commands and workflows.