Skip to content

Python cheatsheet

The whole Python surface is three handles plus two result objects. Read one file, then chain — every method below hangs off what read() gives you.

# what this shows: the lazy single-result AgsQuery builder + its four terminals.
import laterite
import pandas as pd
import polars as pl
from duckdb import DuckDBPyRelation

# Build a query lazily: nothing runs until a terminal is called.
q = (
    laterite.read("examples/sample_site.ags")
    .query("SELECT * FROM LOCA")
    .filter("LOCA_GL > 28")
    .select("LOCA_ID", "LOCA_TYPE", "LOCA_GL")
)

# Four terminals materialise the same lazy plan:
frame = q.frame()  # handle's default backend (polars)
pl_df = q.to_polars()  # always polars
pd_df = q.to_pandas()  # always pandas
rel = q.relation()  # the lazy DuckDBPyRelation (not yet materialised)

print(pl_df)

# Projection is honoured exactly as declared.
assert frame.columns == ["LOCA_ID", "LOCA_TYPE", "LOCA_GL"]
assert pl_df.columns == ["LOCA_ID", "LOCA_TYPE", "LOCA_GL"]

# Each terminal returns its expected type.
assert isinstance(frame, pl.DataFrame)
assert isinstance(pl_df, pl.DataFrame)
assert isinstance(pd_df, pd.DataFrame)
assert isinstance(rel, DuckDBPyRelation)
shape: (7, 3)
┌─────────┬───────────┬─────────┐
│ LOCA_ID ┆ LOCA_TYPE ┆ LOCA_GL │
│ ---     ┆ ---       ┆ ---     │
│ str     ┆ str       ┆ f64     │
╞═════════╪═══════════╪═════════╡
│ BH02    ┆ RC        ┆ 32.49   │
│ BH03    ┆ RC        ┆ 28.54   │
│ BH04    ┆ RC        ┆ 29.04   │
│ BH05    ┆ RC        ┆ 31.62   │
│ BH07    ┆ RC        ┆ 31.33   │
│ BH08    ┆ CP        ┆ 28.67   │
│ BH09    ┆ CP        ┆ 30.98   │
└─────────┴───────────┴─────────┘

A lazy AgsQuery (above), built up and then materialised. The tables that follow are the full lookup — what each method returns, and whether it keeps the chain alive.

Ags4File — the read handle

laterite.read(...) returns this. Methods that return self (or a new Ags4File) keep you on the handle; the rest hand you a query, a relation, or a materialised value.

Method / attr Returns Chainable? Example
read(path) / read(text=) / read(data=bytes) Ags4File start of chain ex01
.validate() Ags4File (same handle) yes — runs the rule engine, parks the Report on .report ex02
.fix() Ags4File (new) yes — applies safe auto-fixes, original untouched ex15
.at(code, [ids]) AgsQuery → query (fan-out from a parent group) ex03
.query(sql) AgsQuery (lazy) → query (build with .filter / .select) ex05
.sql(sql) DuckDBPyRelation → relation (terminal: .pl() / .df() / .arrow()) ex06
.pipe(fn, *args) whatever fn returns depends on fn ex07
["CODE"] / .table(code) polars.DataFrame no — one group, materialised ex01
.groups list[str] no — group codes present ex01
.report Report | None no — set by .validate() ex02
.text str no — byte-faithful AGS4 source ex08
.bytes bytes no — raw encoded source ex08
.certify(path) writes an .ags.idx sidecar no — mints the validity cert ex08
.save(path) writes the file no — terminal ex08

Born typed

["CODE"] and the query terminals hand back polars frames where the column dtype is the AGS data typeLOCA_GL arrives as f64, not a string to re-parse. See Born typed.

AgsQuery — the lazy query handle

.at(...) and .query(...) return this. Builder methods are immutable: each returns a new AgsQuery, so the plan only runs when you call a terminal.

Method Returns Chainable?
.filter(expr) AgsQuery (new) yes — builder
.select(*cols) AgsQuery (new) yes — builder
.frame() polars.DataFrame terminal — single result, handle's backend
.to_polars() polars.DataFrame terminal — single result
.to_pandas() pandas.DataFrame terminal — single result
.relation() DuckDBPyRelation terminal — single result, still lazy
.frames() dict[str, DataFrame] terminal — fan-out (e.g. ["SAMP"])
.groups list[str] fan-out — group codes in the result

Single-result and fan-out are mutually exclusive

An AgsQuery is either a single-result query (use .frame / .to_polars / .to_pandas / .relation) or a fan-out (use .frames / .groups) — .at(code, [ids]) produces the fan-out shape, .query(sql) the single-result shape. Calling the wrong family raises. And every builder call (.filter, .select) returns a new immutable AgsQuery; the original is unchanged, so you can branch a plan safely. See ex04 and ex05.

Report — the validation verdict

Parked on Ags4File.report after .validate().

Method / attr Returns Notes
.is_valid bool True when zero findings
.count int number of findings
.dict_version str AGS edition the rules resolved to
.resolution str how that edition was chosen
.findings list[Finding] rule · line · group · description
.to_json() str JSON, same shape as lat validate --json

→ worked example: Validate (ex02).

BuildResult — the producer result

build_ags4(frames | typed PROJ graph) returns this. Same .text / .bytes / .save door as Ags4File, plus what the build did.

Method / attr Returns Notes
.text str the emitted AGS4
.bytes bytes the emitted AGS4, encoded
.findings list[Finding] validation findings on the built file
.fixes_applied list[str] what the emitter normalised
.save(path) writes the file terminal

→ worked examples: Produce — from frames (ex09a) or from a typed PROJ graph (ex09b).


See also: the CLI reference for lat, and the Cookbook for end-to-end recipes.