Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ models our transcripts name, plus local overrides for models litellm doesn't pri
yet. Refresh it by hand, never by editing the JSON:

```bash
uv run scripts/update_prices.py --ref v1.102.0 # a litellm tag, branch, or commit
uv run scripts/update_prices.py --ref v1.103.2 # a litellm tag, branch, or commit
```

The script drops an override once upstream prices the same key and says so; delete
Expand Down
2 changes: 1 addition & 1 deletion docs/insights/contract-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ dependencies = [
"atif-corpus==0.1.0",
"atif-duck==0.1.0",
"atif-embed==0.1.0",
"cyclopts>=4.10.2",
"cyclopts>=5.1.1",
"loguru>=0.7.3",
]
```
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The `atif-sql` console script (`packages/atif-cli/pyproject.toml:42`) is the repo's public contract: one cyclopts router (`packages/atif-cli/src/atif_cli/app.py:52`) dispatches the subcommands below, and a root-level `--version` resolves the installed `atif-sql` distribution (`:60`).

These conventions hold across every subcommand. `--format` takes `auto`, `table`, `json`, or `csv` from the `OutputFormat` `StrEnum` at `packages/atif-cli/src/atif_cli/output.py:58`, and `auto` resolves to `table` when stdout is a TTY and `json` otherwise (`packages/atif-cli/src/atif_cli/output.py:74`). Exit codes come from one table, `EXIT_CODES` at `packages/atif-cli/src/atif_cli/errors.py:28`: `0` ok, `2` empty-session / no-embeddings, `64` invalid input or SQL parse error, `65` catalog error / validation error / embedding mismatch, `70` runtime error, `78` terminal state or suspicious scan, `127` harbor missing. A number in that table is a wire contract: keys may be added, never renumbered. And stderr carries WARNING and above only, so a piped read emits data on stdout and nothing on stderr unless something is wrong; `ATIF_SQL_LOG_LEVEL` widens it (`INFO`, `DEBUG`) and is the only way to reach the INFO surface, because the entry point replaces loguru's default handler and with it the `LOGURU_LEVEL` that would have parameterized it (`packages/atif-cli/src/atif_cli/app.py:1141-1148`).
These conventions hold across every subcommand. `--format` takes `auto`, `table`, `json`, or `csv` from the `OutputFormat` `StrEnum` at `packages/atif-cli/src/atif_cli/output.py:58`, and `auto` resolves to `table` when stdout is a TTY and `json` otherwise (`packages/atif-cli/src/atif_cli/output.py:74`). Exit codes come from one table, `EXIT_CODES` at `packages/atif-cli/src/atif_cli/errors.py:28`: `0` ok, `2` empty-session / no-embeddings, `64` invalid input or SQL parse error, `65` catalog error / validation error / embedding mismatch, `70` runtime error, `78` terminal state or suspicious scan, `127` harbor missing. A number in that table is a wire contract: keys may be added, never renumbered. A usage error cyclopts refuses (an unknown option or command, a missing or uncoercible value) prints cyclopts' error panel and exits `64`, never cyclopts' own `2`. And stderr carries WARNING and above only, so a piped read emits data on stdout and nothing on stderr unless something is wrong; `ATIF_SQL_LOG_LEVEL` widens it (`INFO`, `DEBUG`) and is the only way to reach the INFO surface, because the entry point replaces loguru's default handler and with it the `LOGURU_LEVEL` that would have parameterized it (`packages/atif-cli/src/atif_cli/app.py:1141-1148`).

`analyze`, `embed`, and `search` call Amazon Bedrock and spend money; the others are offline.

Expand Down
2 changes: 1 addition & 1 deletion packages/atif-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ dependencies = [
"atif-corpus==0.1.0",
"atif-duck==0.1.0",
"atif-embed==0.1.0",
"cyclopts>=4.10.2",
"cyclopts>=5.1.1",
# Imported directly by duck_errors.py to classify a DuckDB failure into the
# parse/catalog/runtime exit-code taxonomy. Reaching it transitively through
# atif-duck would make this package's exit codes depend on a sibling's
Expand Down
12 changes: 11 additions & 1 deletion packages/atif-cli/src/atif_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2368,6 +2368,10 @@ def main() -> None:
An unknown level falls back to WARNING and says so, rather than replacing
the caller's command with a loguru traceback — a bad log level is not a
reason to refuse to run.

A usage error cyclopts refuses (an unknown option, a missing or uncoercible
value) exits ``EXIT_CODES["invalid_input"]`` (64) after cyclopts prints its
error panel, never cyclopts' own 2, which the table gives to ``empty_session``.
"""
from loguru import logger

Expand All @@ -2380,7 +2384,13 @@ def main() -> None:
logger.warning(
"{}={!r} is not a loguru level; using {}", LOG_LEVEL_ENV, level, DEFAULT_LOG_LEVEL
)
app()
try:
app(sys.argv[1:], exit_on_error=False)
except cyclopts.CycloptsError:
# cyclopts 5 exits 2 on a usage error (unknown option, missing or uncoercible
# value), and EXIT_CODES gives 2 to empty_session and no_embeddings. A malformed
# flag is invalid_input. cyclopts has already printed its error panel.
raise SystemExit(EXIT_CODES["invalid_input"]) from None


__all__ = [
Expand Down
4 changes: 3 additions & 1 deletion packages/atif-cli/src/atif_cli/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
view/macro/column), 70 for a runtime error. No other package owns a mapping —
atif-converter raises typed :class:`~atif_converter.domain.errors.DomainError`
subclasses and deliberately carries no codes, so ``convert`` translates them
here and every command exits from the same dict.
here and every command exits from the same dict. A usage error cyclopts
refuses exits ``invalid_input``: ``atif_cli.app.main`` maps cyclopts 5's own
exit 2, which this table gives to ``empty_session``.

A number in this dict is a WIRE CONTRACT. An agent branches on it, so
reassigning an existing key to a different number is a breaking change even
Expand Down
35 changes: 34 additions & 1 deletion packages/atif-cli/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2042,7 +2042,10 @@ def _emit_through_main(
"""
from loguru import logger

monkeypatch.setattr(app_module, "app", lambda: None)
def _no_dispatch(*_args: object, **_kwargs: object) -> None:
return None

monkeypatch.setattr(app_module, "app", _no_dispatch)
app_module.main()
try:
logger.info("an info line")
Expand Down Expand Up @@ -2076,3 +2079,33 @@ def test_unknown_level_falls_back_and_says_so(
assert "is not a loguru level" in err
assert "an info line" not in err
assert "a warning line" in err


class TestUsageErrorExitCode:
"""A flag cyclopts refuses exits invalid_input (64), never cyclopts 5's own 2.

cyclopts 5 exits 2 on every parse error, and :data:`EXIT_CODES` gives 2 to
``empty_session`` and ``no_embeddings``, so an agent branching on 2 would read
a typo as an empty session. ``main`` maps the parse error onto the table.
"""

@pytest.mark.parametrize(
"argv",
[["--bogus"], ["convert", "--bogus"], ["search", "a question", "-k", "many"]],
)
def test_parse_error_exits_invalid_input(
self,
argv: list[str],
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr(sys, "argv", ["atif-sql", *argv])
try:
with pytest.raises(SystemExit) as exc:
app_module.main()
finally:
logger.remove()
assert exc.value.code == EXIT_CODES["invalid_input"] == 64
captured = capsys.readouterr()
assert captured.out == ""
assert "Error" in captured.err, "cyclopts must still print its error panel"
2 changes: 1 addition & 1 deletion packages/atif-converter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ value: `atif_converter.domain.pricing` returns the same floats, bit for bit,
without litellm being installed. It reads `domain/model_prices.json`, a
filtered copy of litellm's public `model_prices_and_context_window.json` (MIT;
the notice and source ref are in its `meta` block), and repeats litellm
1.102.0's arithmetic in the same float operation order. The table holds the
1.103.2's arithmetic in the same float operation order. The table holds the
Claude and OpenAI text models our transcripts name under the `anthropic`,
`openai`, `bedrock` and `bedrock_converse` providers, and keeps litellm's
`fallback_generalizations` rules, which route an unmapped `claude-<family>-<n>`
Expand Down
Loading
Loading