Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions elementary/artifacts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# `edr artifacts`

Query Elementary's warehouse artifact tables from the CLI for agent and script consumption.

Every subcommand emits a JSON envelope on stdout by default. Logs and warnings go to stderr, so `edr artifacts ... | jq` always works without filtering. Errors are written to stderr as JSON with a stable `{error, code, details}` shape. Exit code `0` on success, `1` on user error (bad arguments, not found), `2` on system error (connection, unexpected).

## Global options

All commands accept:

| Flag | Purpose |
| --- | --- |
| `-o, --output {json,table}` | Output format. `json` (default) for agents; `table` for humans. |
| `--profile NAME` | Override the profile in `profiles.yml` (default `elementary`). |
| `-t, --profile-target NAME` | Target to load from the selected profile. |
| `-p, --profiles-dir PATH` | Directory containing `profiles.yml`. Defaults to CWD then `~/.dbt/`. |
| `--project-dir PATH` | Directory containing `dbt_project.yml`. Defaults to CWD. |
| `-c, --config-dir PATH` | Directory containing edr's `config.yml`. |
| `--target-path PATH` | Where edr writes its logs. |

List commands also accept `--limit N` (1–1000, default 200) and return `has_more: true` when the page is truncated. Fetch the next page by narrowing filters — cursors are not supported.

## JSON envelopes

List responses:

```json
{"count": 10, "has_more": false, "<entity_plural>": [...], "data": {"length": 10}}
```

Single-get responses:

```json
{"<entity_singular>": {...}}
```

Error responses (stderr):

```json
{"error": "Test test.x.y.z not found.", "code": "NOT_FOUND", "details": {"unique_id": "test.x.y.z"}}
```

## Commands

### Test results

- `edr artifacts test-results` — list test execution results.
- `edr artifacts test-result <test_execution_id>` — fetch a single test result.

Filters: `--test-unique-id`, `--model-unique-id`, `--test-type`, `--test-sub-type`, `--test-name` (LIKE), `--status`, `--table-name` (LIKE), `--column-name`, `--database-name`, `--schema-name`, `--severity`, `--detected-after`, `--detected-before`.

### Run results (dbt model execution)

- `edr artifacts run-results` — list model run results.
- `edr artifacts run-result <model_execution_id>` — fetch a single run result.

Filters: `--unique-id`, `--invocation-id`, `--status`, `--resource-type`, `--materialization`, `--name` (LIKE), `--started-after`, `--started-before`, `--execution-time-gt`, `--execution-time-lt`, `--include-compiled-code/--no-include-compiled-code` (default: exclude).

### Invocations

- `edr artifacts invocations` — list dbt invocations (default window: last 7 days).
- `edr artifacts invocation <invocation_id>` — fetch a single invocation.

Filters: `--invocation-id` (repeatable), `--command`, `--project-name`, `--orchestrator`, `--job-id`, `--job-run-id`, `--target-name`, `--target-schema`, `--target-profile-name`, `--full-refresh/--no-full-refresh`, `--started-after`, `--started-before`.

### Models

- `edr artifacts models` — list dbt model definitions.
- `edr artifacts model <unique_id>` — fetch a single model definition.

Filters: `--database-name`, `--schema-name`, `--materialization`, `--name` (LIKE over name/alias), `--package-name`, `--group-name`, `--generated-after`, `--generated-before`.

### Sources

- `edr artifacts sources` — list dbt source definitions.
- `edr artifacts source <unique_id>` — fetch a single source.

Filters: `--database-name`, `--schema-name`, `--source-name`, `--name` (LIKE on table name), `--identifier`, `--package-name`, `--generated-after`, `--generated-before`.

### Tests

- `edr artifacts tests` — list dbt test definitions.
- `edr artifacts test <unique_id>` — fetch a single test definition.

Filters: `--database-name`, `--schema-name`, `--name` (LIKE over name/short_name/alias), `--package-name`, `--test-type {generic,singular,expectation}`, `--test-namespace`, `--severity {warn,error}`, `--parent-model-unique-id`, `--quality-dimension`, `--group-name`, `--generated-after`, `--generated-before`.

## Examples

```bash
# Recent failed tests as JSON
edr artifacts test-results --status fail --detected-after 2026-01-01 --limit 50

# One invocation, human-readable
edr artifacts invocation 4a0b... -o table

# Models in a schema, filtered by name
edr artifacts models --schema-name analytics --name orders

# Tests covering a specific model
edr artifacts tests --parent-model-unique-id model.my_pkg.orders
```
Empty file.
48 changes: 48 additions & 0 deletions elementary/artifacts/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import sys

import click

from elementary.artifacts.entities import invocations as invocations_cmds
from elementary.artifacts.entities import models as models_cmds
from elementary.artifacts.entities import run_results as run_results_cmds
from elementary.artifacts.entities import sources as sources_cmds
from elementary.artifacts.entities import test_results as test_results_cmds
from elementary.artifacts.entities import tests as tests_cmds
from elementary.artifacts.fetching import ArtifactFetchError
from elementary.artifacts.output import ErrorCode, emit_error


@click.group("artifacts")
def artifacts():
"""Query Elementary's artifact tables for agent and script consumption.

Every subcommand emits JSON to stdout by default (use `-o table` for
human-readable output). Errors are written to stderr as JSON with a
stable `{error, code, details}` shape. Exit code 0 on success, 1 on
user error, 2 on system error.
"""
pass


def _handle_fetch_error(exc: ArtifactFetchError) -> None:
code = emit_error(str(exc), exc.code, exc.details)
sys.exit(code)


def _handle_bad_argument(message: str, details: dict = None) -> None:
code = emit_error(message, ErrorCode.BAD_ARGUMENT, details or {})
sys.exit(code)


artifacts.add_command(test_results_cmds.test_results)
artifacts.add_command(test_results_cmds.test_result)
artifacts.add_command(run_results_cmds.run_results)
artifacts.add_command(run_results_cmds.run_result)
artifacts.add_command(invocations_cmds.invocations)
artifacts.add_command(invocations_cmds.invocation)
artifacts.add_command(models_cmds.models)
artifacts.add_command(models_cmds.model)
artifacts.add_command(sources_cmds.sources)
artifacts.add_command(sources_cmds.source)
artifacts.add_command(tests_cmds.tests)
artifacts.add_command(tests_cmds.test)
92 changes: 92 additions & 0 deletions elementary/artifacts/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import sys
from typing import Callable

import click

from elementary.config.config import Config


def is_artifacts_invocation() -> bool:
"""True when the current edr command is `edr artifacts ...`.

Used at CLI bootstrap to gate stdout-polluting side effects (logo,
version-upgrade banner, info logs) so `edr artifacts` can emit pure JSON.
"""
argv = sys.argv[1:]
for arg in argv:
if arg.startswith("-"):
continue
return arg == "artifacts"
return False


def common_options(func: Callable) -> Callable:
func = click.option(
"--output",
"-o",
type=click.Choice(["json", "table"]),
default="json",
help="Output format. JSON is default and intended for agent/script use.",
)(func)
func = click.option(
"--target-path",
type=str,
default=Config.DEFAULT_TARGET_PATH,
help="Absolute target path for saving edr files such as logs.",
)(func)
func = click.option(
"--config-dir",
"-c",
type=click.Path(),
default=Config.DEFAULT_CONFIG_DIR,
help="Directory containing edr's config.yml.",
)(func)
func = click.option(
"--profile",
"profile_name",
type=str,
default=None,
help=(
"Override the profile name from profiles.yml (defaults to "
"'elementary'). Useful when elementary artifact tables live "
"in a warehouse configured under a different profile."
),
)(func)
func = click.option(
"--profile-target",
"-t",
type=str,
default=None,
help="Which target to load from the selected profile.",
)(func)
func = click.option(
"--profiles-dir",
"-p",
type=click.Path(exists=True),
default=None,
help="Directory containing profiles.yml. Defaults to CWD then HOME/.dbt/.",
)(func)
func = click.option(
"--project-dir",
type=click.Path(exists=True),
default=None,
help="Directory containing dbt_project.yml. Defaults to CWD.",
)(func)
return func


def build_config(
config_dir: str,
profiles_dir,
project_dir,
profile_target,
target_path: str,
) -> Config:
return Config(
config_dir=config_dir,
profiles_dir=profiles_dir,
project_dir=project_dir,
profile_target=profile_target,
target_path=target_path,
quiet_logs=True,
)
Empty file.
Loading
Loading