YAML specs¶
A spec can live in a file instead of a class body, so tooling outside Python can read it and so it can be reviewed as a document.
Orders.to_yaml("orders.yaml")
Loaded = FrameSpec.from_yaml("orders.yaml")
Loaded.generate(1_000, seed=1)
The output is plain, readable YAML — defaults are omitted so the file shows only what you actually declared:
version: 3
name: Orders
columns:
order_id:
dtype: Int64
bounds: [1, 100000]
unique: true
status:
dtype:
Enum: [NEW, PAID, SHIPPED]
total:
dtype: Float64
bounds: [0.0, null]
placed:
dtype: Date
nullable: true
unique_together:
- [order_id, status]
An open-ended bound writes as null and reads back unchanged.
version: records the file format that wrote the file. A file from an
earlier version is migrated on the way in; a file from a later polspec is
refused with a message saying so. A key the reader does not know is an
error naming the closest known key, since silently reading a misspelt option
as its default is the worst outcome -- pass strict=False to
from_yaml to downgrade that to a warning.
What survives a round-trip¶
| Round-trips | |
|---|---|
dtypes, including parametrized Enum / Datetime / Duration / named Categorical |
yes |
nullable, null_probability, bounds, string_length, list_length, fields, format, pattern, seed_name, unique, tags |
yes |
choices, weights, distribution, distribution_params |
yes |
rules (ColRule) |
yes |
__unique_together__ |
yes |
__foreign_keys__, self-referencing or to another spec (by name) |
yes |
__checks__ and ColSpec.validators written with col() |
yes |
__checks__ and ColSpec.validators over a raw pl.Expr |
no |
A foreign key to another spec is written as that spec's name; nothing
checks it until a spec of that name is supplied, through references= on
generate/validate or a registry. What cannot be written is an arbitrary
polars.Expr, which Polars cannot serialize stably. to_yaml() warns about
each, naming exactly what will be lost:
UserWarning: Orders declares 1 __checks__ ('total_covers_subtotal') that cannot
be represented in YAML (a Check wraps an arbitrary polars.Expr) and will NOT be
written to orders.yaml. They will be lost on FrameSpec.from_yaml() unless
re-declared on a subclass of the loaded spec.
The suggested recovery is to subclass what you loaded:
Loaded = FrameSpec.from_yaml("orders.yaml")
class Orders(Loaded):
__checks__ = [Check(pl.col("total") >= pl.col("subtotal"), name="total_covers_subtotal")]
Columns, rules, unique keys, foreign keys, and any check or validator
written with col() come from the file; only raw-expression parts need
re-declaring in Python.
A check in YAML is its predicate in data form:
Sharing categories between files¶
A spec file can reference a CatSpec registry by path, resolved relative to
the spec file:
name: Orders
categories: categories.yaml
columns:
status:
dtype:
Enum: STATUS
currency:
dtype:
Categorical: CURRENCY
$categories.STATUS and categories.STATUS are accepted as prefixed forms of
the same reference.
Or pass a registry explicitly, which wins over anything the file names:
FrameSpec.from_yaml("orders.yaml", categories=CatSpec.from_yaml("categories.yaml"))
FrameSpec.from_yaml("orders.yaml", categories="categories.yaml")
A spec can also emit the registry its own columns imply:
Python instead of YAML¶
The same spec can be written as an importable Python module. It is the right choice when the spec will be edited by hand from now on, or when it needs the parts YAML cannot hold:
"""Declares the Orders schema."""
import polars as pl
from polspec import ColSpec, FrameSpec
class Orders(FrameSpec):
__columns__ = {
'order_id': ColSpec(pl.Int64, bounds=(1, 100000), unique=True),
'status': ColSpec(pl.Enum(['NEW', 'PAID', 'SHIPPED'])),
'total': ColSpec(pl.Float64, bounds=(0.0, None)),
'placed': ColSpec(pl.Date, nullable=True),
}
__unique_together__ = [['order_id', 'status']]
Columns are declared through __columns__ because a name straight from data
is not always a valid identifier. What survives is exactly the
round-trip table above: __checks__,
cross-spec ForeignKeys and ColSpec.validators warn and are dropped, and
the file is where you then add them back by hand. polspec schema infer uses
this path when its output ends in .py; see
Command line.
Column names from data¶
from_yaml and to_python declare columns through __columns__, so names
that could not be class attributes — a leading underscore, a collision with a
method name like schema, or a name with spaces — load correctly. The YAML
key is the column's real name; a col_name set in a class body is not
written, because the key already carries it. See
Column names that are not identifiers.
Several specs in one file¶
A Registry writes every spec it holds, and the categories it
was declared with, to one file keyed by spec name, and reads it back with the
same version and strictness rules: