Not all work produces an asset. An op is a plain unit of computation, and a job wires ops into a graph by passing outputs to inputs, much like TaskFlow (The TaskFlow API and XComs). Ops were Dagster 177,056 's original model; assets are built on them. Both receive resources: typed, configurable objects such as database clients, injected by parameter name and swapped in tests, so no function reads a password:
class BookNestDB(dg.ConfigurableResource):
host: str = "l2-pg"
port: int = 5432
password: str
def connect(self):
return psycopg2.connect(host=self.host, port=self.port, dbname="booknest",
user="postgres", password=self.password,
options="-c TimeZone=UTC")
@dg.op
def table_sizes(db: BookNestDB) -> dict:
... # {table: bytes} for orders and order_items
@dg.op
def analyze_tables(context: dg.OpExecutionContext, db: BookNestDB, sizes: dict) -> None:
conn = db.connect()
conn.autocommit = True # VACUUM cannot run inside a transaction
with conn.cursor() as cur:
for table, size in sizes.items():
cur.execute(f"VACUUM (ANALYZE) {table}")
context.log.info(f"vacuumed {table} ({size / 2**20:.1f} MiB)")
@dg.job
def booknest_maintenance():
analyze_tables(table_sizes()) # data flow between ops is the dependencyThe Definitions object at the bottom of the module binds db to BookNestDB(password=dg.EnvVar("BOOKNEST_PG_PASSWORD")), read when a run starts. dagster job execute -j booknest_maintenance ran both ops and logged vacuumed orders (11.9 MiB) and vacuumed order_items (10.3 MiB).
Use ops for side effects; model anything a consumer reads as an asset, or Dagster cannot show its lineage.