Skip to content

The fluent model

laterite is fluent by design: every method returns a known type, so chains compose without surprises. You read a file into an Ags4File, then each call either hands the same handle back (so it slots mid-chain), forks off a query, or terminates into a frame, a relation, or bytes. Nothing in the lazy parts runs until you ask for a result.

file = laterite.read("delivery.ags")                 # ── Ags4File
locas = file.validate().query("SELECT * FROM LOCA")  # ── self → AgsQuery (lazy)

What each method returns

Method Returns Chainable?
read(path \| text= \| data=) Ags4File the chain root
.validate(warnings=) self (Ags4File) — Report on .report yes, slots mid-chain
.fix() a new Ags4File yes (off the fixed handle)
.at(code, ids) AgsQueryfan-out (a borehole's group set) yes (query branch)
.query(sql) AgsQuerysingle-result, lazy yes (query branch)
.sql(sql) DuckDBPyRelation terminal (materialise with .pl()/.df()/.arrow())
.pipe(fn, *args) whatever fn returns yes, if fn returns a handle
["LOCA"] polars frame terminal
.save / .certify / .text / .bytes path / .ags.idx / str / bytes terminal

The two query branches have different terminals: a .query(sql) builder takes .filter / .select (each returning a new AgsQuery) and materialises with .frame() / .to_polars() / .to_pandas() / .relation(); an .at(...) fan-out exposes .groups and .frames().

Single-result and fan-out are mutually exclusive

The single-result methods (.filter / .select / .frame()) and the fan-out methods (.groups / .frames()) live on different AgsQuery shapes and don't mix on one handle. And every builder call returns a new, immutable AgsQuery — reassign (q = q.filter(...)) rather than expecting in-place mutation, which also lets you fork a chain without one branch disturbing another.

Why it composes

Because the return type of each step is fixed and documented, you can read a chain left-to-right and know what's in your hand at every point: read gives an Ags4File, .validate() keeps it, .query() opens a lazy plan, a terminal cashes it in. The escape hatch — .pipe(fn) — never breaks the chain, since it just threads the handle through your own function.

See also

  • Chaining — the seven-rung power ladder, each rung runnable.
  • Reference cheatsheet — the one-screen map of every method and its return type.