Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dlt/config.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[sources.hydrovu]
[sources.pvacd_hydrovu]
api_base_url = "https://www.hydrovu.com/public-api/v1"
token_url = "https://hydrovu.com/public-api/oauth/token"
gcp_secret = "hydrovu_pvacd"
Expand Down
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ Each source is an independent pipeline with three stages:
API → dlt → GCS (parquet) → Adapter → CanonicalBundle → FROST loader → FROST
```

| Stage | Asset (HydroVu) | Asset (CABQ) |
| Stage | Asset (PVACD HydroVu) | Asset (CABQ) |
|---|---|---|
| Ingest (dlt → GCS) | `raw_hydrovu_readings` | `raw_cabq_readings` |
| Transform (GCS → CanonicalBundles) | `canonical_bundles_hydrovu` | `canonical_bundles_cabq` |
| Load (CanonicalBundles → FROST) | `frost_load_hydrovu` | `frost_load_cabq` |
| Ingest (dlt → GCS) | `raw_pvacd_hydrovu_readings` | `raw_cabq_readings` |
| Transform (GCS → CanonicalBundles) | `canonical_bundles_pvacd_hydrovu` | `canonical_bundles_cabq` |
| Load (CanonicalBundles → FROST) | `frost_load_pvacd_hydrovu` | `frost_load_cabq` |

HydroVu is live; CABQ is scaffolded (`cabq_*` raise `NotImplementedError`). Use
HydroVu as the reference implementation when wiring up a new source.
Expand All @@ -48,7 +48,7 @@ src/aqueduct_dagster/
│ ├── pipeline.py # build_source_pipeline() — shared dlt pipeline factory
│ ├── http.py # retry_transient(), TokenManager, BearerAuth, build_authenticated_client()
│ └── source_registry.py # SOURCE_REGISTRY — single per-source config, read by definitions.py and load.py
├── sources/ # one folder per agency source (vertical slice) — see hydrovu/ as the reference
├── sources/ # one folder per source key (vertical slice) — see pvacd_hydrovu/ as the reference
│ └── <name>/
│ ├── adapter.py # raw rows → CanonicalBundle (source-specific)
│ ├── dlt_pipeline.py # dlt source/resource/pipeline factory
Expand Down
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ Dagster + dlt + GCS + FROST SensorThings
Two independent source pipelines, each running on its own schedule:

```
HydroVu API → dlt → GCS (parquet) → HydroVuAdapter → CanonicalBundle → frost_load_hydrovu → FROST
CABQ API → dlt → GCS (parquet) → CabqAdapter → CanonicalBundle → frost_load_cabq → FROST
HydroVu API → dlt → GCS (parquet) → HydroVuAdapter → CanonicalBundle → frost_load_pvacd_hydrovu → FROST
CABQ API → dlt → GCS (parquet) → CabqAdapter → CanonicalBundle → frost_load_cabq → FROST
```

Orchestrated by Dagster. Each pipeline has three assets:

| Asset | HydroVu | CABQ |
|-------|---------|------|
| Ingest (dlt → GCS) | `raw_hydrovu_readings` | `raw_cabq_readings` |
| Transform (GCS → CanonicalBundles) | `canonical_bundles_hydrovu` | `canonical_bundles_cabq` |
| Load (CanonicalBundles → FROST) | `frost_load_hydrovu` | `frost_load_cabq` |
| Asset | PVACD HydroVu | CABQ |
|-------|---------------|------|
| Ingest (dlt → GCS) | `raw_pvacd_hydrovu_readings` | `raw_cabq_readings` |
| Transform (GCS → CanonicalBundles) | `canonical_bundles_pvacd_hydrovu` | `canonical_bundles_cabq` |
| Load (CanonicalBundles → FROST) | `frost_load_pvacd_hydrovu` | `frost_load_cabq` |



Expand Down Expand Up @@ -60,21 +60,21 @@ Aqueduct/
│ │ ├── http.py # retry_transient(), TokenManager, BearerAuth, build_authenticated_client()
│ │ ├── backfill.py # month_chunks(), BackfillCheckpointStore, ChunkResult — Mode A refetch infra
│ │ └── source_registry.py # SOURCE_REGISTRY — single per-source config for definitions.py + load.py
│ ├── sources/ # one folder per agency source (vertical slice)
│ │ ├── hydrovu/
│ ├── sources/ # one folder per source key (vertical slice)
│ │ ├── pvacd_hydrovu/ # PVACD's HydroVu tenant; BernCo's will sit beside it
│ │ │ ├── adapter.py # HydroVu → CanonicalBundle mapping
│ │ │ ├── dlt_pipeline.py # dlt source + resource + pipeline factory
│ │ │ ├── ingest.py # Dagster asset: raw_hydrovu_readings
│ │ │ ├── transform.py # Dagster asset: canonical_bundles_hydrovu
│ │ │ ├── ingest.py # Dagster asset: raw_pvacd_hydrovu_readings
│ │ │ ├── transform.py # Dagster asset: canonical_bundles_pvacd_hydrovu
│ │ │ └── backfill.py # Mode A refetch: isolated ingest + transform + load per chunk
│ │ └── cabq/ # same shape as hydrovu/ — currently a stub
│ │ └── cabq/ # same shape as pvacd_hydrovu/ — currently a stub
│ │ ├── adapter.py
│ │ ├── dlt_pipeline.py
│ │ ├── ingest.py
│ │ └── transform.py
│ ├── defs/
│ │ ├── assets/
│ │ │ └── load.py # Dagster assets: frost_load_hydrovu, frost_load_cabq (shared factory)
│ │ │ └── load.py # Dagster assets: frost_load_pvacd_hydrovu, frost_load_cabq (shared factory)
│ │ ├── jobs/
│ │ │ └── backfill.py # <source>_backfill_refetch job factory (BackfillRefetchConfig, chunk loop)
│ │ ├── definitions.py # Dagster entry point — jobs, schedules, asset registry
Expand All @@ -84,7 +84,7 @@ Aqueduct/
│ └── watermark_store.py # FrostWatermarkStore — per-run dedup via Dagster context
└── tests/ # mirrors src/aqueduct_dagster/'s layout above
├── conftest.py # cross-file test helpers (e.g. httpx.MockTransport/BearerAuth builders)
├── sources/{hydrovu,cabq}/
├── sources/{pvacd_hydrovu,cabq}/
├── shared/
├── defs/assets/
├── defs/jobs/
Expand Down Expand Up @@ -190,7 +190,7 @@ Open the Dagster UI at `http://localhost:3000`.

To run the full HydroVu pipeline end-to-end:
1. Click **Assets** in the left nav
2. Select all three `hydrovu` group assets (`raw_hydrovu_readings`, `canonical_bundles_hydrovu`, `frost_load_hydrovu`)
2. Select all three `pvacd_hydrovu` group assets (`raw_pvacd_hydrovu_readings`, `canonical_bundles_pvacd_hydrovu`, `frost_load_pvacd_hydrovu`)
3. Click **Materialize selected**

On first run, dlt fetches from `initial_start_date` in `.dlt/config.toml` (currently `2026-05-01`). Subsequent runs are incremental.
Expand Down Expand Up @@ -225,6 +225,6 @@ dlt tracks a cursor (`timestamp` field) per source. On first run it fetches from
`FrostWatermarkStore` tracks the last observation timestamp successfully loaded into FROST per datastream. Each run skips any observation at or before the watermark — FROST has no built-in deduplication.

**Independent pipelines**
`hydrovu_pipeline` and `cabq_pipeline` are completely independent Dagster jobs. Each has its own schedule and its own terminal load asset (`frost_load_hydrovu` / `frost_load_cabq`). Running one never triggers or blocks the other.
`pvacd_hydrovu_pipeline` and `cabq_pipeline` are completely independent Dagster jobs. Each has its own schedule and its own terminal load asset (`frost_load_pvacd_hydrovu` / `frost_load_cabq`). Running one never triggers or blocks the other.

---
4 changes: 2 additions & 2 deletions deploy/00_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export SQL_DB_VERSION="POSTGRES_17"
# --- Secret Manager ---------------------------------------------------------
export SECRET_FROST_DB_PW="frost-db-password"
# HydroVu OAuth client id/secret, read at ingest time via ADC. Matches
# [sources.hydrovu] gcp_secret in .dlt/config.toml.
export SECRET_HYDROVU="hydrovu_pvacd"
# [sources.pvacd_hydrovu] gcp_secret in .dlt/config.toml.
export SECRET_PVACD_HYDROVU="hydrovu_pvacd"

# --- GCS buckets ------------------------------------------------------------
# Where Dagster+ writes raw parquet. BUCKET_PROD is the deployment target;
Expand Down
10 changes: 5 additions & 5 deletions deploy/30_dagster_gcp_auth.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
#
# Requires: roles/storage.admin (bucket create + IAM), roles/iam.serviceAccountAdmin
# (only if the SA does not exist yet), roles/iam.serviceAccountKeyAdmin (only with
# --emit-key), and permission to bind secretAccessor on ${SECRET_HYDROVU}.
# --emit-key), and permission to bind secretAccessor on ${SECRET_PVACD_HYDROVU}.

set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/00_config.sh"
Expand Down Expand Up @@ -116,8 +116,8 @@ done

# The HydroVu OAuth credentials are themselves fetched through ADC at ingest time,
# so bucket access alone is not enough to run the pipeline.
echo "== Grant secretAccessor on ${SECRET_HYDROVU} =="
gcloud secrets add-iam-policy-binding "${SECRET_HYDROVU}" \
echo "== Grant secretAccessor on ${SECRET_PVACD_HYDROVU} =="
gcloud secrets add-iam-policy-binding "${SECRET_PVACD_HYDROVU}" \
--project="${PROJECT_ID}" \
--member="serviceAccount:${DAGSTER_SA}" \
--role="roles/secretmanager.secretAccessor" >/dev/null
Expand Down Expand Up @@ -184,7 +184,7 @@ cat <<EOF

Done. Service account : ${DAGSTER_SA}
Buckets : gs://${BUCKET_PROD} (prod), gs://${BUCKET_POC} (poc/verify)
Secret : ${SECRET_HYDROVU}
Secret : ${SECRET_PVACD_HYDROVU}
FROST : ${FROST_URL:-<not deployed>}

Dagster+ → Deployment → Environment variables (code location aqueduct_dagster_defs_definitions):
Expand All @@ -206,6 +206,6 @@ Verify without minting a key (needs roles/iam.serviceAccountTokenCreator on the

Confirm the bindings landed:
gcloud storage buckets get-iam-policy gs://${BUCKET_PROD} --format=json
gcloud secrets get-iam-policy ${SECRET_HYDROVU} --project=${PROJECT_ID}
gcloud secrets get-iam-policy ${SECRET_PVACD_HYDROVU} --project=${PROJECT_ID}
gcloud run services get-iam-policy ${FROST_SERVICE} --project=${PROJECT_ID} --region=${REGION}
EOF
2 changes: 1 addition & 1 deletion deploy/31_verify_dagster_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def check_secret_manager() -> str:

config = load_config()
project_number = config["destination"]["filesystem"]["gcp_project_number"]
secret_id = config["sources"]["hydrovu"]["gcp_secret"]
secret_id = config["sources"]["pvacd_hydrovu"]["gcp_secret"]

client = secretmanager.SecretManagerServiceClient()
name = client.secret_version_path(project_number, secret_id, "latest")
Expand Down
2 changes: 1 addition & 1 deletion deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ uv run python deploy/31_verify_dagster_auth.py
This checks four layers independently: ADC bootstrap, GCS write/read/delete, Secret
Manager access, and an ID-token GET against FROST. `VERBOSE=1` adds
tracebacks. For a full end-to-end run instead, `uv run dagster dev` with the same two
variables set and materialize `hydrovu_pipeline`.
variables set and materialize `pvacd_hydrovu_pipeline`.

## How Dagster+ connects to FROST

Expand Down
10 changes: 5 additions & 5 deletions docs/BACKFILL_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ regardless of count.
**Option 3 — Per-source generated jobs, two per source (recommended).**

This follows the pattern already used in this codebase for
`frost_load_hydrovu` / `frost_load_cabq` and `hydrovu_pipeline` /
`frost_load_pvacd_hydrovu` / `frost_load_cabq` and `pvacd_hydrovu_pipeline` /
`cabq_pipeline`: one shared factory function per job type, looped over the
source registry, so that adding a third source requires one registry entry
and no new job-wiring code. "Which source" is determined by which job is
Expand Down Expand Up @@ -182,7 +182,7 @@ load together:
(DTW filtering, location join, grouping, adapter mapping), factored into
a plain function callable from both the normal transform asset and this
job, mirroring how `_frost_load()` is already factored out separately
from the `frost_load_hydrovu` asset wrapper.
from the `frost_load_pvacd_hydrovu` asset wrapper.
3. **Load** — always uses the window-scoped, delete-then-repost mechanism
described in §4.4, rather than the normal `frost_load` asset's
single-watermark filter. This mechanism is safe for every situation in
Expand Down Expand Up @@ -316,8 +316,8 @@ or list specific ids to scope the run to just those.
Each source's existing schedule fires its existing pipeline job
automatically, with no operator involvement:

- `hydrovu_schedule` (`0 6 * * *`) triggers `hydrovu_pipeline`, which runs
`raw_hydrovu_readings` → `canonical_bundles_hydrovu` → `frost_load_hydrovu`
- `pvacd_hydrovu_schedule` (`0 6 * * *`) triggers `pvacd_hydrovu_pipeline`, which runs
`raw_pvacd_hydrovu_readings` → `canonical_bundles_pvacd_hydrovu` → `frost_load_pvacd_hydrovu`
in sequence.
- `cabq_schedule` (`0 8 * * *`) triggers `cabq_pipeline` independently, with
its own three assets. Running one source's pipeline never triggers or
Expand All @@ -331,7 +331,7 @@ and `<source>_backfill_replay`. Neither has a schedule attached — both are
launched manually, on demand:

1. Open the Dagster UI and select **Jobs**.
2. Select the relevant job (for example, `hydrovu_backfill_refetch`).
2. Select the relevant job (for example, `pvacd_hydrovu_backfill_refetch`).
3. Open the **Launchpad** and provide the run configuration — entity list,
start and end dates, and (for Mode A) an isolated pipeline name — as YAML
or JSON directly in the browser. No command-line access is required.
Expand Down
Loading
Loading