NRL Bucket-First Local Architecture
Summary
- Use
Scrapy -> DigitalOcean Spaces -> DuckDB -> DBeaver for the next 2 weeks.
- Do not introduce dbt in v1. Keep transforms as plain DuckDB SQL so you can observe feed behavior with minimal moving parts, then promote to
dbt-duckdb later if the schemas settle.
- Treat the four workbook-backed datasets as the initial contract:
draw, ladder, players, stats.
- Use a bucket GUI for convenience, not as a dependency. Recommended pairing:
DBeaver for SQL and Cyberduck or the DigitalOcean Spaces web console for blob inspection.
Architecture
flowchart LR
A["Scrapy spiders<br/>draw / ladder / players / stats"] --> B["Shared pipeline sink"]
B --> C["Local debug exports<br/>CSV / logs"]
B --> D["Raw bucket zone<br/>html/json/ndjson.gz + manifest"]
D --> E["DuckDB local stage job"]
E --> F["Stage bucket zone<br/>partitioned parquet"]
E --> G["Local DuckDB catalog<br/>views + inventory"]
G --> H["DBeaver<br/>SQL exploration"]
D --> I["Bucket GUI<br/>Spaces console / Cyberduck"]
flowchart TD
A["Now: observe real feed behavior"] --> B["Immutable raw files in bucket"]
B --> C["DuckDB SQL staging"]
C --> D["Manual/local runs + run inventory"]
D --> E["Later, if stable"]
E --> F["dbt-duckdb for tests, docs, lineage, marts"]
Key Changes
- In
nrl_scraper/pipelines.py, replace the current CSV-only outcome with a shared sink that writes three artifacts for every spider run:
- raw payload objects: original HTML or source JSON where available
- canonical extracted records:
records.ndjson.gz
- run manifest:
_run.json
- Keep
Output_Sheets/*.csv only as a developer/debug convenience. They should no longer be the system-of-record input to analytics.
- Add a single local post-scrape stage job, invoked after
run_spiders.py, that reads raw records from Spaces and writes Parquet into a stage zone.
- Add a local DuckDB catalog file and SQL scripts for:
stg_nrl_draw
stg_nrl_ladder
stg_nrl_players
stg_nrl_stats
ops_nrl_run_inventory
ops_nrl_file_inventory
- Use an immutable object layout:
raw/nrl/dataset=<dataset>/load_type=<historical|weekly>/season=<yyyy|unknown>/round=<rr|unknown>/run_date=<yyyy-mm-dd>/run_id=<utc_ts>/
stage/nrl/dataset=<dataset>/season=<yyyy|unknown>/round=<rr|unknown>/snapshot_date=<yyyy-mm-dd>/
- Standardize every extracted row with a metadata envelope:
run_id, dataset, scraped_at_utc, source_url, load_type, season, round, schema_version.
- Standardize every manifest with:
run_id, spider_name, dataset, started_at_utc, finished_at_utc, status, record_count, raw_object_keys, stage_object_keys, content_hash, schema_version, error.
- Add bucket config as explicit env vars:
NRL_BUCKET_ENDPOINT
NRL_BUCKET_REGION
NRL_BUCKET_NAME
NRL_BUCKET_ACCESS_KEY
NRL_BUCKET_SECRET_KEY
NRL_UPLOAD_ENABLED
- Support exactly two run labels in the runner:
historical
weekly
- If season or round cannot be derived, write
unknown; never infer values in the pipeline.
- Recommended now:
DuckDB + SQL scripts.
- Lowest setup cost
- Excellent fit for Parquet in S3-compatible storage
- Easy to inspect from DBeaver
- Defer
dbt-duckdb until after the observation period.
- Add it later if you want model docs, tests, lineage graph, and repeatable marts
- Keep SQL files one-model-per-file so migration to dbt is straightforward
- Open-source alternatives worth considering later:
SQLMesh: strongest dbt-like alternative if you want SQL modeling without adopting dbt
dlt: good if ingestion complexity grows and you want stronger loading abstractions
Dagster or Prefect: orchestration only, useful later, unnecessary for local observation mode
- Bucket GUI recommendation:
- Not required for correctness
- Helpful for debugging raw payloads, checking manifests, and downloading spot samples
- Use
Cyberduck or the DigitalOcean Spaces web console rather than building a custom UI
Access Pattern
flowchart LR
A["Spaces raw objects"] --> B["Cyberduck / Spaces console"]
C["Spaces staged parquet"] --> D["DuckDB local catalog"]
D --> E["DBeaver SQL editor"]
D --> F["Saved SQL views<br/>latest ladder / latest draw / inventory"]
- Primary analysis path: DBeaver connected to a local
.duckdb file that reads stage Parquet directly from Spaces.
- Primary blob path: Cyberduck or Spaces console for raw HTML/JSON/NDJSON review.
- Do not query raw blobs directly unless debugging; query staged Parquet through DuckDB.
- Preserve raw and staged history, but use
stg_nrl_*_latest views or ops_nrl_run_inventory.is_latest_run = true when you want deduped current-state analysis.
Test Plan
- Verify each spider writes raw payloads, canonical
ndjson.gz, and a manifest for one run.
- Verify manifests and raw files share the same
run_id and correct dataset labels.
- Verify the stage job writes partitioned Parquet for each dataset and preserves row counts from manifests.
- Verify DuckDB can read every staged dataset and expose
stg_* views without manual file path edits.
- Verify schema drift handling:
- new nullable column is accepted and recorded in manifest
schema_version
- missing required business fields fail the stage job with a clear error
- Verify DBeaver can query the local DuckDB catalog and that bucket browsing works in the chosen blob UI.
Assumptions
- The tree you shared is the current repo shape, with spiders in
nrl_scraper/spiders and a root-level test_spaces.py indicating DigitalOcean Spaces connectivity.
- The workbook at
C:/Users/mclar/OneDrive/Documents/Personal_Projects/NRL-scrapper-reqs/NRL-requirement-data-structures.xlsx is the initial data contract for Draw, Ladder, Players, and STATS.
- The goal of this phase is
raw + stage first, not a full warehouse or full dbt project.
- Local execution remains the operating mode during the observation period; no Airflow, Kubernetes, or managed scheduler is introduced yet.