API¶
The public API — everything importable from bloomery, kept in lockstep with the
package's __all__. Supporting types named in signatures (Project, Catalog,
ProjectIR, EmittedArtifact, naming policies) live in their subpackages and are
stable to import from there.
Loading¶
load_catalog(text: str) -> Catalog¶
Parse a catalog document from YAML text. Pure — strings in, model out; all I/O belongs
to the caller. Raises SpecParseError on YAML, shape, or kind failures.
load_project(sources: Mapping[str, str]) -> Project¶
Parse a project from named YAML documents. Keys are document names (used as
source-path prefixes in errors), values are YAML text. Each document self-identifies
via its version key; documents are processed in sorted-name order, and all failures
across all documents are batched into a single SpecParseError.
Compiling¶
compile_project(project, *, target, dialect, naming=None, catalog=None) -> tuple[EmittedArtifact, ...]¶
Compile a parsed project into target artifacts — the whole pipeline (resolve,
typecheck, guardrails, lower, emit) as one pure function. target is a Target or
the string name of a registered extension emitter; dialect is "duckdb",
"trino", or "postgres"; naming defaults to layer-based naming
(silver.<entity>, gold.mart_<name>). Same specs in, byte-identical artifacts out.
Each EmittedArtifact carries path, content, kind, and checksum.
Target¶
The shipped emit targets: Target.SQLMESH, Target.CUBE, Target.DBT. A string
enum, so target="sqlmesh" also works.
Analysis¶
resolve(project: Project, catalog: Catalog | None = None) -> Resolution¶
Run reference validation, recipe validation, and reachability without emitting
anything. The Resolution carries reachable_metrics, unreachable_metrics (each
with its specific missing leaves), per-field provenance (direct / recipe / native),
and the deterministic topological order.
build_project_ir(project: Project, catalog: Catalog | None = None) -> ProjectIR¶
Compile specs into the frozen intermediate representation without emitting — the
input to project_fingerprint, plan, and MetricFlowPlanner.plan. Runs the same
resolution, typecheck, and guardrail stages as compile_project, so a spec that
builds an IR is a spec that compiles.
project_fingerprint(ir: ProjectIR) -> str¶
The blm1:-prefixed SHA-256 content hash of a project IR — the value stamped into
every emitted artifact's header. Stable within a bloomery version, deliberately not
across versions.
Spec-diff planning¶
plan(old: ProjectIR | None, new: ProjectIR) -> Plan¶
Diff two compiled IRs into a classified migration plan. plan(None, new) is the
initial deploy; plan(ir, ir) is empty. Raises RenameTargetMissing on a stale
renamed_from annotation and ContractViolation on an expand/contract breach; every
other change, breaking included, is classified and returned. See
Evolve a spec safely.
Plan¶
changes (sorted Change tuple), backfill_scope (BackfillScope), replay_scope
(ReplayScope), and downstream_impact (affected metric names). Properties:
has_changes, breaking.
Change¶
One classified difference: entity, subject (<kind>:<name>), change_class,
human-readable detail, and compact old/new value reprs.
ChangeClass¶
The closed classification vocabulary: ADDITIVE, WIDENING, RENAME, RESTATING,
BREAKING.
BackfillScope¶
entities — the sorted entities whose stored rows a plan invalidates — and
restates_history, true when any RESTATING change is present.
ReplayScope¶
entities — the sorted entities whose <entity>__reject tables a plan invalidates.
Distinct from BackfillScope because the two name different storage: a backfill
recomputes an entity from bronze, while a replay re-runs the current mapping against
rows that are not in bronze's incremental window at all. Populated only where a change
can actually free rows: the old disposition was quarantine, and the rule is now
gone, now disposes as flag, or has relaxed parameters. A tightening — a narrowed
bound, or quarantine → fail — needs a backfill and no replay: every quarantined row
still fails the rule, so replaying it drains nothing. Where relaxation is undecidable
from the parameters (a pattern regex, an expression), the replay is reported.
bloomery emits the replay merge artifact; executing it is the caller's. See Add quality
rules.
Request-time planning¶
MetricFlowPlanner(hydrator, max_limit=50_000, default_limit=None, *, naming=None)¶
The planner port backed by an embedded, render-only MetricFlow. naming must match
the policy the artifacts were emitted with.
.plan(ir, request, *, dialect, policy=None) -> QueryPlan — validate the request,
check mart coverage, and render SQL; nothing is executed. Refusals raise the planner
error taxonomy (UnknownMember, UnreachableAtGrain, AmbiguousDimension,
InvalidRequest, FilterTypeMismatch, and the UnsupportedFilter family). See
Plan a metric request.
MetricRequest(metrics, dimensions=(), filters=(), time_grain=None, order_by=(), limit=None)¶
A structured metric request. Construction enforces the structural rules (at least one
metric, no duplicates, order over requested members only, limit >= 1). filters is
CNF: a tuple of clauses (implicit AND), each a Predicate or one AnyOf group.
Predicate(dimension, op, values=())¶
One typed single-dimension filter (RFC 0015). op is an Op member; value arity is
checked per operator (is_null takes exactly one bool; like/ilike take one or
more patterns). Floats are accepted and normalized to Decimal(str(value)) at
construction; non-finite numerics raise InvalidLiteral.
AnyOf(predicates)¶
One disjunction group — OR across its predicates, AND with every other clause.
Exactly one level: members are Predicate only, and may span different dimensions.
Op¶
The closed filter-operator vocabulary: EQ, NE, GT, GTE, LT, LTE, IN,
NOT_IN, IS_NULL, LIKE, ILIKE. A string enum, so op="eq" also works.
like/ilike operands are SQL LIKE patterns — caller-owned wildcards with \ as
the escape character; nothing is auto-wrapped.
OrderSpec(field, direction="asc")¶
One ordering term over a requested metric or dimension — never arbitrary SQL. Carries no nulls placement (non-default placements are refused at the JSON front door).
TimeGrain¶
Requestable grains: HOUR, DAY, WEEK, MONTH, QUARTER, YEAR. Applies to
every date-role dimension in the request; HOUR is refused at coverage (marts carry
day–year buckets).
RowPolicy(dimension, op, value)¶
A row-level scoping filter — dimension, Op, scalar or scalar tuple — rendered
through the same escaping pipeline as user filters (via as_clause()) and prepended
to them, reaching every scan. A policy is one predicate; range policies compose into
the request filters instead. Deciding whose policy applies is upstream work.
QueryPlan¶
The planner's product: sql, columns (tuple of ColumnDescriptor), mart,
warnings, explanation (with .render()), and fingerprint (sha256(sql)).
ColumnDescriptor¶
One output column in bloomery names: name, logical type, role
("dimension" or "measure"), optional label.
bloomery.planner.parse_filter_json(payload, *, clause_cap=64) -> tuple[Clause, ...]¶
The public JSON front door for the Mongo-flavoured filter grammar ($and/$or/
$not, field maps {field: scalar | {op: value} | [array]}, spellings $eq $neq $gt
$gte $lt $lte $in $nin $null $like $ilike). Normalizes before refusing — De Morgan
push-down, complement inversion, CNF distribution with the clause cap enforced during
distribution — and refuses only with UnsupportedFilter leaves carrying stable
.reason codes.
Two failure classes, deliberately distinct. A construct the vocabulary reviewed and
declined — a set relation, a hierarchy operator, $regex, an over-cap CNF expansion, a
non-invertible negation — raises UnsupportedFilter with a .reason from
KNOWN_UNSUPPORTED. Refusals fire wherever the parser reaches them: operator refusals,
non-finite literals, and the nesting-depth cap during tree construction;
UnsupportedNegation and the CNF clause cap after the rewrite. .normalized is set
wherever it says something useful — the form the document had reached for
UnsupportedNegation, a size sentinel (>64 levels deep, >64 clauses) for the two
caps. A malformed document — a non-mapping payload, a
field map of the wrong shape, an unknown $op, an operand of the wrong type — raises
InvalidRequest: it never reaches the closed list, because malformed input is a schema
error, not a reviewed gap. The same split holds for parse_sort_json and
parse_page_json: only a well-formed placement or a well-formed non-zero offset reaches
UnsupportedSortNulls/UnsupportedPagination; anything else is InvalidRequest.
bloomery.planner.parse_sort_json(payload) -> tuple[OrderSpec, ...]¶
Sort documents ({field: "asc" | "desc" | {"dir": …, "nulls": …}}) to order terms. A
nulls equal to the canonical default (first for asc, last for desc) is dropped; a
well-formed non-default placement raises UnsupportedSortNulls. A present nulls key
must hold exactly "first" or "last" — a wrong type, an explicit null, or an unknown
word is InvalidRequest; omitting the key is the canonical default.
bloomery.planner.parse_page_json(payload) -> int | None¶
Pagination documents ({"limit": …, "offset": …}) to the request limit. Non-zero
offsets and cursor keys (after/before) raise UnsupportedPagination; a malformed
payload — a non-mapping document, an unknown key, a non-int limit/offset — raises
InvalidRequest.
bloomery.planner.KNOWN_UNSUPPORTED: frozenset[str]¶
The closed refusal list: exactly the .reason codes the three parse functions can
raise, drift-guarded by test. Adapters assert their refusal handling covers this set.
LruManifestHydrator(naming, *, max_entries=500, fetch_l2=None, prewarm=False)¶
The default in-process manifest cache behind the planner: an LRU of hydrated semantic
manifests keyed by HydrationKey. On a miss it consults the caller-injected
fetch_l2 callable (your storage, your I/O), falling back to rebuilding from the IR.
hits/misses/hit_rate are plain counters to poll into your metrics system.
.get(ir) returns the hydrated lookup for a project.
HydrationKey¶
The cache key covering all three invalidation axes: spec_fingerprint,
bloomery_version, metricflow_version. A spec edit or a version bump changes the
key, so stale entries are a miss, never an error.
Extension points¶
register_transform(spec: TransformSpec) -> None¶
Register an extension transform into the process-global overlay consulted after the
built-in whitelist. Name collisions raise TransformRegistrationError. See
Transforms.
register_emitter(emitter: TargetEmitter) -> None¶
Register an extension target emitter; the emitter's name becomes a valid target=
string for compile_project. Collisions raise EmitError.
Errors¶
BloomeryError¶
The base of every error the package raises — str(exc) is the human message,
exc.source_path addresses the offending spec node, exc.collected holds individual
failures on batched aggregates. The full tree lives in Errors.