Week 3 · Orchestrate with Dagster › Day 16
Day 16. Dagster assets and your first real project
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
- You write the file (above) and declare
deps=. Explicit, easy to understand, good for big files and SQL tables. - 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
- Move your Day 12 hotel functions into
core/pms.pywith tests that don't import Dagster. - Build three assets:
hotels,stg_bookings,otb_by_stay_date(confirmed room nights per hotel per stay date, expanding each booking's nights). - Add a fourth asset
occupancy_forecast_input: OTB ÷ rooms by stay date for the next 90 days. - Materialise everything in the UI. Inspect the metadata. Break something on purpose (wrong path) and read the error in the run log.
- Add
description,ownersandgroup_nameto 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?
Pure functions are testable without Dagster and portable to any orchestrator.
2. You change stg_bookings. Which assets need re-materialising?
Staleness flows down the graph.
3. What does MaterializeResult metadata give you?
It's lightweight observability attached to each run.