Skip to content

Reference

Generated from the package docstrings with mkdocstrings.

The rendered signatures come from the source, so they read the same in both languages.

Top-level surface

tempest_cli

Framework-agnostic quality gate for Python projects.

tempest-cli runs ruff, mypy and pytest behind one command, with a typing-strictness dial read from [tool.tempest] in the project's pyproject.toml — and a generator for the prompt that makes an AI fill a pull-request description from the branch's own diff.

It knows nothing about any web framework. ruff ships with it, so the lint/format commands work the moment it is installed; mypy and pytest are left to the project, which pins the versions it wants. The tools are invoked from the project's environment first, so a pinned version always beats the bundled one.

tempest-cli check                 # lint + fmt-check + type + test
tempest-cli fix                   # every ruff autofix, then format
tempest-cli type -s strict        # override the configured strictness
tempest-cli pr-prompt | claude -p

tc check                          # `tc` is the short alias for the same CLI

Everything is importable too, for a project that would rather wire the gate into its own tooling:

from tempest_cli import load_tempest_config, run_full_check

config = load_tempest_config()
exit_code = run_full_check(".", config=config)

And :func:tempest_cli.main.register_commands mounts the whole gate onto an existing :class:typer.Typer, so another CLI can expose these commands under its own name without copying them.

DEFAULT_TYPING_STRICTNESS module-attribute

DEFAULT_TYPING_STRICTNESS: TypingStrictness = 'standard'

Level applied when the key is absent or no pyproject.toml is found.

TypingStrictness module-attribute

TypingStrictness = Literal['lenient', 'standard', 'strict']

Allowed values for [tool.tempest] typing_strictness.

TempestConfig dataclass

TempestConfig(typing_strictness: TypingStrictness = DEFAULT_TYPING_STRICTNESS)

Resolved [tool.tempest] settings.

Attributes:

Name Type Description
typing_strictness TypingStrictness

How strictly the CLI gates enforce typing. One of "lenient", "standard", "strict".

ruff_ann_select

ruff_ann_select() -> list[str]

Return the ANN rule codes to add to ruff for this level.

Returns:

Type Description
list[str]

list[str]: Rule codes for --extend-select (empty for

list[str]

"lenient"). ANN401 is never present.

Source code in tempest_cli/config.py
def ruff_ann_select(self) -> list[str]:
    """Return the ANN rule codes to add to ruff for this level.

    Returns:
        list[str]: Rule codes for ``--extend-select`` (empty for
        ``"lenient"``). ANN401 is never present.
    """
    return list(_RUFF_ANN_BY_LEVEL[self.typing_strictness])

mypy_flags

mypy_flags() -> list[str]

Return the extra mypy flags to add for this level.

Returns:

Type Description
list[str]

list[str]: Flags layered on top of the project's

list[str]

[tool.mypy] config (empty for "lenient").

Source code in tempest_cli/config.py
def mypy_flags(self) -> list[str]:
    """Return the extra mypy flags to add for this level.

    Returns:
        list[str]: Flags layered on top of the project's
        ``[tool.mypy]`` config (empty for ``"lenient"``).
    """
    return list(_MYPY_FLAGS_BY_LEVEL[self.typing_strictness])

GitError

Bases: RuntimeError

A git invocation failed, or the repository lacks what was asked.

Carries the command's own stderr so the caller can print the reason git gave instead of a generic failure.

PromptLanguage

Bases: StrEnum

Language of the bundled template and of the prompt's instructions.

Only the bundled template is translated: a repository template is used verbatim in whatever language it was written in, since it is that repository's contract.

find_pyproject

find_pyproject(start: Path | None = None) -> Path | None

Locate the nearest pyproject.toml walking up from start.

Parameters:

Name Type Description Default
start Path | None

Directory to begin the search. Defaults to the current working directory.

None

Returns:

Type Description
Path | None

Path | None: The path to the first pyproject.toml found in

Path | None

start or an ancestor, or None when none exists.

Source code in tempest_cli/config.py
def find_pyproject(start: Path | None = None) -> Path | None:
    """Locate the nearest ``pyproject.toml`` walking up from ``start``.

    Args:
        start (Path | None): Directory to begin the search. Defaults to
            the current working directory.

    Returns:
        Path | None: The path to the first ``pyproject.toml`` found in
        ``start`` or an ancestor, or ``None`` when none exists.
    """
    current = (start or Path.cwd()).resolve()
    for directory in (current, *current.parents):
        candidate = directory / "pyproject.toml"
        if candidate.is_file():
            return candidate
    return None

load_tempest_config

load_tempest_config(start: Path | None = None) -> TempestConfig

Load [tool.tempest] from the nearest pyproject.toml.

Parameters:

Name Type Description Default
start Path | None

Directory to begin the search. Defaults to the current working directory.

None

Returns:

Name Type Description
TempestConfig TempestConfig

The resolved config. Falls back to defaults when

TempestConfig

no pyproject.toml or no [tool.tempest] table is found.

Raises:

Type Description
ValueError

When typing_strictness is present but invalid.

Source code in tempest_cli/config.py
def load_tempest_config(start: Path | None = None) -> TempestConfig:
    """Load ``[tool.tempest]`` from the nearest ``pyproject.toml``.

    Args:
        start (Path | None): Directory to begin the search. Defaults to
            the current working directory.

    Returns:
        TempestConfig: The resolved config. Falls back to defaults when
        no ``pyproject.toml`` or no ``[tool.tempest]`` table is found.

    Raises:
        ValueError: When ``typing_strictness`` is present but invalid.
    """
    pyproject = find_pyproject(start)
    if pyproject is None:
        return TempestConfig()
    with pyproject.open("rb") as handle:
        data = tomllib.load(handle)
    table = data.get("tool", {}).get("tempest", {})
    strictness = (
        _coerce_strictness(table["typing_strictness"], source=str(pyproject))
        if "typing_strictness" in table
        else DEFAULT_TYPING_STRICTNESS
    )
    return TempestConfig(typing_strictness=strictness)

resolve_tool

resolve_tool(executable: str) -> list[str] | None

Return an argv prefix invoking executable or None when absent.

Public because callers outside the gate need the same lookup — the SDK's OpenAPI code generator formats what it emits with the project's own ruff, and reimplementing the environment/uv run fallback there would be a second answer to the same question.

Preference order:

  1. the environments that belong to this run (see :func:_environment_dirs): the CLI's own interpreter directory, $VIRTUAL_ENV, then the nearest .venv;
  2. executable on PATH — skipped when it resolves to a version-manager shim that does not dispatch anywhere;
  3. uv run --with <executable> <executable> when uv is on the PATH: the project's own environment plus the tool, without requiring activation or a prior uv sync.

Step 3 carries --with on purpose. A plain uv run ruff falls back to PATH when the project environment has no ruff — landing right back on the dead shim this lookup just rejected. --with puts the tool in the run's own overlay, so the command is always the one that runs. When the project pins a version, uv resolves the overlay against the project's requirements, so the pin still wins.

Parameters:

Name Type Description Default
executable str

The command name (ruff/mypy/pytest).

required

Returns:

Type Description
list[str] | None

list[str] | None: argv prefix to extend with extra arguments, or

list[str] | None

None when no runner could be found.

Source code in tempest_cli/lint.py
def resolve_tool(executable: str) -> list[str] | None:
    """Return an argv prefix invoking ``executable`` or ``None`` when absent.

    Public because callers outside the gate need the same lookup — the
    SDK's OpenAPI code generator formats what it emits with the project's
    own ruff, and reimplementing the environment/``uv run`` fallback
    there would be a second answer to the same question.

    Preference order:

    1. the environments that belong to this run (see
       :func:`_environment_dirs`): the CLI's own interpreter directory,
       ``$VIRTUAL_ENV``, then the nearest ``.venv``;
    2. ``executable`` on ``PATH`` — skipped when it resolves to a
       version-manager shim that does not dispatch anywhere;
    3. ``uv run --with <executable> <executable>`` when ``uv`` is on the
       ``PATH``: the project's own environment plus the tool, without
       requiring activation or a prior ``uv sync``.

    Step 3 carries ``--with`` on purpose. A plain ``uv run ruff`` falls
    back to ``PATH`` when the project environment has no ruff — landing
    right back on the dead shim this lookup just rejected. ``--with``
    puts the tool in the run's own overlay, so the command is always the
    one that runs. When the project pins a version, uv resolves the
    overlay against the project's requirements, so the pin still wins.

    Args:
        executable (str): The command name (``ruff``/``mypy``/``pytest``).

    Returns:
        list[str] | None: argv prefix to extend with extra arguments, or
        ``None`` when no runner could be found.
    """
    for directory in _environment_dirs():
        local = shutil.which(executable, path=str(directory))
        if local is not None:
            return [local]
    direct = _path_lookup(executable)
    if direct is not None:
        return [direct]
    uv = _path_lookup("uv")
    if uv is not None:
        return [uv, "run", "--with", executable, executable]
    return None

run_full_check

run_full_check(target: str, *, config: TempestConfig | None = None) -> int

Run the entire quality gate sequentially.

Order: ruff checkruff format --checkmypypytest. Stops at the first non-zero exit code so failures surface fast.

Parameters:

Name Type Description Default
target str

The path inspected by ruff/mypy. Pytest always runs against the project's configured testpaths.

required
config TempestConfig | None

Resolved [tool.tempest] config controlling the ANN rules and mypy flags layered onto the ruff/mypy steps. When None the default level is used.

None

Returns:

Name Type Description
int int

The first non-zero exit code, or 0 when every gate passed.

Source code in tempest_cli/lint.py
def run_full_check(target: str, *, config: TempestConfig | None = None) -> int:
    """Run the entire quality gate sequentially.

    Order: ``ruff check`` → ``ruff format --check`` → ``mypy`` → ``pytest``.
    Stops at the first non-zero exit code so failures surface fast.

    Args:
        target (str): The path inspected by ruff/mypy. Pytest always runs
            against the project's configured ``testpaths``.
        config (TempestConfig | None): Resolved ``[tool.tempest]`` config
            controlling the ANN rules and mypy flags layered onto the
            ruff/mypy steps. When ``None`` the default level is used.

    Returns:
        int: The first non-zero exit code, or ``0`` when every gate passed.
    """
    resolved = config or TempestConfig()
    steps: list[tuple[str, list[str]]] = [
        ("ruff", ["check", *_ruff_ann_args(resolved), target]),
        ("ruff", ["format", "--check", target]),
        ("mypy", [*resolved.mypy_flags(), target]),
        ("pytest", []),
    ]
    for executable, args in steps:
        typer.echo(f"$ {executable} {' '.join(args)}", err=True)
        code = _execute(executable, args)
        if code != 0:
            return code
    return 0

run_mypy

run_mypy(target: str, *, config: TempestConfig | None = None) -> int

Invoke mypy <target> with the configured strictness flags.

Parameters:

Name Type Description Default
target str

The path passed verbatim to mypy.

required
config TempestConfig | None

Resolved [tool.tempest] config controlling the mypy strictness flags layered on top of the project's [tool.mypy]. When None the default level is used.

None

Returns:

Name Type Description
int int

The mypy exit code.

Source code in tempest_cli/lint.py
def run_mypy(target: str, *, config: TempestConfig | None = None) -> int:
    """Invoke ``mypy <target>`` with the configured strictness flags.

    Args:
        target (str): The path passed verbatim to mypy.
        config (TempestConfig | None): Resolved ``[tool.tempest]`` config
            controlling the mypy strictness flags layered on top of the
            project's ``[tool.mypy]``. When ``None`` the default level is
            used.

    Returns:
        int: The mypy exit code.
    """
    flags = (config or TempestConfig()).mypy_flags()
    return _execute("mypy", [*flags, target])

run_pytest

run_pytest(target: str | None) -> int

Invoke pytest with an optional target.

Parameters:

Name Type Description Default
target str | None

Optional pytest path filter. None runs the default test suite.

required

Returns:

Name Type Description
int int

The pytest exit code.

Source code in tempest_cli/lint.py
def run_pytest(target: str | None) -> int:
    """Invoke ``pytest`` with an optional target.

    Args:
        target (str | None): Optional pytest path filter. ``None`` runs
            the default test suite.

    Returns:
        int: The pytest exit code.
    """
    args = [target] if target else []
    return _execute("pytest", args)

run_ruff_check

run_ruff_check(target: str, *, config: TempestConfig | None = None) -> int

Invoke ruff check <target> with the configured ANN rules.

Parameters:

Name Type Description Default
target str

The path passed verbatim to ruff.

required
config TempestConfig | None

Resolved [tool.tempest] config controlling the typing-strictness ANN rules layered on. When None the default level is used.

None

Returns:

Name Type Description
int int

The ruff exit code.

Source code in tempest_cli/lint.py
def run_ruff_check(target: str, *, config: TempestConfig | None = None) -> int:
    """Invoke ``ruff check <target>`` with the configured ANN rules.

    Args:
        target (str): The path passed verbatim to ruff.
        config (TempestConfig | None): Resolved ``[tool.tempest]`` config
            controlling the typing-strictness ANN rules layered on. When
            ``None`` the default level is used.

    Returns:
        int: The ruff exit code.
    """
    return _execute("ruff", ["check", *_ruff_ann_args(config), target])

run_ruff_fix

run_ruff_fix(
    target: str, *, unsafe: bool = False, config: TempestConfig | None = None
) -> int

Apply every automatic fix ruff can perform, then format the target.

Runs in two passes so the second one sees the rewritten file:

  1. ruff check --fix [--unsafe-fixes] <target> — autofix imports (sort + dedupe), remove unused imports, normalize string quotes, drop trailing whitespace, fix the rest of the lint rules that have safe (or, with unsafe=True, also unsafe) autofixers.
  2. ruff format <target> — normalize indentation, line length, blank lines and trailing newlines.

Both passes always run. ruff check --fix exits non-zero whenever any residual violation remains that it cannot autofix (an over-length string/comment, an undefined name, etc.) — even though it already rewrote everything it could. Short-circuiting on that exit code would skip ruff format entirely, leaving the file un-wrapped and its extra blank lines intact. So the formatter runs unconditionally; the lint exit code is surfaced afterwards so CI still fails on the leftover issues.

Parameters:

Name Type Description Default
target str

The path passed verbatim to ruff.

required
unsafe bool

When True, pass --unsafe-fixes so ruff also applies the fixes it would otherwise leave alone.

False
config TempestConfig | None

Resolved [tool.tempest] config controlling the typing-strictness ANN rules layered onto the fix pass. When None the default level is used.

None

Returns:

Name Type Description
int int

0 when both passes succeed with nothing left to fix;

int

otherwise the lint pass exit code (residual violations), or the

int

format pass exit code when the lint pass was clean.

Source code in tempest_cli/lint.py
def run_ruff_fix(
    target: str,
    *,
    unsafe: bool = False,
    config: TempestConfig | None = None,
) -> int:
    """Apply every automatic fix ruff can perform, then format the target.

    Runs in two passes so the second one sees the rewritten file:

    1. ``ruff check --fix [--unsafe-fixes] <target>`` — autofix imports
       (sort + dedupe), remove unused imports, normalize string quotes,
       drop trailing whitespace, fix the rest of the lint rules that
       have safe (or, with ``unsafe=True``, also unsafe) autofixers.
    2. ``ruff format <target>`` — normalize indentation, line length,
       blank lines and trailing newlines.

    Both passes always run. ``ruff check --fix`` exits non-zero whenever
    *any* residual violation remains that it cannot autofix (an
    over-length string/comment, an undefined name, etc.) — even though
    it already rewrote everything it could. Short-circuiting on that
    exit code would skip ``ruff format`` entirely, leaving the file
    un-wrapped and its extra blank lines intact. So the formatter runs
    unconditionally; the lint exit code is surfaced afterwards so CI
    still fails on the leftover issues.

    Args:
        target (str): The path passed verbatim to ruff.
        unsafe (bool): When True, pass ``--unsafe-fixes`` so ruff also
            applies the fixes it would otherwise leave alone.
        config (TempestConfig | None): Resolved ``[tool.tempest]`` config
            controlling the typing-strictness ANN rules layered onto the
            fix pass. When ``None`` the default level is used.

    Returns:
        int: ``0`` when both passes succeed with nothing left to fix;
        otherwise the lint pass exit code (residual violations), or the
        format pass exit code when the lint pass was clean.
    """
    check_args = ["check", "--fix", *_ruff_ann_args(config)]
    if unsafe:
        check_args.append("--unsafe-fixes")
    check_args.append(target)
    check_code = _execute("ruff", check_args)
    format_code = _execute("ruff", ["format", target])
    return check_code or format_code

run_ruff_format

run_ruff_format(target: str, *, check: bool) -> int

Invoke ruff format (write or check-only).

Parameters:

Name Type Description Default
target str

The path passed verbatim to ruff.

required
check bool

When True, run ruff format --check (read-only).

required

Returns:

Name Type Description
int int

The ruff exit code.

Source code in tempest_cli/lint.py
def run_ruff_format(target: str, *, check: bool) -> int:
    """Invoke ``ruff format`` (write or check-only).

    Args:
        target (str): The path passed verbatim to ruff.
        check (bool): When True, run ``ruff format --check`` (read-only).

    Returns:
        int: The ruff exit code.
    """
    args = ["format"]
    if check:
        args.append("--check")
    args.append(target)
    return _execute("ruff", args)

generate_pr_prompt

generate_pr_prompt(
    *,
    base: str = DEFAULT_BASE,
    head: str | None = None,
    cwd: Path | None = None,
    template: Path | None = None,
    language: PromptLanguage = PT_BR,
    max_files: int | None = DEFAULT_MAX_FILES,
    max_chars: int | None = DEFAULT_MAX_CHARS,
) -> tuple[str, PullRequestContext, ResolvedTemplate]

Read the repository and render the prompt in one call.

Parameters:

Name Type Description Default
base str

The base ref the pull request targets.

DEFAULT_BASE
head str | None

The branch being described. Defaults to the checked-out one.

None
cwd Path | None

Any directory inside the repository.

None
template Path | None

An explicit template path.

None
language PromptLanguage

Language of the instructions and of the bundled fallback template.

PT_BR
max_files int | None

How many files contribute a patch excerpt. 0 drops every patch, None excerpts them all.

DEFAULT_MAX_FILES
max_chars int | None

Characters kept per patch, or None for the whole patch.

DEFAULT_MAX_CHARS

Returns:

Type Description
str

tuple[str, PullRequestContext, ResolvedTemplate]: The prompt plus

PullRequestContext

the context and template it was built from, so a caller can

ResolvedTemplate

report what was read and what was dropped.

Raises:

Type Description
GitError

When the repository, the base ref or the template path cannot be resolved.

Source code in tempest_cli/pr_prompt.py
def generate_pr_prompt(
    *,
    base: str = DEFAULT_BASE,
    head: str | None = None,
    cwd: Path | None = None,
    template: Path | None = None,
    language: PromptLanguage = PromptLanguage.PT_BR,
    max_files: int | None = DEFAULT_MAX_FILES,
    max_chars: int | None = DEFAULT_MAX_CHARS,
) -> tuple[str, PullRequestContext, ResolvedTemplate]:
    """Read the repository and render the prompt in one call.

    Args:
        base (str): The base ref the pull request targets.
        head (str | None): The branch being described. Defaults to the
            checked-out one.
        cwd (Path | None): Any directory inside the repository.
        template (Path | None): An explicit template path.
        language (PromptLanguage): Language of the instructions and of
            the bundled fallback template.
        max_files (int | None): How many files contribute a patch
            excerpt. ``0`` drops every patch, ``None`` excerpts them all.
        max_chars (int | None): Characters kept per patch, or ``None``
            for the whole patch.

    Returns:
        tuple[str, PullRequestContext, ResolvedTemplate]: The prompt plus
        the context and template it was built from, so a caller can
        report what was read and what was dropped.

    Raises:
        GitError: When the repository, the base ref or the template path
            cannot be resolved.
    """
    context = collect_context(
        base=base,
        head=head,
        cwd=cwd,
        max_files=max_files,
        max_chars=max_chars,
    )
    root = repository_root((cwd or Path.cwd()).expanduser().resolve())
    resolved_template = resolve_template(root, template=template, language=language)
    prompt = build_prompt(context, resolved_template, language=language)
    return prompt, context, resolved_template