Skip to content

Support modules

The supporting surfaces — the dictionary registry, the AGS type system, and the transport (pack / encrypt) helpers. See Concepts for the ideas behind them.

laterite.registry

The AGS group graph projected from the single-source dictionary — group descriptors, the KEY chain, and parent/child links.

registry

laterite.registry — Rust-backed AGS group registry (read-only).

Exposes typed group/heading descriptors, populated from the Rust crate's OnceLock singleton via PyO3 (no second JSON parse in Python).

Read-only: dictionary content is established at Rust crate load time. Runtime extension for AGS4 passthrough groups (groups present in a file but not in the bundled dictionary) lives in laterite.dynamic.

HeadingStatus module-attribute

HeadingStatus = Literal[
    "KEY", "REQUIRED", "OTHER", "KEY+REQUIRED", "DEPRECATED"
]

GROUPS module-attribute

GROUPS: dict[str, GroupDescriptor] = _ReadOnlyGroups(
    _load()
)

Heading dataclass

Heading(
    name: str,
    status: HeadingStatus,
    type: str,
    unit: str | None = None,
    description: str = "",
)

One heading (column) of an AGS group, as the dictionary defines it.

Frozen because the registry is read-only — a Heading is a fact about the AGS spec, not mutable state. status keeps the dictionary's own vocabulary verbatim, including the combined "KEY+REQUIRED" and the "DEPRECATED" marker; use is_key rather than an == test so those combined forms are handled.

name instance-attribute

name: str

status instance-attribute

status: HeadingStatus

type instance-attribute

type: str

unit class-attribute instance-attribute

unit: str | None = None

description class-attribute instance-attribute

description: str = ''

py_name property

py_name: str

The heading name lower-cased — the identifier the typed classes and DuckDB columns use, where the dictionary's upper-case form isn't a legal attribute/column name.

is_key property

is_key: bool

Whether this heading is a KEY column. Tests the +-separated parts of status so the combined "KEY+REQUIRED" form counts, where a plain == "KEY" would miss it.

GroupDescriptor dataclass

GroupDescriptor(
    code: str,
    contents: str,
    parent: str | None,
    headings: tuple[Heading, ...],
)

The full spec of one AGS group: its 4-letter code, the dictionary's prose contents, the parent code that places it in the PROJ tree (None at the root), and its headings in dictionary order.

Frozen for the same reason as Heading — this describes the AGS dictionary, which is fixed at Rust crate load. The table/view getters give the conventional DuckDB table/view names for the group.

code instance-attribute

code: str

contents instance-attribute

contents: str

parent instance-attribute

parent: str | None

headings instance-attribute

headings: tuple[Heading, ...]

table property

table: str

The group's physical table name (g_<code> lower-cased) in a DuckDB store.

view property

view: str

The group's view name (v_<code> lower-cased) in a DuckDB store — the parent-joined view that complements the raw table.

key_headings property

key_headings: tuple[Heading, ...]

The KEY headings — the columns that identify a row in this group — in dictionary order.

non_key_headings property

non_key_headings: tuple[Heading, ...]

The non-KEY headings — every column that doesn't take part in identifying a row — in dictionary order.

get

get(code: str) -> GroupDescriptor | None

Single-group lookup. None for unknown codes (mirrors GROUPS.get(code)).

dictionary

dictionary(
    edition: Edition | None = None,
) -> dict[str, Any]

The bundled STANDARD dictionary for one AGS edition — the per-edition view of the official dictionary.

Where the module-level :data:GROUPS is the union registry across all editions (the typed-graph / DDL model, and the default), this is a single edition's standard dictionary: canonical group + heading names, descriptions, UNIT/TYPE, and status. It's the same content the browser's dictionary reference and Node's registry.dictionary() render, built from one shared Rust builder (dict::dictionary_dto).

The shape is {ags_edition, groups: [{code, contents, parent, headings: [{name, status, type, unit?, description}]}]} — groups sorted by code, each group's headings in canonical dictionary order.

Args: edition: One of "4.0.3" | "4.0.4" | "4.1" | "4.1.1" | "4.2". None (or "auto") uses the fallback edition.

Returns: The dictionary snapshot as a nested dict.

Raises: ValueError: If edition is not a recognised edition.

ancestor_chain

ancestor_chain(code: str) -> list[str]

Parent chain from code to root: [code, parent, ..., root].

Raises ValueError if code isn't in the registry (so callers can distinguish "no parent" — root groups return [code] — from "unknown code"). Computed in Rust against the static registry.

inherited_key_names

inherited_key_names(code: str) -> set[str]

The KEY heading names this group shares with its direct parent — the keys it inherits rather than declaring fresh.

The intersection of this group's KEY headings with its immediate parent's (only the direct parent, not the whole ancestor chain), computed in Rust against the static registry. Rust returns a sorted list for determinism; we hand it back as a set since order carries no meaning to callers. Empty when the group is a root or its parent is unknown.

child_groups

child_groups(parent_code: str) -> list[GroupDescriptor]

Every direct child group of parent_code, in alphabetical order.

options: show_root_heading: false members_order: source

laterite.ags_types

The AGS type system — canonical types and value casting.

ags_types

laterite.ags_types — Rust-backed AGS4 type system.

Exposes parse_value / canonical_type / display_hint semantics backed by the Rust engine in rust-packages/laterite-ags4-core/src/ags_types.rs.

The CanonicalType StrEnum lives Python-side so existing canonical_type(x) is CanonicalType.DECIMAL checks keep their identity. Rust returns the lowercase label; we coerce back to the enum.

parse_value returns native Python types — int / float / bool / datetime.datetime / datetime.date / datetime.time / str / None — matching the pure-Python implementation. Returns the trimmed string for unknown AGS codes (passthrough), matching the prior behaviour.

CanonicalType

Bases: StrEnum

Cross-system target types. Values match the lowercase labels the Rust side returns from canonical_type so the enum doubles as the string interchange representation (e.g. for _spec_headings).

STRING class-attribute instance-attribute

STRING = 'string'

INTEGER class-attribute instance-attribute

INTEGER = 'integer'

DECIMAL class-attribute instance-attribute

DECIMAL = 'decimal'

DATETIME class-attribute instance-attribute

DATETIME = 'datetime'

DATE class-attribute instance-attribute

DATE = 'date'

TIME class-attribute instance-attribute

TIME = 'time'

BOOL class-attribute instance-attribute

BOOL = 'bool'

ENUM class-attribute instance-attribute

ENUM = 'enum'

canonical_type

canonical_type(ags_type: str) -> CanonicalType

AGS spec type code → canonical category.

Raises ValueError for unknown codes — the AGS4 codec coerces passthrough groups' heading types to 'X' before calling, so in practice this never raises on dictionary-resident headings.

display_hint

display_hint(ags_type: str) -> str | None

Presentation hint for a numeric AGS type, or None.

'2DP''%.2f', '3SF''%.3g', '1SCI''%.1e'. String / datetime / bool types return None.

parse_value

parse_value(raw: Any, ags_type: str) -> Any

Parse an AGS4-shaped raw string into its canonical Python type.

Permissive by design — this is the single source of truth for AGS4 ingest (the Rust base path), so it never raises on bad data: an unparseable value, or an empty / whitespace-only string, comes back as None rather than an error. Anything that isn't already a string is stringified first, mirroring the original pure-Python implementation's isinstance(s, str) guards.

Args: raw: The raw AGS4 field value. None (explicit null) returns None untouched; non-string input is coerced with str() before parsing. ags_type: The AGS spec type code (e.g. '2DP', 'DT', 'YN', 'ID') that selects the canonical category.

Returns: The value in its canonical Python type, keyed off the type code: int (INTEGER), float (DECIMAL), bool (BOOL), datetime.datetime (DATETIME), datetime.date (DATE), datetime.time (TIME), and str (STRING / ENUM). An unknown AGS code passes the trimmed string through unchanged. None for a null, empty, whitespace-only, or unparseable value.

options: show_root_heading: false members_order: source

laterite.transport

Pack / unpack and lock / unlock for content-agnostic transport (zstd + optional age encryption).

transport

laterite.transport — zstd compression + age passphrase encryption for any file.

The four operations are content-agnostic: they read a file's bytes, (de)compress and optionally (de)encrypt them, and write the result. They work on any file — an AGS4 .ags transfer file, anything — the Rust core just runs zstd/age over raw bytes.

Two pairs of operations:

  • pack / unpack — zstd only. The default level 9 is the empirical sweet spot on AGS data (~10% ratio in a few seconds; higher levels buy minutes not bytes).
  • lock / unlock — zstd + age passphrase encryption. The age envelope is interoperable with pyrage and the lat lock binary subcommand — both link the same Rust age crate.

    from laterite import transport transport.pack("delivery.ags") PosixPath('delivery.ags.zst') transport.lock("delivery.ags", password="hunter2") PosixPath('delivery.ags.zst.age')

pack

pack(
    src: str | PathLike[str],
    *,
    level: int = 9,
    dest: str | PathLike[str] | None = None,
) -> Path

Compress any file to <src>.zst for transport (zstd only).

Works on any file.ags, anything. level is the zstd level (1=fastest, 22=highest ratio); default 9 is the sweet spot on AGS data. dest overrides the default <src>.zst output path.

Args: src: Path to the file to compress (str or os.PathLike). An Ags4File is rejected early — call .save(path) first. level: zstd compression level, 1 (fastest) to 22 (highest ratio). Defaults to 9. dest: Output path. Defaults to <src>.zst (original suffix preserved, .zst appended).

Returns: The Path written.

Raises: TypeError: If src is not a path-like (e.g. an Ags4File). RuntimeError: If the underlying zstd operation fails.

unpack

unpack(
    src: str | PathLike[str],
    *,
    dest: str | PathLike[str] | None = None,
) -> Path

Decompress a .zst produced by pack back to the original file.

By default the output strips the .zst suffix; pass dest to override.

Args: src: Path to the .zst file to decompress. dest: Output path. Defaults to src with its .zst suffix stripped (or <src>.unpacked if it has no .zst suffix).

Returns: The Path written.

Raises: TypeError: If src is not a path-like. RuntimeError: If the input is not valid zstd or the operation fails.

lock

lock(
    src: str | PathLike[str],
    *,
    password: str,
    level: int = 9,
    log_n: int | None = None,
    dest: str | PathLike[str] | None = None,
) -> Path

Compress + age-passphrase-encrypt any file to <src>.zst.age.

Zstd first (low-entropy data compresses well), then age (scrypt + ChaCha20-Poly1305). The envelope is interoperable with lat lock and pyrage — both use the same Rust age crate. password is required (no agent-default path). level is the zstd level (default 9); dest overrides the default <src>.zst.age output path.

Args: src: Path to the file to compress and encrypt. password: The age passphrase. Required — there is no agent-key path. level: zstd compression level, 1 (fastest) to 22 (highest ratio). Defaults to 9. log_n: scrypt work factor (log2(N)) for the age passphrase KDF. None uses the pinned default (18 — age's standard tier, openable everywhere). A lower value is faster but weaker; must be 1..=20 (>20 produces a file the browser age decoder refuses). dest: Output path. Defaults to <src>.zst.age (flagging both the compression and encryption layers).

Returns: The Path written.

Raises: TypeError: If src is not a path-like. RuntimeError: If the underlying zstd or age operation fails, or log_n is outside 1..=20.

unlock

unlock(
    src: str | PathLike[str],
    *,
    password: str,
    dest: str | PathLike[str] | None = None,
) -> Path

Decrypt + decompress a .zst.age produced by lock back to the original.

Wrong passphrase or non-passphrase envelopes raise RuntimeError. By default the output strips .age then .zst.

Args: src: Path to the .zst.age file to decrypt and decompress. password: The age passphrase used by lock. dest: Output path. Defaults to src with .age then .zst stripped (or <src>.unlocked if neither suffix is present).

Returns: The Path written.

Raises: TypeError: If src is not a path-like. RuntimeError: If the passphrase is wrong, the envelope is not a passphrase envelope, or decompression fails.

pack_bytes

pack_bytes(data: bytes, *, level: int = 9) -> bytes

Compress bytes → bytes in memory (zstd only) — no filesystem.

The in-memory form of :func:pack. Output is a standard zstd frame, so it opens with :func:unpack_bytes, :func:unpack (write it to a file first), or stock zstd.

Args: data: The bytes to compress. level: zstd level, 1 (fastest) to 22 (highest ratio). Defaults to 9.

Returns: The compressed bytes.

Raises: RuntimeError: If the underlying zstd operation fails.

unpack_bytes

unpack_bytes(data: bytes) -> bytes

Decompress zstd bytes → bytes in memory — the in-memory form of :func:unpack.

Args: data: The zstd-compressed bytes (e.g. from :func:pack_bytes).

Returns: The decompressed bytes.

Raises: RuntimeError: If the input is not valid zstd or the operation fails.

lock_bytes

lock_bytes(
    data: bytes,
    *,
    password: str,
    level: int = 9,
    log_n: int | None = None,
) -> bytes

Compress + age-passphrase-encrypt bytes → bytes in memory — no plaintext on disk.

The in-memory form of :func:lock (zstd, then age scrypt + ChaCha20-Poly1305). Ideal for sealing sensitive data you hold in memory — e.g. a fixed Ags4File's .bytes — without ever writing the plaintext out. The .zst.age envelope matches :func:lock's, so the result opens with :func:unlock_bytes, :func:unlock (write it out first), pyrage, or the browser, given the passphrase.

Args: data: The bytes to compress and encrypt. password: The age passphrase. Required — there is no agent-key path. level: zstd level, 1 (fastest) to 22 (highest ratio). Defaults to 9. log_n: scrypt work factor (log2(N)). None uses the pinned default (18); a lower value is faster but weaker; must be 1..=20.

Returns: The sealed bytes.

Raises: RuntimeError: If the underlying zstd or age operation fails, or log_n is outside 1..=20.

unlock_bytes

unlock_bytes(data: bytes, *, password: str) -> bytes

Decrypt + decompress .zst.age bytes → bytes — the in-memory form of :func:unlock.

Args: data: The sealed bytes (e.g. from :func:lock_bytes). password: The age passphrase used to seal them.

Returns: The original bytes.

Raises: RuntimeError: If the passphrase is wrong, the envelope is not a passphrase envelope, or decompression fails.

options: show_root_heading: false members_order: source