tempest-cli 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/CHANGELOG.md +19 -0
  2. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/PKG-INFO +24 -2
  3. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/README.md +23 -1
  4. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/changelog.en.md +13 -0
  5. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/changelog.md +13 -0
  6. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/commands.en.md +1 -1
  7. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/commands.md +1 -1
  8. tempest_cli-0.5.0/docs/library.en.md +193 -0
  9. tempest_cli-0.5.0/docs/library.md +192 -0
  10. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/reference.md +6 -0
  11. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/pyproject.toml +1 -1
  12. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/__init__.py +8 -1
  13. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/lint.py +55 -13
  14. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/main.py +32 -2
  15. tempest_cli-0.5.0/tempest_cli/steps.py +54 -0
  16. tempest_cli-0.5.0/tests/test_extra_steps.py +201 -0
  17. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/uv.lock +1 -1
  18. tempest_cli-0.4.0/docs/library.en.md +0 -97
  19. tempest_cli-0.4.0/docs/library.md +0 -97
  20. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/.github/workflows/ci.yml +0 -0
  21. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/.github/workflows/docs.yml +0 -0
  22. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/.github/workflows/release-pypi.yml +0 -0
  23. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/.gitignore +0 -0
  24. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/LICENSE +0 -0
  25. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/Makefile +0 -0
  26. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/configuration.en.md +0 -0
  27. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/configuration.md +0 -0
  28. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/index.en.md +0 -0
  29. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/index.md +0 -0
  30. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/installation.en.md +0 -0
  31. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/installation.md +0 -0
  32. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/pr-prompt.en.md +0 -0
  33. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/pr-prompt.md +0 -0
  34. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/reference.en.md +0 -0
  35. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/rules.en.md +0 -0
  36. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/rules.md +0 -0
  37. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/mkdocs.yml +0 -0
  38. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
  39. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
  40. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/config.py +0 -0
  41. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/pr_prompt.py +0 -0
  42. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/py.typed +0 -0
  43. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/__init__.py +0 -0
  44. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_cli.py +0 -0
  45. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_config.py +0 -0
  46. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_fast.py +0 -0
  47. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_lint_runners.py +0 -0
  48. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_lint_strictness.py +0 -0
  49. {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_pr_prompt.py +0 -0
@@ -5,6 +5,25 @@ All notable changes to **tempest-cli** are listed below.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] — 2026-10-13
9
+
10
+ ### Added
11
+
12
+ - **`extra_steps`: a framework's own rules inside `check`.** The new
13
+ `CheckStep(name, run)` (frozen dataclass, exported from `tempest_cli`)
14
+ is a step `check` runs after `mypy` and before `pytest`: `run` receives
15
+ the `check` target and returns an exit code. `run_full_check(...,
16
+ extra_steps=)` and `register_commands(app, *, extra_steps=)` take a
17
+ sequence of them — keyword-only, empty by default, so every existing
18
+ call runs the same four steps as before. Each extra step is echoed as
19
+ `$ <name>` on stderr, runs in list order, and the first non-zero code
20
+ stops the gate and becomes its exit code, with or without `--fast`.
21
+ They sit before `pytest` because they are static checks that should
22
+ fail in a second, not after the suite. Requested in
23
+ mauriciobenjamin700/tempest-fastapi-sdk#520, where the SDK adds its
24
+ service-layer rules to `tempest check` without this package learning
25
+ about any framework.
26
+
8
27
  ## [0.4.0] — 2026-09-27
9
28
 
10
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tempest-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Framework-agnostic quality gate for Python projects: ruff + mypy + pytest behind one command, with a typing-strictness dial and a PR-description prompt generator.
5
5
  Project-URL: Homepage, https://github.com/mauriciobenjamin700/tempest-cli
6
6
  Project-URL: Repository, https://github.com/mauriciobenjamin700/tempest-cli
@@ -75,7 +75,7 @@ remembered to pass.
75
75
  | `tempest-cli fmt-check` | `ruff format --check` (read-only) |
76
76
  | `tempest-cli type` | `mypy` |
77
77
  | `tempest-cli test` | `pytest` (`--fast` spreads it across cores with pytest-xdist: `-n auto -p no:cacheprovider`, `-w N` to choose) |
78
- | `tempest-cli check` | all four, in order, stopping at the first failure (`--fast` runs the test step in parallel) |
78
+ | `tempest-cli check` | all four, in order, stopping at the first failure (`--fast` runs the test step in parallel; a host CLI can add steps between `mypy` and `pytest`) |
79
79
  | `tempest-cli pr-prompt` | builds the prompt that makes an AI write this branch's PR description |
80
80
 
81
81
  Every command takes an optional path (`tempest-cli lint src/`) and
@@ -150,6 +150,28 @@ cli: typer.Typer = typer.Typer(name="mytool")
150
150
  register_commands(cli)
151
151
  ```
152
152
 
153
+ A framework that wants its own rules in `check` passes them as
154
+ `CheckStep`s — they run after `mypy` and before `pytest`, and the first
155
+ non-zero exit code stops the gate:
156
+
157
+ ```python
158
+ import typer
159
+
160
+ from tempest_cli import CheckStep
161
+ from tempest_cli.main import register_commands
162
+
163
+
164
+ def layers(target: str) -> int:
165
+ """Return 0 when every module under ``target`` follows the layering."""
166
+ return 0
167
+
168
+
169
+ cli: typer.Typer = typer.Typer(name="mytool")
170
+ register_commands(cli, extra_steps=[CheckStep(name="layers", run=layers)])
171
+ ```
172
+
173
+ `run_full_check(".", extra_steps=[...])` takes the same list.
174
+
153
175
  ## Where the tools come from
154
176
 
155
177
  **`ruff` comes with the package** — six of the eight commands are ruff,
@@ -43,7 +43,7 @@ remembered to pass.
43
43
  | `tempest-cli fmt-check` | `ruff format --check` (read-only) |
44
44
  | `tempest-cli type` | `mypy` |
45
45
  | `tempest-cli test` | `pytest` (`--fast` spreads it across cores with pytest-xdist: `-n auto -p no:cacheprovider`, `-w N` to choose) |
46
- | `tempest-cli check` | all four, in order, stopping at the first failure (`--fast` runs the test step in parallel) |
46
+ | `tempest-cli check` | all four, in order, stopping at the first failure (`--fast` runs the test step in parallel; a host CLI can add steps between `mypy` and `pytest`) |
47
47
  | `tempest-cli pr-prompt` | builds the prompt that makes an AI write this branch's PR description |
48
48
 
49
49
  Every command takes an optional path (`tempest-cli lint src/`) and
@@ -118,6 +118,28 @@ cli: typer.Typer = typer.Typer(name="mytool")
118
118
  register_commands(cli)
119
119
  ```
120
120
 
121
+ A framework that wants its own rules in `check` passes them as
122
+ `CheckStep`s — they run after `mypy` and before `pytest`, and the first
123
+ non-zero exit code stops the gate:
124
+
125
+ ```python
126
+ import typer
127
+
128
+ from tempest_cli import CheckStep
129
+ from tempest_cli.main import register_commands
130
+
131
+
132
+ def layers(target: str) -> int:
133
+ """Return 0 when every module under ``target`` follows the layering."""
134
+ return 0
135
+
136
+
137
+ cli: typer.Typer = typer.Typer(name="mytool")
138
+ register_commands(cli, extra_steps=[CheckStep(name="layers", run=layers)])
139
+ ```
140
+
141
+ `run_full_check(".", extra_steps=[...])` takes the same list.
142
+
121
143
  ## Where the tools come from
122
144
 
123
145
  **`ruff` comes with the package** — six of the eight commands are ruff,
@@ -3,6 +3,19 @@
3
3
  The full history lives in the repository's
4
4
  [`CHANGELOG.md`](https://github.com/mauriciobenjamin700/tempest-cli/blob/main/CHANGELOG.md).
5
5
 
6
+ ## [0.5.0] — 2026-10-13
7
+
8
+ ### Added
9
+
10
+ - **`extra_steps`: a framework's own rules inside `check`.** The new
11
+ `CheckStep(name, run)` is a step `check` runs after `mypy` and before
12
+ `pytest`; `run` receives the target and returns an exit code.
13
+ `run_full_check(..., extra_steps=)` and
14
+ `register_commands(app, *, extra_steps=)` take the list — empty by
15
+ default, so nothing changes for a caller that does not pass one. The
16
+ first non-zero code stops the gate, with or without `--fast`.
17
+ [Details](library.md#adding-steps-to-check).
18
+
6
19
  ## [0.4.0] — 2026-09-27
7
20
 
8
21
  ### Added
@@ -4,6 +4,19 @@ O histórico completo vive no
4
4
  [`CHANGELOG.md`](https://github.com/mauriciobenjamin700/tempest-cli/blob/main/CHANGELOG.md)
5
5
  do repositório.
6
6
 
7
+ ## [0.5.0] — 2026-10-13
8
+
9
+ ### Adicionado
10
+
11
+ - **`extra_steps`: regras do framework dentro do `check`.** O novo
12
+ `CheckStep(name, run)` é um passo que o `check` roda depois do `mypy` e
13
+ antes do `pytest`; `run` recebe o alvo e devolve um código de saída.
14
+ `run_full_check(..., extra_steps=)` e
15
+ `register_commands(app, *, extra_steps=)` aceitam a lista — vazia por
16
+ padrão, então nada muda para quem não passa. O primeiro código diferente
17
+ de `0` para o gate, com ou sem `--fast`.
18
+ [Detalhes](library.md#acrescentar-passos-ao-check).
19
+
7
20
  ## [0.4.0] — 2026-09-27
8
21
 
9
22
  ### Adicionado
@@ -11,7 +11,7 @@ tool's exit code.
11
11
  | `fmt-check` | `ruff format --check` (read-only) |
12
12
  | `type` | `mypy` |
13
13
  | `test` | `pytest` |
14
- | `check` | the four above, in order, stopping at the first failure |
14
+ | `check` | the four above, in order, stopping at the first failure (a CLI that mounts the gate can [add steps](library.md#adding-steps-to-check) between `mypy` and `pytest`) |
15
15
  | `pr-prompt` | builds the PR-description prompt — [its own page](pr-prompt.md) |
16
16
 
17
17
  !!! tip "`tc` is the short form"
@@ -11,7 +11,7 @@ saída da ferramenta por baixo.
11
11
  | `fmt-check` | `ruff format --check` (só leitura) |
12
12
  | `type` | `mypy` |
13
13
  | `test` | `pytest` |
14
- | `check` | os quatro acima, em ordem, parando na primeira falha |
14
+ | `check` | os quatro acima, em ordem, parando na primeira falha (uma CLI que monta o gate pode [acrescentar passos](library.md#acrescentar-passos-ao-check) entre o `mypy` e o `pytest`) |
15
15
  | `pr-prompt` | monta o prompt da descrição do PR — [página própria](pr-prompt.md) |
16
16
 
17
17
  !!! tip "`tc` é a forma curta"
@@ -0,0 +1,193 @@
1
+ # Use as a library
2
+
3
+ Everything the CLI does is importable. Two situations are worth it.
4
+
5
+ ## Calling the runners from your code
6
+
7
+ ```python
8
+ from tempest_cli import load_tempest_config, run_full_check
9
+
10
+ config = load_tempest_config()
11
+ exit_code = run_full_check(".", config=config)
12
+ ```
13
+
14
+ Each step is callable on its own too, with the same exit-code contract:
15
+
16
+ ```python
17
+ from tempest_cli import (
18
+ TempestConfig,
19
+ run_mypy,
20
+ run_pytest,
21
+ run_ruff_check,
22
+ run_ruff_fix,
23
+ run_ruff_format,
24
+ )
25
+
26
+ strict = TempestConfig(typing_strictness="strict")
27
+
28
+ run_ruff_check("src", config=strict) # ruff check + the level's ANN rules
29
+ run_ruff_fix("src", unsafe=False, config=strict)
30
+ run_ruff_format("src", check=True) # --check: writes nothing
31
+ run_mypy("src", config=strict)
32
+ run_pytest("tests/unit")
33
+ run_pytest("tests/unit", fast=True, workers="4") # pytest-xdist: -n 4
34
+ ```
35
+
36
+ `load_tempest_config(start)` walks up from `start` (the cwd by default)
37
+ to the nearest `pyproject.toml` and returns the resolved
38
+ `TempestConfig`. `find_pyproject` does the lookup alone, when you want
39
+ the path.
40
+
41
+ ## Mounting the gate on your own CLI
42
+
43
+ A tool that already has its own CLI can expose these commands under its
44
+ own name — without copying a single command body:
45
+
46
+ ```python
47
+ import typer
48
+
49
+ from tempest_cli.main import register_commands
50
+
51
+ cli: typer.Typer = typer.Typer(name="mytool")
52
+
53
+
54
+ @cli.command("deploy")
55
+ def deploy() -> None:
56
+ """A command the tool already had."""
57
+
58
+
59
+ register_commands(cli)
60
+ ```
61
+
62
+ Now `mytool check`, `mytool lint` and the rest exist, with the same
63
+ behavior and the same `[tool.tempest]` reading.
64
+
65
+ !!! info "This is exactly what `tempest-fastapi-sdk` does"
66
+ The SDK's `tempest check` is this `register_commands` called on its
67
+ app. That is why the two never diverge: there is one
68
+ implementation.
69
+
70
+ ## Adding steps to `check`
71
+
72
+ `tempest-cli` knows nothing about any framework, on purpose. The
73
+ framework does, though: "every service inherits `BaseService`" is a rule
74
+ of whoever lays out the project, not of whoever runs ruff. `CheckStep`
75
+ is the seam — you write the rule, `check` runs it.
76
+
77
+ A `CheckStep` has two fields: `name`, the label `check` echoes as
78
+ `$ <name>` on stderr, and `run`, a function that takes the `check`
79
+ target (the path from the command line, `.` by default) and returns an
80
+ exit code — `0` means it passed.
81
+
82
+ ```python
83
+ from pathlib import Path
84
+
85
+ import typer
86
+
87
+ from tempest_cli import CheckStep
88
+ from tempest_cli.main import register_commands
89
+
90
+
91
+ def no_print(target: str) -> int:
92
+ """Fail when a module under ``target`` calls ``print``."""
93
+ offenders: list[str] = [
94
+ f"{path}:{number}: use logging instead of print()"
95
+ for path in sorted(Path(target).rglob("*.py"))
96
+ for number, line in enumerate(path.read_text().splitlines(), start=1)
97
+ if line.lstrip().startswith("print(")
98
+ ]
99
+ for offender in offenders:
100
+ typer.echo(offender, err=True)
101
+ return 1 if offenders else 0
102
+
103
+
104
+ cli: typer.Typer = typer.Typer(name="mytool")
105
+
106
+
107
+ @cli.command("deploy")
108
+ def deploy() -> None:
109
+ """A command the tool already had."""
110
+
111
+
112
+ register_commands(cli, extra_steps=[CheckStep(name="no-print", run=no_print)])
113
+
114
+ if __name__ == "__main__":
115
+ cli()
116
+ ```
117
+
118
+ With a module `pkg/a.py` that calls `print("x")`:
119
+
120
+ ```console
121
+ $ python mytool.py check pkg
122
+ $ ruff check --extend-select ANN001,ANN201,ANN202,ANN205,ANN206 pkg
123
+ All checks passed!
124
+ $ ruff format --check pkg
125
+ 1 file already formatted
126
+ $ mypy --disallow-untyped-defs --disallow-incomplete-defs pkg
127
+ Success: no issues found in 1 source file
128
+ $ no-print
129
+ pkg/a.py:2: use logging instead of print()
130
+ $ echo $?
131
+ 1
132
+ ```
133
+
134
+ The order is `ruff check` → `ruff format --check` → `mypy` → the extra
135
+ steps, in list order → `pytest`. An extra step is a static check: it
136
+ sits before the suite so it fails in a second, not after it. The
137
+ contract is the built-in steps' — the first non-zero code stops the gate
138
+ and becomes `check`'s exit code, with or without `--fast`.
139
+
140
+ !!! tip "The message is yours"
141
+ `check` only echoes the name. What the step prints is what the user
142
+ reads, so name the file, the line and the fix — as `no-print` does
143
+ above.
144
+
145
+ Without the CLI, the same holds for `run_full_check`:
146
+
147
+ ```python
148
+ from tempest_cli import CheckStep, run_full_check
149
+
150
+
151
+ def always_passes(target: str) -> int:
152
+ """A step that accepts any target."""
153
+ return 0
154
+
155
+
156
+ exit_code: int = run_full_check(
157
+ ".", extra_steps=[CheckStep(name="always-passes", run=always_passes)]
158
+ )
159
+ ```
160
+
161
+ `extra_steps` is empty by default: without it, the gate is exactly what
162
+ it was.
163
+
164
+ ## Generating the PR prompt without the CLI
165
+
166
+ ```python
167
+ from pathlib import Path
168
+
169
+ from tempest_cli import PromptLanguage, generate_pr_prompt
170
+
171
+ prompt, context, template = generate_pr_prompt(
172
+ base="main",
173
+ head=None,
174
+ cwd=Path("."),
175
+ template=None,
176
+ language=PromptLanguage.EN_US,
177
+ max_files=20,
178
+ max_chars=8000,
179
+ )
180
+ print(len(context.commits), "commits;", template.source)
181
+ ```
182
+
183
+ `generate_pr_prompt` raises `GitError` when git fails or a ref does not
184
+ resolve.
185
+
186
+ ## Recap
187
+
188
+ - Runners, config and the prompt generator are all importable.
189
+ - `register_commands(app)` mounts the eight commands on any `Typer`.
190
+ - `extra_steps=[CheckStep(...)]` puts your own rules in `check`, after
191
+ `mypy` and before `pytest`.
192
+ - Exit codes are the tools', identical from the CLI and from a direct
193
+ call.
@@ -0,0 +1,192 @@
1
+ # Usar como biblioteca
2
+
3
+ Tudo que a CLI faz está importável. Duas situações valem a pena.
4
+
5
+ ## Chamar os runners do seu código
6
+
7
+ ```python
8
+ from tempest_cli import load_tempest_config, run_full_check
9
+
10
+ config = load_tempest_config()
11
+ exit_code = run_full_check(".", config=config)
12
+ ```
13
+
14
+ Cada etapa também é chamável isolada, com o mesmo contrato de código de
15
+ saída:
16
+
17
+ ```python
18
+ from tempest_cli import (
19
+ TempestConfig,
20
+ run_mypy,
21
+ run_pytest,
22
+ run_ruff_check,
23
+ run_ruff_fix,
24
+ run_ruff_format,
25
+ )
26
+
27
+ strict = TempestConfig(typing_strictness="strict")
28
+
29
+ run_ruff_check("src", config=strict) # ruff check + regras ANN do nível
30
+ run_ruff_fix("src", unsafe=False, config=strict)
31
+ run_ruff_format("src", check=True) # --check: não escreve
32
+ run_mypy("src", config=strict)
33
+ run_pytest("tests/unit")
34
+ run_pytest("tests/unit", fast=True, workers="4") # pytest-xdist: -n 4
35
+ ```
36
+
37
+ `load_tempest_config(start)` procura o `pyproject.toml` mais próximo
38
+ subindo a partir de `start` (o cwd por padrão) e devolve o
39
+ `TempestConfig` resolvido. `find_pyproject` faz só a busca, quando você
40
+ quer o caminho.
41
+
42
+ ## Montar o gate na sua própria CLI
43
+
44
+ Uma ferramenta que já tem CLI própria pode expor estes comandos sob o
45
+ nome dela — sem copiar corpo de comando nenhum:
46
+
47
+ ```python
48
+ import typer
49
+
50
+ from tempest_cli.main import register_commands
51
+
52
+ cli: typer.Typer = typer.Typer(name="minhaferramenta")
53
+
54
+
55
+ @cli.command("deploy")
56
+ def deploy() -> None:
57
+ """Um comando que a ferramenta já tinha."""
58
+
59
+
60
+ register_commands(cli)
61
+ ```
62
+
63
+ Agora `minhaferramenta check`, `minhaferramenta lint` e os demais
64
+ existem, com o mesmo comportamento e a mesma leitura de
65
+ `[tool.tempest]`.
66
+
67
+ !!! info "É exatamente o que o `tempest-fastapi-sdk` faz"
68
+ O `tempest check` do SDK é este `register_commands` chamado no app
69
+ dele. Por isso os dois nunca divergem: há uma implementação só.
70
+
71
+ ## Acrescentar passos ao `check`
72
+
73
+ O `tempest-cli` não sabe nada de framework, e é de propósito. Mas o
74
+ framework sabe: "todo service herda `BaseService`" é regra de quem monta
75
+ o projeto, não de quem roda o ruff. O `CheckStep` é a costura — você
76
+ escreve a regra, o `check` roda.
77
+
78
+ Um `CheckStep` tem dois campos: `name`, o rótulo que o `check` ecoa como
79
+ `$ <name>` no stderr, e `run`, uma função que recebe o alvo do `check`
80
+ (o caminho da linha de comando, `.` por padrão) e devolve um código de
81
+ saída — `0` é passou.
82
+
83
+ ```python
84
+ from pathlib import Path
85
+
86
+ import typer
87
+
88
+ from tempest_cli import CheckStep
89
+ from tempest_cli.main import register_commands
90
+
91
+
92
+ def no_print(target: str) -> int:
93
+ """Fail when a module under ``target`` calls ``print``."""
94
+ offenders: list[str] = [
95
+ f"{path}:{number}: use logging instead of print()"
96
+ for path in sorted(Path(target).rglob("*.py"))
97
+ for number, line in enumerate(path.read_text().splitlines(), start=1)
98
+ if line.lstrip().startswith("print(")
99
+ ]
100
+ for offender in offenders:
101
+ typer.echo(offender, err=True)
102
+ return 1 if offenders else 0
103
+
104
+
105
+ cli: typer.Typer = typer.Typer(name="minhaferramenta")
106
+
107
+
108
+ @cli.command("deploy")
109
+ def deploy() -> None:
110
+ """Um comando que a ferramenta já tinha."""
111
+
112
+
113
+ register_commands(cli, extra_steps=[CheckStep(name="no-print", run=no_print)])
114
+
115
+ if __name__ == "__main__":
116
+ cli()
117
+ ```
118
+
119
+ Com um módulo `pkg/a.py` que chama `print("x")`:
120
+
121
+ ```console
122
+ $ python minhaferramenta.py check pkg
123
+ $ ruff check --extend-select ANN001,ANN201,ANN202,ANN205,ANN206 pkg
124
+ All checks passed!
125
+ $ ruff format --check pkg
126
+ 1 file already formatted
127
+ $ mypy --disallow-untyped-defs --disallow-incomplete-defs pkg
128
+ Success: no issues found in 1 source file
129
+ $ no-print
130
+ pkg/a.py:2: use logging instead of print()
131
+ $ echo $?
132
+ 1
133
+ ```
134
+
135
+ A ordem é `ruff check` → `ruff format --check` → `mypy` → os passos
136
+ extras, na ordem da lista → `pytest`. Passo extra é checagem estática:
137
+ fica antes da suíte para falhar em um segundo, não depois dela. O
138
+ contrato é o dos passos embutidos — o primeiro código diferente de `0`
139
+ para o gate e vira o código de saída do `check`, com ou sem `--fast`.
140
+
141
+ !!! tip "A mensagem é sua"
142
+ O `check` só ecoa o nome. O que o passo imprime é o que o usuário lê,
143
+ então diga arquivo, linha e a correção — como o `no-print` acima.
144
+
145
+ Sem a CLI, o mesmo vale para `run_full_check`:
146
+
147
+ ```python
148
+ from tempest_cli import CheckStep, run_full_check
149
+
150
+
151
+ def always_passes(target: str) -> int:
152
+ """A step that accepts any target."""
153
+ return 0
154
+
155
+
156
+ exit_code: int = run_full_check(
157
+ ".", extra_steps=[CheckStep(name="always-passes", run=always_passes)]
158
+ )
159
+ ```
160
+
161
+ `extra_steps` é vazio por padrão: sem ele, o gate é exatamente o de
162
+ antes.
163
+
164
+ ## Gerar o prompt de PR sem a CLI
165
+
166
+ ```python
167
+ from pathlib import Path
168
+
169
+ from tempest_cli import PromptLanguage, generate_pr_prompt
170
+
171
+ prompt, context, template = generate_pr_prompt(
172
+ base="main",
173
+ head=None,
174
+ cwd=Path("."),
175
+ template=None,
176
+ language=PromptLanguage.PT_BR,
177
+ max_files=20,
178
+ max_chars=8000,
179
+ )
180
+ print(len(context.commits), "commits;", template.source)
181
+ ```
182
+
183
+ `generate_pr_prompt` levanta `GitError` quando o git falha ou uma ref não
184
+ resolve.
185
+
186
+ ## Recap
187
+
188
+ - Runners, config e o gerador de prompt são todos importáveis.
189
+ - `register_commands(app)` monta os oito comandos em qualquer `Typer`.
190
+ - `extra_steps=[CheckStep(...)]` põe regras suas no `check`, depois do
191
+ `mypy` e antes do `pytest`.
192
+ - Código de saída é o da ferramenta, igual na CLI e na chamada direta.
@@ -35,6 +35,12 @@ Gerada a partir das docstrings do pacote via
35
35
 
36
36
  ---
37
37
 
38
+ ## Passos extras
39
+
40
+ ::: tempest_cli.steps.CheckStep
41
+
42
+ ---
43
+
38
44
  ## Prompt de PR
39
45
 
40
46
  ::: tempest_cli.pr_prompt.generate_pr_prompt
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tempest-cli"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Framework-agnostic quality gate for Python projects: ruff + mypy + pytest behind one command, with a typing-strictness dial and a PR-description prompt generator."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -30,6 +30,11 @@ config = load_tempest_config()
30
30
  exit_code = run_full_check(".", config=config)
31
31
  ```
32
32
 
33
+ A framework that wants its own rules in ``check`` passes them as
34
+ :class:`tempest_cli.CheckStep` values — ``extra_steps`` on
35
+ :func:`run_full_check` and on :func:`tempest_cli.main.register_commands`
36
+ — and they run after ``mypy``, before ``pytest``.
37
+
33
38
  And :func:`tempest_cli.main.register_commands` mounts the whole gate onto
34
39
  an existing :class:`typer.Typer`, so another CLI can expose these
35
40
  commands under its own name without copying them.
@@ -50,12 +55,14 @@ from tempest_cli.lint import run_ruff_format as run_ruff_format
50
55
  from tempest_cli.pr_prompt import GitError as GitError
51
56
  from tempest_cli.pr_prompt import PromptLanguage as PromptLanguage
52
57
  from tempest_cli.pr_prompt import generate_pr_prompt as generate_pr_prompt
58
+ from tempest_cli.steps import CheckStep as CheckStep
53
59
 
54
- __version__: str = "0.4.0"
60
+ __version__: str = "0.5.0"
55
61
  """Installed package version."""
56
62
 
57
63
  __all__: list[str] = [
58
64
  "DEFAULT_TYPING_STRICTNESS",
65
+ "CheckStep",
59
66
  "GitError",
60
67
  "PromptLanguage",
61
68
  "TempestConfig",