Skip to content

feat!: give clair a Python API, and make the CLI a thin wrapper - #39

Draft
OmerBaddour wants to merge 3 commits into
mainfrom
worktree-python-api
Draft

feat!: give clair a Python API, and make the CLI a thin wrapper#39
OmerBaddour wants to merge 3 commits into
mainfrom
worktree-python-api

Conversation

@OmerBaddour

@OmerBaddour OmerBaddour commented Aug 26, 2026

Copy link
Copy Markdown
Member

feat!: give clair a Python API, and make the CLI a thin wrapper

clair.run(), clair.compile(), clair.test(), clair.catalog() and
clair.serve_docs() do the complete operation. The new module clair/api.py
holds them. Each CLI command now parses the arguments, calls one function, and
sets the exit code.

A caller reads the result object, and parses no log line:

  • RunResult gives the statements that clair executed, the staging address,
    the effective run mode, the row count and the test results.
  • RunSummary gives the run_id, the project_root, the run mode, a result()
    lookup by address, and test_results.
  • CompiledNodeInfo gives the logical address, the physical address, the
    staging address, the effective run mode and the artifact path.
  • CompileOutput gives the run_id, the project_root, the env_name, the run
    mode and a node() lookup by address.

Behaviour changes:

  • clair.serve_docs() is the name of the docs operation, because clair.docs
    is the docs package.
  • The routing warnings are structured log messages, not click output. The
    clair validate output does not change.
  • format_run_output() and format_test_output() are removed. Make a
    RunSummary or a TestSummary directly.
  • expand_selectors() accepts each sequence of patterns, not a tuple only.

refactor!: run the integration tests through the Python API

The integration tests ran the CLI as a subprocess and read the JSON log lines.
A test saw the exit code and the log stream only, thus it could not ask which
statement failed, or which data quality test rejected the data. The tests now
call clair.run() and clair.test() in the process of the test, and they read
the summary that the call gives.

New assertions that the log lines could not carry:

  • The failed test names the Trouve, the test type and the query id.
  • The MERGE that fails is the statement at failed_statement_index, and clair
    ran no data quality test after it.
  • A staged run reports the staging address that it built at, thus a test names
    that object, and computes no name of its own.
  • An incremental run reports the effective run mode of each Trouve.

Other changes:

  • clair.docs() is the name of the docs operation, and the docs package is
    clair/webui/. Each command of the CLI now has one function of the same
    name in clair/api.py.
  • RunResult.failed_statement_index gives the index in sql of the statement
    that failed.
  • discover_project() removes the modules of each project that this process
    loaded, and takes the other project roots off sys.path. Two projects can
    give one module the same name, for example mydb.refined.checked. The second
    discovery read the files of the first project.
  • tests/unit/test_cli_commands.py covers the CLI: the arguments that each
    command gives to the API, and the status code that it gives back.

test: assert the statements of each example run, and start no extra run

The full refresh test has the summary of the run already. It therefore asserts
the staging address and the statements of each Trouve there. A test of its own
would start one more run on the warehouse, and prove the same thing.

🤖 Generated with Claude Code

OmerBaddour and others added 3 commits August 26, 2026 18:08
`clair.run()`, `clair.compile()`, `clair.test()`, `clair.catalog()` and
`clair.serve_docs()` do the complete operation. The new module `clair/api.py`
holds them. Each CLI command now parses the arguments, calls one function, and
sets the exit code.

A caller reads the result object, and parses no log line:

* `RunResult` gives the statements that clair executed, the staging address,
  the effective run mode, the row count and the test results.
* `RunSummary` gives the run_id, the project_root, the run mode, a `result()`
  lookup by address, and `test_results`.
* `CompiledNodeInfo` gives the logical address, the physical address, the
  staging address, the effective run mode and the artifact path.
* `CompileOutput` gives the run_id, the project_root, the env_name, the run
  mode and a `node()` lookup by address.

Behaviour changes:

* `clair.serve_docs()` is the name of the docs operation, because `clair.docs`
  is the docs package.
* The routing warnings are structured log messages, not click output. The
  `clair validate` output does not change.
* `format_run_output()` and `format_test_output()` are removed. Make a
  `RunSummary` or a `TestSummary` directly.
* `expand_selectors()` accepts each sequence of patterns, not a tuple only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The integration tests ran the CLI as a subprocess and read the JSON log lines.
A test saw the exit code and the log stream only, thus it could not ask which
statement failed, or which data quality test rejected the data. The tests now
call `clair.run()` and `clair.test()` in the process of the test, and they read
the summary that the call gives.

New assertions that the log lines could not carry:

* The failed test names the Trouve, the test type and the query id.
* The MERGE that fails is the statement at `failed_statement_index`, and clair
  ran no data quality test after it.
* A staged run reports the staging address that it built at, thus a test names
  that object, and computes no name of its own.
* An incremental run reports the effective run mode of each Trouve.

Other changes:

* `clair.docs()` is the name of the docs operation, and the docs package is
  `clair/webui/`. Each command of the CLI now has one function of the same
  name in `clair/api.py`.
* `RunResult.failed_statement_index` gives the index in `sql` of the statement
  that failed.
* `discover_project()` removes the modules of each project that this process
  loaded, and takes the other project roots off `sys.path`. Two projects can
  give one module the same name, for example `mydb.refined.checked`. The second
  discovery read the files of the first project.
* `tests/unit/test_cli_commands.py` covers the CLI: the arguments that each
  command gives to the API, and the status code that it gives back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The full refresh test has the summary of the run already. It therefore asserts
the staging address and the statements of each Trouve there. A test of its own
would start one more run on the warehouse, and prove the same thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant