Python API

Everything the command line and the MCP server do is available from Python, through one class, konstruct.Project, and typed results.

import konstruct

project = konstruct.Project('path/to/repo')
project.build(update=True)

walk = project.impact('stg_payments.amount')
print(walk.summary())  # 7 columns, 3 relations, 5 tests, 1 exposure, 2 tasks and 2 business terms downstream.
for column in walk.columns:
    print(column.depth, column.label, column.transform, column.path)

Every result is a dataclass. str(result) is the text the CLI prints to a pipe, result.text(style) the same result drawn for a terminal (with a konstruct.interfaces.layout.Style giving colour, Unicode and width), and result.to_dict() the JSON that --json and the MCP server return, so a script can move between them freely (--json and MCP add the result’s format_version, see Compatibility). A name that matches nothing, or several things, raises konstruct.QueryError, whose suggestions are the closest names and hint a command to try, and a graph.json written by a newer Konstruct raises konstruct.FormatError.

konstruct.Project, the result types it returns and konstruct.QueryError are imported from konstruct itself. The modules behind them are grouped by role, for code that needs one directly:

Package

Modules

konstruct.core

graph (the graph and its traversal), sql (column lineage from SQL), mutate (mutants of a query), jsonio, yamlio (YAML read through one loader)

konstruct.inputs

dbt, dbt_commit (a dbt manifest as a commit’s files are), jinja, airflow, dagster, spark, looker, powerbi, loaders, ingestion, hcl, warehouse, usage

konstruct.engine

build, config, worker, snapshot (a commit’s files, and the user’s private cache)

konstruct.semantics

definitions, vocabulary, ontology, world, world_facts (the world as facts to compare), ossie, rows (whether a composed formula counts its column’s rows), registry

konstruct.analysis

query, semantic, explain, english, conflicts, diff, gate, verify, integrity, mutation, why, release_notes, effect, timeline, laws, history, receipts, lock, sensitivity, erasure

konstruct.outputs

report, html, export, compiled (the ontology as tables, relationships and metrics for the platform exports), owl, semantic_view, metric_view

konstruct.interfaces

cli, project, mcp_server, skill, console, status

konstruct.plugins

api (the plugin API: protocols and types), registry (finding, enabling and loading plugins)

Opening and Building a Project

A Project points at a repository. It reads nothing until asked: build() writes konstruct-out/, and every question reads the graph from there, rereading it whenever a build changes it.

project = konstruct.Project('analytics')  # or any directory inside a built project
if not project.has_graph() or project.changed_inputs():
    result = project.build(update=True)  # reuse the analysis of anything unchanged
    print(result.inputs)  # {'dbt_projects': 1, 'warehouses': 1, ...}
    print(result.graph.warnings)  # problems with the inputs, if any

The repository’s own warehouse connections and paths outside it are used, and dbt is run, once trust() has recorded them, as konstruct trust does (see Trusting a Project’s Connections).

class konstruct.Project(path='.', *, out_dir=None, plugins=True)[source]

Bases: object

A repository Konstruct builds a graph for, and every question that can be asked of it.

Creating a Project reads nothing. build() writes the graph; the question methods read it, rereading graph.json whenever a build changes it, and raise QueryError when there is no graph or a name matches nothing (or several things). Names are accepted as the CLI accepts them: orders, main.orders, orders.amount, a dbt source as source_name.table, a task as dag_id.task_id, or a node id.

Every result is a dataclass: str(result) is the CLI’s text and result.to_dict() the JSON that --json and the MCP server return, which add its format_version. A graph.json written by a newer Konstruct raises FormatError rather than being misread.

Parameters:
  • path (str | Path) – The project root, or any directory inside one that has already been built.

  • out_dir (str | Path | None) – Where the graph is written and read. By default, the konstruct-out directory found at or above path, or <path>/konstruct-out.

Example:

project = Project('analytics')
if not project.has_graph() or project.changed_inputs():
    project.build(update=True)
print(project.explain('orders.amount').composed)
Project.build(*, update=False, run_dbt=None, warehouses=True, dialect=None, config=None)[source]

Build the graph and write graph.json, REPORT.md and graph.html.

Discovers dbt projects, warehouses, Airflow and Dagster files, SQL files and definitions (or reads what the configuration pins), analyses the SQL and writes the outputs. Problems with the inputs do not fail the build; they are recorded in result.graph.warnings.

Parameters:
  • update (bool) – Reuse the cached analysis of anything unchanged since the last build.

  • run_dbt (bool | None) – Run dbt docs generate when a project’s artefacts are missing or stale. None follows the configuration (on by default).

  • warehouses (bool) – Read warehouse metadata. False builds from files alone.

  • dialect (str | None) – The SQL dialect, overriding the one taken from dbt or the warehouse.

  • config (Config | None) – A configuration to build with instead of reading konstruct.toml.

Returns:

The graph, the files written, the time taken, cache use and counts of what was read.

Return type:

BuildResult

Example:

result = project.build(update=True, warehouses=False)
print(result.inputs, result.graph.warnings)
Project.has_graph()[source]

Whether a graph has been built (graph.json exists).

Project.changed_inputs()[source]

Input files that changed after the graph was built.

Returns:

Paths relative to the project root; empty when the graph is up to date or not built.

Return type:

list[str]

Project.config()[source]

The project’s configuration: konstruct.toml (or [tool.konstruct] in pyproject.toml) over the defaults, with nothing discovered yet.

Raises:

konstruct.engine.config.ConfigError – The configuration file is invalid.

Project.plugins()[source]

The plugin registry for this project: Konstruct’s own inputs, export formats, gate checks and ontology packs, and every installed plugin with whether it runs here and why.

Example:

for plugin in project.plugins().installed():
    print(plugin.distribution, plugin.group, plugin.name, plugin.enabled, plugin.reason)
Project.trust(*, revoke=False)[source]

Trust the project’s own connections and paths, as they are now, and let the build run dbt.

A repository’s konstruct.toml and profiles.yml can name warehouses, read environment variables into them and point outside the project, and dbt runs the repository’s macros and models. The build uses those settings, and runs dbt, only once the project is trusted, and any change to the settings withdraws the trust until they are trusted again. The record does not cover the dbt project’s code. It is kept outside the repository, in the user’s configuration directory.

Parameters:

revoke (bool) – Remove the record instead.

Returns:

Whether the project is now trusted, the files the trust covers, the environment variables their connections read and the dbt projects it lets the build run dbt in.

Return type:

Trust

Example:

print(project.trust())  # after reviewing konstruct.toml and profiles.yml
property Project.graph: Graph

The built graph, reread whenever graph.json changes on disk.

Raises:

konstruct.QueryError – No graph has been built.

property Project.root: Path

The directory the graph was built from (path until a graph exists).

property Project.graph_path: Path

The path of graph.json in the output directory.

konstruct.interfaces.project.find_out_dir(start)[source]

Find the output directory for start.

Parameters:

start (Path) – A project root, or any directory inside one.

Returns:

The nearest konstruct-out at or above start that holds a graph.json, otherwise start/konstruct-out.

Return type:

Path

Impact, Trace and Paths

downstream = project.impact('orders.amount')
upstream = project.trace('customer_ltv.lifetime_value')
for column in upstream.columns:
    print(column.depth, column.label, column.transform, column.verified)

path = project.path_between('raw_payments', 'customer_dashboard')
print(' -> '.join(step.label for step in path.steps))
Project.impact(ref, max_depth=None)[source]

Everything downstream of a table, model or column: what a change to it affects.

For a column: the columns derived from it, the relations that join, filter or group on it, and the tests, exposures, orchestration tasks and business terms on anything reached. A join key reaching a relation does not mark every column of that relation as affected.

Parameters:
  • ref (str) – A table, model or column, such as orders or stg_payments.amount.

  • max_depth (int | None) – Stop after this many steps.

Returns:

The walk, grouped into columns, relations, tests, exposures, tasks and terms.

Raises:

konstruct.QueryError – ref matches nothing or several things.

Return type:

Walk

Example:

walk = project.impact('stg_payments.amount')
walk.summary()  # '7 columns, 3 relations, 5 tests, 1 exposure, 2 tasks and ...'
[column.label for column in walk.columns if column.transform == 'direct']
Project.trace(ref, max_depth=None)[source]

Everything upstream of a table, model or column: where its data comes from.

Each column step is marked direct (passed through or renamed) or transformed. For a column, filtered_by lists the columns that join or filter its relation, and the tasks that build each relation on the way are included.

Parameters:
  • ref (str) – A table, model or column.

  • max_depth (int | None) – Stop after this many steps.

Returns:

The walk upstream.

Raises:

konstruct.QueryError – ref matches nothing or several things.

Return type:

Walk

Example:

for column in project.trace('customer_ltv.lifetime_value').columns:
    print(column.depth, column.label, column.transform, column.verified)
Project.path_between(start, end)[source]

The shortest connection between two nodes, along data flow where there is one.

Parameters:
  • start (str) – Where the path starts.

  • end (str) – Where it ends.

Returns:

The steps from start to end; result.found is False when they are not connected.

Raises:

konstruct.QueryError – Either name matches nothing or several things.

Return type:

PathResult

Example:

path = project.path_between('raw_payments', 'customer_dashboard')
[step.label for step in path.steps]

Finding and Describing Nodes

project.search('lifetime value', kinds=['column']).results
project.search('revenue', mode='semantic').results  # by meaning: synonyms, formulas, lineage
project.describe('orders').columns
project.list(['model', 'table']).items
project.query('what breaks if stg_payments.amount changes').impact
Project.search(text, limit=20, kinds=None, mode='keyword')[source]

Find nodes whose names, synonyms or descriptions match the words in text, or, with mode='semantic', that match them by meaning.

Words matching a name count for more than words in a description, rarer words for more than common ones, and business terms rank above columns with the same words. Search by meaning also finds a node through the abbreviations and synonyms of data work, its formula and its lineage (see konstruct.analysis.semantic.search()), with exact names still first; its index is made on first use and saved as vectors.bin in the output directory.

Parameters:
  • text (str) – The words to look for.

  • limit (int) – The most results to return.

  • kinds (list[str] | None) – Only these node kinds, such as ['column'] or ['model', 'table'].

  • mode (Literal['keyword', 'semantic']) – keyword (the default) or semantic.

Returns:

Matching nodes, best first, each with a score, and in a search by meaning what the match rests on (matched).

Return type:

SearchResult

Example:

project.search('lifetime value', kinds=['column']).results[0].label
project.search('revenue', mode='semantic').results[0].matched
Project.describe(ref)[source]

One node in detail.

For a relation: its columns with types and descriptions, tests, the joins, filters and grouping that shape its rows, and its immediate neighbours. For a column: its expression and the business terms that define it. For a task: what it runs and the tasks around it. For a business term: its definition, owner, status and columns.

Parameters:

ref (str) – Any node: a relation, column, task, DAG, test, exposure or term.

Raises:

konstruct.QueryError – ref matches nothing or several things.

Example:

orders = project.describe('orders')
[(c.name, c.data_type) for c in orders.columns]
orders.conditions  # ['GROUP BY: stg_payments.order_id', 'LEFT JOIN ON: ...']
Project.list(kinds=None)[source]

Every node of the given kinds.

Parameters:

kinds (list[str] | None) – Node kinds such as ['model'] or ['task', 'dag']. By default, tables, models, sources, seeds and snapshots.

Example:

[item.label for item in project.list(['model']).items]
Project.query(question, depth=1)[source]

Answer a question in words with the nodes it is about and their connections.

Ranks nodes by the question’s words (there is no language model), returns the best matches with the edges around them, leads with the definitions of any business terms matched, and adds impact or lineage when the question asks what breaks or where something comes from.

Parameters:
  • question (str) – The question, such as what breaks if stg_payments.amount changes.

  • depth (int) – How many steps of connections around each match to include.

Example:

answer = project.query('what is lifetime value')
answer.definitions[0].definition

Formulas, Definitions, Conflicts and the Ontology

explanation = project.explain('orders.credit_card_amount')
print(explanation.composed)
# SUM(CASE WHEN raw_payments.payment_method = 'credit_card' THEN raw_payments.amount / 100 ELSE 0 END)
print(project.english('orders.credit_card_amount').english)
# the sum of ([amount] divided by 100 if [payment method] is 'credit_card', else 0)

term = project.define('lifetime value').terms[0]
print(term.definition, term.owner, [column.label for column in term.columns])

for conflict in project.conflicts().conflicts:
    print(conflict.idea, conflict.a.label, conflict.b.label, [d.detail for d in conflict.differences])

history = project.history('customers.customer_lifetime_value')
print([(change.commit.author, change.before, change.after) for change in history.formulas])
print(history.last_approval.commit.author if history.last_approval else None)

ontology = project.ontology()
print([(entity.name, entity.key) for entity in ontology.entities])
print(ontology.measure('customer lifetime value').columns[0].formula)
Project.explain(ref)[source]

How a column is calculated.

Returns the column’s expression with CTEs, subqueries and aliases inlined, the same formula composed through every model upstream until only source columns remain, and step by step each column on the way with the joins, filters and grouping of its relation.

Parameters:

ref (str) – A column, such as orders.credit_card_amount.

Returns:

The explanation. composed is None for a source column, and complete is False when some column on the way has no recorded expression.

Raises:

konstruct.QueryError – ref is not a column, or matches nothing or several things.

Return type:

Explanation

Example:

explanation = project.explain('customer_ltv.lifetime_value')
explanation.expression  # 'customers.customer_lifetime_value'
explanation.composed    # 'SUM(raw_payments.amount / 100)'
[step.conditions for step in explanation.steps]
Project.english(ref)[source]

A column’s formula in plain English, worded from the formula by a fixed grammar.

The formula is composed down to source columns, as explain() gives it, and each kind of expression has one wording, with brackets where the SQL’s grouping matters, so the sentence says what the SQL says and nothing more. A part with no wording is quoted as SQL and listed in verbatim. The joins, filters and grouping of every relation on the way, the grain of the column’s relation and the required filters of each bound business term are worded the same way.

Parameters:

ref (str) – A column, such as orders.credit_card_amount.

Returns:

The sentence, the conditions of each relation in English, the grain, the bound terms and the column each wording names.

Raises:

konstruct.QueryError – ref is not a column, or matches nothing or several things.

Return type:

English

Example:

result = project.english('orders.credit_card_amount')
result.english
# "the sum of ([amount] divided by 100 if [payment method] is 'credit_card', else 0)"
[(n.wording, n.column) for n in result.names]  # [('[amount]', 'raw_payments.amount'), ...]
Project.history(ref, *, limit=50)[source]

When a column’s formula changed, and who changed the business terms bound to it.

Reads the git history of the file that defines the column and of each definitions.yml that binds a term to it. The formula is worked out at each commit, so a commit that only reformats the SQL is not a change; uncommitted edits are the last entry.

Parameters:
  • ref (str) – A column, such as customers.lifetime_value.

  • limit (int) – The most commits to read for each file.

Returns:

The formula changes and term changes, oldest first, each with its commit’s author and date, and whether a bound term was approved after the latest formula change.

Raises:

konstruct.QueryError – ref is not a column, or matches nothing or several things, or limit is below 1.

Return type:

History

Example:

history = project.history('customers.lifetime_value')
[(c.commit.author, c.before, c.after) for c in history.formulas]
history.last_approval.commit.author if history.last_approval else None
Project.define(ref)[source]

The business definition of a term, or the terms that define a column.

Looks ref up as a term name or synonym first (case and spacing ignored), then as a column or relation, returning the terms bound to it.

Parameters:

ref (str) – A term (lifetime value), a synonym (LTV) or a column (orders.amount).

Returns:

The matching terms, approved first and retired last, with their definition, owner, status, grain, required filters and columns. terms is empty when a column is not bound to any term.

Raises:

konstruct.QueryError – Nothing matches ref, or it names several columns.

Return type:

Definitions

Example:

term = project.define('lifetime value').terms[0]
term.status, term.owner, [c.label for c in term.columns]
Project.conflicts(limit=None, *, write=False)[source]

Columns that appear to be one business idea computed in different ways.

Computed columns are grouped by what their names mean once qualifiers and synonyms are folded together, by business-term synonyms and by shared terms, then compared on their formulas in terms of sources, their source columns, and the filters and joins on their routes back to source. Pairs that agree, or where one is derived from the other, are left out.

Parameters:
  • limit (int | None) – The most conflicts to return.

  • write (bool) – Also write CONFLICTS.md and conflicts.json beside the graph, and show the conflicts in graph.html.

Returns:

The conflicts, ranked by what depends on each side downstream.

Return type:

Conflicts

Example:

for conflict in project.conflicts().conflicts:
    print(conflict.a.label, conflict.b.label)
    for difference in conflict.differences:
        print(' ', difference.kind, difference.detail, difference.path)
Project.ontology()[source]

The ontology of the data stack, derived from the graph.

Entities come from identifier-like columns with uniqueness tests, single-column primary keys and identifier fields, terms of kind entity and the grains of measures; measures are business terms with a formula, each bound column with its formula composed down to source and how many edges on the way are verified; relationships come from dbt relationships tests and from joins that match a column to an entity’s key. The rules list what breaks the ontology now. konstruct build writes the same to ontology.yml. It is derived once and kept, for entity(), metric() and relate() too, until the graph is reread, gains a node or an edge, or its meta changes; after editing a node or an edge in place, call graph.changed(). Each call returns a copy of its own.

Returns:

The entities, measures, relationships and rules.

Return type:

Ontology

Example:

ontology = project.ontology()
[(e.name, e.key, e.relations) for e in ontology.entities]
ontology.measure('customer lifetime value').columns[0].formula
ontology.violations  # [] when every rule holds
Project.entity(name)[source]

One business entity: how it is identified, what describes it, how it relates to the others and what is measured about it.

Parameters:

name (str) – The entity’s name or its term’s (customer, Customer, customers); case, spaces and underscores are not told apart, and a plural finds the singular.

Returns:

The entity with its key, the relations holding it, its confidence and evidence; its properties (the columns that describe it, with their descriptions, types and terms); the relationships it takes part in; and the metrics whose grain it is.

Raises:

konstruct.QueryError – No entity has that name.

Return type:

EntityDetail

Example:

customer = project.entity('customer')
customer.entity.confidence  # 'verified' once konstruct verify confirmed its key
[p.name for p in customer.properties]
Project.metric(name=None, limit=None)[source]

The business metrics, or those a name finds, each with its definition, formula, grain, filters, owner, status and confidence.

Metrics are the ontology’s measures, those dbt’s semantic layer, Looker and Power BI define, and proposals Konstruct reads from the code where nothing defines one: aggregate columns with a grain, numeric columns named as measures and the count of each entity. A proposal is inferred, has no status and is never approved.

Parameters:
  • name (str | None) – A metric’s name, or a column that holds it (orders.amount); all of them when left out.

  • limit (int | None) – At most this many metrics; total still counts every one found.

Returns:

defined ones first, then proposals.

Return type:

The metrics found

Raises:

konstruct.QueryError – name finds no metric.

Example:

[m.name for m in project.metric().metrics if m.proposed]
project.metric('lifetime value').metrics[0].formula
Project.relate(start, end)[source]

How two business entities connect: the relationships between them, the cardinality and confidence of the path, and the SQL joins that follow it.

Parameters:
  • start (str) – The entity to start from, named as entity() takes it.

  • end (str) – The entity to reach.

Returns:

The shortest path of relationships, the most trusted of the shortest, with whether it fans out (one-to-many: aggregate before joining) and the joins; found is false when no relationship connects them.

Raises:

konstruct.QueryError – start or end is no entity.

Return type:

Connection

Example:

connection = project.relate('customer', 'payment')
print(connection.join)  # FROM ... AS customers JOIN ... AS orders ON ...
Project.sensitivity(ref=None)[source]

Where sensitive data is, where the column lineage takes it and where a transform stops it.

A column is marked sensitive by a business term with a sensitivity (in definitions.yml or an ontology pack) that binds it, or by a warehouse tag or masking policy on it. Every column computed from a sensitive column is sensitive too, a hashed one as pseudonymised, unless every read of it is inside a function [sensitivity] sanitisers names or an aggregate over groups the SQL requires to hold at least [sensitivity] k rows. Built relations and exposures that receive it are checked against [sensitivity] holders.

Parameters:

ref (str | None) – A table, model or column to report on alone. By default, the whole graph.

Returns:

The sensitive columns with their marks, sources and paths, the reads a sanitiser stops, the exposures that show the data, the columns that only join or filter, the unmasked copies of masked columns, and the relations whose lineage is not known.

Raises:
Return type:

Sensitivity

Example:

result = project.sensitivity()
[(c.label, c.sensitivity, c.held) for c in result.unheld]
Project.erasure(ref, *, levels=3)[source]

Where one person’s data ends up: every relation holding their rows, for an erasure request.

Follows the key up and down the column lineage (copies, transformed copies and hashed copies), across every key column of its entity, through the foreign keys that refer to it, and on to the rows the person owns (an order’s payments), as the ontology derives them.

Parameters:
  • ref (str) – A key column (customers.customer_id), an entity (customer) or a relation, for the key of its entity.

  • levels (int) – How many steps of owned rows to follow.

Returns:

Each relation with the columns holding the key and how, its sensitive columns, and the action it needs (delete, rebuild, full refresh or none for a view), and the exposures that show the data.

Raises:

konstruct.QueryError – ref matches nothing, several things, or a relation with no key.

Return type:

ErasureMap

Example:

for holding in project.erasure('customer').holdings:
    print(holding.relation, holding.action, [k.column for k in holding.keys])
Project.compile_metric(metric, *, by=(), where=(), since=None, until=None, time=None, dialect=None)[source]

A metric, grouped and filtered by the business’s entities and properties, compiled to SQL without running it.

The SQL reads the metric from the relation that holds it and joins only along relationships the project shows many-to-one or one-to-one, each onto a key a test, a primary key something enforces or konstruct verify shows unique; it refuses to add up a value its lineage shows copied from another grain, and it always applies the filters the metric’s definition requires. What it rests on is what the project states and Konstruct reads: a key that repeats despite its test can still repeat rows. The result lists every fact it rests on with its confidence, and flags those that are only inferred, and a where condition on a joined property, which leaves out the rows that find no match. A query whose joins could repeat rows, that rests on a check konstruct verify failed, or that asks for what cannot be placed is refused, with the reason and the link, and has no SQL; so is a condition that is not one SQL condition, a date that is not YYYY-MM-DD and a dialect sqlglot does not know.

Parameters:
  • metric (str) – The metric’s name or synonym, or a column holding it, as metric() takes it.

  • by (Sequence[str]) – Entities (customer, for its key), properties (customer.first_name, or first_name when one entity reached has it) and time grains (month, or order_date:month) to group by.

  • where (Sequence[str]) – Conditions on properties, as SQL (customer.first_name = 'Michael').

  • since (str | None) – The first date covered, YYYY-MM-DD.

  • until (str | None) – The date after the last one covered.

  • time (str | None) – The property that dates the metric, for a grain or a range; by default the one date or time column of the metric’s relation.

  • dialect (str | None) – The SQL dialect to write, by default the project’s.

Returns:

The SQL, the relationships its joins follow, the required filters it added, the facts with their confidence, the flags and anything refused.

Raises:

konstruct.QueryError – metric finds no metric.

Return type:

Compiled

Example:

result = project.compile_metric('lifetime value', by=['customer.first_name'])
print(result.sql)
result.confidence, result.flags, result.refused

Checking SQL

result = project.check_sql(open('query.sql').read())
if not result.passed:
    for finding in result.findings:
        print(finding.severity, finding.rule, finding.message, finding.fix)
    print(result.corrected)  # the SQL with the safe fixes applied, or None

history = project.check_history(days=30, pseudonymise=True)
for metric in history.shadow:
    print(metric.term, metric.detail, metric.queries, metric.users)
Project.check_sql(sql)[source]

Check SQL against the project’s definitions, keys and relationships, without running it.

Each statement is read in the graph’s dialect. Every output column that aggregates is a measure: its formula is composed down to source columns, with the filters on the way, and compared with the business term it is named for or the column of the project that already holds it. The checks find a measure that leaves out a filter its definition requires or computes it differently, a column of a retired term, a sum, count or average over a join that repeats the rows it aggregates, a measure of a coarser grain repeated on the rows of a finer one, a join off the columns of a relationship, and a relation or column the project does not have. Each finding carries its fix, and corrected holds the SQL with every fix that changes nothing else applied.

Parameters:

sql (str) – One or more statements.

Returns:

The findings, errors first. passed is True when every statement was analysed (checked) and nothing breaks an approved definition.

Return type:

SqlCheck

Example:

result = project.check_sql('select region, sum(amount) as revenue from orders group by 1')
result.passed
[(f.rule, f.fix) for f in result.findings]
result.corrected
Project.check_history(*, days=None, limit=5000, pseudonymise=False)[source]

Run check_sql() over the query history of the project’s Snowflake, BigQuery and Databricks warehouses, and gather the queries that compute a measure differently from its definition (shadow metrics), with how many queries, runs and people each has.

Reads history metadata only, over the project’s own connections, so the project must be trusted (konstruct trust).

Parameters:
  • days (int | None) – How far back to read, by default [usage] days in konstruct.toml.

  • limit (int) – The most distinct query texts to read and check, those run most often first.

  • pseudonymise (bool) – Count people without naming them, and leave the literals out of the example queries.

Returns:

The shadow metrics, approved terms first, and how often each term is read through its own columns.

Raises:

konstruct.QueryError – days or limit is below 1, no such warehouse is configured, the project is not trusted, or the warehouse refuses the history.

Return type:

HistoryCheck

Example:

history = project.check_history(days=30, pseudonymise=True)
[(s.term, s.detail, s.queries, s.users) for s in history.shadow]

Reviewing Changes and Checking Lineage

change = project.diff(base='origin/main')
if change.breaking:
    for relation, column in change.breaking:
        print(f'{relation}.{column.column} is still read by {", ".join(column.breaks)}')
print(change.selection.command())  # dbt build --select <only what changed>

result = project.verify(seed=1729)
print(result.totals())  # {'claimed': 36, 'confirmed': 36, ...}

tests = project.mutate(models=['orders'], max_mutants=10)
print(tests.score)  # killed / (killed + survived)
for survivor in tests.models[0].survivors:
    print(survivor.mutant.description, survivor.suggestions[:1])

rows = project.why('customer_ltv.lifetime_value', {'customer_id': '3'})
print(rows.value, [c.values for c in rows.contributions])

notes = project.release_notes('v1.4')
print(notes.markdown())
effects = project.effect('v1.4', models=['customers'])
print([(c.headline(), c.effect.summary()) for c in effects.changes if c.effect])

timeline = project.timeline('v1.4')  # how the business's meaning changed, commit by commit
print([(e.commit.by, [c.headline() for c in e.changes]) for e in timeline.entries])
print(project.timeline_effect('v1.4').release_notes())

receipt = project.receipt(['customer_ltv.lifetime_value'], question='LTV of customer 3?')
print(receipt.citation)
print([(r.id, r.status) for r in project.check_receipts().receipts])

laws = project.laws(confirm=True)  # writes konstruct-out/laws.json, which the gate reads
for law in laws.laws:
    print(law.status, law.statement, law.detail)

project.lock()  # pin the upstream columns this project reads in konstruct.lock
pins = project.check_lock()
for pinned in pins.changes:
    print(pinned.column, pinned.reasons, pinned.accept)
Project.diff(base=None, head=None)[source]

What a change does to the data stack, column by column.

Compares each changed dbt model, SQL file, DAG, dbt YAML file and definitions file between base and head. Columns are compared by their canonical expressions, so formatting and renamed CTEs do not count as changes. A removed column that something downstream still reads is a breaking change.

Parameters:
  • base (str | None) – The commit to compare with. By default, the merge base with main.

  • head (str | None) – The commit to compare. By default, the working tree.

Returns:

The changes, with breaking, the downstream impact of each, and a column-aware dbt build --select in selection.

Raises:

konstruct.analysis.diff.DiffError – The project is not in a git repository, or a commit is unknown.

Return type:

Diff

Example:

change = project.diff(base='origin/main')
for relation, column in change.breaking:
    print(relation, column.column, column.breaks)
print(change.selection.command())
Project.gate(base=None, *, update=True, run_dbt=None, warehouses=True)[source]

The check a change must pass before it is proposed.

Brings the graph up to date with the working tree, compares with base as diff() does, and reports what stops the change: a removed column something still reads, an approved term whose meaning changes (in definitions.yml or through a column it names), a column the change alters that computes a business idea differently from another (as conflicts() finds), a data law in laws.json that held on all the data and that the changed SQL no longer gives (see laws()), a column the change alters that a repository in repositories pins in its konstruct.lock (see lock()), sensitive data the change moves into a model or exposure not marked to hold it, a masked column it copies in clear and a sensitivity mark it takes away (as sensitivity() finds, with the stricter of the base’s and the working tree’s [sensitivity] settings, and no mark the change itself adds counting as marking what it alters to hold the data), and the findings of the gate checks of the plugins the project enables. The plugins enabled at base run as well as those enabled now, in the update and in the checks, so a change cannot turn off a plugin that judges it. Each finding says what to do about it.

Parameters:
  • base (str | None) – The commit to compare with. By default, the merge base with main.

  • update (bool) – Bring the graph up to date first. False uses it as it is, as when it was just built.

  • run_dbt (bool | None) – For that build, as for build().

  • warehouses (bool) – For that build, as for build().

Example:

result = project.gate()
if not result.passed:
    for finding in result.findings:
        print(finding.rule, finding.message, finding.fix)
Project.verify(*, warehouse=None, seed=1729, max_rows=10000, models=None, write=True, ontology=True)[source]

Check the graph’s column lineage against the data.

For every dbt model and SQL-built table, copies its inputs (DuckDB), works inside a transaction that is rolled back (Postgres), or copies a sample of each input into an in-memory DuckDB database (Snowflake, BigQuery, Databricks, Trino), perturbs one input column at a time by shifting, nulling and shuffling it, reruns the model’s SQL and records which outputs change. A claimed edge is confirmed when its output changes; an observed influence the graph does not claim is reported as unexplained.

Parameters:
  • warehouse (str | Path | None) – A DuckDB file, a postgresql:// URL, or snowflake, bigquery, databricks or trino for the configured warehouse of that type, sampled. By default, the DuckDB or Postgres warehouse the build read.

  • seed (int) – The random seed, so a run can be repeated exactly.

  • max_rows (int) – The most rows sampled from any input.

  • models (Sequence[str] | None) – Only these models or tables; the ontology’s keys and relationships are checked only where all their tables are among them.

  • write (bool) – Write VERIFY.md and verify.json, and mark verified edges in graph.json, graph.html, explain.json and ontology.yml.

  • ontology (bool) – Also check each key and relationship of the ontology over its whole tables, on DuckDB and Postgres; False reads only what the lineage check needs.

Returns:

One result per model, with totals() across them.

Raises:

konstruct.QueryError – No warehouse was found.

Return type:

Verification

Example:

result = project.verify(seed=7)
result.totals()  # {'claimed': 36, 'confirmed': 36, 'unexplained': 0, ...}
Project.mutate(*, warehouse=None, seed=1729, max_rows=1000, max_mutants=30, operators=None, models=None, suggest=True, write=True)[source]

Mutation test the project’s dbt tests: would they fail if a model were wrong?

For each dbt model, makes small semantic mutants of its compiled SQL (an aggregate swapped, a filter or join condition dropped or negated, a join type, a comparison or a date moved, a COALESCE removed, a CASE branch dropped), runs each in verify’s read-only engines on a sample of the model’s inputs, and runs the dbt data tests and unit tests that read the model against it. A mutant is killed when a test fails on it. For each survivor, a test that would have killed it is suggested.

Parameters:
  • warehouse (str | Path | None) – As for verify().

  • seed (int) – The seed of the mutant sample, so a run can be repeated exactly.

  • max_rows (int) – The most rows sampled from any input.

  • max_mutants (int) – The most mutants run per model.

  • operators (Sequence[str] | None) – The kinds of mutation to make (by default all of konstruct.core.mutate.OPERATORS).

  • models (Sequence[str] | None) – Only these models.

  • suggest (bool) – Find, for each survivor, a test that would kill it.

  • write (bool) – Write MUTATION.md and mutation.json.

Returns:

One result per model, with totals() and score across them.

Raises:

konstruct.QueryError – No warehouse was found, or an operator is unknown.

Return type:

MutationTesting

Example:

result = project.mutate(models=['orders'], max_mutants=10)
result.score  # killed / (killed + survived)
Project.why(column, where, *, warehouse=None, limit=50, max_rows=1000000)[source]

The source rows behind one value of a column, each with its share, and the related rows a filter or a join left out, with the reason.

The value is the row of the column’s model that where picks ({'customer_id': '42'}). Runs read-only in verify’s engines: the model’s SQL is composed down to source tables, each source is copied for the key alone where that is exact, and the computing step is rerun with each row carrying the ids of its source rows.

Parameters:
  • column (str) – A column, such as customer_ltv.lifetime_value.

  • where (Mapping[str, str]) – Output columns of the model and the values that pick one row.

  • warehouse (str | Path | None) – As for verify().

  • limit (int) – The most rows listed in each table.

  • max_rows (int) – The most rows copied from any source.

Returns:

The value, its contributing rows with their shares, and the rows left out. Values of sensitive columns (see sensitivity()) are masked, and so is every share when any column the value is computed from is sensitive; masked names what was masked.

Raises:

konstruct.QueryError – The column is not built by SQL that verify can rerun, the key does not lead to the rows that compute it, or the SQL is of a shape whose rows cannot be traced.

Return type:

Why

Example:

result = project.why('customer_ltv.lifetime_value', {'customer_id': '3'})
result.value, [c.values for c in result.contributions]
Project.release_notes(since=None, *, until=None, limit=200)[source]

The changes to formulas, rows and business terms in a range of commits, newest first.

Each dbt model and SQL file a commit changed is compared with its parent by canonical expressions, as diff() compares a branch, so reformatting is not a change. Each changed column, and each relation whose joins, filters or grouping changed, is an entry with the formula or conditions before and after as SQL and in plain English, the commit’s author and date, what it affects downstream (columns, exposures and their owners, business terms), and for each business term bound to it or downstream of it the commit that approved the term after the change, if one has. Changes to the business terms themselves are entries too. Reads only git and the graph.

Parameters:
  • since (str | None) – A commit, tag or date (2026-10-01) the range starts after; by default the latest tag, or the whole history when there is none.

  • until (str | None) – A commit, tag or date the range ends at (default HEAD).

  • limit (int) – The most commits read.

Returns:

The entries, newest commit first. markdown() gives them as Markdown to post.

Raises:

konstruct.QueryError – since or until is not a commit, tag or date, the project is not in a git repository, or limit is below 1.

Return type:

ReleaseNotes

Example:

notes = project.release_notes('v1.4')
[(c.commit.author, c.headline(), c.after_english) for c in notes.changes]
print(notes.markdown())
Project.effect(since=None, *, until=None, warehouse=None, models=None, limit=200, max_rows=1000000)[source]

The release notes, with what each formula and row change does on today’s data.

For each change, the changed model’s SQL as it was before the commit and as it was after are run on the same data, the warehouse as it is now, so the difference is the definition’s alone. Runs read-only in verify’s engines: each version must be a single query that only reads, the tables it reads are copied, and nothing is written to the warehouse.

Parameters:
  • since (str | None) – As for release_notes().

  • until (str | None) – As for release_notes().

  • warehouse (str | Path | None) – As for verify().

  • models (Sequence[str] | None) – Only changes to these models or tables, by name or label.

  • limit (int) – As for release_notes().

  • max_rows (int) – The most rows copied from any table.

Returns:

the rows each version returns and, for the changed column (or for a change to the rows, each column), its sum in each and, with a key, the rows whose value changed. A version whose templating cannot be rendered as it would be run is not run, and its effect says why.

Return type:

The release notes, each formula and row change with its effect

Raises:

konstruct.QueryError – As for release_notes(), or no warehouse is found.

Example:

notes = project.effect('2026-10-01')
[(c.headline(), c.effect.summary()) for c in notes.changes if c.effect]
Project.timeline(since=None, *, until=None, paths=(), limit=50)[source]

How the business’s meaning changed, commit by commit.

Builds the world (entities, relationships, properties and metrics) at each commit from that commit’s own files and compares it with the world at the commit before: what was added, removed, renamed or changed, a formula or the rows a metric is computed over, required filters, trust, and each definition’s status, owner and approval date, with the commit’s author, co-authors and date and in plain English. A commit that changes none of that, such as one that only reformats SQL, has no entry. dbt models are rendered with each commit’s macros and today’s manifest; nothing runs dbt or connects to a warehouse. Each commit’s world is kept in the user’s cache directory, so a second call builds only new commits.

Parameters:
  • since (str | None) – A commit, tag or date (2026-10-01) the range starts after; by default the whole history.

  • until (str | None) – A commit, tag or date the range ends at (default HEAD).

  • paths (Sequence[str]) – Only commits that changed these files or directories, relative to the project.

  • limit (int) – The most commits read, the most recent.

Returns:

The entries, oldest first; release_notes() gives a sentence for each metric change and markdown() the whole as Markdown to post.

Raises:

konstruct.QueryError – since or until is not a commit, tag or date, the project is not in a git repository, or limit is below 1.

Return type:

Timeline

Example:

timeline = project.timeline('v1.4')
[(e.commit.author, [c.headline() for c in e.changes]) for e in timeline.entries]
timeline.release_notes()
Project.timeline_effect(since=None, *, until=None, paths=(), limit=50, warehouse=None, max_rows=1000000)[source]

The timeline, with what each change to a metric’s formula or rows does on today’s data.

For each such change, the change in the same commit to one of the metric’s columns or relations, or to one upstream of them, is run as it was before the commit and as it was after on the same data, as effect() runs it: read-only in verify’s engines, the tables it reads copied, nothing written to the warehouse.

Parameters:
  • since (str | None) – As for timeline().

  • until (str | None) – As for timeline().

  • paths (Sequence[str]) – As for timeline().

  • limit (int) – As for timeline().

  • warehouse (str | Path | None) – As for verify().

  • max_rows (int) – The most rows copied from any table.

Returns:

The timeline, each metric change of formula or rows with its effect, and its release notes saying what the change does on today’s data.

Raises:

konstruct.QueryError – As for timeline(), or no warehouse is found.

Return type:

Timeline

Example:

timeline = project.timeline_effect('v1.4')
timeline.release_notes()
Project.receipt(refs, *, question=None, answer=None, write=True)[source]

A receipt for an answer: what it rests on, recorded so that check_receipts() can say later whether that still holds.

For each column refs names (directly, or through a relation, an exposure or a business term), the receipt records its formula composed down to source columns, the joins, filters and grouping that decide its rows, its source columns, its type, the terms bound to it with their status and owner, and whether konstruct verify confirmed its lineage; and the build it answered from. It is appended to konstruct-out/receipts/<date>.jsonl with a digest of its contents.

Parameters:
  • refs (Sequence[str]) – What the answer rests on: columns, relations, exposures or business terms.

  • question (str | None) – The question answered, recorded as given.

  • answer (str | None) – The answer given, recorded as given.

  • write (bool) – False to make the receipt without recording it.

Returns:

The receipt, with citation, a paragraph to put under the answer.

Raises:

konstruct.QueryError – A name matches nothing or several things, or names something an answer cannot rest on, or refs is empty.

Return type:

Receipt

Example:

receipt = project.receipt(['customers.customer_lifetime_value'], question='LTV of 3?')
print(receipt.citation)
Project.receipts(ids=None)[source]

The receipts issued for this project, in the order issued, or those ids name (each an id, or the start of one, such as r-7f3a).

Raises:

konstruct.QueryError – An id names no receipt, or several, or a receipt it names does not match its digest or cannot be read.

Project.check_receipts(ids=None, *, history=True, refresh=True)[source]

Whether what each receipt’s answer rests on still holds in the current graph.

A receipt is stale when a column it rests on was removed, or its formula, row conditions, sources or type changed, or an approved term it rests on changed its meaning or status; tampered when its record does not match its digest. A rebuild that changes none of these leaves it holding. A graph older than its inputs cannot say a receipt holds, so the graph is brought up to date first, as build(update=True) does; with refresh False, a receipt it would say holds is unchecked instead.

Parameters:
  • ids (Sequence[str] | None) – Only these receipts (each an id or the start of one); by default, every one.

  • history (bool) – Read git history to say which commit changed what.

  • refresh (bool) – Update the graph first when inputs changed since it was built.

Returns:

Each receipt with its status, what changed and who changed it; passed is True when every one holds.

Raises:

konstruct.QueryError – An id names no receipt, or several.

Return type:

ReceiptCheck

Example:

result = project.check_receipts()
[(r.id, r.status, [c.reasons for c in r.changes]) for r in result.receipts]
Project.laws(*, confirm=False, warehouse=None, tolerance=None, max_rows=10000, write=True)[source]

The data laws the lineage implies: equalities between totals that the data must obey, proposed from each model’s compiled SQL and, with confirm, checked against the data.

Where a model sums or maps an input through joins that can neither repeat nor drop a row (a uniqueness test, or a GROUP BY in the same SQL, covers the key joined to), the total of its output equals the total of its input under the same filters and factors, followed across models for as long as each keeps the total: a conservation law, a count law for row counts and COUNT columns, and an additivity law for each aggregate measure. Confirming compares the totals per day, read-only, in verify’s engines, and probes each measure’s additivity on a sample of its model’s input.

Parameters:
  • confirm (bool) – Check the laws against the warehouse, read-only. Without it, nothing is run.

  • warehouse (str | Path | None) – As for verify().

  • tolerance (float | None) – The relative tolerance of a floating-point total, against the day’s sum of magnitudes (default konstruct.analysis.laws.TOLERANCE). Integer and decimal totals are compared exactly.

  • max_rows (int) – The most rows of each input the additivity probes copy.

  • write (bool) – With confirm, write laws.json, which gate() reads: a law that held on every day and that a change no longer satisfies fails the gate. Laws that were only proposed are never written, so they cannot replace the ones checked.

Returns:

Every law with its status, and the measures that have none with the reason.

Raises:

konstruct.QueryError – confirm and no warehouse was found.

Return type:

Laws

Example:

laws = project.laws(confirm=True)
print(laws.summary())
for law in laws.laws:
    if law.status == 'broken':
        print(law.statement, law.broken_at, law.failures[:1])
Project.lock(reads=None, *, update=False, write=True)[source]

Pin, in konstruct.lock, what each upstream column this project reads means: a fingerprint of its formula composed down to source columns, the row conditions of every relation on the way, its sources, its type and the business terms bound to it.

An upstream column is one the project reads and does not build itself: from another repository listed in repositories, or a table in a shared warehouse. Commit the file, and check_lock() (konstruct lock --check) fails when one of them comes to mean something else. Formatting, renamed CTEs, passthrough models and a filter moved upstream are not changes.

Parameters:
  • reads (Sequence[str] | None) – What to pin: columns, relations, exposures or terms. By default, what the lockfile names already, or every upstream column the project’s models and exposures read.

  • update (bool | Sequence[str]) – Accept every change to a pinned column (True), or the changes to these columns. Otherwise a pinned column keeps its pin, as a package lockfile keeps its versions, and only what fails no check (an approval date, a verification) is brought up to date.

  • write (bool) – Write konstruct.lock at the project’s root.

Returns:

The pins, with the columns added, accepted, removed and kept.

Raises:

konstruct.QueryError – Nothing to pin, a name matches nothing, or the lockfile cannot be read.

Return type:

LockResult

Example:

project.lock()  # every upstream column the project reads
project.lock(update=['main.ltv.total'])  # accept a reviewed change
Project.check_lock(*, history=True)[source]

Whether every column konstruct.lock pins still means what was locked.

Fails on a pinned column whose composed formula, row conditions, sources or type changed, that is no longer built, or whose approved business term was reworded. Each change says what changed (the locked and current formula side by side), who changed it (from git history), which of this project’s columns read it, and how to accept it or keep the locked meaning.

Parameters:

history (bool) – Read git history to say who made each change.

Returns:

passed, and each change with what it affects.

Raises:

konstruct.QueryError – There is no lockfile, or it cannot be read.

Return type:

LockCheck

Example:

result = project.check_lock()
if not result.passed:
    for change in result.changes:
        print(change.column, change.reasons, change.accept)
Project.export(fmt, *, select=None, columns=False, namespace=None)[source]

The graph in a format other tools read.

Parameters:
  • fmt (str) – openlineage (JSON lines of static JobEvent objects with schema and column lineage facets), mermaid, dot, csv (one row per column edge), or the ontology as ossie (an Apache Ossie semantic model), owl (OWL 2 in Turtle), semantic-view (a Snowflake CREATE SEMANTIC VIEW statement) or metric-view (Databricks metric views), or a format a plugin the project enables adds.

  • select (str | None) – Only the lineage, both directions, of this table or column (not for the ontology formats).

  • columns (bool) – For mermaid and dot, draw columns and column lineage.

  • namespace (str | None) – For openlineage, the dataset namespace (default <dialect>://konstruct); for owl, the ontology’s IRI (default urn:konstruct:<project>).

Returns:

The export as text.

Raises:
  • konstruct.outputs.export.ExportError – fmt is not one of the formats, select is given with an ontology format, the ontology has no entities or measures to export, or a plugin’s format failed. It is a ValueError.

  • konstruct.QueryError – select matches nothing or several things.

Return type:

str

Example:

Path('lineage.jsonl').write_text(project.export('openlineage', namespace='duckdb://shop'))
print(project.export('mermaid', select='orders.amount', columns=True))
Path('shop.ttl').write_text(project.export('owl', namespace='https://example.com/shop'))

Results

Walks and Items

class konstruct.Walk(direction, target, columns=<factory>, relations=<factory>, joins_and_filters=<factory>, filtered_by=<factory>, tests=<factory>, exposures=<factory>, tasks=<factory>, terms=<factory>, entities=<factory>, parents=<factory>, owners=<factory>, start=<factory>)[source]

Bases: object

Everything downstream (impact) or upstream (trace) of one node, grouped by what it is.

direction: Literal['downstream', 'upstream']

downstream for impact, upstream for trace.

target: Item

The node the walk started from.

columns: list[Item]

Columns reached, nearest first.

relations: list[Item]

Tables and models reached.

joins_and_filters: list[Item]

Relations that join, filter or group on the target (impact of a column).

filtered_by: list[Item]

Columns that join or filter the target’s relation (trace of a column).

tests: list[Item]

dbt tests on anything reached.

exposures: list[Item]

dbt exposures (dashboards) reached.

tasks: list[Item]

Orchestration tasks that read, write or run anything reached.

terms: list[Item]

Business terms bound to the target or, downstream, to anything reached.

entities: dict[str, list[str]]

The business entities (from the ontology) whose relations the walk reaches, each with those relations by label: which business objects a change touches.

parents: dict[str, str | None]

the walk as a tree, for rendering. Not part of to_dict().

Type:

For each node reached, the id of the node it was reached from

owners: dict[str, str]

For each column reached, its relation’s id, so a tree can be drawn between relations.

start: list[str]

the target and, for a relation, its columns.

Type:

The ids the walk started from

property total: int
summary()[source]
to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.Item(id, kind, label, name, relation_kind=None, path=None, data_type=None, description=None, depth=None, via=None, transform=None, verified=None, role=None, command=None, through=None, direction=None, score=None, matched=None, status=None)[source]

Bases: object

One node in a result, with how it was reached when it came from a walk.

id: str

The node id, such as model.shop.orders#amount.

kind: str

The node kind, such as column, model, table, source, task or term.

label: str

The short name the CLI prints, such as orders.amount.

name: str

The node’s own name, such as amount for that column.

relation_kind: str | None = None

For a column, the kind of relation it belongs to.

path: str | None = None

The file that defines the node (or its relation), relative to the project.

data_type: str | None = None

Column type, when known.

description: str | None = None

Description from dbt, the warehouse or definitions.yml.

depth: int | None = None

In a walk, how many steps from the starting node.

via: str | None = None

In a walk, the kind of edge that reached this node.

transform: str | None = None

direct (passed through or renamed) or transformed, for column edges.

verified: str | None = None

konstruct verify’s result for the edge that reached it, such as confirmed.

role: str | None = None

reference when the node joins, filters or groups the next relation.

command: str | None = None

For an orchestration task, the dbt command that runs the node.

through: str | None = None

The task whose SQL produced the edge, when one did.

direction: str | None = None

In a path, forward or backward along data flow.

score: float | None = None

In a search, how well the node matched.

matched: str | None = None

the node’s own words, its lineage, its formula or its structure.

Type:

In a semantic search, what a match by meaning rests on most

status: str | None = None

For a business term, its status (draft, agreed, approved or retired).

to_dict()[source]
class konstruct.PathResult(start: 'Item', end: 'Item', steps: 'list[Item]')[source]

Bases: object

start: Item

The node the path starts from.

end: Item

The node it ends at.

steps: list[Item]

Every node on the path in order, the start first; empty when there is no path.

property found: bool
to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.SearchResult(query: 'str', results: 'list[Item]')[source]

Bases: object

query: str

The words searched for.

results: list[Item]

Matching nodes, best first, each with a score.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.Listing(kinds: 'list[str]', items: 'list[Item]')[source]

Bases: object

kinds: list[str]

The node kinds listed.

items: list[Item]

Every node of those kinds.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.Description(node: 'Item', relation: 'str | None' = None, parent: 'str | None' = None, origins: 'list[str]' = <factory>, attrs: 'dict[str, Any]'=<factory>, columns: 'list[Item]' = <factory>, tests: 'list[str]' = <factory>, upstream: 'list[str]' = <factory>, downstream: 'list[str]' = <factory>, tasks: 'list[str]' = <factory>, runs: 'list[str]' = <factory>, after: 'list[str]' = <factory>, before: 'list[str]' = <factory>, expression: 'str | None' = None, conditions: 'list[str]' = <factory>, terms: 'list[str]' = <factory>, defines: 'list[str]' = <factory>, related: 'list[str]' = <factory>, neighbours: 'dict[str, list[Item]]'=<factory>)[source]

Bases: object

node: Item

The node described.

relation: str | None = None

For a relation, its warehouse name.

parent: str | None = None

For a column, its relation’s id.

origins: list[str]

Inputs the node came from, such as dbt and duckdb.

attrs: dict[str, Any]

Every other attribute, such as materialisation, schedule, operator or owner.

columns: list[Item]

A relation’s columns, in order.

tests: list[str]

Names of the dbt tests on the node or its columns.

upstream: list[str]

Immediate upstream neighbours, as label (kind).

downstream: list[str]

Immediate downstream neighbours.

tasks: list[str]

For a DAG or job, its tasks.

runs: list[str]

For a task, what it runs.

after: list[str]

For a task, the tasks it runs after.

before: list[str]

For a task, the tasks that run after it.

expression: str | None = None

For a column, its SQL over physical columns (see konstruct explain).

conditions: list[str]

For a relation, the joins, filters and grouping that shape its rows.

terms: list[str]

For a column or relation, the business terms that define it.

defines: list[str]

For a term, the columns it is bound to.

related: list[str]

For a term, its related terms.

neighbours: dict[str, list[Item]]

upstream, downstream and terms as items rather than text, for rendering. Not part of to_dict().

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.QueryResult(question: 'str', matches: 'list[Item]' = <factory>, edges: 'list[EdgeItem]' = <factory>, neighbours: 'list[Item]' = <factory>, impact: 'Walk | None' = None, lineage: 'Walk | None' = None, definitions: 'list[TermDefinition]' = <factory>)[source]

Bases: object

question: str

The question asked.

matches: list[Item]

The best-matching nodes.

edges: list[EdgeItem]

Edges around the matches.

neighbours: list[Item]

Nodes those edges reach.

impact: Walk | None = None

Everything downstream of the first match, when the question asks what breaks.

lineage: Walk | None = None

Everything upstream of the first match, when the question asks where it comes from.

definitions: list[TermDefinition]

Business terms among the matches, or defining them, with their agreed definitions.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.EdgeItem(source: 'str', target: 'str', kind: 'str', source_label: 'str', target_label: 'str', transform: 'str | None' = None, role: 'str | None' = None)[source]

Bases: object

source: str

Source node id.

target: str

Target node id.

kind: str

The edge kind, such as feeds, derived_from, runs, tests or defines.

source_label: str

Short name of the source.

target_label: str

Short name of the target.

transform: str | None = None

For derived_from, direct or transformed.

role: str | None = None

For feeds, reference when the source joins, filters or groups.

to_dict()[source]

Formulas

class konstruct.Explanation(column: 'str', label: 'str', data_type: 'str | None', description: 'str | None', expression: 'str | None', composed: 'str | None', sources: 'list[str]' = <factory>, steps: 'list[Step]' = <factory>, complete: 'bool' = True)[source]

Bases: object

column: str

The column explained.

label: str

Its short name.

data_type: str | None

Its type.

description: str | None

Its description.

expression: str | None

Its own expression, with CTEs and aliases inlined, or None for a source column.

composed: str | None

The formula in terms of source columns, or None when nothing could be substituted.

sources: list[str]

The source columns the formula reaches.

steps: list[Step]

Every column from this one upstream, nearest first.

complete: bool = True

False when some column on the way has no recorded expression (Python, unparsed SQL).

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.explain.Step(column, label, relation_kind, expression, conditions=<factory>, inputs=<factory>, verified=<factory>)[source]

Bases: object

One column on the way down: its formula and the conditions on the rows of its relation.

column: str

The column id.

label: str

Its short name.

relation_kind: str

The kind of the column’s relation.

expression: str | None

The column’s own expression with short names, or None for a source column.

conditions: list[str]

Joins, filters and grouping of the column’s relation.

inputs: list[str]

Labels of the columns the expression reads.

verified: list[str]

For each input, verify’s result for the edge, when it has been run.

to_dict()[source]
class konstruct.analysis.english.English(column, label, formula, english, sentence, composed=False, complete=True, rows=<factory>, grain=None, terms=<factory>, names=<factory>, verbatim=<factory>)[source]

Bases: object

A column’s formula, the rows it is computed over and its business terms, in English.

column: str

The column id.

label: str

Its short name.

formula: str | None

the formula composed down to sources where it could be composed, else the column’s own expression; None for a source column.

Type:

The SQL the sentence renders

english: str | None

formula in English.

sentence: str | None

The column and its formula as one sentence (customers.ltv is the sum of amount.).

composed: bool = False

Whether formula is in terms of source columns.

complete: bool = True

False when some column on the way has no recorded expression, so the formula stops short.

rows: list[Rows]

The joins, filters and grouping of each relation on the way, nearest first.

grain: str | None = None

a bound term’s grain, or the key of the entity the relation holds.

Type:

What one row of the column’s relation is, and the evidence

terms: list[Term]

The business terms bound to the column, approved first.

names: list[Name]

Each column’s wording in the sentences and the column it names.

verbatim: list[str]

The SQL of each part the grammar has no wording for, quoted between backticks instead.

property exact: bool

Whether every part is worded, with nothing quoted as SQL.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.english.Rows(relation, conditions=<factory>, english=<factory>)[source]

Bases: object

The joins, filters and grouping of one relation on the way, in SQL and in English.

relation: str

The relation, by label.

conditions: list[str]

Each condition as explain shows it (WHERE: stg_orders.status <> 'returned').

english: list[str]

Each condition in English, in the same order.

to_dict()[source]
class konstruct.analysis.english.Term(name, status=None, kind=None, owner=None, definition=None, grain=None, unit=None, currency=None, filters=<factory>, english=<factory>)[source]

Bases: object

A business term bound to the column, with what its definition adds to the formula.

name: str

The term’s name.

status: str | None = None

draft, agreed, approved or retired.

kind: str | None = None

entity, measure or term, where the definition says.

owner: str | None = None

Who owns the definition.

definition: str | None = None

The agreed wording.

grain: str | None = None

The term’s grain as written (one row per customer).

unit: str | None = None

Its unit, such as currency.

currency: str | None = None

Its currency, for a monetary measure.

filters: list[str]

The filters any answer must apply, as SQL.

english: list[str]

The same filters in English.

to_dict()[source]
class konstruct.analysis.english.Name(wording, column)[source]

Bases: object

How a sentence names one column.

wording: str

The name in the sentences, in square brackets ([customer id]).

column: str

The column it names, as relation.column.

to_dict()[source]

History

class konstruct.analysis.history.History(column: 'str', label: 'str', formula: 'str | None', path: 'str | None', formulas: 'list[FormulaChange]' = <factory>, terms: 'list[TermChange]' = <factory>, read_formulas: 'bool' = False, unknown: 'list[Commit]' = <factory>, window: 'Commit | None' = None, notes: 'list[str]' = <factory>)[source]

Bases: object

column: str
label: str
formula: str | None

The formula now, as the graph has it.

path: str | None

The file that defines the column, relative to the project.

formulas: list[FormulaChange]

In history order, oldest first.

terms: list[TermChange]

In history order, oldest first.

read_formulas: bool = False

Whether the formula’s history was read (it is defined by SQL in a git repository).

unknown: list[Commit]

Commits at which the formula could not be worked out.

window: Commit | None = None

When the formula’s history was cut short (a shallow clone or the limit), the oldest commit read: what came before it is not known.

notes: list[str]
property last_approval: TermChange | None
property approved_since_last_change: bool | None

Whether a bound term was approved after every formula change, by ancestry.

True when one approval follows every change read (and, if the history was cut short, the oldest commit read); False when a term was approved at or before a change that no later approval follows; None when the formula’s history was not read, nothing settles it, or a commit after the last approval could not be worked out.

summary()[source]
findings()[source]

The summary without the column’s name: how often the formula changed and when a bound term was last approved.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.history.FormulaChange(commit: 'Commit', path: 'str', before: 'str | None', after: 'str | None')[source]

Bases: object

commit: Commit
path: str

The file at that commit, relative to its repository.

before: str | None

The formula before this commit, or None where the column was added.

after: str | None

The formula after it, or None where the column was removed.

to_dict()[source]
class konstruct.analysis.history.TermChange(commit: 'Commit', term: 'str', path: 'str', changes: 'dict[str, tuple[Any, Any]]', status: 'str | None' = None)[source]

Bases: object

commit: Commit
term: str
path: str

The definitions file at that commit, relative to its repository.

changes: dict[str, tuple[Any, Any]]

Each changed field (status, approved, owner, definition) with its value before and after.

status: str | None = None

The term’s status after this commit (None where it is gone).

property approved: bool

made an existing term approved, or, while it stayed approved, set a new approval date. A term first seen already approved, such as a renamed one, is not approved by this commit.

Type:

Whether this commit approved the term

property leaves_approved: bool

Whether the term is approved after this commit.

to_dict()[source]
class konstruct.analysis.history.Commit(sha: 'str', author: 'str', date: 'str', subject: 'str', order: 'int' = 0, repository: 'Path | None' = None, authored: 'str' = '', co_authors: 'list[str]' = <factory>)[source]

Bases: object

sha: str

Empty for uncommitted changes.

author: str
date: str

When the commit was made (its committer date), in UTC, ISO 8601.

subject: str
order: int = 0

Its place in a topological order of history, oldest first, for display.

repository: Path | None = None

The repository it is in, to ask git about ancestry.

authored: str = ''

When its author wrote it (its author date), in UTC, ISO 8601; a rebase keeps it.

co_authors: list[str]

The people its Co-authored-by trailers name.

follows(other)[source]

Whether this commit comes after other in history: other is one of its ancestors, or this is the working tree. Commits on parallel branches follow neither way.

at_or_after(other)[source]
property short: str
property by: str

its author, with any co-authors (Sam with Ada).

Type:

Who wrote it

to_dict()[source]

Business Terms

class konstruct.Definitions(query, terms=<factory>, column=None)[source]

Bases: object

The answer to konstruct define: the terms a name or column refers to, approved first.

query: str

What was asked for.

terms: list[TermDefinition]

Matching terms, approved first and retired last.

column: Item | None = None

When the query named a column, that column.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.TermDefinition(name, definition, status, owner=None, approved=None, synonyms=<factory>, grain=None, columns=<factory>, filters=<factory>, unit=None, currency=None, related=<factory>, broader=<factory>, narrower=<factory>, exceptions=None, path=None, line=None, iri=None, id='')[source]

Bases: object

One business term: what it means, who owns it and the columns that hold it.

name: str

The term.

definition: str | None

The agreed wording.

status: str

draft, agreed, approved or retired.

owner: dict[str, Any] | None = None

{"name": ..., "role": ...}, when set.

approved: str | None = None

The approval date, YYYY-MM-DD.

synonyms: list[str]

Other names for the term.

grain: str | None = None

What one row means, such as one row per customer.

columns: list[Item]

The columns (or relations) that hold the term.

filters: list[str]

Filters any answer about the term must apply, as SQL.

unit: str | None = None

Unit of measure, such as currency.

currency: str | None = None

ISO 4217 currency code.

related: list[str]

Names of related terms.

broader: list[str]

The wider concepts this term is a kind of.

narrower: list[str]

The terms that are kinds of this one.

exceptions: str | None = None

Known exceptions to the definition.

path: str | None = None

The definitions file.

line: int | None = None

The line the term starts on.

iri: str | None = None

For a term from a SKOS or OWL vocabulary, the concept’s IRI.

id: str = ''

The node id, term.<slug>.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

Ontology

class konstruct.semantics.ontology.Ontology(entities=<factory>, measures=<factory>, relationships=<factory>, rules=<factory>, concepts=<factory>, unique_ids=<factory>)[source]

Bases: object

Entities, measures, relationships and rules, derived from one graph.

entities: list[Entity]

The business objects.

measures: list[Measure]

The business terms with a formula.

relationships: list[Relationship]

How the entities refer to each other.

rules: list[Rule]

The rules and their violations.

concepts: list[Concept]

each term with a broader or narrower term.

Type:

The concept hierarchy

unique_ids: frozenset[str]

The graph ids of the columns a test, a primary key or an identifier field shows unique, for tools; not written to ontology.yml.

property violations: list[str]

Every rule violation, each prefixed with its rule.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

to_dict()[source]
to_yaml()[source]

The ontology as the YAML written to ontology.yml.

write(path)[source]

Write the ontology to path as YAML and return path.

entity(name)[source]

The entity called name, or named by the term name; case, spaces, hyphens and underscores are not told apart, and a plural finds the singular.

measure(name)[source]

The measure called name, matched as entity() matches.

entity_of(graph, relation)[source]

The entity whose instances relation holds one row each of, as relationships read it (see holder()).

class konstruct.semantics.ontology.Entity(name, key, keys=<factory>, relations=<factory>, evidence=<factory>, term=None, description=None, confidence='inferred', check=None, key_ids=<factory>, unique_ids=<factory>)[source]

Bases: object

A business object, such as a customer or an order.

name: str

The entity’s name, singular and lower case (customer).

key: str

The name of the column that identifies one instance (customer_id); when relations name it differently, the most common name.

keys: list[str]

The key column of each relation, by label (customers.customer_id).

relations: list[str]

The relations that hold one row per instance, by label.

evidence: list[str]

the tests, primary keys, identifier fields, terms and grains that say so.

Type:

Why it is an entity

term: str | None = None

The business term that names it, if one does.

description: str | None = None

The term’s definition, if one names it.

confidence: str = 'inferred'

verified when konstruct verify found every one of its key columns unique and never null; declared when a test, constraint, primary key, identifier field, Ossie model or term of kind entity states every one of them; inferred when only names, a grain or an ontology pack show one of them.

Type:

As trusted as its least trusted key

check: str | None = None

what konstruct verify found of its key columns in the warehouse, with the detail in evidence; left out when it checked none.

Type:

confirmed or failed

key_ids: list[str]

The graph ids of the key columns, for tools; not written to ontology.yml.

unique_ids: list[str]

The graph ids of the key columns known to be unique (by a unique test, a primary key, an identifier field or an Ossie primary key), for tools; not written to ontology.yml. A key named only by a term is not among them.

class konstruct.semantics.ontology.Measure(name, definition=None, status='draft', owner=None, approved=None, entity=None, grain=None, unit=None, currency=None, filters=<factory>, columns=<factory>, path=None, expression=None, confidence='inferred', evidence=<factory>)[source]

Bases: object

A business term with a formula: what is measured, how, and on whose authority.

name: str

The term’s name.

definition: str | None = None

The agreed wording.

status: str = 'draft'

draft, agreed, approved or retired.

owner: str | None = None

Who owns the definition.

approved: str | None = None

When it was approved.

entity: str | None = None

The entity its grain names (one row per customer gives customer).

grain: str | None = None

The term’s grain as written.

unit: str | None = None

Its unit, such as currency.

currency: str | None = None

Its currency, for a monetary measure.

filters: list[str]

Filters any answer must apply.

columns: list[MeasureColumn]

The columns that hold it, each with its formula.

path: str | None = None

The file that defines it, with the line.

expression: str | None = None

For a measure defined as a metric in an Apache Ossie model, the metric’s own expression; its columns are then the columns the expression reads.

confidence: str = 'inferred'

verified when konstruct verify confirmed every lineage edge from its columns to source; declared when definitions.yml, a vocabulary or an Ossie model defines it; inferred when an ontology pack bound it to the project’s tables by their names.

evidence: list[str]

Where it comes from and what checked it.

class konstruct.semantics.ontology.MeasureColumn(column, formula, sources=<factory>, complete=True, verified=0, edges=0, usage=None, id=None)[source]

Bases: object

One column that holds a measure, with its formula and how far to trust it.

column: str

The column, by label (customers.customer_lifetime_value).

formula: str | None

The column’s formula composed down to source columns, if it could be composed.

sources: list[str]

The source columns the formula reaches.

complete: bool = True

False when some column on the way had no expression, so the formula stops short of source.

verified: int = 0

Lineage edges on the way to source that konstruct verify confirmed.

edges: int = 0

Lineage edges on the way to source.

usage: int | None = None

The column’s usage score from query history (0 to 100), when usage is on.

id: str | None = None

The column’s graph id, for tools; not written to ontology.yml.

class konstruct.semantics.ontology.Relationship(source, target, via, cardinality=None, tested=False, evidence=<factory>, confidence='inferred', check=None, columns=None)[source]

Bases: object

One entity referring to another, such as an order belonging to a customer.

source: str

The entity that refers (order).

target: str

The entity referred to (customer).

via: str

The columns that join them (orders.customer_id = customers.customer_id).

cardinality: str | None = None

many-to-one, or one-to-one when both columns are unique; left out when the target column is not known to be unique, so the relationship is claimed without its cardinality.

tested: bool = False

Whether a dbt relationships test vouches for it, rather than only a join.

evidence: list[str]

The tests and joins that show it, and what konstruct verify found.

confidence: str = 'inferred'

verified when konstruct verify found every value of the referring column in the key it refers to, and that key unique; declared when a dbt relationships test or an Ossie model states it; inferred when only a join or an ontology pack shows it.

check: str | None = None

what konstruct verify found in the warehouse, with the detail in evidence; left out when it did not check it.

Type:

confirmed or failed

columns: tuple[str, str] | None = None

The graph ids of the joining columns in via, for tools; not written to ontology.yml.

class konstruct.semantics.world.EntityDetail(entity, properties=<factory>, relationships=<factory>, metrics=<factory>)[source]

Bases: object

One entity of the business: how it is identified, what describes it, how it relates to the others and what is measured about it.

entity: Entity

The entity, with its key, relations, confidence and evidence.

properties: list[Property]

The columns that describe it, across the relations holding it, keys left out.

relationships: list[Relationship]

The relationships it takes part in, either side.

metrics: list[Metric]

The metrics whose grain is this entity, proposals included.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

to_dict()[source]
class konstruct.semantics.world.Property(name, columns=<factory>, description=None, data_type=None, terms=<factory>, refers_to=None)[source]

Bases: object

A column that describes an entity, gathered across the relations that hold it.

name: str

The column’s name, lower case (first_name).

columns: list[str]

The columns of that name in the entity’s relations, by label.

description: str | None = None

The first description among those columns.

data_type: str | None = None

The type of the first column with one.

terms: list[str]

The business terms bound to any of the columns.

refers_to: str | None = None

The entity it refers to, when it is the referring column of a relationship.

class konstruct.semantics.world.Metrics(query, metrics=<factory>, total=0)[source]

Bases: object

The metrics a name finds, or every metric.

query: str | None

The name asked for, or None for every metric.

metrics: list[Metric]

Defined metrics first, then proposals, each in name order; at most the limit asked for.

total: int = 0

How many metrics were found, before any limit.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

to_dict()[source]
class konstruct.semantics.world.Metric(name, source, confidence, proposed=False, synonyms=<factory>, definition=None, status=None, owner=None, approved=None, entity=None, grain=None, grain_confidence=None, unit=None, currency=None, filters=<factory>, formula=None, aggregation=None, additive=None, columns=<factory>, evidence=<factory>, path=None, exposure=None)[source]

Bases: object

A business measure: what it measures, how it is computed, at what grain, on whose authority and how far to trust it.

name: str

Its name (Customer lifetime value, or for a proposal one made from its column).

source: str

definitions, vocabulary, ossie, ontology pack, dbt semantic layer, looker, power bi or the name of the plugin input that read it when something defines it; aggregate, measure column or entity count for a proposal Konstruct read from the code.

Type:

Where it comes from

confidence: str

verified, declared or inferred; a proposal is always inferred.

proposed: bool = False

it has no status and is not approved.

Type:

True when nothing defines it and Konstruct proposes it from the code

synonyms: list[str]

Other names it goes by.

definition: str | None = None

The agreed wording, or the description where it is defined.

status: str | None = None

draft, agreed, approved or retired for a business term; None for anything else, a proposal included.

owner: str | None = None

Who owns the definition.

approved: str | None = None

When it was approved.

entity: str | None = None

The entity it measures, from its grain.

grain: str | None = None

Its grain (one row per customer).

grain_confidence: str | None = None

declared when its definition states the grain, inferred when Konstruct read the grain and the entity from the relation that holds it; None without a grain.

unit: str | None = None

Its unit, such as currency.

currency: str | None = None

Its currency, for a monetary measure.

filters: list[str]

Filters any answer must apply, as its definition writes them.

formula: str | None = None

Its formula as written where it is defined, or composed down to source columns from SQL; left out when its columns compute it in different ways or the tool’s aggregation has no exact SQL.

aggregation: str | None = None

sum, count or average when its formula aggregates, as far as it can be read.

additive: bool | None = None

true for a sum or a count, false for an average, a distinct count or a measure its definition makes non-additive; None when it cannot be said, as for a price or a balance.

Type:

Whether its values add up across groups

columns: list[MeasureColumn]

The columns that hold it (or, for a metric defined over a model, that it reads), each with its formula and how many of its lineage edges konstruct verify confirmed.

evidence: list[str]

Where it comes from and what checked it.

path: str | None = None

The file that defines it, with the line where known.

exposure: str | None = None

For a measure of a BI view or semantic model (Looker, Power BI or a plugin’s input), the graph id of the exposure that defines it: one file can hold several views, each with a measure of the same name.

to_dict()[source]
class konstruct.semantics.world.Connection(start, end, found, hops=<factory>, cardinality=None, confidence=None, check=None, join=None)[source]

Bases: object

How two entities connect: the relationships between them, the cardinality and confidence of the whole path, and the joins that follow it.

start: str

The entity it starts from.

end: str

The entity it ends at.

found: bool

Whether the relationships connect them.

hops: list[Hop]

fewest failed checks first, then fewest relationships, then the most trusted.

Type:

The relationships on the way

cardinality: str | None = None

many-to-one when each start row has at most one end, one-to-many when it can have several (aggregate end before joining it to start), many-to-many, one-to-one, or None when a relationship on the way has no known cardinality.

Type:

Of the whole path

confidence: str | None = None

the path is as trustworthy as that.

Type:

The weakest confidence of a relationship on the way

check: str | None = None

failed when konstruct verify found the data contradicting a relationship on the way, so the joins can lose or repeat rows; confirmed when it confirmed every one.

join: str | None = None

the warehouse’s relation names, each with an alias, joined on the columns of each relationship or, between two relations holding one entity, on that entity’s keys, each shown unique where it is joined. None when that cannot be written: a relation with no warehouse name, or a key not shown unique.

Type:

SQL joins that follow the path, from a relation that holds start

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

to_dict()[source]
class konstruct.semantics.world.Hop(start, end, via, cardinality, confidence, check=None, evidence=<factory>, same_row=False)[source]

Bases: object

One relationship on the way from one entity to another, in the direction it is walked.

start: str

The entity walked from.

end: str

The entity walked to.

via: str

The columns that join them.

cardinality: str | None

many-to-one (each row of start has one end), one-to-many, one-to-one, or None when it is not known, as when konstruct verify found the key it refers to repeated.

Type:

In the direction walked

confidence: str

The relationship’s confidence.

check: str | None = None

what konstruct verify found of the relationship, with the detail in evidence; left out when it did not check it.

Type:

confirmed or failed

evidence: list[str]

The tests, joins and checks that show it.

same_row: bool = False

True for a step along one row of a relation that holds both entities, which joins nothing and is no relationship.

class konstruct.analysis.compile.Compiled(query, metric=None, sql=None, dialect=None, relation=None, grain=None, aggregation=None, path=<factory>, filters=<factory>, facts=<factory>, confidence=None, flags=<factory>, refused=<factory>)[source]

Bases: object

A metric query compiled to SQL, with the joins, filters and facts it rests on, or what was refused and why.

query: OntologyQuery

What was asked.

metric: str | None = None

The metric found.

sql: str | None = None

The SQL, or None when anything was refused.

dialect: str | None = None

The dialect the SQL is written in.

relation: str | None = None

The relation whose rows the metric is read from.

grain: str | None = None

The entity that relation holds one row per, when it holds one.

aggregation: str | None = None

none (the query keeps one row per row of its relation, so the column is read as it is), sum (its values added up) or formula (its own aggregate over the relation’s rows).

Type:

How the metric is read

path: list[Hop]

The relationships the joins follow, each in the direction walked, every one many-to-one or one-to-one as the project shows it.

filters: list[RequiredFilter]

The filters the metric’s definition requires, every one in the SQL.

facts: list[Fact]

What the SQL rests on, each with its confidence.

confidence: str | None = None

the result is as trustworthy as that.

Type:

The weakest confidence of the facts

flags: list[str]

The facts that are only inferred, and anything else to say when the result is used.

refused: list[Refusal]

What was refused, with why; the SQL is None when anything was.

property compiled: bool

nothing was refused.

Type:

Whether SQL was written

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

to_dict()[source]
class konstruct.analysis.compile.OntologyQuery(metric, by=<factory>, where=<factory>, since=None, until=None, time=None)[source]

Bases: object

A question about a metric in the business’s terms: the metric, what to group it by, the conditions on the rows it adds up and the time it covers.

metric: str

The metric’s name or synonym (lifetime value), or a column that holds it, as konstruct metric finds it.

by: list[str]

an entity (customer, its key), an entity’s property (customer.first_name, or first_name when one entity has it), a time grain of the metric’s time property (month) or of a named one (order_date:month). The grains are day, week, month, quarter and year.

Type:

What to group by

where: list[str]

Conditions on properties, as SQL (customer.first_name = 'Michael'), each named as in by; all of them hold.

since: str | None = None

The first date the metric covers (2024-01-01), on its time property, whatever by groups by.

until: str | None = None

the range is since up to, not including, until.

Type:

The date after the last one it covers

time: str | None = None

by default the one date or time column of the relation that holds the metric.

Type:

The metric’s time property, for a bare time grain in by and for the range

to_dict()[source]
class konstruct.analysis.compile.Fact(kind, name, confidence, check=None, evidence=<factory>)[source]

Bases: object

One thing the compiled SQL rests on, with how far to trust it.

kind: str

metric, grain, column, relationship, key or filter.

name: str

the metric’s name, a relation’s grain, a column, a relationship’s columns, a filter.

Type:

What it is

confidence: str

verified, declared or inferred, as the ontology gives it.

check: str | None = None

confirmed when konstruct verify confirmed it against the data.

evidence: list[str]

What shows it.

class konstruct.analysis.compile.RequiredFilter(filter, applied, source, by='query')[source]

Bases: object

A filter the metric’s definition requires, and how the SQL applies it.

filter: str

As the definition writes it.

applied: str | None

As the SQL’s WHERE clause applies it, on the query’s own relations; None when the SQL of the column that holds the metric already applies it, on a query the column is computed through, to the rows it aggregates: Konstruct’s reading of that SQL, so inferred.

source: str

the metric and its file.

Type:

Where it is required

by: str = 'query'

query when the SQL’s WHERE clause applies it, else the relations whose SQL applies it on the way to the metric’s column.

class konstruct.analysis.compile.Refusal(item, reason, link=None)[source]

Bases: object

Something the compiler would not write, and why.

item: str

the metric, a by item, a condition, a filter or the time range.

Type:

What was asked for

reason: str

Why it was refused.

The relationship (its columns) whose join could repeat rows, when that is the reason.

class konstruct.semantics.ontology.Rule(name, description, violations=<factory>)[source]

Bases: object

A rule the build holds the ontology to, with what breaks it now.

name: str

A short identifier (one-formula-per-measure).

description: str

What the rule requires.

violations: list[str]

What breaks it in this build; each is also a build warning.

Sensitive Data

class konstruct.analysis.sensitivity.Sensitivity(columns=<factory>, sanitised=<factory>, exposures=<factory>, references=<factory>, unmasked=<factory>, uncertain=<factory>, settings=<factory>, subject=None, seconds=0.0)[source]

Bases: object

Where sensitive data is, where the lineage takes it and where a transform stops it.

columns: list[SensitiveColumn]

Every sensitive column, by relation.

sanitised: list[Sanitised]

Reads of sensitive columns that a transform stops.

exposures: list[ExposureFlow]

Exposures that show sensitive data.

references: list[Reference]

Sensitive columns that only shape another relation’s rows.

unmasked: list[UnmaskedCopy]

Tables that copy a masked column in clear.

uncertain: list[Uncertain]

Relations that may hold sensitive data through lineage Konstruct does not know.

settings: Settings

The settings used.

subject: str | None = None

The table or column asked about, when the result is about one.

seconds: float = 0.0

Time taken.

property marked: list[SensitiveColumn]

The columns marked sensitive themselves.

property unheld: list[SensitiveColumn]

Sensitive columns of built relations that nothing marks to hold the data.

property unheld_exposures: list[ExposureFlow]

Exposures showing sensitive data that nothing marks to hold it.

column(ref)[source]

The sensitive column with this id or short name, if it is one.

summary()[source]

One sentence on what was found.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.sensitivity.SensitiveColumn(column, label, relation, relation_id, sensitivity, marks=<factory>, sources=<factory>, path=<factory>, transform='marked', built=False, held=None, masking_policy=None)[source]

Bases: object

A column holding sensitive data: marked itself, or computed from one that is.

column: str

The column id.

label: str

Its short name.

relation: str

Its relation’s short name.

relation_id: str

Its relation’s id.

sensitivity: list[str]

The kinds of sensitive data it holds.

marks: list[Mark]

What marks it, when it is marked itself.

sources: list[str]

The marked columns it is computed from, by short name; empty for a marked column.

path: list[str]

The columns from a marked column to this one, the marked one first.

transform: str = 'marked'

marked; direct when every step on the path passes the value unchanged; pseudonymised when every path to it hashes the value; else transformed.

built: bool = False

Whether something in the graph builds its relation (a model, a SQL file, a task).

held: str | None = None

What marks it, or its relation, to hold sensitive data; None when nothing does.

masking_policy: str | None = None

The warehouse’s masking policy on it, if it has one.

to_dict()[source]
class konstruct.analysis.sensitivity.Mark(sensitivity, by)[source]

Bases: object

What marks a column sensitive.

sensitivity: str

The kind of data, such as personal, or the warehouse tag’s name or value.

by: str

a business term, a warehouse tag or a masking policy.

Type:

What says so

to_dict()[source]
class konstruct.analysis.sensitivity.Sanitised(column, source, by)[source]

Bases: object

A read of a sensitive column that a transform stops.

column: str

The column computed, by short name.

source: str

The sensitive column it reads, by short name.

by: str

a sanitiser (TOKENISE), or SUM over groups of at least 10 distinct keys.

Type:

What stops it

to_dict()[source]
class konstruct.analysis.sensitivity.ExposureFlow(exposure, exposure_id, sensitivity, columns=<factory>, column_ids=<factory>, held=None, path=None)[source]

Bases: object

An exposure (a dashboard, a BI view) that shows sensitive data.

exposure: str

The exposure’s short name.

exposure_id: str

Its id.

sensitivity: list[str]

The kinds of sensitive data it shows.

columns: list[str]

The sensitive columns it reads, by short name.

column_ids: list[str]

Their ids, for tools.

held: str | None = None

What marks it to hold sensitive data; None when nothing does.

path: str | None = None

The file that defines it, for the gate.

to_dict()[source]
class konstruct.analysis.sensitivity.Reference(relation, column, sensitivity)[source]

Bases: object

A sensitive column that only joins, filters or groups a relation’s rows.

relation: str

The relation whose rows it shapes, by short name.

column: str

The sensitive column, by short name.

sensitivity: list[str]

Its kinds of sensitive data.

to_dict()[source]
class konstruct.analysis.sensitivity.UnmaskedCopy(column, column_id, source, policy)[source]

Bases: object

A column of a table that holds, in clear, the values of a column the warehouse masks.

column: str

The copy, by short name.

column_id: str

Its id.

source: str

The masked column it comes from, by short name.

policy: str

The source’s masking policy.

to_dict()[source]
class konstruct.analysis.sensitivity.Uncertain(relation, source, sensitivity, relation_id='')[source]

Bases: object

A relation fed by one holding sensitive data whose column lineage Konstruct does not know, so it may hold that data.

relation: str

The relation fed, by short name.

source: str

The relation with sensitive columns that feeds it.

sensitivity: list[str]

The kinds of data the source holds.

relation_id: str = ''

The id of the relation fed, for the gate.

to_dict()[source]
class konstruct.analysis.sensitivity.Settings(holders=(), sanitisers=(), tags=(), k=10)[source]

Bases: object

[sensitivity] in konstruct.toml.

holders: tuple[str, ...] = ()

Relations and exposures marked to hold sensitive data, by name, label, warehouse name or id, each a glob pattern (stg_*).

sanitisers: tuple[str, ...] = ()

Functions that stop sensitivity, such as a tokenisation function.

tags: tuple[str, ...] = ()

Warehouse tags that mark a column sensitive, besides the built-in ones.

k: int = 10

The smallest group an aggregate must be taken over to stop sensitivity.

classmethod read(table)[source]

The settings in a [sensitivity] table (None for none).

Raises:

konstruct.engine.config.ConfigError – The table is not valid.

strictest(other)[source]

These settings and other together, each as strict as either: a holder or a sanitiser only when both have it, a tag when either does, and the larger k.

to_dict()[source]
class konstruct.analysis.erasure.ErasureMap(subject, key, entity=None, holdings=<factory>, exposures=<factory>, seconds=0.0)[source]

Bases: object

Every relation holding one person’s rows, by the key that finds them.

subject: str

The key, entity or relation asked about.

key: str

The key column the map starts from, by short name.

entity: str | None = None

The entity the key identifies, when the ontology knows it.

holdings: list[Holding]

The relations holding the person’s rows, those keyed by the person first.

exposures: list[str]

The exposures that read any of those relations.

seconds: float = 0.0

Time taken.

actions()[source]

How many relations take each action.

summary()[source]

One sentence on what holds the person’s data.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.erasure.Holding(relation, relation_id, kind, action, keys=<factory>, personal=<factory>, level=0, path=<factory>)[source]

Bases: object

A relation that holds rows of the person.

relation: str

The relation, by short name.

relation_id: str

Its id.

kind: str

source, model, seed, snapshot or table.

Type:

Its node kind

action: str

delete, rebuild, full refresh or none (see the module documentation).

keys: list[ErasureKey]

The columns holding the key, the first found first.

personal: list[str]

Its sensitive columns, by name.

level: int = 0

0 for rows keyed by the person, 1 for rows of something the person owns, and so on.

path: list[str]

The columns from the key asked about to this relation’s first key column.

to_dict()[source]
class konstruct.analysis.erasure.ErasureKey(column, how, via=None, entity=None)[source]

Bases: object

A column of a relation that holds the person’s key, or the key of rows the person owns.

column: str

The column, by short name.

how: str

key (the entity’s key), copy, transformed, hashed or foreign key.

via: str | None = None

The column it was reached from, by short name; None for the key asked about.

entity: str | None = None

the person’s, or one the person owns.

Type:

The entity whose instance the value identifies

to_dict()[source]

Conflicts

class konstruct.analysis.conflicts.Conflicts(conflicts: 'list[Conflict]' = <factory>, candidates: 'int' = 0, groups: 'int' = 0)[source]

Bases: object

conflicts: list[Conflict]

Every conflict, most consequential first.

candidates: int = 0

Computed columns considered.

groups: int = 0

Groups of columns that appear to be one idea.

summary()[source]
to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

markdown()[source]
class konstruct.analysis.conflicts.Conflict(idea: 'str', reason: 'str', a: 'Side', b: 'Side', differences: 'list[Difference]' = <factory>)[source]

Bases: object

idea: str

What the columns appear to measure, as a term name or the shared words of their names.

reason: str

Why the two columns were compared.

a: Side

One side.

b: Side

The other side.

differences: list[Difference]

What differs between them.

property score: int
to_dict()[source]
lines()[source]
class konstruct.analysis.conflicts.Side(column: 'str', label: 'str', relation_kind: 'str', path: 'str | None', expression: 'str | None', composed: 'str | None', sources: 'list[str]' = <factory>, filters: 'list[tuple[str, str, str | None]]'=<factory>, joins: 'list[tuple[str, str, str | None]]'=<factory>, reach: 'dict[str, int]'=<factory>, terms: 'list[str]' = <factory>, carried_into: 'list[str]' = <factory>)[source]

Bases: object

column: str

The column id.

label: str

Its short name.

relation_kind: str

The kind of its relation.

path: str | None

The file that builds it.

expression: str | None

Its own expression.

composed: str | None

Its formula in terms of source columns.

sources: list[str]

The source columns it reads.

filters: list[tuple[str, str, str | None]]

(relation, condition, file) for each filter on the route back to source.

joins: list[tuple[str, str, str | None]]

(relation, condition, file) for each join on its route back to source.

reach: dict[str, int]

Counts of what depends on it downstream (exposures, tasks, columns and approved terms).

terms: list[str]

Business terms bound to it or to anything downstream.

carried_into: list[str]

Columns of the same idea that are derived from this one, and so inherit its definition.

property weight: int
to_dict()[source]
class konstruct.analysis.conflicts.Difference(kind: 'str', detail: 'str', side: 'str | None' = None, path: 'str | None' = None, witness: 'Witness | None' = None)[source]

Bases: object

kind: str

formula, source, filter, join or aggregation.

detail: str

What differs, such as a condition, a source column or two formulas.

side: str | None = None

Which column has it (a or b) when only one does; None when both differ.

path: str | None = None

The file the filter or join is in.

witness: Witness | None = None

For a formula or aggregation not shown to be the same, values at which the two give different results (a proof only when it sets source columns alone; see Witness), or the function they apply differently.

to_dict()[source]

Changes

class konstruct.analysis.diff.Diff(base: 'str', head: 'str', files: 'dict[str, str]', base_commit: 'str | None' = None, head_commit: 'str | None' = None, selection: 'Selection' = <factory>, relations: 'list[RelationChange]' = <factory>, dags: 'list[DagChange]' = <factory>, other: 'list[str]' = <factory>, yaml: 'list[YamlChange]' = <factory>, definitions: 'list[DefinitionsChange]' = <factory>, paths: 'list[str]' = <factory>)[source]

Bases: object

base: str

The commit compared with.

head: str

The commit compared, or working tree.

files: dict[str, str]

Every changed file.

base_commit: str | None = None

The commit a date given as base stands for.

head_commit: str | None = None

The commit a date given as head stands for.

selection: Selection

The column-aware dbt selection for the change.

relations: list[RelationChange]

Changes to dbt models and SQL.

dags: list[DagChange]

Changes to DAGs.

other: list[str]

Changed files that were not analysed.

yaml: list[YamlChange]

Changes to dbt YAML.

definitions: list[DefinitionsChange]

Changes to definitions.yml.

paths: list[str]

Every changed file inside the project, relative to it, generated ones included.

property shown_base: str

base, with the commit a date stands for (2026-10-01 (abc1234)).

property shown_head: str

head, with the commit a date stands for.

property source_files: list[str]

Changed files, leaving out generated ones (target/, logs/, konstruct-out/, packages).

property breaking: list[tuple[str, ColumnChange]]
counts()[source]
summary()[source]
to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

markdown()[source]
class konstruct.analysis.diff.RelationChange(path: 'str', label: 'str', kind: 'str', status: 'str', node_id: 'str | None' = None, columns: 'list[ColumnChange]' = <factory>, conditions_added: 'list[str]' = <factory>, conditions_removed: 'list[str]' = <factory>, rows_impact: 'dict[str, Any] | None'=None, note: 'str | None' = None)[source]

Bases: object

path: str

The changed file.

label: str

The relation it builds.

kind: str

model, table and so on.

status: str

added, removed or modified.

node_id: str | None = None

The graph node, when there is one.

columns: list[ColumnChange]

Column changes.

conditions_added: list[str]

Join, filter and grouping conditions only at the head.

conditions_removed: list[str]

Conditions only at the base.

rows_impact: dict[str, Any] | None = None

What is downstream of a relation whose rows changed.

note: str | None = None

Why the relation could not be compared, when it could not.

property changed: bool
class konstruct.analysis.diff.ColumnChange(column: 'str', change: 'str', before: 'str | None' = None, after: 'str | None' = None, impact: 'dict[str, Any] | None'=None, breaks: 'list[str]' = <factory>)[source]

Bases: object

column: str

The column name.

change: str

added, removed or changed.

before: str | None = None

The canonical expression at the base (CTEs and aliases inlined).

after: str | None = None

The canonical expression at the head.

impact: dict[str, Any] | None = None

What is downstream of the column, as Walk.to_dict().

breaks: list[str]

Relations that still read a removed column, which makes the change breaking.

class konstruct.analysis.diff.DagChange(path: 'str', dag: 'str', status: 'str', tasks_added: 'list[str]' = <factory>, tasks_removed: 'list[str]' = <factory>, dependencies_added: 'list[str]' = <factory>, dependencies_removed: 'list[str]' = <factory>, runs_changed: 'list[str]' = <factory>, sql_changed: 'list[str]' = <factory>)[source]

Bases: object

path: str

The changed DAG file.

dag: str

The DAG id.

status: str

added, removed or modified.

tasks_added: list[str]

Task ids only at the head.

tasks_removed: list[str]

Task ids only at the base.

dependencies_added: list[str]

Dependencies only at the head, as a >> b.

dependencies_removed: list[str]

Dependencies only at the base.

runs_changed: list[str]

Tasks whose dbt selection changed.

sql_changed: list[str]

Tasks whose SQL changed.

class konstruct.analysis.diff.YamlChange(path, tests_added=<factory>, tests_removed=<factory>, descriptions=<factory>, sources_added=<factory>, sources_removed=<factory>, exposures=<factory>, note=None)[source]

Bases: object

What changed in one dbt YAML file: tests, descriptions, sources and exposures.

path: str

The changed dbt YAML file.

tests_added: list[str]

Tests only at the head.

tests_removed: list[str]

Tests only at the base.

descriptions: list[str]

Descriptions that changed.

sources_added: list[str]

Source tables only at the head.

sources_removed: list[str]

Source tables only at the base.

exposures: list[str]

Exposures added, removed or changed.

note: str | None = None

Why the file could not be compared, when one of its versions cannot be read.

property changed: bool
lines()[source]
class konstruct.analysis.diff.DefinitionsChange(path, added=<factory>, removed=<factory>, changed=<factory>, approved_changed=<factory>, marked=<factory>, unmarked=<factory>)[source]

Bases: object

What changed in a definitions.yml: terms added or removed, and per term which fields.

path: str

The changed definitions file.

added: list[str]

Terms only at the head.

removed: list[str]

Terms only at the base.

changed: list[str]

One line per changed field, such as Order amount: status agreed -> approved.

approved_changed: list[str]

Approved terms whose meaning changed (definition, columns or filters) but stayed approved.

marked: list[str]

those of a term that gains a sensitivity or a new one, and those added to a sensitive term.

Type:

The columns, as the file names them, that the change marks sensitive

unmarked: list[str]

a sensitive term removed, its sensitivity removed or changed, or a column taken out of it.

Type:

One line per sensitivity mark the change takes away

property changed_any: bool
lines()[source]
class konstruct.analysis.diff.Selection(models=<factory>, tests=<factory>, modified_plus=<factory>)[source]

Bases: object

The dbt nodes a change actually needs rebuilt and tested, column by column, against what state:modified+ would build.

models: list[str]

dbt models whose data the change can affect.

tests: list[str]

dbt tests on those models.

modified_plus: list[str]

What state:modified+ would select, for comparison.

property selector: str
command()[source]
summary()[source]
to_dict()[source]

The Gate

class konstruct.analysis.gate.Gate(base, findings=<factory>, changed=0, laws_from=None)[source]

Bases: object

The result of the gate on one change.

base: str

The commit compared with, as given.

findings: list[Finding]

Everything that stops the change.

changed: int = 0

Files the change touches.

laws_from: str | None = None

Where the data laws the change is judged against came from (konstruct-out/laws.json at main), or that there were none, so no data law was checked.

property passed: bool

Whether nothing stops the change.

property verdict: str

Gate passed or Gate failed.

detail()[source]

What the verdict rests on: the files changed, or how many findings there are.

summary()[source]

One line: passed, or how many findings there are.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

markdown()[source]

The result as markdown, for a pull request comment or a job summary.

to_dict()[source]

The result as JSON-ready data, each finding with its fix.

class konstruct.analysis.gate.Finding(rule, message, path=None, fix='')[source]

Bases: object

One thing that stops a change, from a gate check.

rule: str

breaking, approved-definition-changed, conflicting-definition, data-law-broken or consumer-pin-changed for Konstruct’s own checks, <plugin>.<rule> for a plugin’s.

Type:

The rule

message: str

What is wrong.

path: str | None = None

The file to change, when one is known, relative to the project.

fix: str = ''

What to do about it.

SQL Checks

class konstruct.analysis.checksql.SqlCheck(sql, findings=<factory>, corrected=None, statements=0, terms=<factory>, dialect=None)[source]

Bases: object

The result of checking one piece of SQL against the project.

sql: str

The SQL as given.

findings: list[Finding]

Everything found, errors first.

corrected: str | None = None

The SQL with every safe fix applied, when there was one to apply.

statements: int = 0

Statements checked.

terms: list[str]

Business terms the query reads through their own columns.

dialect: str | None = None

The SQL dialect the query was read in.

count(severity)[source]
property checked: bool

one that parses but cannot be analysed has an unchecked note, and makes this false.

Type:

Whether every statement was analysed

property passed: bool

Whether every statement was checked and nothing breaks an approved definition (no finding of severity error): a statement never analysed does not pass.

failed(*, strict=False)[source]

Whether the check fails: on an error, or with strict on a warning as well.

summary()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

markdown()[source]

The result as markdown, for a pull request comment or an agent’s notes.

to_dict()[source]
class konstruct.analysis.checksql.Finding(rule, severity, message, fix, column=None, term=None, detail=None, rewrite=None, evidence=<factory>)[source]

Bases: object

One thing the query gets wrong, or may, with what to do about it.

rule: str

One of RULES.

severity: str

error (an approved definition is broken), warning or info.

message: str

What is wrong.

fix: str

What to change.

column: str | None = None

The output column it concerns, when there is one.

term: str | None = None

The business term it concerns, when there is one.

detail: str | None = None

what groups findings in history.

Type:

The filter that is missing or extra, or the formula the query uses

rewrite: str | None = None

The change made in the corrected SQL, when the fix was safe to make.

evidence: list[str]

the definitions, keys, tests and relationships the finding rests on.

Type:

Why

to_dict()[source]
class konstruct.analysis.checksql.HistoryCheck(queries=0, runs=0, unparsed=0, redacted=0, shadow=<factory>, adoption=<factory>, rules=<factory>, notes=<factory>)[source]

Bases: object

The checks run over query history: shadow metrics, and how widely each term’s own columns are read.

queries: int = 0

Distinct query texts checked.

runs: int = 0

Runs of those queries.

unparsed: int = 0

Query texts that could not be parsed.

redacted: int = 0

Runs whose text the warehouse withheld, which could not be checked.

shadow: list[ShadowMetric]

Measures computed differently from their definitions, approved terms first, then by people.

adoption: list[Adoption]

Terms read through their own columns.

rules: dict[str, int]

Queries with a finding of each rule.

notes: list[str]

What the warehouse withheld, and what would show it.

property approved_shadow: list[ShadowMetric]

queries that compute an approved measure differently from its definition.

Type:

The shadow metrics of approved terms

summary()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

to_dict()[source]
class konstruct.analysis.checksql.ShadowMetric(term, status, rule, detail, fix, queries=0, runs=0, users=0, people=<factory>, example='')[source]

Bases: object

One way that queries in history compute a measure differently from its definition.

term: str

The measure, or the output name it was computed under when no term holds it.

status: str | None

The term’s status, or None for an idea with no term.

rule: str

missing-filter or formula-differs.

detail: str

the filter left out or added, or the formula used.

Type:

What they do differently

fix: str

What to do instead, from the first query found.

queries: int = 0

Distinct query texts that compute it this way.

runs: int = 0

Runs of those queries.

users: int = 0

Distinct people who ran them.

people: list[str]

Who ran them, by name; empty when the check pseudonymises.

example: str = ''

The text of the query run most often; with its literals left out when the check pseudonymises, since they can identify a customer or a person.

to_dict()[source]
class konstruct.analysis.checksql.Adoption(term, queries=0, runs=0, users=0)[source]

Bases: object

How many queries read a term through its own columns.

term: str

The business term.

queries: int = 0

Distinct query texts that read it.

runs: int = 0

Runs of those queries.

users: int = 0

Distinct people who ran them.

to_dict()[source]

Verification

class konstruct.analysis.verify.Verification(warehouse: 'str', seed: 'int', max_rows: 'int', strategies: 'tuple[str, ...]', models: 'list[ModelResult]' = <factory>, seconds: 'float' = 0.0, keys: 'list[KeyCheck]' = <factory>, links: 'list[LinkCheck]' = <factory>, ontology_seconds: 'float' = 0.0, only: 'list[str]' = <factory>)[source]

Bases: object

warehouse: str

The DuckDB file or Postgres database read.

seed: int

The random seed, so a run can be repeated exactly.

max_rows: int

The most rows sampled from any input.

strategies: tuple[str, ...]

The perturbations applied (shift, null and shuffle).

models: list[ModelResult]

One result per model or table checked.

seconds: float = 0.0

Total time taken.

keys: list[KeyCheck]

Each entity key of the ontology, checked for duplicate and null values.

Each relationship of the ontology, checked for values its key lacks.

ontology_seconds: float = 0.0

Time taken by the checks of the ontology’s keys and relationships.

only: list[str]

The models and tables the run was limited to; empty for every one.

property checked: list[ModelResult]
totals()[source]
to_dict()[source]
checks()[source]

The checks of the ontology’s keys and relationships, in a sentence, if any ran.

summary()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

markdown()[source]
class konstruct.analysis.verify.ModelResult(node_id: 'str', label: 'str', rows: 'int' = 0, claimed: 'int' = 0, confirmed: 'list[tuple[str, str]]'=<factory>, not_exercised: 'list[tuple[str, str]]'=<factory>, through_references: 'list[tuple[str, str]]'=<factory>, unexplained: 'list[tuple[str, str]]'=<factory>, skipped: 'str | None' = None, seconds: 'float' = 0.0)[source]

Bases: object

node_id: str

The model or table checked.

label: str

Its short name.

rows: int = 0

Rows sampled from each input.

claimed: int = 0

Column edges the graph claims, as (output column, input column).

confirmed: list[tuple[str, str]]

Claimed edges whose output changed when the input was perturbed.

not_exercised: list[tuple[str, str]]

Claimed edges no perturbation moved on this data.

through_references: list[tuple[str, str]]

Observed influences the graph records as joins, filters or grouping.

unexplained: list[tuple[str, str]]

Observed influences the graph does not record, which are gaps in the static analysis.

skipped: str | None = None

Why the model was not checked, when it was not.

seconds: float = 0.0

Time taken.

to_dict()[source]
class konstruct.analysis.integrity.KeyCheck(entity, column, state, rows=0, nulls=0, duplicates=0, reason=None, id='')[source]

Bases: object

One entity key column, checked for duplicate and null values.

entity: str

The entity it is the key of.

column: str

The key column, by label (customers.customer_id).

state: str

confirmed (no value repeats and none is null), failed, or skipped.

rows: int = 0

The rows of its table.

nulls: int = 0

Rows where the key is null.

duplicates: int = 0

Values that a second row repeats.

reason: str | None = None

Why it was skipped, when it was.

id: str = ''

The column’s graph id, for tools; not written out.

property detail: str

What the check found, in a sentence.

to_dict()[source]
class konstruct.analysis.integrity.LinkCheck(source, target, via, state, values=0, orphans=0, reason=None, one_to_one=None, columns=('', ''))[source]

Bases: object

One relationship, checked for values of the referring column that the key lacks.

source: str

The entity that refers (order).

target: str

The entity referred to (customer).

via: str

The columns that join them (orders.customer_id = customers.customer_id).

state: str

confirmed (every value is found and the key is confirmed), failed, or skipped.

values: int = 0

The non-null values of the referring column.

orphans: int = 0

Those not found in the key.

reason: str | None = None

Why it was skipped, or why a check that found no orphan still failed.

one_to_one: str | None = None

For a relationship claimed one-to-one, whether the referring column is unique and never null: confirmed, failed (it is many-to-one; every value may still be found), or skipped; left out for one claimed many-to-one.

columns: tuple[str, str] = ('', '')

The graph ids of the referring column and the key, for tools; not written out.

property detail: str

What the check found, in a sentence.

to_dict()[source]

Mutation Testing

class konstruct.analysis.mutation.MutationTesting(warehouse, seed, max_rows, max_mutants, operators, models=<factory>, seconds=0.0)[source]

Bases: object

The result of mutate(): one ModelMutation per model, and the settings that make the run repeatable.

warehouse: str

The DuckDB file, Postgres database or sampled warehouse the mutants ran on.

seed: int

The seed of the mutant sample, so a run can be repeated exactly.

max_rows: int

The most rows sampled from any input.

max_mutants: int

The most mutants run per model.

operators: tuple[str, ...]

The kinds of mutation made.

models: list[ModelMutation]

One result per model.

seconds: float = 0.0

Total time taken.

property checked: list[ModelMutation]
totals()[source]
property score: float | None
to_dict()[source]
summary()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

markdown()[source]
class konstruct.analysis.mutation.ModelMutation(node_id, label, rows=0, sites=0, tests=<factory>, mutants=<factory>, skipped=None, seconds=0.0)[source]

Bases: object

The mutation testing of one dbt model: its tests, its mutants and its score.

node_id: str

The dbt model.

label: str

Its short name.

rows: int = 0

Rows the original model returns on the sample.

sites: int = 0

Every mutation its SQL admits, before sampling.

tests: list[TestCheck]

The dbt tests that read the model.

mutants: list[MutantResult]

The sampled mutants and what became of each.

skipped: str | None = None

Why the model was not mutation tested, when it was not.

seconds: float = 0.0

Time taken.

count(status)[source]
property score: float | None

the share of the changes the data shows that the tests notice. None when no mutant changed the output.

Type:

Killed over killed and survived

columns()[source]

The score of each output column (and (row count)), over the mutants that changed it.

property survivors: list[MutantResult]

Survivors, the ones with the most downstream reach first.

to_dict()[source]
class konstruct.analysis.mutation.MutantResult(mutant, status, rows=0, changed=<factory>, killed_by=<factory>, reach=0, suggestions=<factory>, error=None)[source]

Bases: object

What became of one mutant: its status, what it changed and what killed it.

mutant: Mutant

The mutation.

status: str

One of STATUSES.

rows: int = 0

Rows the mutant returned.

changed: list[str]

Output columns whose values changed, and (row count) when the number of rows did.

killed_by: list[str]

The tests that failed on the mutant.

reach: int = 0

Nodes downstream of the changed columns, which ranks survivors by what they would break.

suggestions: list[Suggestion]

For a survivor, the tests that would have killed it.

error: str | None = None

Why an invalid mutant could not be judged.

to_dict()[source]
class konstruct.analysis.mutation.TestCheck(id, name, test, column=None, state='used', reason=None, kills=0)[source]

Bases: object

One dbt test of the model (a data test or a unit test) and whether it could be used.

id: str

The dbt test’s unique id.

name: str

Its name.

test: str

not_null, relationships, a package test, singular, or unit_test.

Type:

Its kind

column: str | None = None

The column it tests, when it is a column test.

state: str = 'used'

used; fails on the sample (a data test that fails before any mutation, so it cannot tell a mutant apart); fails on the original (a unit test the original model already fails); or not run (it has no compiled SQL, its SQL will not run here, or its rows cannot be read).

reason: str | None = None

Why it was not used.

kills: int = 0

Mutants it killed.

to_dict()[source]
class konstruct.analysis.mutation.Suggestion(test, column, reason, yaml, where='')[source]

Bases: object

A dbt test that passes on the original model and fails on a survivor.

test: str

not_null, unique, accepted_values, relationships, dbt_utils.expression_is_true or unit_test.

Type:

The dbt test

column: str | None

The column it is on, for a column test.

reason: str

Why it kills the mutant, from the two outputs.

yaml: str

The test as a YAML list item, to paste where where says. Empty when the test is one already suggested for another survivor.

where: str = ''

Where the YAML goes in the project’s properties files.

to_dict()[source]
class konstruct.analysis.mutation.ColumnScore(killed=0, survived=0)[source]

Bases: object

The mutants that changed one output column, killed and survived.

killed: int = 0

Mutants that changed the column and that a test killed.

survived: int = 0

Mutants that changed the column and that every test let through.

property score: float | None
class konstruct.core.mutate.Mutant(id, operator, description, before, after, sql, scope=None, column=None)[source]

Bases: object

One mutation of a query: what changed, where, and the whole mutated SQL.

id: str

the operator and the mutation’s position in the query.

Type:

Stable within one query

operator: str

One of OPERATORS.

description: str

What changed, in words.

before: str

The SQL fragment that changed.

after: str

What it became (empty when it was removed).

sql: str

The whole mutated query, in the dialect it was read in.

scope: str | None = None

The CTE the change is in, or None for the outer query.

column: str | None = None

The outer query’s output column the change sits in, when it is inside one.

to_dict()[source]
konstruct.core.mutate.mutants(sql, dialect='duckdb', *, operators=('aggregate', 'filter', 'join_condition', 'join_type', 'boundary', 'comparison', 'date', 'coalesce', 'case'), limit=None, seed=0)[source]

The mutants of the query sql: all of them, or a seeded sample of at most limit.

The sample is reproducible for a given seed and draws from each operator in turn. A mutation whose SQL is the original’s, or another mutant’s, is dropped as a duplicate.

The Rows Behind a Value

class konstruct.analysis.why.Why(column, label, where, warehouse, value=None, matches=0, expression=None, composed=None, step=None, aggregates=<factory>, contributions=<factory>, contributors=0, excluded=<factory>, exclusions=0, limit=50, consistent=None, notes=<factory>, seconds=0.0, masked=<factory>)[source]

Bases: object

The rows behind one value of a column.

column: str

The column id.

label: str

Its short name.

where: dict[str, str]

output column to value.

Type:

The key that picks the row

warehouse: str

Where it ran.

value: Any = None

The value, computed by the model’s SQL in the sandbox.

matches: int = 0

How many rows of the model the key matches, 1 when it picks one row.

expression: str | None = None

The column’s own formula.

composed: str | None = None

The formula in terms of source columns.

step: str | None = None

The model whose step computes the value, which the contributions are rows of.

aggregates: list[Aggregate]

The aggregates of that step; empty for a value computed row by row.

contributions: list[Contribution]

The contributing rows, up to limit, largest share first.

contributors: int = 0

How many rows contribute, before the cap.

excluded: list[Exclusion]

Related rows left out, up to limit.

exclusions: int = 0

How many related rows were left out, before the cap.

limit: int = 50

The most rows listed in each table.

consistent: bool | None = None

Whether the shares add up to the value (None where that cannot be checked).

notes: list[str]

sampled inputs, untraced exclusions, approximate shares.

Type:

Limits of this explanation

seconds: float = 0.0

Time taken.

masked: list[str]

The sensitive columns whose values are shown masked, by short name (see konstruct.analysis.sensitivity).

property capped: bool

Whether either table was cut at limit.

property decomposable: bool
explanation()[source]

A few sentences on how the rows make up the value.

to_dict()[source]
text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.why.Contribution(rows, values, chosen=False)[source]

Bases: object

One input row of the computing step, with the source rows behind it and its share.

rows: list[SourceRow]

The source rows that make up this input row, the one the formula reads first.

values: list[Any]

the addend of a SUM or AVG, 1 or 0 for a COUNT, the candidate of a MIN or MAX, or the input value of an aggregate that cannot be split. For a value computed without an aggregate, the value itself.

Type:

Per aggregate, this row’s share

chosen: bool = False

this row holds the result.

Type:

For MIN and MAX

to_dict()[source]
class konstruct.analysis.why.Exclusion(row, reason)[source]

Bases: object

A source row related to the value by the key that a filter or a join left out.

row: SourceRow

The source row.

reason: str

The model and the WHERE condition, HAVING condition or join that removed it.

to_dict()[source]
class konstruct.analysis.why.SourceRow(relation, row_id, values=<factory>, shown=<factory>)[source]

Bases: object

One row of a source table.

relation: str

The source’s short name.

row_id: str

The row’s position in the sandbox copy of the source, which identifies it in this run.

values: dict[str, Any]

The row’s columns.

shown: list[str]

The columns that identify it or that the formula reads, shown in the table.

text()[source]
to_dict()[source]
class konstruct.analysis.why.Aggregate(sql, function, decomposable, total=None, count=0, addends=None)[source]

Bases: object

One aggregate in the computing step.

sql: str

The aggregate as written, with the step’s names.

function: str

SUM, COUNT, AVG, MIN, MAX, or another aggregate’s name.

decomposable: bool

Whether the value splits into one share per row (not for DISTINCT, MEDIAN and the like).

total: Any = None

The aggregate recomputed from the rows’ shares.

count: int = 0

those with a non-null input.

Type:

The rows it counts

addends: Any = None

For AVG, the sum of the addends, which count divides; None for the others.

to_dict()[source]

Data Laws

konstruct.analysis.laws.propose(graph, root, *, models=None, sql=None, without_tests=())[source]

The laws the lineage implies, from every dbt model’s compiled SQL and every SQL-built table’s, without reading the warehouse.

Each numeric measure column (not a key) whose total follows from an input through safe joins gets a conservation or count law, followed upstream through every model that keeps the total; each model that maps an input one row to one row gets a count law; each aggregate measure gets an additivity law for confirm() to classify. A UNION ALL gives a law whose total is the sum of its branches’. Columns with no law are in blocked.

models (graph ids) limits the reading to those models, such as the ones the laws of Laws.models() run through, which is all violations() needs. sql replaces the compiled SQL of the models it names, such as a model edited since dbt last compiled it, as konstruct.analysis.diff.rendered_now() renders it; None for a model whose file is gone. without_tests (graph ids) are tests the change removes, which the graph may still hold.

konstruct.analysis.laws.confirm(graph, root, warehouse, laws=None, *, tolerance=1e-09, max_rows=10000)[source]

Check laws (proposed from the graph when not given) against the warehouse, read-only, in the engine verify uses: a DuckDB file attached read-only, a Postgres transaction that is rolled back, or a sample of a remote warehouse.

Conservation and count laws compare totals per day within tolerance (relative to the day’s sum of magnitudes; counts exactly) and need the full tables, so on a sampled warehouse they are left unchecked, as is a law with no rows to compare. A filter or partition that is not one expression over the relation’s columns, as a hand-edited laws.json could give, leaves its law unchecked. Additivity laws rerun the model’s SQL on up to max_rows rows of each input: with every row of the measure’s input twice, with one row twice, and on two halves of it split at the middle value of a date (or of the measure’s input column).

konstruct.analysis.laws.violations(graph, laws, change, *, changed=None)[source]

The laws in laws (as confirmed before a change) that held on every partition and that the changed project no longer satisfies.

change is propose() on the changed project and graph its graph. A conservation or count law is violated when one of its steps no longer follows from that model’s SQL (a new join on a key no uniqueness test covers, a new filter, a removed test, a different factor) and the violation names the model, nearest the source first; an additivity law is violated when the SQL now suggests another class. Nothing is run.

changed (the files a change touches, relative to the project) keeps only the violations the change makes: a step that differs although none of its files changed was broken before it, so a law broken on the base fails the change that broke it, not every change after it.

konstruct.analysis.laws.load(path)[source]

Read a laws.json written by Laws.write().

konstruct.analysis.laws.recorded(root, out_dir, base=None)[source]

The laws as recorded before a change, and where they came from: in a git repository, the laws.json committed at base (default: the merge base with main) and nothing else, so a copy left in the output directory never stands in for the base’s; with no git base to read, the one konstruct verify --laws last wrote to out_dir.

Raises:

konstruct.QueryError – The file cannot be read.

class konstruct.analysis.laws.Laws(laws=<factory>, blocked=<factory>, steps=<factory>, dialect=None, warehouse=None, tolerance=1e-09, seconds=0.0)[source]

Bases: object

The laws of a project, proposed and, once confirmed, checked.

FORMAT_VERSION = 1
laws: list[Law]

Every law, ordered by relation and column.

blocked: list[Blocked]

Every column (and row count) of a model with no step, with the reason. laws.json keeps the measures among them.

steps: dict[tuple[str, str | None], tuple[Step, ...]]

Every step the SQL proves, by model id and column (None for the row count), one for each branch of a UNION ALL, whether or not a law starts at it. Kept in memory, not in laws.json.

dialect: str | None = None

The SQL dialect of the filters and partitions.

warehouse: str | None = None

What the laws were checked against, once confirmed.

tolerance: float = 1e-09

The relative tolerance of conservation laws.

seconds: float = 0.0

Time taken.

models()[source]

The models the laws run through (or, for additivity, are about), by graph id.

law(law_id)[source]

The law with this id.

totals()[source]

Laws by status and by kind.

summary()[source]

One line on what was found.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

to_dict()[source]

The laws.json document.

write(path)[source]

Write laws.json: the document --json prints, with its format_version.

classmethod from_dict(data)[source]
class konstruct.analysis.laws.Law(id, kind, relation, label, column, terms=<factory>, partition=None, expected=None, status='proposed', held=0, checked=0, failures=<factory>, broken_at=None, classification=None, evidence=None, note=None, unprobed=None, files=<factory>)[source]

Bases: object

An equality the data must obey, with the result of checking it once checked.

id: str

kind:relation id#column, or count:relation id for a row count.

kind: str

conservation (some term sums), count (every term counts) or additivity.

relation: str

The relation the law is about, by graph id.

label: str

Its short name.

column: str | None

Its column, or None for its row count.

terms: list[Term]

the input the probes change.

Type:

What the total equals

Type:

the sum of the terms. For additivity, one term

partition: str | None = None

The day each row falls on, as SQL over the relation’s columns; each term has the same day over its source’s.

expected: str | None = None

additive, semi-additive or non-additive.

Type:

For additivity, what the SQL suggests

status: str = 'proposed'

proposed, holds, broken or unchecked.

held: int = 0

Partitions (or, for additivity, probe sets) on which the law held.

checked: int = 0

Partitions (or probe sets) checked.

failures: list[Failure]

The first partitions on which it did not hold.

broken_at: str | None = None

For a broken law, the model where the totals first part, by graph id.

classification: str | None = None

For additivity, what the probes found.

evidence: str | None = None

For additivity, what the probes did and saw.

note: str | None = None

Why the law was not checked, when it was not.

unprobed: str | None = None

a model that keeps rows by comparing them with others (DISTINCT, QUALIFY, LIMIT, HAVING or a filter with a subquery) keeps different rows once its input is repeated or split.

Type:

For additivity, why the probes cannot classify the column, read from its SQL

files: list[str]

For additivity, the files the relation’s SQL follows from, relative to the project, as a step records them; the other kinds keep them on their steps.

property steps: list[Step]

Every step of every term, each once, in the order the terms reach them.

property relies_on: list[str]

Every test the law’s joins depend on.

property detail: str | None

15 of 15 days, where the totals first part, or why the law was not checked.

Type:

What checking found, beside the statement

property always_held: bool

Whether the law was checked and held on every partition.

property statement: str

The law in one line.

to_dict()[source]
classmethod from_dict(data)[source]
class konstruct.analysis.laws.Term(source, source_label, source_column=None, op='sum', factor=1.0, filter=None, partition=None, steps=())[source]

Bases: object

One source of a law’s total: factor times the total of a source column. A law over a UNION ALL has one term for each branch; any other law has one.

source: str

The relation the total comes from (for additivity, the input the probes change), by graph id.

source_label: str

Its short name.

source_column: str | None = None

The source’s column, or None for its row count.

op: str = 'sum'

it counts the column’s values (or, with no column, the rows). The branches of a UNION ALL can differ.

Type:

sum

Type:

the term totals the source column; count

factor: float = 1.0

The constant between the two totals.

filter: str | None = None

The source rows that count, as SQL over the source’s columns.

partition: str | None = None

The day each source row falls on, as SQL over the source’s columns, when the law has one.

steps: tuple[Step, ...] = ()

The models the term runs through, from the law’s relation back to the source.

text()[source]

The term in a statement, such as SUM(raw.payments.amount) / 100.

to_dict()[source]
classmethod from_dict(data)[source]
class konstruct.analysis.laws.Step(model, column, input, input_column, op, factor=1.0, filter=None, partition=None, relies_on=(), branch=0, files=())[source]

Bases: object

One model’s part in a law: how a total of its output follows from a total of one input.

model: str

The model or table whose SQL makes the step, by graph id.

column: str | None

Its column, or None for its row count.

input: str

The relation the step reads, by graph id.

input_column: str | None

The input’s column, or None for its row count.

op: str

SUM(column) = factor * COUNT(input column, or rows); rows: the output has one row for each input row.

Type:

sum

Type:

SUM(column) = factor * SUM(input column); count

factor: float = 1.0

The constant the SQL multiplies by, such as 0.01 for cents to units.

filter: str | None = None

The input rows that count, as SQL over the input’s columns.

partition: tuple[str, str] | None = None

The day each row falls on, as SQL over the output’s columns and over the input’s.

relies_on: tuple[str, ...] = ()

The tests that make the step’s joins safe, by graph id.

branch: int = 0

The branch of the model’s UNION ALL the step reads, from 0; 0 for any other query.

files: tuple[str, ...] = ()

the model’s and those of the tests it relies on. A change that touches none of them leaves the step as it was.

Type:

The files the step follows from, relative to the project

same(other)[source]

Whether other makes the same claim (tests and partitions aside).

to_dict()[source]
classmethod from_dict(data)[source]
class konstruct.analysis.laws.Violation(law, statement, model, label, message, path=None)[source]

Bases: object

A law that held on every partition and that a change no longer satisfies.

law: str

The law’s id.

statement: str

The law in one line.

model: str

The model the change breaks it in, by graph id.

label: str

The model’s short name.

message: str

What changed.

path: str | None = None

The model’s file, relative to the project, when known.

to_dict()[source]
class konstruct.analysis.laws.Failure(partition, expected, actual)[source]

Bases: object

One partition on which a law did not hold.

partition: str

The day (2026-01-31), null for rows without one, or all.

expected: float

The total the source gives.

actual: float

The total the relation has.

text()[source]

2026-01-31: 120 against 100: the day, the relation’s total and the source’s.

class konstruct.analysis.laws.Blocked(model, column, reason, measure=True)[source]

Bases: object

A measure column that has no law, and why.

model: str

The model, by graph id.

column: str | None

The column, or None for the model’s row count.

reason: str

What in the SQL stops the law, such as a join on a key no uniqueness test covers.

measure: bool = True

Whether the column is a measure (numeric and not a key), which would have a law of its own.

to_dict()[source]

Answer Receipts

class konstruct.analysis.receipts.Receipt(id, issued, refs, build, citation, question=None, answer=None, canonical='konstruct-5/sqlglot-30.30', pins=<factory>, terms=<factory>, digest='')[source]

Bases: object

What one answer rests on, as it was when the receipt was issued.

id: str

r- and 12 hex digits.

issued: str

When it was issued, in UTC, ISO 8601, to the second.

refs: list[str]

columns, relations, exposures or business terms.

Type:

What the answer rests on, as named

build: Build

The build it answered from.

citation: str

each column with its formula and verification state, each term with its status, and the receipt’s id.

Type:

One paragraph to put under the answer

question: str | None = None

The question answered, when given.

answer: str | None = None

The answer given, when given.

canonical: str = 'konstruct-5/sqlglot-30.30'

The canonical form the formulas and conditions were printed in, as the lockfile records it.

pins: list[Pin]

Each column the answer rests on, pinned as the lineage lockfile pins it, sorted by column.

terms: list[PinnedTerm]

Every business term named in refs, and every term bound to the relation of a column the answer rests on, as they were, sorted by name.

digest: str = ''

sha256: and the SHA-256 of the rest of the record (see digest()).

to_dict()[source]
classmethod from_dict(data)[source]

A receipt as it is stored. Fields it does not know, which a later release may add, are ignored.

Raises:

ReceiptError – data is not a receipt.

text(style=None)[source]

The receipt as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.receipts.Build(graph, generated=None, konstruct=None, commit=None, uncommitted=None, changed_inputs=<factory>)[source]

Bases: object

The build a receipt answered from, and the project when it was issued.

graph: str

sha256: and the SHA-256 of graph.json, which names the exact graph read.

generated: str | None = None

When the graph was built.

konstruct: str | None = None

The Konstruct release that built it.

commit: str | None = None

The commit checked out when the receipt was issued; None outside git.

uncommitted: bool | None = None

Whether tracked files had uncommitted changes then; None outside git.

changed_inputs: list[str]

the answer came from a graph older than these.

Type:

Inputs the build read that had changed since it ran, when the receipt was issued

to_dict()[source]
classmethod from_dict(data)[source]
class konstruct.analysis.receipts.ReceiptCheck(directory, receipts=<factory>, changed_inputs=<factory>)[source]

Bases: object

konstruct receipts check: whether what each answer rests on still holds.

directory: str

The folder of receipts, relative to the project.

receipts: list[CheckedReceipt]

Every receipt checked, in the order issued.

changed_inputs: list[str]

while there are any, no receipt is said to hold.

Type:

Inputs changed since the graph was built

property passed: bool

True when every receipt checked holds.

counts()[source]

How many receipts have each status.

summary()[source]
to_dict()[source]
text(style=None)[source]

The check as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.receipts.CheckedReceipt(id, status, file, line, issued=None, refs=<factory>, question=None, changes=<factory>, terms=<factory>, notes=<factory>, rebuilt=False, problem=None)[source]

Bases: object

One receipt, checked against the current graph.

id: str

The receipt’s id, or for a line that is not a receipt, where it is.

status: Literal['holds', 'stale', 'unchecked', 'tampered', 'unreadable']

holds (everything it rests on is as it was), stale (something changed), unchecked (the graph is older than its inputs, so it cannot say), tampered (the record does not match its digest) or unreadable.

file: str

The file the receipt is in, relative to the project.

line: int

Its line in the file.

issued: str | None = None

When it was issued.

refs: list[str]

What it rests on, as named.

question: str | None = None

The question it answered, when it was given.

changes: list[PinChange]

Each column it rests on that changed, with what changed and, from git history, who changed it.

terms: list[str]

Each change to an approved term it rests on other than through a column’s pin.

notes: list[str]

a verification lost, a draft term reworded.

Type:

What changed without making it stale

rebuilt: bool = False

Whether the graph is another build than the one it answered from.

problem: str | None = None

For a receipt that is unchecked, tampered or unreadable, why.

to_dict()[source]

The Lineage Lockfile

konstruct.analysis.lock.lock(graph, root, reads=None, *, update=False, path=None, write=True)[source]

Pin the upstream columns this project reads in konstruct.lock.

reads names what to pin (columns, relations, exposures or terms); None keeps what the lockfile already names, and an empty list (or no lockfile) pins every upstream column the project’s own models and exposures read. Columns already pinned keep their pin, as a package lockfile keeps its versions, unless update is True (accept every change) or names them; what changed without failing a check is brought up to date.

konstruct.analysis.lock.check(graph, root, *, path=None, consumer=None, history=True, only=None)[source]

Compare every pin in konstruct.lock with this build of the graph.

consumer names the repository (one of repositories) whose lockfile it is, when it is not the project’s own: then only the pins on columns this build builds are judged, each composed as far as the consumer’s build saw it. history False skips reading git history to say who changed what. only (graph ids) judges only the pins on those columns, such as the ones a change alters, and leaves the rest, and the reads not pinned, unremarked.

konstruct.analysis.lock.check_consumers(graph, root, *, history=True, only=None)[source]

The lockfile of every repository in repositories that has one, checked against this graph: run in the producer, it names each consumer a change would break. only is as for check().

class konstruct.analysis.lock.LockResult(path, lockfile, added=<factory>, removed=<factory>, updated=<factory>, kept=<factory>, stale=<factory>, missing=<factory>, written=True)[source]

Bases: object

What konstruct lock wrote.

path: str

The lockfile, relative to the project.

lockfile: Lockfile

What the lockfile holds, as written (or, with write False, as it would be).

added: list[str]

Columns pinned for the first time.

removed: list[str]

Columns no longer read, or whose removal was accepted, so no longer pinned.

updated: list[str]

Pinned columns whose change was accepted.

kept: list[str]

Pinned columns left as they were locked, or brought up to date where nothing that fails a check changed (an approval date, a verification, a term that was not approved).

stale: list[str]

Kept columns that fail konstruct lock --check against this build.

missing: list[str]

Columns this project reads that the relation’s builder no longer builds.

written: bool = True

Whether the lockfile was written.

to_dict()[source]
summary()[source]

One line: what was written, and how the pins changed.

stale_advice()[source]

What to do about the pins kept as locked that differ from this build; empty when none do.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.lock.LockCheck(path, pinned, changes=<factory>, notes=<factory>, consumer=None, canonical='konstruct-5/sqlglot-30.30', relock=0)[source]

Bases: object

konstruct lock --check: whether every pinned column still means what was locked.

path: str

The lockfile, relative to the project.

pinned: int

How many columns the lockfile pins.

changes: list[PinChange]

each fails the check.

Type:

Pinned columns that changed

notes: list[str]

a verification lost, a term re-approved, a new upstream read not yet pinned, a pin on a column this build does not build.

Type:

What changed without failing

consumer: str | None = None

The repository whose lockfile this is, when it is not the project’s own.

canonical: str = 'konstruct-5/sqlglot-30.30'

The canonical form the lockfile was written with.

relock: int = 0

they fail the check once, as a whole, rather than one by one.

Type:

With a canonical form this Konstruct does not write, how many pins read differently

property passed: bool
summary()[source]
to_dict()[source]
relock_lines()[source]

With a canonical form this Konstruct does not write, what to do: review, then re-lock.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

markdown()[source]

The check for a pull request comment. Built with history False, it names no commit, author or subject, so a private producer’s history stays out of a public pull request.

class konstruct.analysis.lock.PinChange(column, label, reasons, repository=None, path=None, removed=None, formula_before=None, formula_after=None, conditions_added=<factory>, conditions_removed=<factory>, sources_added=<factory>, sources_removed=<factory>, files=<factory>, type_before=None, type_after=None, terms=<factory>, changed_by=<factory>, affected=<factory>)[source]

Bases: object

One pinned column that is not what was locked.

column: str

The pin’s key.

label: str

The column’s short name in this build.

reasons: list[str]

removed, unknown (the formula could not be worked out now), formula, conditions, sources, files (a file that builds a column with no recorded expression), upstream (a column further up than a formula cut short shows), type or definition.

Type:

What changed

repository: str | None = None

The repository that builds the column, when it is one listed in repositories.

path: str | None = None

The file that builds the column, relative to the project.

removed: str | None = None

no such column in this build or no longer built by ....

Type:

For a removed column, why

formula_before: str | None = None

The locked formula, composed down to source, with short names.

formula_after: str | None = None

The formula now.

conditions_added: list[str]

Row conditions the column has now and did not have when locked.

conditions_removed: list[str]

Row conditions it had when locked and no longer has.

sources_added: list[str]

Source columns the formula reaches now and did not.

sources_removed: list[str]

Source columns it reached and no longer does.

files: list[str]

Relations whose file changed, for columns with no recorded expression.

type_before: str | None = None

The locked type.

type_after: str | None = None

The type now.

terms: list[str]

One line per change to a bound approved term.

changed_by: list[Attribution]

The commits (or uncommitted edits) that made the change, read from git history.

affected: list[str]

This project’s columns and exposures that read the column, directly or further down.

property accept: str
unprint()[source]

Set aside what differs only as a canonical form can print it (a formula, a condition, a column upstream), keeping the rest; whether there was any.

to_dict()[source]
property keep: str | None

undo the change where it was made, or, for a commit in another team’s repository, ask them to.

Type:

How to keep the locked meaning instead

lines()[source]
class konstruct.analysis.lock.Attribution(what, sha, author, date, subject, path=None, repository=None, before=None, after=None)[source]

Bases: object

A commit (or uncommitted edit) that changed something a pin relies on.

what: str

a column’s formula, a file, or a term.

Type:

What it changed

sha: str

Empty for uncommitted changes.

author: str

Who made the change.

date: str

When it was committed, in ISO 8601.

subject: str

The commit’s subject line.

path: str | None = None

The file changed, relative to the project.

repository: str | None = None

The git repository it is in, relative to the project (. for the project’s own).

before: str | None = None

What it changed, before the change, where that can be shown.

after: str | None = None

And after.

property short: str
to_dict()[source]
class konstruct.analysis.lock.Lockfile(pins=<factory>, reads=<factory>, format=1, canonical='konstruct-5/sqlglot-30.30', _index=<factory>)[source]

Bases: object

The contents of konstruct.lock.

pins: list[Pin]

Sorted by column.

reads: list[str]

columns, relations, exposures or terms, as given to konstruct lock; empty for the default, every upstream column this project’s own models and exposures read.

Type:

What was locked

format: int = 1

The layout of the file (see FORMAT).

canonical: str = 'konstruct-5/sqlglot-30.30'

The canonical form the pins were printed in (see CANONICAL).

pin(column)[source]
stored()[source]

The pins, sorted, each with the names of the relations it relies on, and those relations’ conditions by name: once per relation, so pins that read the same relations share them. Pins kept as locked may rely on conditions a relation no longer has, which are stored beside the current ones as name~2 and so on.

to_dict()[source]
dumps()[source]

The lockfile as TOML: keys in a fixed order, pins sorted, then the relations whose conditions they rely on, one list item per line.

classmethod loads(text, where='konstruct.lock')[source]
classmethod read(path)[source]
write(path)[source]
class konstruct.analysis.lock.Pin(column, id, fingerprint, formula=None, rows=<factory>, sources=<factory>, files=<factory>, type=None, complete=True, verified=None, repository=None, used_by=<factory>, terms=<factory>)[source]

Bases: object

One upstream column, pinned by what it means.

column: str

the lock’s key.

Type:

The column as relation.column with the relation’s warehouse name

id: str

The graph node, which finds the column again in a build from this project.

fingerprint: str

sha256: and the 64 hex digits of a hash over the formula, conditions, sources and files, and, for a formula cut short, the expression of every column on its way.

formula: str | None = None

The formula composed down to source columns, with full relation names, in canonical form; None for a column read straight from a source.

rows: list[Rows]

The conditions of every relation on the formula’s way and of the relations they join, filter or group on, relation by relation.

sources: list[str]

The source columns the formula reaches.

files: list[str]

For each relation on the way whose columns have no recorded expression (a seed, a Python model, SQL that could not be read), the relation and a checksum of the file that builds it.

type: str | None = None

The column’s type, when the build knows it.

complete: bool = True

False when some column on the way, other than a seed’s, has no recorded expression.

verified: str | None = None

confirmed, partly confirmed or not exercised once konstruct verify has run.

repository: str | None = None

The repository that builds the column, when it is one listed in repositories.

used_by: list[str]

This project’s columns and exposures that read the column directly.

terms: list[PinnedTerm]

The business terms bound to the column, as they were when it was locked.

property conditions: list[str]

The joins, filters and grouping that decide which rows feed the column, composed down to source columns the same way as the formula, each as it applies to the column.

to_dict(references=None)[source]

The pin as data. With references, as the lockfile stores it: the relations whose conditions it relies on, by their names in the file, rather than the conditions.

classmethod from_dict(data, relations)[source]

A pin as the lockfile stores it, with relations the file’s relations by name.

class konstruct.analysis.lock.Rows(relation, conditions, side=None, before=<factory>)[source]

Bases: object

The conditions on one relation’s rows that a pin relies on, and where they apply.

relation: str

The relation’s warehouse name.

conditions: list[str]

Its joins, filters and grouping, composed down to source columns in canonical form.

side: str | None = None

LEFT, RIGHT or FULL when the pin reaches the relation through the side of an outer join that may find no row of it: its filters then decide which rows join.

before: list[str]

The window functions (WINDOW) and LIMITs (LIMIT) between the relation and the pinned column, nearest first: its conditions change what they see.

property effective: list[str]

The conditions as they apply to the pinned column (see in_context()).

property reference: str
class konstruct.analysis.lock.PinnedTerm(name, status, version, approved=None, owner=None, definition=None, grain=None, filters=<factory>, columns=<factory>, unit=None, currency=None, exceptions=None, formula=None)[source]

Bases: object

A business term bound to a pinned column, as it was when the column was locked.

name: str

The term’s name.

status: str

draft, agreed, approved or retired.

version: str

its definition, grain, columns, filters, unit, currency, exceptions and, for a measure with one, its formula. Rewording any of them is a new version; a new approval date alone is not.

Type:

A hash of the term’s meaning

approved: str | None = None

The approval date.

owner: str | None = None

Who owns the term.

definition: str | None = None

What the term means, in words.

grain: str | None = None

What one row of the term is.

filters: list[str]

The filters the term requires.

columns: list[str]

The columns the term names.

unit: str | None = None

The unit of its values.

currency: str | None = None

The currency of its values.

exceptions: str | None = None

What the term leaves out.

formula: str | None = None

For a measure defined by a formula (an Ossie metric), the formula.

to_dict()[source]
classmethod from_dict(data)[source]
class konstruct.analysis.lock.LockError(message, *, suggestions=None, hint=None)[source]

Bases: QueryError

A lockfile that cannot be read or written, or a selection that names nothing to pin.

Builds

class konstruct.engine.build.BuildResult(graph: 'Graph', outputs: 'list[Path]', seconds: 'float', cache_hits: 'int', cache_misses: 'int', inputs: 'dict[str, int]'=<factory>)[source]

Bases: object

graph: Graph

The graph that was built.

outputs: list[Path]

The files written (graph.json, REPORT.md, graph.html and ontology.yml).

seconds: float

Time taken.

cache_hits: int

Analyses reused from the previous build.

cache_misses: int

Analyses computed afresh.

inputs: dict[str, int]

Counts of what was read, such as dbt projects, warehouses, DAGs and terms.

The Graph

project.graph is the konstruct.Graph behind every answer, for anything the questions above do not cover. Nodes and edges are dataclasses (dataclasses.fields, asdict and replace work on them; vars(node) is empty, as their fields are not in a __dict__); the edge kinds and their directions are listed in Outputs and the Graph Format.

graph = project.graph
orders = graph.resolve('orders')[0]
for column in graph.columns_of(orders.id):
    upstream = [graph.label(graph.nodes[e.target]) for e in graph.out_edges(column.id, 'derived_from')]
    print(column.name, column.attrs.get('expression'), upstream)
class konstruct.Graph

Bases: object

The graph.

add_column(parent, name, *, data_type=None, description=None, origin=None, attrs=None)
add_edge(source, target, kind, **attrs)

Add an edge, or merge attrs into an existing one of the same kind between the same nodes. Self-loops are ignored and return None.

add_node(node)

Add a node, or merge it into an existing node with the same id.

built(relation)

Whether something in the graph builds relation: a model, seed or snapshot, a file of its own, a task, or SQL that gives its columns expressions. A source, or a table the graph only reads, is not built.

changed()

Note that nodes or edges were edited in place (a name, a relation key, an edge’s role), so the indexes queries use are made again.

column(parent_id, name)

The column name of the relation parent_id, or None.

columns_of(node_id)

The columns of a relation, in their order.

downstream_edges(node_id)

Edges along which a change to node_id propagates, with the node they reach: feeds forwards, and derived_from, tests and runs backwards.

edges

Every edge, in the order they were added.

expand(node)

A relation stands for itself and all its columns; anything else for itself.

fed_by_columns_of(consumer, relation)

Whether consumer (a Looker view, say) records which of relation’s columns it reads, so a column reaches it only through its own edge, and walks do not pass it every column of relation.

classmethod from_dict(data, source='the graph')

The graph in data, as to_dict writes it in this or any earlier format.

Raises:

konstruct.FormatError – A newer Konstruct wrote data (source names it).

classmethod from_json(data, source='the graph')

The graph in data, the bytes of a graph.json, as read reads a file.

Raises:
  • json.JSONDecodeError – data is not JSON; the message says where and why.

  • konstruct.FormatError – A newer Konstruct wrote it, it is not a graph, or it is larger than 2 GiB (source names it).

guessed_relations()

The relations whose columns in the graph may not be all they have: the build marks a relation columns_guessed unless the warehouse or a dbt catalog lists its columns, or SQL that writes it selects them without a star it cannot expand.

impact(node, max_depth=None)

Everything downstream of node.

A column reached through a join, filter or group-by (role = “reference”) is reported, but the walk does not continue from the whole relation it reached, so one join key does not mark every downstream column as affected. For a column, the exposures that use and the tasks that read or run each affected relation are reported too, and so are the tasks whose SQL an edge on the way came from (an Airflow SQL task, a Dagster asset). Tests and the tasks that run a node are reported, not walked through.

in_edges(node_id, kind=None)

Edges arriving at node_id, optionally only of one kind.

indexed(name, build, *, basis='graph')

The index name that build makes from the graph, kept until what it is made from changes: anything in the graph (‘graph’), a node (‘nodes’, for an index that reads no edges) or a relation (‘relations’).

label(node)

Short human label: orders.amount for a column, orders for a relation.

meta

its name, the inputs it read and so on.

Type:

What the build records about the project

node_for_reading(name)

The relation a name in canonical text stands for, whether the relation itself or one reading of it (see split_reading).

node_for_relation(key)

The node SQL naming this relation refers to (see RELATION_PRIORITY).

nodes

The nodes by id, in the order they were added.

nodes_for_relation(key)

Every node that describes this relation, the preferred one first.

out_edges(node_id, kind=None)

Edges leaving node_id, optionally only of one kind.

parent_of(node)

A column’s relation, or None for anything else.

path(start, end)

Shortest path from start to end, following data flow in either direction: downstream if there is a path that way, else upstream, else either way through contains and precedes too. Of several paths of the same length, each step takes the neighbour whose id sorts first, so the same path is found whatever order the build added the edges in.

classmethod read(path)

Load a graph from a graph.json file, written by this or any earlier Konstruct.

Raises:
  • json.JSONDecodeError – The file is not JSON; the message says where and why.

  • konstruct.FormatError – A newer Konstruct wrote it, it is not a graph, or it is larger than 2 GiB.

relations()
remove_edge(edge)
resolve(ref)

Find the nodes a user reference such as orders.amount or stg_orders names.

shared_relations()
stats()
to_dict()

The graph as plain data: format_version, meta, nodes, edges and warnings.

trace(node, max_depth=None)

Everything upstream of node. For a column, the tasks that build each relation on the way (including the column’s own) are reported too.

upstream_edges(node_id)

Edges along which node_id depends on something, with the node they reach: feeds and runs backwards, and derived_from forwards.

warn(message)
warnings

Notes about what the build could not read or resolve.

write(path)

Write the graph to path as JSON (graph.json).

class konstruct.Node(id, kind, name, parent=None, relation=None, description=None, data_type=None, path=None, origins=None, attrs=None)

Bases: object

A node of the graph.

attrs: dict[str, Any]

Everything else, such as expressions, conditions, schedules, usage and term fields.

data_type: str | None

Column type.

description: str | None

Description from dbt, the warehouse or definitions.yml.

classmethod from_dict(data)

The node data describes, as to_dict writes it.

id: str

A unique id, such as model.<package>.<name>, table.<relation>, <relation id>#<column>, task.<dag>.<task>, looker.view.<view>, powerbi.table.<model>.<table> or term.<slug>.

kind: Literal['source', 'table', 'column', 'model', 'seed', 'snapshot', 'dag', 'task', 'test', 'exposure', 'term']

What the node is.

name: str

Its own name.

origins: list[str]

Inputs it came from.

parent: str | None

For a column, its relation’s id.

path: str | None

The file that defines it, relative to the project.

relation: str | None

For a relation, its lower-case warehouse name, such as shop.main.orders.

to_dict()

The node as plain data: id, kind and name, the fields that are set, its origins in order, and attrs when there are any.

class konstruct.Edge(source, target, kind, attrs=None)

Bases: object

An edge of the graph: source -> target, with the meaning its kind gives (see konstruct.core.graph).

attrs: dict[str, Any]

Details such as transform, role, verified and via.

classmethod from_dict(data)

The edge data describes, as to_dict writes it.

key

(source, target, kind), which names an edge in its graph.

kind: Literal['contains', 'feeds', 'derived_from', 'runs', 'precedes', 'tests', 'defines', 'relates_to']

Edge kind.

source: str

Source node id.

target: str

Target node id.

to_dict()

The edge as plain data: source, target and kind, and attrs when there are any.

class konstruct.core.graph.Step(node, depth, via, previous)

Bases: object

One node reached by a traversal, with the edge that reached it.

depth: int

Steps from the start.

node: Node

The node reached.

previous: str | None

The id of the node it was reached from.

via: Edge | None

The edge that reached it.

Lower-Level Modules

These are stable enough to build on, but Project is the supported entry point.

SQL Lineage

konstruct.core.sql.analyse(sql, *, dialect=None, schema=None, default_catalog=None, default_db=None, target=None, guessed=())[source]

Analyse every statement in sql.

target names the relation a bare SELECT writes (a dbt model’s relation). Relations created by earlier statements become part of the schema for later ones. guessed names the relations of schema whose columns are only those other queries were seen to read: they place a column, but are not taken to be all the columns the relation has.

class konstruct.core.sql.StatementLineage(kind: 'str', target: 'str | None' = None, reads: 'list[str]' = <factory>, columns: 'dict[str, list[ColumnSource]]'=<factory>, references: 'list[tuple[str, str]]'=<factory>, errors: 'list[str]' = <factory>, expressions: 'dict[str, str]'=<factory>, conditions: 'list[str]' = <factory>, row_conditions: 'dict[str, list[int]]'=<factory>, aggregated: 'list[str]' = <factory>, readings: 'dict[str, str]'=<factory>, nullable: 'dict[str, str]'=<factory>, read_before: 'dict[str, list[str]]'=<factory>, star_reads: 'dict[str, list[str]]'=<factory>)[source]

Bases: object

kind: str
target: str | None = None
reads: list[str]
columns: dict[str, list[ColumnSource]]
references: list[tuple[str, str]]
errors: list[str]
expressions: dict[str, str]
conditions: list[str]
row_conditions: dict[str, list[int]]
aggregated: list[str]
readings: dict[str, str]
nullable: dict[str, str]
read_before: dict[str, list[str]]
star_reads: dict[str, list[str]]
to_dict()[source]
classmethod from_dict(data)[source]
class konstruct.core.sql.ColumnSource(relation: 'str', column: 'str', transform: 'Transform')[source]

Bases: object

relation: str
column: str
transform: Literal['direct', 'transformed']

Release Notes

class konstruct.analysis.release_notes.ReleaseNotes(since, until, changes=<factory>, commits=0, notes=<factory>, warehouse=None)[source]

Bases: object

The changes to formulas, rows and business terms in a range of commits, newest first.

since: str | None

a commit, tag or date, or None for the whole history.

Type:

Where the range starts

until: str

Where it ends.

changes: list[Change]

The entries, newest commit first.

commits: int = 0

Commits read in the range.

notes: list[str]

What could not be read, and why.

warehouse: str | None = None

Where konstruct verify --effect ran the changes, when it did.

summary()[source]
to_dict()[source]
markdown()[source]

The notes as Markdown, to post to a wiki, a chat channel or a pull request.

by_commit()[source]

The changes grouped by commit, newest first.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.release_notes.Change(commit, path, kind, status, relation=None, column=None, term=None, node_id=None, previous_path=None, before=None, after=None, before_english=None, after_english=None, added=<factory>, removed=<factory>, added_english=<factory>, removed_english=<factory>, fields=<factory>, affected=<factory>, approvals=<factory>, effect=None)[source]

Bases: object

One entry of the notes: a column whose formula changed, a relation whose rows changed, or a business term whose definition changed, in one commit.

commit: Commit

The commit.

path: str

The file it changed, relative to the project.

kind: str

formula (a column added, removed or changed), rows (joins, filters or grouping) or term (a business term’s status, approval, owner or definition).

status: str

For a formula, added, removed or changed; for rows and terms, changed.

relation: str | None = None

The relation, by label; None for a term.

column: str | None = None

The column, by label, for a formula.

term: str | None = None

The term’s name, for a term.

node_id: str | None = None

The relation’s or column’s graph id, when the graph has it.

previous_path: str | None = None

Where the file was before the commit, relative to the project, when the commit moved it.

before: str | None = None

The formula before, as SQL with short names.

after: str | None = None

The formula after.

before_english: str | None = None

before in plain English.

after_english: str | None = None

after in plain English.

added: list[str]

For rows, the conditions only after the commit, as SQL.

removed: list[str]

For rows, the conditions only before it.

added_english: list[str]

added in plain English.

removed_english: list[str]

removed in plain English.

fields: dict[str, tuple[Any, Any]]

For a term, each changed field with its value before and after.

affected: Affected

What is downstream.

approvals: list[Approval]

The business terms bound to the column or downstream of it, and whether each was approved after this change.

effect: Effect | None = None

What the change does on today’s data, from konstruct verify --effect.

property subject: str
headline()[source]

What changed, in a few words.

lines()[source]

The entry as sentences, for text and markdown.

to_dict()[source]
class konstruct.analysis.release_notes.Affected(columns=0, relations=0, exposures=<factory>, terms=<factory>)[source]

Bases: object

What is downstream of a change.

columns: int = 0

Columns downstream.

relations: int = 0

Tables and models downstream, including those that join or filter on it.

exposures: list[str]

Dashboards and other dbt exposures downstream, with their owners.

terms: list[str]

Business terms bound to the column or downstream of it.

to_dict()[source]
text()[source]
class konstruct.analysis.release_notes.Approval(term, status, owner, approved=None, bound=True)[source]

Bases: object

A business term bound to a changed column, or downstream of it, and whether it was approved after the change.

term: str

The term’s name.

status: str | None

Its status now.

owner: str | None

Who owns it now.

approved: Commit | None = None

The first commit at or after the change that approved it; None when none has.

bound: bool = True

Whether the term is bound to the changed column itself (for a change to a relation’s rows, to one of its columns), rather than downstream of it.

to_dict()[source]
text()[source]

A sentence on the approval: by whom and when, or that there has been none since.

class konstruct.analysis.effect.Effect(rows_before=None, rows_after=None, columns=<factory>, every_column=False, key=<factory>, gained=None, only_before=None, only_after=None, lost=None, sampled=False, skipped=None)[source]

Bases: object

What one change does on today’s data: the changed model’s SQL before and after the commit, run on the same rows.

rows_before: int | None = None

Rows the model returns as it was before the commit.

rows_after: int | None = None

Rows it returns as it was after.

columns: list[ColumnEffect]

For a formula change, the changed column; for a change to the relation’s joins, filters or grouping, every column in both versions other than the key.

every_column: bool = False

Whether every column was compared, for a change to the relation’s joins, filters or grouping; then only the columns that moved are named in the summary.

key: list[str]

The key rows are matched by, when the relation has one.

gained: int | None = None

Keys only after the commit.

only_before: int | None = None

For a change to the rows of a relation with no key, the rows (over the columns both versions return) only the version before the commit gives, counted with repeats.

only_after: int | None = None

The rows only the version after the commit gives.

lost: int | None = None

Keys only before it.

sampled: bool = False

Whether a copy reached the row limit, so the result may be for a sample.

skipped: str | None = None

Why the effect could not be worked out, when it could not.

to_dict()[source]
summary()[source]

The effect as one sentence, every number in it from the two runs. For a change to the rows, only the columns that moved are named.

class konstruct.analysis.effect.ColumnEffect(column, total_before=None, total_after=None, matched=None, changed=None, only_before=None, only_after=None)[source]

Bases: object

What one change does to one column of the model on today’s data.

column: str

The column.

total_before: float | None = None

The sum of its values before the commit, when they are numbers.

total_after: float | None = None

The sum after.

matched: int | None = None

Rows of the same key in both versions, when the relation has a key.

changed: int | None = None

Of those, the rows whose value of the column differs.

only_before: int | None = None

For a formula change with no key to match rows by, the column’s values only the version before the commit gives, counted with repeats.

only_after: int | None = None

The values only the version after the commit gives.

property moved: bool

Whether the column’s sum or any of its values changed.

to_dict()[source]
text(key)[source]

Timeline

class konstruct.analysis.timeline.Timeline(since, until, paths=<factory>, entries=<factory>, commits=0, start=None, unknown=<factory>, notes=<factory>, warehouse=None)[source]

Bases: object

How the business’s meaning changed over a range of commits, oldest first.

since: str | None

a commit, tag or date, or None for the whole history.

Type:

Where the range starts

until: str

Where it ends.

paths: list[str]

Only commits that changed these files or directories, relative to the project.

entries: list[Entry]

The commits that changed something, in the order of history, oldest first.

commits: int = 0

Commits read in the range.

start: Commit | None = None

The commit whose world the first commit read is compared with, when it has a parent.

unknown: list[Commit]

Commits whose world could not be built.

notes: list[str]

What could not be read, and why.

warehouse: str | None = None

Where effects() ran the changes, when it did.

changes(kind=None)[source]

Every change with its commit, oldest first, or only those of one kind.

summary()[source]
release_notes()[source]

One sentence for each metric change, newest first, as a release note: what changed, in which commit and by whom, who approved it since, and what it does on today’s data once effects() ran it.

to_dict()[source]
markdown()[source]

The timeline as Markdown, to post to a wiki, a chat channel or a pull request.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

class konstruct.analysis.timeline.Entry(commit, changes=<factory>, compared_with=None)[source]

Bases: object

What one commit changed in the business’s meaning.

commit: Commit

The commit, with its author, co-authors and dates.

changes: list[WorldChange]

What changed, by kind and then by name.

compared_with: Commit | None = None

The commit whose world it is compared with, when that is not its parent, whose world could not be built.

to_dict()[source]
class konstruct.semantics.world_facts.WorldChange(kind, name, status, before=None, after=None, fields=<factory>, renamed_from=None, english=<factory>, approval=None, effect=None, effect_column=None, worded=<factory>)[source]

Bases: object

One thing whose meaning changed between two worlds.

kind: str

entity, relationship, property or metric.

name: str

What names it in the later world (in the earlier one, for one removed).

status: str

added, removed, renamed (and perhaps changed besides) or changed.

before: dict[str, Any] | None = None

Its facts in the earlier world; None when it was added.

after: dict[str, Any] | None = None

Its facts in the later world; None when it was removed.

fields: dict[str, tuple[Any, Any]]

For one renamed or changed, each fact that differs, with its value before and after.

renamed_from: str | None = None

For one renamed, its name in the earlier world.

english: list[str]

The change in plain English, one sentence for each part of it.

approval: Commit | None = None

For a metric, the first commit that approved it at or after this change, by ancestry.

effect: Any = None

For a metric whose formula or rows changed, what the change does on today’s data (konstruct.analysis.effect.Effect), once konstruct verify --effect ran it.

effect_column: str | None = None

The column or relation whose change effect is of.

worded: dict[str, list[str]]

english by the fact each sentence is about, and under note the release note’s words for a change of required filters.

property facts: dict[str, Any]

Its facts in the later world, or in the earlier one when it was removed.

mirrored()[source]

The same change read the other way, from the later world to the earlier one.

headline()[source]

What changed, in a few words.

to_dict()[source]
class konstruct.semantics.world_facts.World(entities=<factory>, relationships=<factory>, properties=<factory>, metrics=<factory>, dialect=None, notes=<factory>, digests=<factory>, texts=<factory>)[source]

Bases: object

The facts of one world that two builds are compared by.

entities: dict[str, dict[str, Any]]
relationships: dict[str, dict[str, Any]]
properties: dict[str, dict[str, Any]]
metrics: dict[str, dict[str, Any]]
dialect: str | None = None

The SQL dialect its formulas and conditions are written in.

notes: list[str]

What the build of this world could not read, and why.

digests: dict[str, str]

Each property column’s formula digest (see Formulas), by the column’s label.

texts: dict[str, str]

not compared, only worded.

Type:

The composed formula of a digest, for those composed

of(kind)[source]

The facts of one kind (entity, relationship, property or metric).

to_dict()[source]
classmethod from_dict(data)[source]
konstruct.semantics.world_facts.world_diff(a, b)[source]

What changed from world a to world b, by kind and then by name. Something that is gone and something new with the same signature (see _signature()), where exactly one of each has it, is one thing renamed. The diff of a world with itself is empty, and world_diff(b, a) is world_diff(a, b) with each change mirrored.

Search by Meaning

konstruct.analysis.semantic.search(graph, text, limit=20, kinds=None, out_dir=None)[source]

Nodes that match text by meaning or by name, best first: the word search and the vector search fused, an exact name first. Each result’s matched says what a match by meaning rests on most: the node’s own words, its lineage, its formula or its structure.

konstruct.analysis.semantic.similar(graph, ref, limit=10, kinds=None, out_dir=None)[source]

The nodes most like ref: named alike, near it in the lineage, computed by the same formula, or built from the same sources by the same path.

class konstruct.analysis.semantic.SimilarResult(node, results)[source]

Bases: object

The nodes most like one node, by words, lineage, formula and structure.

node: Item

The node compared.

results: list[Item]

The most similar nodes, best first, each with a score and what it matched on.

to_dict()[source]
text(style=None)[source]

The result as the CLI would print it: plain text, as str() gives, or drawn for a terminal with style.

konstruct.analysis.semantic.vector_index(graph, out_dir=None)[source]

The graph’s vector index, made on first use and kept until the graph changes. With out_dir, the index saved there is read and brought up to date rather than built anew, and saved there again when that changed it.

Formulas, Conflicts and Export

class konstruct.analysis.explain.Explainer(graph, max_composed=4000, *, qualified=False, boundary=())[source]

Bases: object

Explains columns of one graph, composing each column once however many ask for it.

boundary

where another build’s view stopped.

Type:

Columns composed as sources whatever their formula

memo: dict[str, _Composed]
active: set[str]
texts: dict[str, _Text]
writing: set[str]
deep_memo: dict[tuple[str, int], _Composed]
deep_texts: dict[tuple[str, int], _Text]
shared: dict[str, dict[str, frozenset[str]]]
upstream: dict[str, frozenset[str]]
composed_text(node)[source]

The composed formula of node with short names, rendered once per column.

compose_text(node, depth=0)[source]

What compose() gives, tidied and printed, built as text: each column’s own formula is printed once with its inputs left as placeholders, and their composed texts are put in their places, with the brackets the composed tree would keep. This gives the same text without copying and printing ever larger trees.

compose(node, depth=0)[source]

node’s expression with every input column replaced by that column’s own composed expression; a column without an expression is a source (or, if it has inputs, a gap).

compose_sql(tree, reads=None, owner=None)[source]

A free expression, such as one of a relation’s conditions, with every column in it replaced by that column’s composed formula, as compose() replaces a column’s inputs, with brackets tidied. tree is rewritten in place; reads is called with each column of the graph it refers to; owner is the relation whose expression it is.

inputs(node, upstream=None)[source]

How the columns in node’s expression resolve: for each, the relation and column name it names, and the column node it reads when the graph has one.

steps(column)[source]

Every column from column upstream, nearest first, with its formula and conditions.

explain(column)[source]

The formula behind column and how it composes down to sources (see explain()).

fingerprints()[source]

A hash per column of everything its summary depends on: its expression, its label and the fingerprints of the columns it is derived from (a Merkle hash over the lineage), so a cached summary is reused exactly when nothing upstream of the column changed.

summary(node)[source]
summaries(limit=50000, cache=None)[source]

Formulas for every column that has one, and conditions for every relation, compactly: {"columns": {id: {"e": expression, "c": composed, "s": sources, "p": partial}}, "conditions": {relation id: [condition, ...]}}. Used by graph.html.

With cache (the build cache), each column’s summary is keyed by its fingerprint and each relation’s conditions by their text and the names in the graph, so an update only recomputes what changed.

konstruct.analysis.explain.explain(graph, column)[source]

The formula behind column and how it composes down to sources.

konstruct.analysis.conflicts.find_conflicts(graph, limit=None, *, touching=None)[source]

Pairs of columns that appear to be one business idea computed differently, ranked.

With touching, a set of column ids, only the pairs with a column in it are compared: the conflicts are those of every pair that involve one of those columns, in the same order, at a cost that grows with how many columns it names rather than with the number of pairs.

konstruct.analysis.conflicts.core(name)[source]

The idea a column name stands for: its words, as search by meaning reads them (konstruct._core.terms()), without qualifiers, each folded to its concept. total_revenue, revenue and sales_amount all give {revenue}.

konstruct.outputs.export.export(graph, fmt, *, select=None, columns=False, namespace=None, job_namespace='konstruct', event_time=None, plugins=None)[source]

The graph (or the lineage of select) in fmt; see the module docstring. plugins is the registry to take the format from (default: Konstruct’s own formats).

Business Definitions

konstruct.semantics.definitions.parse(text, path)[source]

Parse a definitions file. Problems are collected as path:line: message, so one bad term does not hide the rest.

konstruct.semantics.definitions.parse_file(path, relative=None)[source]

Parse a definitions file from disk; relative is the path reported in problems.

konstruct.semantics.definitions.validate(terms)[source]

Problems that need every term at once: duplicate names, unknown related terms, and approved terms without an owner or an approval date. Column checks need the graph and are done by the build.

class konstruct.semantics.definitions.Term(name: 'str', definition: 'str', path: 'str', line: 'int', synonyms: 'list[str]' = <factory>, owner: 'Owner | None' = None, status: 'str' = 'draft', approved: 'str | None' = None, grain: 'str | None' = None, columns: 'list[str]' = <factory>, filters: 'list[str]' = <factory>, unit: 'str | None' = None, currency: 'str | None' = None, related: 'list[str]' = <factory>, broader: 'list[str]' = <factory>, exceptions: 'str | None' = None, kind: 'str | None' = None, iri: 'str | None' = None, sensitivity: 'str | None' = None)[source]

Bases: object

name: str
definition: str
path: str
line: int
synonyms: list[str]
owner: Owner | None = None
status: str = 'draft'
approved: str | None = None
grain: str | None = None
columns: list[str]
filters: list[str]
unit: str | None = None
currency: str | None = None
related: list[str]
broader: list[str]

a refund rate is a rate.

Type:

The wider concepts this one is a kind of (SKOS broader)

exceptions: str | None = None
kind: str | None = None

entity, measure or term; unset, the ontology infers it from the bound columns.

iri: str | None = None

For a term from a SKOS or OWL vocabulary, the concept’s IRI.

sensitivity: str | None = None

each bound column is marked sensitive, and so is every column computed from it, and a relation the term is bound to is marked to hold such data (see konstruct.analysis.sensitivity).

Type:

What kind of sensitive data the term’s columns hold, such as personal

property where: str

path:line, or for a vocabulary concept its file and IRI.

Type:

Where the term is defined

property slug: str
property names: list[str]

The name and every synonym.

to_dict()[source]
classmethod from_dict(data)[source]
class konstruct.semantics.definitions.Owner(name: 'str', role: 'str | None' = None)[source]

Bases: object

name: str
role: str | None = None

Configuration, Usage and Builds

konstruct.engine.config.load_config(root, out_dir=None, *, trusted=None)[source]

Read konstruct.toml (or [tool.konstruct] in pyproject.toml) from root over the defaults.

Parameters:
  • root (Path) – The project.

  • out_dir (Path | None) – The output directory, over out_dir in the configuration.

  • trusted (bool | None) – Whether the project’s own connections and paths are trusted. By default, as konstruct trust recorded them (see konstruct.engine.trust).

Raises:

ConfigError – The configuration is invalid, or a path setting (see PATH_SETTINGS) leads outside the project without being trusted.

class konstruct.engine.trust.Trust(root, state, sha256, files=<factory>, variables=<factory>, dbt_projects=<factory>, record=<factory>, action='')[source]

Bases: object

Whether a project’s own connection settings are trusted, and what that covers.

root: Path

The project.

state: str

trusted (recorded and unchanged), environment (KONSTRUCT_TRUST_CONFIG), changed (recorded, but the content has changed since) or untrusted.

sha256: str

The digest of the content that carries connections, as konstruct trust records it.

files: list[str]

The files that content comes from, relative to the project.

variables: list[str]

The environment variables that content reads once it is trusted.

dbt_projects: list[str]

The dbt projects Konstruct runs dbt in only once the project is trusted. The record does not cover their models and macros, which dbt runs with the user’s environment.

record: Path

The user’s file that records trust.

action: str = ''

recorded, unchanged (the same record was already there), revoked, or empty for a check.

Type:

What the call did

property trusted: bool

Whether the build uses the project’s own connections and paths.

to_dict()[source]
property environment_variable: str

The environment variable that trusts every project.

text(style=None)[source]

The result as the CLI prints it: plain text, as str() gives, or drawn for a terminal with style.

konstruct.engine.trust.check(root)[source]

Whether the project at root has its own connection settings trusted.

Raises:

konstruct.engine.config.ConfigError – The configuration cannot be read.

konstruct.engine.trust.trust(root)[source]

Trust the project’s own connection settings as they are now, until they change.

Raises:

konstruct.engine.config.ConfigError – The configuration cannot be read.

konstruct.engine.trust.revoke(root)[source]

Remove the record that trusts the project’s own connection settings.

Raises:

konstruct.engine.config.ConfigError – The configuration cannot be read.

konstruct.engine.trust.trust_file()[source]

Where trust is recorded: $XDG_CONFIG_HOME/konstruct/trusted.json, otherwise the platform’s user configuration directory (~/.config, ~/Library/Application Support or %APPDATA%).

class konstruct.inputs.usage.UsageSettings(enabled: 'bool' = False, days: 'int' = 90, exclude_users: 'list[str]' = <factory>, objects: 'str' = 'direct', max_queries: 'int' = 50000)[source]

Bases: object

enabled: bool = False
days: int = 90
exclude_users: list[str]

Service accounts to leave out, such as the one dbt and the orchestrator run as.

objects: str = 'direct'

For Snowflake, count direct objects (as queried) or base objects (underlying tables).

max_queries: int = 50000

For BigQuery, the most distinct (query text, user) pairs parsed.

konstruct.engine.build.build(config, *, update=False)[source]

Build the graph for config and write graph.json, REPORT.md, graph.html, ontology.yml and explain.json; konstruct.Project.build() is the usual way in.

Plugins

The plugin API (see Writing a Plugin): what a plugin provides for each entry-point group, and what Konstruct gives it. Everything here is imported from konstruct.plugins.

konstruct.plugins.api.API_VERSION: tuple[int, int] = (1, 2)

The version of the plugin API this release of Konstruct runs, as (major, minor). The minor grows when the API gains something a plugin may use; the major only when a type or protocol here changes incompatibly. Konstruct runs a plugin that targets the same major and a minor no higher than its own; a plugin’s api_version = 1 means (1, 0).

konstruct.plugins.api.RESERVED_NODE_ATTRS

The attribute names a plugin’s node may not set, which are every name Konstruct reads from attributes bar PLUGIN_NODE_ATTRS; sorted(RESERVED_NODE_ATTRS) lists them.

konstruct.plugins.api.PLUGIN_NODE_ATTRS = frozenset({'bash_command', 'label', 'maturity', 'operator', 'orchestrator', 'owner', 'schedule', 'severity', 'test', 'type', 'url'})

The attribute names Konstruct reads that a plugin’s node may set, each a string with the meaning Konstruct gives it, which are test and severity on a test node, naming the test (such as unique or not_null) and how much a failure counts, owner, who owns a DAG or task, and those the graph viewer shows: schedule, operator, orchestrator and bash_command for a DAG or task, and type, maturity, label and url for an exposure.

konstruct.plugins.api.PLUGIN_LIST_ATTRS = frozenset({'fields'})

The attribute names Konstruct reads that a plugin’s node or edge may set as a list of strings, with the meaning Konstruct gives it: fields, the fields a BI view or explore shows, which the Looker input sets and search by meaning reads.

konstruct.plugins.api.PLUGIN_MAP_ATTRS = frozenset({'measure_filters', 'measures'})

The attribute names Konstruct reads that a plugin’s node may set as a map from strings to strings, with the meaning Konstruct gives them: measures, each measure a BI view or model defines with its formula, and measure_filters, a measure’s filters as the tool writes them, which every answer from it applies. The Looker input sets both and the ontology’s metrics read them. Added in plugin API 1.1.

konstruct.plugins.api.RESERVED_EDGE_ATTRS

The attribute names a plugin’s edge may not set, which are those Konstruct sets on every edge (origin, source, target, kind) and every name Konstruct reads from attributes, such as verified, transform, expression, via and role, which carry its own lineage claims; sorted(RESERVED_EDGE_ATTRS) lists them.

konstruct.plugins.api.MAX_DEPTH = 64

How deeply a node’s or edge’s attribute values, or a cached value, may nest lists and dicts.

class konstruct.plugins.InputPlugin(*args, **kwargs)[source]

Bases: Protocol

konstruct.inputs: a reader for a kind of file (an orchestrator, BI tool or loader).

api_version: tuple[int, int] | int

The plugin API version the input targets.

patterns: Sequence[str]

File-name patterns to discover, such as *.pipeline.yml; [<name>] paths in konstruct.toml replaces discovery. With no patterns, read() is called once with no files (an input that reads from elsewhere).

read(files, context)[source]

What the files hold, as a fragment of the graph.

class konstruct.plugins.InputFile(path, content)[source]

Bases: object

One file discovery found for an input.

path: str

The file, relative to the project root, as node path values use it.

content: bytes

Its bytes.

property text: str

The content as UTF-8 text, with undecodable bytes replaced.

class konstruct.plugins.InputContext(root, dialect, options, cached)[source]

Bases: object

What Konstruct gives an input besides its files.

root: Path

The project root, for reference; read files from InputFile, not from here.

dialect: str | None

The project’s SQL dialect, when one is known.

options: Mapping[str, Any]

The input’s own table in konstruct.toml ([<name>]), or an empty mapping.

cached: Callable[[tuple[Any, ...], Callable[[], Any]], Any]

Called as cached(key, compute), gives the JSON-ready value compute() returns, kept for konstruct build --update while key (a tuple of JSON-ready parts, such as a hash of a file’s content) and the plugin’s version are unchanged. A value that is not JSON-ready, or nests deeper than MAX_DEPTH, raises an error and is not kept.

class konstruct.plugins.Fragment(nodes=<factory>, edges=<factory>, sql=<factory>, warnings=<factory>, sources=<factory>, counts=<factory>)[source]

Bases: object

What an input read: nodes, edges and SQL to merge into the graph. Konstruct validates the whole fragment first and merges nothing from one that is not valid.

nodes: list[PluginNode]

Nodes to add.

edges: list[PluginEdge]

Edges to add, in order.

sql: list[Sql]

SQL to analyse for lineage.

warnings: list[str]

Problems with the input, given as build warnings with the input’s name in front.

sources: list[Source]

What each file held, for the build summary.

counts: dict[str, int]

Counts for konstruct.engine.build.BuildResult.inputs, each a 64-bit integer named <input>_<key>.

problems()[source]

Everything that stops the fragment being merged; empty when it is valid. Each node, edge, SQL statement, source and warning is checked as graph.json would hold it, so what passes can be written there and read back unchanged.

class konstruct.plugins.PluginNode(id, kind, name, path=None, description=None, attrs=<factory>)[source]

Bases: object

A node an input adds. Its id in the graph is the input’s name, a dot and id.

id: str

An id unique within the fragment, such as pipeline.daily_revenue; no # or line break.

kind: str

One of PLUGIN_NODE_KINDS.

name: str

Its own name, as people know it.

path: str | None = None

The file that defines it, relative to the project.

description: str | None = None

What it is.

attrs: dict[str, Any]

Anything else, as JSON-ready data, under names not in RESERVED_NODE_ATTRS.

class konstruct.plugins.PluginEdge(source, target, kind, attrs=<factory>, missing=None)[source]

Bases: object

An edge an input adds, in the graph’s directions (source -> target).

source: str | Relation | Column

Where it starts.

target: str | Relation | Column

Where it ends.

kind: str

One of PLUGIN_EDGE_KINDS; derived_from joins two Column ends.

attrs: dict[str, Any]

Details, as JSON-ready data, under names not in RESERVED_EDGE_ATTRS; origin is set by Konstruct.

missing: str | None = None

The warning to give, instead of the default, when a Column end is not a known column.

class konstruct.plugins.Relation(key)[source]

Bases: object

A warehouse relation named by its key (schema.table or catalog.schema.table), as SQL names it. It resolves to the node the build already has for that relation, or to a new inferred table.

key: str

The relation’s name, such as analytics.orders.

class konstruct.plugins.Column(relation, name)[source]

Bases: object

A column of a warehouse relation. When the build knows the relation’s columns from something that defines it (the warehouse, dbt, or SQL that writes it) and this column is not one of them, the edge is left out with a warning; otherwise the column is created.

relation: str

The relation’s name, as for Relation.

name: str

The column’s name.

class konstruct.plugins.Sql(sql, path, task=None, dialect=None)[source]

Bases: object

SQL that Konstruct analyses for column lineage, as it does SQL files.

sql: str

One or more statements.

path: str

The file it comes from, relative to the project, for warnings and via.

task: str | None = None

the task then feeds the tables the SQL writes, and the tables it reads feed the task.

Type:

The id of the fragment’s task node that runs it

dialect: str | None = None

Its SQL dialect, when it is not the project’s.

class konstruct.plugins.Source(path, detail)[source]

Bases: object

One line of the build summary and of graph.json’s meta.sources.

path: str

The file read.

detail: str

What was found in it.

class konstruct.plugins.ExportPlugin(*args, **kwargs)[source]

Bases: Protocol

konstruct.exports: a format for konstruct export, named by the entry point.

api_version: tuple[int, int] | int

The plugin API version the format targets.

description: str

what the format is for.

Type:

One line

export(graph, request)[source]

The graph (or the lineage request.keep names) in the format.

class konstruct.plugins.ExportRequest(select=None, keep=None, columns=False, namespace=None, job_namespace='konstruct', event_time=None)[source]

Bases: object

The options of one konstruct export.

select: Node | None = None

The node given with --select, if any.

keep: frozenset[str] | None = None

With select, the ids of the relations and tasks in its lineage, both directions.

columns: bool = False

draw columns as well as relations.

Type:

--columns

namespace: str | None = None

a dataset namespace.

Type:

--namespace

job_namespace: str = 'konstruct'

The namespace of jobs, for formats that have one.

event_time: str | None = None

A fixed event time, for formats that have one.

class konstruct.plugins.CheckPlugin(*args, **kwargs)[source]

Bases: Protocol

konstruct.checks: a rule of konstruct gate. Each Finding it returns fails the gate; a third-party plugin’s rules are named <entry point>.<rule>.

api_version: tuple[int, int] | int

The plugin API version the check targets.

description: str

what the check stops.

Type:

One line

check(context)[source]

What stops the change, each finding with its fix.

class konstruct.plugins.CheckContext(graph, diff, conflicts, laws=None)[source]

Bases: object

What a gate check sees: the graph at the head and what the change does.

graph: Graph

The graph, up to date with the change.

diff: Diff

The change, as konstruct diff describes it.

conflicts: Conflicts

The conflicting definitions at the head, as konstruct conflicts finds them.

laws: Laws | None = None

the laws.json that konstruct verify --laws writes, as committed at the base, or, with no git base to read, as it is in the output directory; None when there is none. Since plugin API 1.2.

Type:

The data laws as recorded before the change

class konstruct.plugins.ToolsPlugin(*args, **kwargs)[source]

Bases: Protocol

konstruct.mcp_tools: tools for konstruct mcp, each named <entry point>_<tool>.

api_version: tuple[int, int] | int

The plugin API version the tools target.

tools(project)[source]

Each tool’s short name with its function, bound to project. The function’s typed parameters are the tool’s input and its docstring the description agents read; it returns JSON-ready data.

class konstruct.plugins.Plugins(plugins, missing=())[source]

Bases: object

The registry for one project: every component, which of them run, and the problems found.

plugins

Every component, Konstruct’s own first, then installed plugins by distribution.

missing

Distributions [plugins] enabled names that are not installed.

off: str | None

Why plugins are turned off, when they are.

property problems: list[str]

What stops a plugin the project enables from running, for build warnings.

classmethod builtin()[source]

Konstruct’s own components alone.

classmethod discover(enabled=(), *, off=False, environment=True)[source]

Konstruct’s own components and every installed plugin, running those whose distributions enabled names unless off (--no-plugins) or, with environment, KONSTRUCT_NO_PLUGINS turns plugins off.

classmethod for_config(config)[source]

The registry as config sets it: its [plugins] enabled list, unless plugins are turned off.

names(group)[source]

Every name in group, whether or not it runs, each once.

get(group, name)[source]

The component of group called name, loaded if it runs.

group(group)[source]

The components of group that run, loaded; one that cannot be loaded is left out, with a problem recorded.

failed(group)[source]

The components of group the project enables that could not be loaded or were refused.

installed()[source]

The components of installed plugins, not Konstruct’s own.

tools(project)[source]

The MCP tools of the plugins that run, each named <plugin>_<tool>, bound to project and with the plugin it comes from. A plugin whose tools raises or returns no mapping, a tool named badly, and a tool whose joined name another plugin’s tool already has, are left out with a warning logged.

load_all()[source]

Load every component that runs, so their API versions and load errors are known.

to_dict()[source]

The registry as JSON-ready data: the plugin API version, every component and the problems.

text(style=None)[source]

The listing as the CLI prints it, the same text on a terminal as in a pipe.

class konstruct.plugins.Plugin(name, group, target, distribution, version, builtin, enabled, reason, api_version=None, error=None, loaded=None, requested=False)[source]

Bases: object

One component in the registry: Konstruct’s own, or one an installed plugin provides.

name: str

an input’s name, an export format, a check’s rule prefix, a tool prefix or a pack directory’s name.

Type:

The entry point’s name

group: str

One of GROUPS.

target: str

module:attribute, or a package path for packs.

Type:

What the entry point names

distribution: str

The distribution that provides it (konstruct for Konstruct’s own).

version: str

The distribution’s version.

builtin: bool

Whether it is part of Konstruct.

enabled: bool

Whether it runs for this project.

reason: str

Why it does or does not run.

api_version: tuple[int, int] | None = None

The plugin API version it targets, once loaded, as (major, minor).

error: str | None = None

Why it could not be loaded or was refused, when it could not.

loaded: Any = None

The loaded object, or the directory for a pack.

requested: bool = False

Whether the project enabled it before it was refused.

property label: str

input 'pipelines' (konstruct-yaml-pipelines 0.1.0).

Type:

How messages name it

to_dict()[source]

The plugin as JSON-ready data.

exception konstruct.plugins.PluginError[source]

Bases: Exception

A plugin that cannot be used as asked, or that failed while it ran.

Errors

exception konstruct.QueryError(message, *, suggestions=None, hint=None)[source]

Bases: Exception

A reference that matches nothing or several things, or a missing graph.

suggestions holds the names the reference may have meant and hint a command that could help, for the CLI to show; the message alone says the same in one line.

exception konstruct.FormatError[source]

Bases: Exception

A document whose format this Konstruct cannot read: written by a newer Konstruct, or not a Konstruct document at all.

exception konstruct.analysis.diff.DiffError[source]

Bases: Exception

exception konstruct.engine.config.ConfigError[source]

Bases: Exception

exception konstruct.engine.config.UntrustedConfig[source]

Bases: ConfigError

A connection or path from the repository’s own configuration that needs konstruct trust.

exception konstruct.inputs.warehouse.MissingDriver[source]

Bases: Exception

A warehouse needs an optional extra: pip install ‘konstruct[snowflake]’ and so on.