bmsdna-devtools 0.30.0__tar.gz → 0.31.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.
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/PKG-INFO +159 -12
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/README.md +157 -11
- bmsdna_devtools-0.31.0/bmsdna/devtools/_api_dump.py +58 -0
- bmsdna_devtools-0.31.0/bmsdna/devtools/api_usage.py +501 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/cli.py +162 -14
- bmsdna_devtools-0.31.0/bmsdna/devtools/dead_code.py +82 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/issue_do.py +11 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/lint_findings.py +2 -2
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/lint_sql.py +7 -1
- bmsdna_devtools-0.31.0/bmsdna/devtools/lint_sql_files.py +149 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/pr_build.py +13 -8
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/pr_info.py +9 -1
- bmsdna_devtools-0.31.0/bmsdna/devtools/translate.py +289 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/pyproject.toml +2 -1
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/skills/bmsdna-devtools/SKILL.md +13 -3
- bmsdna_devtools-0.31.0/tests/test_api_usage.py +654 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_lint_sql.py +10 -0
- bmsdna_devtools-0.31.0/tests/test_lint_sql_files.py +215 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_pr_build_flow.py +20 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_pr_info.py +47 -5
- bmsdna_devtools-0.31.0/tests/test_translate.py +165 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/uv.lock +12 -1
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/.github/workflows/auto-release.yml +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/.github/workflows/python-publish.yml +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/.github/workflows/python-test.yml +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/.gitignore +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/.python-version +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/__init__.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/ado_auth.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/ado_issue.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/app_service_logs.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/bdt_config.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/cli_tools.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/commit.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/env_config.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/find_injection.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/find_repo.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/gh_issue.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/gh_pr.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/gitrepo.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/injection_frontend.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/injection_python.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/lint.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/lint_models.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/lint_tooling.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/lint_typescript.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/logs.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/pr_issue_link.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/pr_labels.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/pr_markdown.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/pull.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/testdb.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/bmsdna/devtools/worktree.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/justfile +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/conftest.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_ado_auth.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_ado_issue.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_ado_issue_flow.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_app_service_logs.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_bdt_config.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_cli_cleanup.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_cli_commit.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_cli_issue.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_cli_pr_create.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_cli_pr_id.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_cli_pull.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_cli_tools.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_commit.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_env_config.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_find_injection.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_find_repo.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_gh_issue.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_gh_pr.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_gitrepo.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_issue_do.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_lint.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_lint_models.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_lint_tooling.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_lint_typescript.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_pr_build.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_pr_issue_link.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_pr_labels.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_pr_markdown.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_pull.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_testdb.py +0 -0
- {bmsdna_devtools-0.30.0 → bmsdna_devtools-0.31.0}/tests/test_worktree_clean.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bmsdna-devtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.0
|
|
4
4
|
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Requires-Dist: pgdevkit[db]>=0.7.1
|
|
7
7
|
Requires-Dist: requests>=2.32.0
|
|
8
8
|
Requires-Dist: rich>=13.0.0
|
|
9
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
9
10
|
Requires-Dist: typer>=0.26.7
|
|
10
11
|
Provides-Extra: lint
|
|
11
12
|
Requires-Dist: sqlglot>=27.0.0; extra == 'lint'
|
|
@@ -18,7 +19,8 @@ one-shot PR summary (`bdt pr info`), PR creation, claiming an issue (`bdt issue
|
|
|
18
19
|
merged-worktree/orphaned-test-DB cleanup), a commit-and-push helper with
|
|
19
20
|
pre-flight checks, Azure log queries, and static checks (`bdt lint`) for
|
|
20
21
|
postgres/psycopg SQL rules, pydantic-model placement, hand-wired HTTP access in
|
|
21
|
-
TypeScript
|
|
22
|
+
TypeScript and baseline tooling, plus `bdt dead-code` for `.sql` files nothing loads and
|
|
23
|
+
backend routes nothing calls.
|
|
22
24
|
`bdt pr *` and `bdt issue *` auto-detect whether the current repo's `origin`
|
|
23
25
|
remote is Azure DevOps or GitHub and use `az`/`gh` accordingly.
|
|
24
26
|
Consolidates near-duplicate scripts that used to be copy-pasted across
|
|
@@ -48,11 +50,12 @@ Find the PR opened from the current branch and report build/check status
|
|
|
48
50
|
GitHub — whichever `origin` points at.
|
|
49
51
|
|
|
50
52
|
```bash
|
|
51
|
-
bdt pr status [--target-branch
|
|
53
|
+
bdt pr status [--target-branch BRANCH] [--wait]
|
|
52
54
|
```
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
The `--target` / `--target-branch` option of `pr create/retry/publish/update/comment/watch-deploy` and `commit`
|
|
57
|
+
defaults to `dev`; pass `--target main` (or whatever your PRs go into) to override. `pr status`, `pr info` and
|
|
58
|
+
`issue take` only look a PR up, so by default they don't filter on the target branch at all.
|
|
56
59
|
|
|
57
60
|
If the PR can't be merged, that's reported immediately instead of polling
|
|
58
61
|
for builds/checks that will never run — e.g. on Azure DevOps:
|
|
@@ -73,7 +76,7 @@ one is also waiting on approval.
|
|
|
73
76
|
`*.visualstudio.com` HTTPS forms). Auth is an explicit PAT (`--pat` or
|
|
74
77
|
`AZURE_DEVOPS_EXT_PAT`/`AZURE_DEVOPS_PAT` env var), falling back to a short-lived token from the
|
|
75
78
|
caller's own `az login` — never embed a PAT literal in a script or CI file.
|
|
76
|
-
`--target-branch`
|
|
79
|
+
`--target-branch` optionally restricts the lookup to PRs into that branch (default: any).
|
|
77
80
|
|
|
78
81
|
**GitHub**: uses `gh`'s own auth (`gh auth login`) and always resolves the
|
|
79
82
|
PR opened from the current branch — `gh pr view` has no target-branch
|
|
@@ -91,7 +94,7 @@ it watches a branch-triggered build/workflow run, not any particular PR.)
|
|
|
91
94
|
## `bdt pr info`
|
|
92
95
|
|
|
93
96
|
```bash
|
|
94
|
-
bdt pr info [--json] [--pr-id N] [--target-branch
|
|
97
|
+
bdt pr info [--json] [--pr-id N] [--target-branch BRANCH]
|
|
95
98
|
```
|
|
96
99
|
|
|
97
100
|
One-shot summary of the current branch's PR: link, state (open/merged/closed, draft), aggregate
|
|
@@ -102,7 +105,7 @@ the check rollup (only a failed check fails it; skipped/cancelled ones don't cou
|
|
|
102
105
|
from the latest build of each pipeline (any failed result fails it; unfinished builds are pending, or
|
|
103
106
|
`waiting` when a stage needs approval). `unknown` if Azure DevOps can't be asked.
|
|
104
107
|
|
|
105
|
-
Azure DevOps limits: the PR is found by the current branch into `--target-branch`
|
|
108
|
+
Azure DevOps limits: the PR is found by the current branch (into `--target-branch` if given, else any target),
|
|
106
109
|
a PR with merge conflicts is reported as an error (exit 1) like `pr status` does, and the closed
|
|
107
110
|
work items are only those whose URL is in the PR description -- not ones linked via
|
|
108
111
|
`bdt pr create --issue`.
|
|
@@ -323,12 +326,12 @@ last argument); extra args are passed through; `--dry-run` prints the command.
|
|
|
323
326
|
It takes the issue first (like `bdt issue take`): before the agent starts it comments `Taken by <you>`
|
|
324
327
|
on the issue, naming the session it is about to start -- for `claude` a `--session-id` is generated and
|
|
325
328
|
passed, so the comment says `claude --resume <id>`; the prompt tells the agent not to take it again. For
|
|
326
|
-
another `--agent` the comment just says `(via <agent>)`. `--dry-run` posts nothing. Like `issue take`, it doesn't comment again when the newest comment already is
|
|
329
|
+
another `--agent` the comment just says `(via <agent>)`. `--dry-run` posts nothing. Like `issue take`, it doesn't comment again when the newest comment already is your claim; if it is someone else's, it stops (exit 1) unless `--force`.
|
|
327
330
|
|
|
328
331
|
## `bdt issue take`
|
|
329
332
|
|
|
330
333
|
```bash
|
|
331
|
-
bdt issue take [NUMBER] [--target-branch
|
|
334
|
+
bdt issue take [NUMBER] [--target-branch BRANCH] [--force]
|
|
332
335
|
```
|
|
333
336
|
|
|
334
337
|
Claims an issue (GitHub) or work item (Azure DevOps): comments `Taken by <you>` on it, followed by the
|
|
@@ -340,6 +343,43 @@ branch's PR closes (`Fixes #N`, or an issue/work-item URL in the PR body); it re
|
|
|
340
343
|
that is none or several. If the issue's newest comment already is a "Taken by ..." claim (by anyone) it
|
|
341
344
|
says so and posts nothing, so it's safe to run repeatedly -- including before `bdt issue do`.
|
|
342
345
|
|
|
346
|
+
## `bdt translate`
|
|
347
|
+
|
|
348
|
+
One `translations.toml` is the source of truth for all UI translations; `bdt translate` generates the
|
|
349
|
+
per-language `de.json` / `en.json` / ... files from it. The generated files are build artifacts --
|
|
350
|
+
add them to `.gitignore` and run `bdt translate` in your `just install` / build / CI steps.
|
|
351
|
+
|
|
352
|
+
```toml
|
|
353
|
+
# translations.toml (keys with dots must be quoted when `nested = true`)
|
|
354
|
+
[ADD_BUTTON]
|
|
355
|
+
en = "Add"
|
|
356
|
+
de = "Hinzufügen"
|
|
357
|
+
fr = "Ajouter"
|
|
358
|
+
it = "Aggiungi"
|
|
359
|
+
server_only = true # optional: never emitted into the JSON files (backend-only text)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```toml
|
|
363
|
+
# pyproject.toml
|
|
364
|
+
[tool.bdt.translate]
|
|
365
|
+
file = "translations.toml" # default
|
|
366
|
+
languages = ["en", "de", "fr", "it"] # default
|
|
367
|
+
required_languages = ["en"] # default; `bdt translate add` fails without these
|
|
368
|
+
output = ["frontend/src/assets/i18n"] # directories receiving <lng>.json
|
|
369
|
+
nested = false # true: "a.b.c" keys become nested JSON objects
|
|
370
|
+
scan = ["frontend/src"] # t("KEY") / $t("KEY") in .ts/.tsx/.js/.jsx/.vue
|
|
371
|
+
scan_jinja = ["backend/print"] # "KEY" | tr in .jinja2/.j2/.html
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- A missing language falls back to `de`, then `en`, then the key itself.
|
|
375
|
+
- Keys used in code but absent from `translations.toml` are appended with an English placeholder and
|
|
376
|
+
the command exits 1 without generating anything -- fill in the translations and rerun.
|
|
377
|
+
- `bdt translate add KEY en=Add de=Hinzufügen fr=Ajouter it=Aggiungi` adds a key to `translations.toml` and
|
|
378
|
+
regenerates all JSON files in one go (the `required_languages` are mandatory; `--force` overwrites an existing key).
|
|
379
|
+
- `bdt translate --check` writes nothing and exits 1 if code uses keys missing from the toml (for CI).
|
|
380
|
+
- `bdt translate --import` merges existing `<lng>.json` files into `translations.toml` once, to migrate
|
|
381
|
+
a repo that so far hand-maintained its JSON files; then `git rm --cached` them and ignore them.
|
|
382
|
+
|
|
343
383
|
## `bdt worktree`
|
|
344
384
|
|
|
345
385
|
```bash
|
|
@@ -571,8 +611,9 @@ bdt logs fetch --env prod --out logs/ --keep-archive
|
|
|
571
611
|
## `bdt lint`
|
|
572
612
|
|
|
573
613
|
Static checks (implementing [bmsuisse/skills#52](https://github.com/bmsuisse/skills/issues/52))
|
|
574
|
-
for the `postgres-best-practices` skill's SQL rules, pydantic-model placement,
|
|
575
|
-
that the repo has its baseline tooling actually set up
|
|
614
|
+
for the `postgres-best-practices` skill's SQL rules, pydantic-model placement, hand-wired HTTP in
|
|
615
|
+
TypeScript, and that the repo has its baseline tooling actually set up. Unused `.sql` files and
|
|
616
|
+
dead backend routes are a separate command, [`bdt dead-code`](#bdt-dead-code):
|
|
576
617
|
|
|
577
618
|
```bash
|
|
578
619
|
bdt lint # scan the current directory, recursively
|
|
@@ -661,6 +702,112 @@ already pulled in transitively via `pgdevkit[db]`, but declared explicitly as
|
|
|
661
702
|
the `bmsdna-devtools[lint]` extra; a clear install hint is printed (not a raw
|
|
662
703
|
`ImportError`) if it's ever missing.
|
|
663
704
|
|
|
705
|
+
## `bdt dead-code`
|
|
706
|
+
|
|
707
|
+
Dead code a linter can't see, in one command and one config table, `[tool.bdt.dead_code]`:
|
|
708
|
+
|
|
709
|
+
- **`sql`** -- `.sql` files that no Python code loads;
|
|
710
|
+
- **`routes`** -- backend (FastAPI) routes that neither non-generated frontend code nor a `url_for(...)`
|
|
711
|
+
call uses.
|
|
712
|
+
|
|
713
|
+
Each check runs when it is configured (`sql_roots`, resp. `[[tool.bdt.dead_code.apps]]`); `--only sql` /
|
|
714
|
+
`--only routes` (repeatable) narrows a run, e.g. to keep the slow one -- `routes` imports the app, so run
|
|
715
|
+
`bdt` with the repo's own interpreter (`uv run bdt dead-code`) -- out of a prek hook. Exit code is 0 (clean), 1
|
|
716
|
+
(findings) or 2 (setup problem: invalid config or pyproject.toml, nothing configured, the app doesn't import
|
|
717
|
+
within 5 minutes -- the error shows the import's output).
|
|
718
|
+
|
|
719
|
+
### `sql` -- unreferenced `.sql` files
|
|
720
|
+
|
|
721
|
+
A `.sql` file that no Python code loads is a query left behind after its caller was deleted or renamed. Name the
|
|
722
|
+
folders that hold *loadable* SQL (not schema/migration scripts, which are applied rather than loaded) and every
|
|
723
|
+
file under them that nothing references is reported (`sql-file-unreferenced`):
|
|
724
|
+
|
|
725
|
+
```toml
|
|
726
|
+
[tool.bdt.dead_code]
|
|
727
|
+
sql_roots = ["backend/db/queries"] # repo-relative; a typo'd root is itself reported
|
|
728
|
+
sql_loader_functions = ["load_sql"] # default; add your own loader's name if it differs
|
|
729
|
+
sql_unreferenced_ignore = ["backend/db/queries/legacy/*.sql"] # globs for files reached some other way
|
|
730
|
+
# exclude_dirs = ["generated"] # extra directory names to skip, beyond .venv/node_modules/etc.
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
A file counts as referenced by a literal `load_sql("topic", "name")` call (positional or `topic=`/`name=`;
|
|
734
|
+
topic = the file's parent directory, name = its stem); by a `load_sql("topic", some_var)` call in a Python file that
|
|
735
|
+
also contains the stem as a string literal (so `name = "a" if x else "b"` and lookup tables work);
|
|
736
|
+
by a string literal that is a path whose trailing segments equal the file's repo-relative path
|
|
737
|
+
(`get_sql_with_prm_list("backend/api/sql/x.sql")`); or by its bare filename as a literal in a
|
|
738
|
+
Python file under the SQL folder's parent (`_SQL_DIR / "x.sql"`). It never executes code, so a
|
|
739
|
+
file reached through a fully computed path is a false positive -- list it in
|
|
740
|
+
`sql_unreferenced_ignore`. References are searched across the whole repo, since the caller can live anywhere.
|
|
741
|
+
A Python file that can't be parsed (including syntax newer than the interpreter running `bdt`) is itself
|
|
742
|
+
reported as `sql-check-python-unparseable`, because references in it are unknown.
|
|
743
|
+
|
|
744
|
+
### `routes` -- backend routes nobody uses
|
|
745
|
+
|
|
746
|
+
Backend (FastAPI) operations that **no non-generated frontend code calls and no backend code references by
|
|
747
|
+
name** -- dead routes that still have to be maintained, secured and tested.
|
|
748
|
+
|
|
749
|
+
```toml
|
|
750
|
+
[[tool.bdt.dead_code.apps]]
|
|
751
|
+
name = "akeneo" # label used in the report
|
|
752
|
+
app = "main_app:app" # module:attribute, imported in a subprocess ...
|
|
753
|
+
app_dir = "akeneo_editor/backend" # ... with this directory (repo-relative) as cwd/import root
|
|
754
|
+
# openapi = "frontend/openapi.json" # alternative to `app`: a committed OpenAPI document
|
|
755
|
+
frontends = ["akeneo_editor/frontend/src"] # repo-relative dirs or globs, e.g. "mdmapp/app/react_apps/*/src"
|
|
756
|
+
exclude_prefixes = ["/external_api"] # routes meant for other callers (external API, webhooks)
|
|
757
|
+
exclude_tags = ["agent"] # e.g. LLM/MCP tools
|
|
758
|
+
exclude_paths = ["/auth/*", "GET /health"] # fnmatch globs on "/path" or "METHOD /path"
|
|
759
|
+
baseline = "dead-code-baseline.txt" # optional ratchet, see below
|
|
760
|
+
# env = { SOME_REQUIRED_SETTING = "x" } # extra environment for importing the app
|
|
761
|
+
# exclude_frontend_globs = ["src/legacy/*"] # repo-relative frontend files to ignore as callers
|
|
762
|
+
# url_for_functions = ["url_for", "url_path_for"] # default; functions whose first argument names a route
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Pair each backend app with *its own* frontends (one `[[...apps]]` entry per backend) -- a route
|
|
766
|
+
called only by another backend's frontend is still unused here.
|
|
767
|
+
|
|
768
|
+
The backend inventory is `app.openapi()` of the app **and of every mounted sub-app** (with the
|
|
769
|
+
mount prefix), so it needs no committed schema and can't go stale; routes with
|
|
770
|
+
`include_in_schema=False` are not considered.
|
|
771
|
+
|
|
772
|
+
**Generated code never counts as a caller.** A generated client lists *every* route, so it is
|
|
773
|
+
skipped (`generated/`, `*.gen.*`, `*.generated.*`, `api-types*`, `openapi_schema*`, `*.d.ts`), as are
|
|
774
|
+
tests and e2e specs (`*.test.*`, `*.spec.*`, `__tests__/`, `tests/`, `e2e/`) -- for TypeScript,
|
|
775
|
+
JavaScript and Vue files alike. Comments are blanked before scanning. An operation is called when
|
|
776
|
+
non-generated code of one of the app's frontends has (strongest first):
|
|
777
|
+
|
|
778
|
+
- **sdk** -- a reference to a hey-api SDK function (read from the `sdk.gen.ts` under that same frontend
|
|
779
|
+
directory, matched on method *and* url) or one of its react-query helpers (`fooOptions`, `fooMutation`,
|
|
780
|
+
`fooQueryKey`, ...). Each `frontends` directory is scoped to its own SDK, so two apps that both generate
|
|
781
|
+
`listItems` aren't confused;
|
|
782
|
+
- **fetch** -- an openapi-fetch call `.GET("/path"` naming this method and path (a `DELETE` of the
|
|
783
|
+
same path with no call of its own is still reported);
|
|
784
|
+
- **url** -- a string/template literal equal to the path template (any method), e.g. a hand-written
|
|
785
|
+
`fetch(`/api/x/${id}`)`, `"/api/x/" + id` or an `<a href=...>` -- nested templates like
|
|
786
|
+
`${qs ? `?a=${b}` : ""}` are handled;
|
|
787
|
+
- **url-sfx** -- a literal (two or more segments) that is a suffix of the route (a client with a base URL,
|
|
788
|
+
or a `${base}` prefix).
|
|
789
|
+
|
|
790
|
+
**Routes the backend refers to by name are used, too.** Auth redirects and OAuth callbacks have no frontend caller
|
|
791
|
+
by nature -- the backend builds their URL: `request.url_for("auth_callback")`, `app.url_path_for("login")`,
|
|
792
|
+
`{{ url_for('login') }}` in a template. A string literal passed (first argument, or `name=`) to one of
|
|
793
|
+
`url_for_functions` in a non-test Python file or Jinja-style template (`.html`, `.jinja`, `.jinja2`, `.j2`) under the app's `app_dir`
|
|
794
|
+
marks the route of that name as used (`"mount:name"` for a mounted sub-app counts as `name`). The route name is recovered from
|
|
795
|
+
its `operationId`: FastAPI's default `name + path + method` form, a bare `name`, or `<prefix>-<name>` from a custom
|
|
796
|
+
`generate_unique_id`; an OpenAPI file without operation ids can't be matched this way. It is a text match, so a call
|
|
797
|
+
in a comment counts too.
|
|
798
|
+
|
|
799
|
+
It is a heuristic: no type information, a URL literal matches every method of that path (and so does an
|
|
800
|
+
SPA `<Link to="/users/${id}">`), and URLs assembled from non-literal pieces (including a non-literal route
|
|
801
|
+
name in `url_for`) or handed to the client by the server are invisible -- exclude those routes explicitly. An app
|
|
802
|
+
that exposes no documented operations (e.g. one wrapped in middleware that hides `openapi()`) is a setup error,
|
|
803
|
+
not a clean pass. Run against our repos, the usual legitimate exclusions are health checks, the SPA
|
|
804
|
+
catch-all, service-to-service endpoints and an external API folder.
|
|
805
|
+
|
|
806
|
+
**Adopting it without fixing everything first:** set `baseline`, run
|
|
807
|
+
`uv run bdt dead-code --update-baseline` once (it concerns the `routes` check only), and commit the file. From
|
|
808
|
+
then on only *new* unused routes fail -- and a baseline line whose route has since been deleted, excluded or
|
|
809
|
+
used is reported as `api-route-baseline-stale`, so the list can only shrink.
|
|
810
|
+
|
|
664
811
|
## `bdt find-injection`
|
|
665
812
|
|
|
666
813
|
Scans backend and frontend code for injection risks, and warns when a web project has no
|
|
@@ -5,7 +5,8 @@ one-shot PR summary (`bdt pr info`), PR creation, claiming an issue (`bdt issue
|
|
|
5
5
|
merged-worktree/orphaned-test-DB cleanup), a commit-and-push helper with
|
|
6
6
|
pre-flight checks, Azure log queries, and static checks (`bdt lint`) for
|
|
7
7
|
postgres/psycopg SQL rules, pydantic-model placement, hand-wired HTTP access in
|
|
8
|
-
TypeScript
|
|
8
|
+
TypeScript and baseline tooling, plus `bdt dead-code` for `.sql` files nothing loads and
|
|
9
|
+
backend routes nothing calls.
|
|
9
10
|
`bdt pr *` and `bdt issue *` auto-detect whether the current repo's `origin`
|
|
10
11
|
remote is Azure DevOps or GitHub and use `az`/`gh` accordingly.
|
|
11
12
|
Consolidates near-duplicate scripts that used to be copy-pasted across
|
|
@@ -35,11 +36,12 @@ Find the PR opened from the current branch and report build/check status
|
|
|
35
36
|
GitHub — whichever `origin` points at.
|
|
36
37
|
|
|
37
38
|
```bash
|
|
38
|
-
bdt pr status [--target-branch
|
|
39
|
+
bdt pr status [--target-branch BRANCH] [--wait]
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
The `--target` / `--target-branch` option of `pr create/retry/publish/update/comment/watch-deploy` and `commit`
|
|
43
|
+
defaults to `dev`; pass `--target main` (or whatever your PRs go into) to override. `pr status`, `pr info` and
|
|
44
|
+
`issue take` only look a PR up, so by default they don't filter on the target branch at all.
|
|
43
45
|
|
|
44
46
|
If the PR can't be merged, that's reported immediately instead of polling
|
|
45
47
|
for builds/checks that will never run — e.g. on Azure DevOps:
|
|
@@ -60,7 +62,7 @@ one is also waiting on approval.
|
|
|
60
62
|
`*.visualstudio.com` HTTPS forms). Auth is an explicit PAT (`--pat` or
|
|
61
63
|
`AZURE_DEVOPS_EXT_PAT`/`AZURE_DEVOPS_PAT` env var), falling back to a short-lived token from the
|
|
62
64
|
caller's own `az login` — never embed a PAT literal in a script or CI file.
|
|
63
|
-
`--target-branch`
|
|
65
|
+
`--target-branch` optionally restricts the lookup to PRs into that branch (default: any).
|
|
64
66
|
|
|
65
67
|
**GitHub**: uses `gh`'s own auth (`gh auth login`) and always resolves the
|
|
66
68
|
PR opened from the current branch — `gh pr view` has no target-branch
|
|
@@ -78,7 +80,7 @@ it watches a branch-triggered build/workflow run, not any particular PR.)
|
|
|
78
80
|
## `bdt pr info`
|
|
79
81
|
|
|
80
82
|
```bash
|
|
81
|
-
bdt pr info [--json] [--pr-id N] [--target-branch
|
|
83
|
+
bdt pr info [--json] [--pr-id N] [--target-branch BRANCH]
|
|
82
84
|
```
|
|
83
85
|
|
|
84
86
|
One-shot summary of the current branch's PR: link, state (open/merged/closed, draft), aggregate
|
|
@@ -89,7 +91,7 @@ the check rollup (only a failed check fails it; skipped/cancelled ones don't cou
|
|
|
89
91
|
from the latest build of each pipeline (any failed result fails it; unfinished builds are pending, or
|
|
90
92
|
`waiting` when a stage needs approval). `unknown` if Azure DevOps can't be asked.
|
|
91
93
|
|
|
92
|
-
Azure DevOps limits: the PR is found by the current branch into `--target-branch`
|
|
94
|
+
Azure DevOps limits: the PR is found by the current branch (into `--target-branch` if given, else any target),
|
|
93
95
|
a PR with merge conflicts is reported as an error (exit 1) like `pr status` does, and the closed
|
|
94
96
|
work items are only those whose URL is in the PR description -- not ones linked via
|
|
95
97
|
`bdt pr create --issue`.
|
|
@@ -310,12 +312,12 @@ last argument); extra args are passed through; `--dry-run` prints the command.
|
|
|
310
312
|
It takes the issue first (like `bdt issue take`): before the agent starts it comments `Taken by <you>`
|
|
311
313
|
on the issue, naming the session it is about to start -- for `claude` a `--session-id` is generated and
|
|
312
314
|
passed, so the comment says `claude --resume <id>`; the prompt tells the agent not to take it again. For
|
|
313
|
-
another `--agent` the comment just says `(via <agent>)`. `--dry-run` posts nothing. Like `issue take`, it doesn't comment again when the newest comment already is
|
|
315
|
+
another `--agent` the comment just says `(via <agent>)`. `--dry-run` posts nothing. Like `issue take`, it doesn't comment again when the newest comment already is your claim; if it is someone else's, it stops (exit 1) unless `--force`.
|
|
314
316
|
|
|
315
317
|
## `bdt issue take`
|
|
316
318
|
|
|
317
319
|
```bash
|
|
318
|
-
bdt issue take [NUMBER] [--target-branch
|
|
320
|
+
bdt issue take [NUMBER] [--target-branch BRANCH] [--force]
|
|
319
321
|
```
|
|
320
322
|
|
|
321
323
|
Claims an issue (GitHub) or work item (Azure DevOps): comments `Taken by <you>` on it, followed by the
|
|
@@ -327,6 +329,43 @@ branch's PR closes (`Fixes #N`, or an issue/work-item URL in the PR body); it re
|
|
|
327
329
|
that is none or several. If the issue's newest comment already is a "Taken by ..." claim (by anyone) it
|
|
328
330
|
says so and posts nothing, so it's safe to run repeatedly -- including before `bdt issue do`.
|
|
329
331
|
|
|
332
|
+
## `bdt translate`
|
|
333
|
+
|
|
334
|
+
One `translations.toml` is the source of truth for all UI translations; `bdt translate` generates the
|
|
335
|
+
per-language `de.json` / `en.json` / ... files from it. The generated files are build artifacts --
|
|
336
|
+
add them to `.gitignore` and run `bdt translate` in your `just install` / build / CI steps.
|
|
337
|
+
|
|
338
|
+
```toml
|
|
339
|
+
# translations.toml (keys with dots must be quoted when `nested = true`)
|
|
340
|
+
[ADD_BUTTON]
|
|
341
|
+
en = "Add"
|
|
342
|
+
de = "Hinzufügen"
|
|
343
|
+
fr = "Ajouter"
|
|
344
|
+
it = "Aggiungi"
|
|
345
|
+
server_only = true # optional: never emitted into the JSON files (backend-only text)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```toml
|
|
349
|
+
# pyproject.toml
|
|
350
|
+
[tool.bdt.translate]
|
|
351
|
+
file = "translations.toml" # default
|
|
352
|
+
languages = ["en", "de", "fr", "it"] # default
|
|
353
|
+
required_languages = ["en"] # default; `bdt translate add` fails without these
|
|
354
|
+
output = ["frontend/src/assets/i18n"] # directories receiving <lng>.json
|
|
355
|
+
nested = false # true: "a.b.c" keys become nested JSON objects
|
|
356
|
+
scan = ["frontend/src"] # t("KEY") / $t("KEY") in .ts/.tsx/.js/.jsx/.vue
|
|
357
|
+
scan_jinja = ["backend/print"] # "KEY" | tr in .jinja2/.j2/.html
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
- A missing language falls back to `de`, then `en`, then the key itself.
|
|
361
|
+
- Keys used in code but absent from `translations.toml` are appended with an English placeholder and
|
|
362
|
+
the command exits 1 without generating anything -- fill in the translations and rerun.
|
|
363
|
+
- `bdt translate add KEY en=Add de=Hinzufügen fr=Ajouter it=Aggiungi` adds a key to `translations.toml` and
|
|
364
|
+
regenerates all JSON files in one go (the `required_languages` are mandatory; `--force` overwrites an existing key).
|
|
365
|
+
- `bdt translate --check` writes nothing and exits 1 if code uses keys missing from the toml (for CI).
|
|
366
|
+
- `bdt translate --import` merges existing `<lng>.json` files into `translations.toml` once, to migrate
|
|
367
|
+
a repo that so far hand-maintained its JSON files; then `git rm --cached` them and ignore them.
|
|
368
|
+
|
|
330
369
|
## `bdt worktree`
|
|
331
370
|
|
|
332
371
|
```bash
|
|
@@ -558,8 +597,9 @@ bdt logs fetch --env prod --out logs/ --keep-archive
|
|
|
558
597
|
## `bdt lint`
|
|
559
598
|
|
|
560
599
|
Static checks (implementing [bmsuisse/skills#52](https://github.com/bmsuisse/skills/issues/52))
|
|
561
|
-
for the `postgres-best-practices` skill's SQL rules, pydantic-model placement,
|
|
562
|
-
that the repo has its baseline tooling actually set up
|
|
600
|
+
for the `postgres-best-practices` skill's SQL rules, pydantic-model placement, hand-wired HTTP in
|
|
601
|
+
TypeScript, and that the repo has its baseline tooling actually set up. Unused `.sql` files and
|
|
602
|
+
dead backend routes are a separate command, [`bdt dead-code`](#bdt-dead-code):
|
|
563
603
|
|
|
564
604
|
```bash
|
|
565
605
|
bdt lint # scan the current directory, recursively
|
|
@@ -648,6 +688,112 @@ already pulled in transitively via `pgdevkit[db]`, but declared explicitly as
|
|
|
648
688
|
the `bmsdna-devtools[lint]` extra; a clear install hint is printed (not a raw
|
|
649
689
|
`ImportError`) if it's ever missing.
|
|
650
690
|
|
|
691
|
+
## `bdt dead-code`
|
|
692
|
+
|
|
693
|
+
Dead code a linter can't see, in one command and one config table, `[tool.bdt.dead_code]`:
|
|
694
|
+
|
|
695
|
+
- **`sql`** -- `.sql` files that no Python code loads;
|
|
696
|
+
- **`routes`** -- backend (FastAPI) routes that neither non-generated frontend code nor a `url_for(...)`
|
|
697
|
+
call uses.
|
|
698
|
+
|
|
699
|
+
Each check runs when it is configured (`sql_roots`, resp. `[[tool.bdt.dead_code.apps]]`); `--only sql` /
|
|
700
|
+
`--only routes` (repeatable) narrows a run, e.g. to keep the slow one -- `routes` imports the app, so run
|
|
701
|
+
`bdt` with the repo's own interpreter (`uv run bdt dead-code`) -- out of a prek hook. Exit code is 0 (clean), 1
|
|
702
|
+
(findings) or 2 (setup problem: invalid config or pyproject.toml, nothing configured, the app doesn't import
|
|
703
|
+
within 5 minutes -- the error shows the import's output).
|
|
704
|
+
|
|
705
|
+
### `sql` -- unreferenced `.sql` files
|
|
706
|
+
|
|
707
|
+
A `.sql` file that no Python code loads is a query left behind after its caller was deleted or renamed. Name the
|
|
708
|
+
folders that hold *loadable* SQL (not schema/migration scripts, which are applied rather than loaded) and every
|
|
709
|
+
file under them that nothing references is reported (`sql-file-unreferenced`):
|
|
710
|
+
|
|
711
|
+
```toml
|
|
712
|
+
[tool.bdt.dead_code]
|
|
713
|
+
sql_roots = ["backend/db/queries"] # repo-relative; a typo'd root is itself reported
|
|
714
|
+
sql_loader_functions = ["load_sql"] # default; add your own loader's name if it differs
|
|
715
|
+
sql_unreferenced_ignore = ["backend/db/queries/legacy/*.sql"] # globs for files reached some other way
|
|
716
|
+
# exclude_dirs = ["generated"] # extra directory names to skip, beyond .venv/node_modules/etc.
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
A file counts as referenced by a literal `load_sql("topic", "name")` call (positional or `topic=`/`name=`;
|
|
720
|
+
topic = the file's parent directory, name = its stem); by a `load_sql("topic", some_var)` call in a Python file that
|
|
721
|
+
also contains the stem as a string literal (so `name = "a" if x else "b"` and lookup tables work);
|
|
722
|
+
by a string literal that is a path whose trailing segments equal the file's repo-relative path
|
|
723
|
+
(`get_sql_with_prm_list("backend/api/sql/x.sql")`); or by its bare filename as a literal in a
|
|
724
|
+
Python file under the SQL folder's parent (`_SQL_DIR / "x.sql"`). It never executes code, so a
|
|
725
|
+
file reached through a fully computed path is a false positive -- list it in
|
|
726
|
+
`sql_unreferenced_ignore`. References are searched across the whole repo, since the caller can live anywhere.
|
|
727
|
+
A Python file that can't be parsed (including syntax newer than the interpreter running `bdt`) is itself
|
|
728
|
+
reported as `sql-check-python-unparseable`, because references in it are unknown.
|
|
729
|
+
|
|
730
|
+
### `routes` -- backend routes nobody uses
|
|
731
|
+
|
|
732
|
+
Backend (FastAPI) operations that **no non-generated frontend code calls and no backend code references by
|
|
733
|
+
name** -- dead routes that still have to be maintained, secured and tested.
|
|
734
|
+
|
|
735
|
+
```toml
|
|
736
|
+
[[tool.bdt.dead_code.apps]]
|
|
737
|
+
name = "akeneo" # label used in the report
|
|
738
|
+
app = "main_app:app" # module:attribute, imported in a subprocess ...
|
|
739
|
+
app_dir = "akeneo_editor/backend" # ... with this directory (repo-relative) as cwd/import root
|
|
740
|
+
# openapi = "frontend/openapi.json" # alternative to `app`: a committed OpenAPI document
|
|
741
|
+
frontends = ["akeneo_editor/frontend/src"] # repo-relative dirs or globs, e.g. "mdmapp/app/react_apps/*/src"
|
|
742
|
+
exclude_prefixes = ["/external_api"] # routes meant for other callers (external API, webhooks)
|
|
743
|
+
exclude_tags = ["agent"] # e.g. LLM/MCP tools
|
|
744
|
+
exclude_paths = ["/auth/*", "GET /health"] # fnmatch globs on "/path" or "METHOD /path"
|
|
745
|
+
baseline = "dead-code-baseline.txt" # optional ratchet, see below
|
|
746
|
+
# env = { SOME_REQUIRED_SETTING = "x" } # extra environment for importing the app
|
|
747
|
+
# exclude_frontend_globs = ["src/legacy/*"] # repo-relative frontend files to ignore as callers
|
|
748
|
+
# url_for_functions = ["url_for", "url_path_for"] # default; functions whose first argument names a route
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
Pair each backend app with *its own* frontends (one `[[...apps]]` entry per backend) -- a route
|
|
752
|
+
called only by another backend's frontend is still unused here.
|
|
753
|
+
|
|
754
|
+
The backend inventory is `app.openapi()` of the app **and of every mounted sub-app** (with the
|
|
755
|
+
mount prefix), so it needs no committed schema and can't go stale; routes with
|
|
756
|
+
`include_in_schema=False` are not considered.
|
|
757
|
+
|
|
758
|
+
**Generated code never counts as a caller.** A generated client lists *every* route, so it is
|
|
759
|
+
skipped (`generated/`, `*.gen.*`, `*.generated.*`, `api-types*`, `openapi_schema*`, `*.d.ts`), as are
|
|
760
|
+
tests and e2e specs (`*.test.*`, `*.spec.*`, `__tests__/`, `tests/`, `e2e/`) -- for TypeScript,
|
|
761
|
+
JavaScript and Vue files alike. Comments are blanked before scanning. An operation is called when
|
|
762
|
+
non-generated code of one of the app's frontends has (strongest first):
|
|
763
|
+
|
|
764
|
+
- **sdk** -- a reference to a hey-api SDK function (read from the `sdk.gen.ts` under that same frontend
|
|
765
|
+
directory, matched on method *and* url) or one of its react-query helpers (`fooOptions`, `fooMutation`,
|
|
766
|
+
`fooQueryKey`, ...). Each `frontends` directory is scoped to its own SDK, so two apps that both generate
|
|
767
|
+
`listItems` aren't confused;
|
|
768
|
+
- **fetch** -- an openapi-fetch call `.GET("/path"` naming this method and path (a `DELETE` of the
|
|
769
|
+
same path with no call of its own is still reported);
|
|
770
|
+
- **url** -- a string/template literal equal to the path template (any method), e.g. a hand-written
|
|
771
|
+
`fetch(`/api/x/${id}`)`, `"/api/x/" + id` or an `<a href=...>` -- nested templates like
|
|
772
|
+
`${qs ? `?a=${b}` : ""}` are handled;
|
|
773
|
+
- **url-sfx** -- a literal (two or more segments) that is a suffix of the route (a client with a base URL,
|
|
774
|
+
or a `${base}` prefix).
|
|
775
|
+
|
|
776
|
+
**Routes the backend refers to by name are used, too.** Auth redirects and OAuth callbacks have no frontend caller
|
|
777
|
+
by nature -- the backend builds their URL: `request.url_for("auth_callback")`, `app.url_path_for("login")`,
|
|
778
|
+
`{{ url_for('login') }}` in a template. A string literal passed (first argument, or `name=`) to one of
|
|
779
|
+
`url_for_functions` in a non-test Python file or Jinja-style template (`.html`, `.jinja`, `.jinja2`, `.j2`) under the app's `app_dir`
|
|
780
|
+
marks the route of that name as used (`"mount:name"` for a mounted sub-app counts as `name`). The route name is recovered from
|
|
781
|
+
its `operationId`: FastAPI's default `name + path + method` form, a bare `name`, or `<prefix>-<name>` from a custom
|
|
782
|
+
`generate_unique_id`; an OpenAPI file without operation ids can't be matched this way. It is a text match, so a call
|
|
783
|
+
in a comment counts too.
|
|
784
|
+
|
|
785
|
+
It is a heuristic: no type information, a URL literal matches every method of that path (and so does an
|
|
786
|
+
SPA `<Link to="/users/${id}">`), and URLs assembled from non-literal pieces (including a non-literal route
|
|
787
|
+
name in `url_for`) or handed to the client by the server are invisible -- exclude those routes explicitly. An app
|
|
788
|
+
that exposes no documented operations (e.g. one wrapped in middleware that hides `openapi()`) is a setup error,
|
|
789
|
+
not a clean pass. Run against our repos, the usual legitimate exclusions are health checks, the SPA
|
|
790
|
+
catch-all, service-to-service endpoints and an external API folder.
|
|
791
|
+
|
|
792
|
+
**Adopting it without fixing everything first:** set `baseline`, run
|
|
793
|
+
`uv run bdt dead-code --update-baseline` once (it concerns the `routes` check only), and commit the file. From
|
|
794
|
+
then on only *new* unused routes fail -- and a baseline line whose route has since been deleted, excluded or
|
|
795
|
+
used is reported as `api-route-baseline-stale`, so the list can only shrink.
|
|
796
|
+
|
|
651
797
|
## `bdt find-injection`
|
|
652
798
|
|
|
653
799
|
Scans backend and frontend code for injection risks, and warns when a web project has no
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Standalone (stdlib-only) helper of the `routes` check of `bdt dead-code`: list the documented HTTP operations of a
|
|
2
|
+
FastAPI-like app, recursing into mounted sub-apps.
|
|
3
|
+
|
|
4
|
+
It is executed in the *target repo's* interpreter via `python -c <this file's source> module:attr out.json`
|
|
5
|
+
and must therefore never import `bmsdna.devtools`: the working directory is first on `sys.path` there, and a repo may
|
|
6
|
+
ship its own top-level `bmsdna` package (CCMT2 does) that would shadow ours. `api_usage` imports it normally for
|
|
7
|
+
`ops_from_doc`, so the OpenAPI-document parsing exists exactly once.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
HTTP_METHODS = ("get", "post", "put", "patch", "delete", "options", "head")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ops_from_doc(doc, mount=""):
|
|
18
|
+
"""Operations of one OpenAPI document as dicts, with `mount` prepended to every path."""
|
|
19
|
+
ops = []
|
|
20
|
+
for path, item in (doc.get("paths") or {}).items():
|
|
21
|
+
for method, op in item.items():
|
|
22
|
+
if method in HTTP_METHODS and isinstance(op, dict):
|
|
23
|
+
ops.append(
|
|
24
|
+
{
|
|
25
|
+
"method": method.upper(),
|
|
26
|
+
"path": mount + path,
|
|
27
|
+
"tags": list(op.get("tags") or []),
|
|
28
|
+
"mount": mount,
|
|
29
|
+
"operation_id": str(op.get("operationId") or ""),
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
return ops
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def collect(app, mount=""):
|
|
36
|
+
"""Operations of `app` and, recursively, of every mounted sub-app. Uses only `app.openapi()` and the
|
|
37
|
+
`.path`/`.app` attributes of mount routes -- not FastAPI internals, which change between releases (FastAPI 0.141 wraps
|
|
38
|
+
included routers in lazy objects that `app.routes` no longer lists as plain routes)."""
|
|
39
|
+
openapi = getattr(app, "openapi", None)
|
|
40
|
+
if not callable(openapi):
|
|
41
|
+
return []
|
|
42
|
+
ops = ops_from_doc(openapi(), mount)
|
|
43
|
+
for route in getattr(app, "routes", None) or []:
|
|
44
|
+
sub = getattr(route, "app", None)
|
|
45
|
+
if sub is not None and callable(getattr(sub, "openapi", None)) and isinstance(getattr(route, "path", None), str):
|
|
46
|
+
ops.extend(collect(sub, mount + route.path.rstrip("/")))
|
|
47
|
+
return ops
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv):
|
|
51
|
+
module_name, _, attr = argv[0].partition(":")
|
|
52
|
+
app = getattr(importlib.import_module(module_name), attr)
|
|
53
|
+
with open(argv[1], "w", encoding="utf-8") as out:
|
|
54
|
+
json.dump(collect(app), out)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main(sys.argv[1:])
|