Pipeline to Dashboardnihalsulaiman.com

Week 3 · Orchestrate with Dagster › Day 16

Day 16. Dagster assets and your first real project

⏱ 4h · learn 60m · lab 140m · ship 25m · review 15m🏷 hospitalityBefore: Day 15

By the end of today you can

  • Define assets with dependencies and metadata
  • Run and inspect materialisations in the Dagster UI
  • Wrap your Week 1–2 functions as assets without moving logic into Dagster
  • Group assets and attach owners and descriptions

1. Why this matters

Today the hotel pipeline becomes a graph you can see, run and reason about. After this, every lab in the course runs through Dagster.

2. Project layout

hotel-dagster/
├── pyproject.toml
├── src/hotel_pipeline/
│   ├── core/            # pure functions: parse, clean, dedup (no dagster import)
│   ├── defs/
│   │   ├── assets.py
│   │   ├── resources.py
│   │   └── checks.py
│   └── definitions.py   # the Definitions object Dagster loads
└── tests/

Dagster's scaffolding tool and recommended layout change between releases (recent versions use create-dagster and the dg CLI). Follow the current quickstart at docs.dagster.io for the scaffold; the code below works the same either way.

3. Assets

# src/hotel_pipeline/defs/assets.py
import dagster as dg
import polars as pl
from hotel_pipeline.core import pms

@dg.asset(group_name="hotel", kinds={"python", "parquet"}, owners=["team:bi"],
          description="Hotel reference data: rooms per hotel.")
def hotels() -> pl.DataFrame:
    return pl.read_csv("data/reference/hotels.csv")

@dg.asset(group_name="hotel", kinds={"polars"})
def stg_bookings(context: dg.AssetExecutionContext) -> dg.MaterializeResult:
    df = pms.read_all("data/landing/sftp/hotel")        # pure function from core/
    latest = pms.latest_version(df)                       # Day 12 logic
    latest.write_parquet("data/staging/hotel/stg_bookings.parquet")
    return dg.MaterializeResult(metadata={
        "rows": len(latest),
        "status_changes": int(df.height - latest.height),
        "preview": dg.MetadataValue.md(latest.head(5).to_pandas().to_markdown()),
    })

@dg.asset(group_name="hotel", deps=[stg_bookings, hotels])
def otb_by_stay_date() -> dg.MaterializeResult:
    """On-the-books room nights per hotel per stay date (confirmed only)."""
    out = pms.on_the_books("data/staging/hotel/stg_bookings.parquet", "data/reference/hotels.csv")
    out.write_parquet("data/marts/hotel/otb_by_stay_date.parquet")
    return dg.MaterializeResult(metadata={"rows": len(out)})
# src/hotel_pipeline/definitions.py
import dagster as dg
from hotel_pipeline.defs import assets
defs = dg.Definitions(assets=dg.load_assets_from_modules([assets]))

Run dagster dev, open http://localhost:3000, and click Materialize all. Every run is recorded with logs, metadata and duration.

4. Two ways to pass data

  1. You write the file (above) and declare deps=. Explicit, easy to understand, good for big files and SQL tables.
  2. Return a value and let an IO manager store it, and receive it as a function argument downstream. Less code; Day 17 covers it.
Maths and principle

Principle: an asset is a function from its upstream assets to a stored value, and a materialisation is one evaluation of it.

Equation: \(A_v = f_v(A_{u_1}, \dots, A_{u_k})\) for parents \(u_i\) of \(v\). Staleness propagates: if \(A_u\) changes, every \(v\) reachable from \(u\) is stale until re-evaluated.

Analogy: a spreadsheet: change one cell and every dependent cell recalculates. Dagster is a spreadsheet of tables that only recalculates when asked (or by automation). Where it breaks: hidden inputs (an API called inside \(f_v\) without being an asset) don't show up in staleness.

Try it: graph raw → stg → otb → report, plus hotels → otb. You fix a bug in stg. Which assets are stale? (otb and report.)

The 5-year lens

Use metadata generously: row counts, null rates, min/max business date, a preview. When someone asks "what did the 06:15 run load?", you open the asset page and the answer is there with a plot of row counts over time. That's observability for free, and it's what makes an orchestrator a data tool rather than a job runner.

Lab

  1. Move your Day 12 hotel functions into core/pms.py with tests that don't import Dagster.
  2. Build three assets: hotels, stg_bookings, otb_by_stay_date (confirmed room nights per hotel per stay date, expanding each booking's nights).
  3. Add a fourth asset occupancy_forecast_input: OTB ÷ rooms by stay date for the next 90 days.
  4. Materialise everything in the UI. Inspect the metadata. Break something on purpose (wrong path) and read the error in the run log.
  5. Add description, owners and group_name to every asset. Screenshot the asset graph for the README.

Ship

feat(dagster): hotel assets wrapping pure core functions.

Check yourself

1. Where should the logic that picks the latest booking version live?

2. You change stg_bookings. Which assets need re-materialising?

3. What does MaterializeResult metadata give you?

My notes