This repo is the current Python leading-edge reference inside this Practice network.
Its package identity follows the Oak Python convention:
- distribution name:
oaknational-python-repo-template - import path:
oaknational.python_repo_template
- package manager and task runner:
uv - Python version management:
.python-version - formatter and linter:
ruff - type checking:
pyright - markdown linting:
pymarkdown - dependency hygiene:
deptry - commit workflow:
commitizen - tests:
pytest - repo-state audit:
uv run python -m oaknational.python_repo_template.devtools repo-audit
- the distribution ships
py.typed pyrightruns in explicit strict mode- the checked repo-owned surface is
src/,tests/, andtools/
uv run python -m oaknational.python_repo_template.devtools cleanuv run python -m oaknational.python_repo_template.devtools builduv run python -m oaknational.python_repo_template.devtools devuv run python -m oaknational.python_repo_template.devtools formatuv run python -m oaknational.python_repo_template.devtools format-fixuv run python -m oaknational.python_repo_template.devtools lintuv run python -m oaknational.python_repo_template.devtools lint-fixuv run python -m oaknational.python_repo_template.devtools markdownlintuv run python -m oaknational.python_repo_template.devtools markdownlint-fixuv run python -m oaknational.python_repo_template.devtools typecheckuv run python -m oaknational.python_repo_template.devtools repo-audituv run python -m oaknational.python_repo_template.devtools testuv run python -m oaknational.python_repo_template.devtools coverageuv run python -m oaknational.python_repo_template.devtools fixuv run python -m oaknational.python_repo_template.devtools checkuv run python -m oaknational.python_repo_template.devtools check-ci
- direct command:
uv run deptry . deptryproves declared dependency hygiene (unused, missing, or misplaced dependencies) — which is distinct from vulnerability scanning (see below)uv run python -m oaknational.python_repo_template.devtools checkanduv run python -m oaknational.python_repo_template.devtools check-ciboth run dependency hygiene before the tracked-repo auditpyarrowremains a deliberatedeptryDEP002exception because pandas exercises it indirectly through the bounded Parquet path
pip-auditscans the project's locked dependencies for known advisories; it is a blocking step incheckandcheck-ci(right after dependency hygiene)- direct command (the pipe equivalent of the gate):
uv export --no-emit-project --no-hashes --format requirements-txt | uv run pip-audit -r -(the gate itself writes the export to a temporary file and passes--requirement <file>, which is equivalent) - the gate exports the locked set with
--no-emit-project(so the project itself is not audited against PyPI) and audits that, so it scans exactly what is pinned inuv.lock
- direct commands:
uv run python -m oaknational.python_repo_template.devtools markdownlint(check) anduv run python -m oaknational.python_repo_template.devtools markdownlint-fix(autofix) pymarkdownscans the tracked Markdown estate, respecting.gitignore- rules are configured in
[tool.pymarkdown]inpyproject.toml; YAML front matter is recognised,MD013,MD041, andMD029are disabled andMD024is relaxed to siblings-only, so structural rules such asMD040stay on checkandcheck-ciboth run the markdown lint after the Ruff lint
codespellspell-checks the tracked text estate; it is a blocking step incheckandcheck-ci(right after the Markdown lint)- direct command:
uv run codespell . - British spellings pass codespell's default dictionary, so no en-GB conversion
fires; configuration lives in
[tool.codespell]inpyproject.toml skipexcludes lock files, caches, and binary artefacts; add repo jargon toignore-words-listonly when codespell genuinely flags a real non-word
gitleaksperforms secret scanning: it looks for committed credentials, tokens, and keys- it runs alongside
check-ci, not inside it:gitleaksis a Go binary, not auvpackage, so the venv cannot carry it anduv syncstays sufficient for the gate sequence - locally it runs as a pinned pre-commit hook (the official
github.com/gitleaks/gitleaksmirror);uv run pre-commit installwires it in and pre-commit installs the binary for you via its Go support — no manual step - in CI a dedicated
secret-scanjob installs the same pinned binary (with a verified checksum) and runsgitleaks dir . - the allowlist lives in
.gitleaks.toml, which extends the upstream default rule set (useDefault = true); every exemption must justify itself in a comment, mirroring the commented-ignore-file doctrine used for Markdown linting - the version is pinned once in
tools/repo_audit_contract.tomland kept in lockstep across the pre-commit mirror and CI by thesecret-scanningrepo-auditcheck; bump all three together - scope boundary: CI scans the checked-out working tree (
gitleaks dir .) and the pre-commit hook scans staged changes — neither walks the full git history, so a secret committed and later removed in an earlier commit is not caught. History scanning (gitleaks gitwith a full-depth checkout) is a deliberate later enhancement, not part of this gate
- every GitHub Actions
uses:in the workflows is pinned to a full commit SHA (with a trailing# vXcomment for readability), so a retagged or compromised upstream release cannot silently change what CI runs .github/dependabot.ymlschedules weekly grouped update PRs for the two pinned ecosystems —uv(the locked Python dependencies) andgithub-actions(the pinned SHAs, which Dependabot bumps together with their# vXcomment)- the
supply-chainrepo-auditcheck enforces both: it fails if any workflowuses:is a tag or branch rather than a 40-hex SHA, or if Dependabot stops watching either ecosystem, so the pins cannot quietly drift back to tags
uv run python -m oaknational.python_repo_template.devtools buildbuilds the repo artefacts and then smoke-tests the newest built wheel from a temporary virtual environment outside the source treeuv run python -m oaknational.python_repo_template.devtools checkanduv run python -m oaknational.python_repo_template.devtools check-cikeep the same proof in their build step- the smoke path proves the installed package import plus both entry surfaces:
activity-reportandpython -m oaknational.python_repo_template
- the
coveragegate runspytestundercoverage.pywith branch coverage on; locally it prints theterm-missingreport and fails under the threshold in[tool.coverage.report](fail_under = 86, an honest floor below the ~87.6% the suite achieves with branches counted) - the
coverage-contractrepo-auditcheck guards what the coverage gate itself cannot: it fails iffail_underdrops below 86, if branch coverage is switched off, or if[tool.coverage.run].omitexcludes any file beyond the justified set (devtools.py) — so the threshold cannot be quietly lowered, branch coverage cannot be disabled to inflate the number, and code cannot be hidden from the coverage denominator. Raisingfail_underis always allowed - CI additionally derives a Cobertura report from the same run with the
coverage xmlsubcommand — thecoveragegate'spytest --covrun writes the.coveragedata file, andcoverage xmlreads it (no second test run) — then uploads it to GitHub Code Quality viaactions/upload-code-coverage, so coverage shows on pull requests coverage.xmland the.coverage*data files are git-ignored, anddevtools cleanremoves them- GitHub Code Quality is a preview that must be enabled for the organisation; the
upload runs with
fail-on-error: falseso it never turns the gate run red before then
Install the repo hooks with:
uv run pre-commit installThe repo config installs pre-commit, pre-push, and commit-msg by
default, so Commitizen validation runs on real commit messages rather than as a
separate advisory command.
Create a conventional commit with:
uv run cz commitValidate a message manually with:
uv run cz check --message "docs: explain the Commitizen workflow"- automated by
.github/workflows/release.ymlas continuous release on merge: it triggers onworkflow_runafter CI succeeds onmain, so a release only cuts once the gates are green - the bump policy is
feat/fix→ minor, everything else → patch, and breaking markers are not auto-mapped to major; it lives in[tool.commitizen].bump_mapbut, becausecz_conventional_commitsignores that map,tools/release_increment.pyreads it and computes the increment, which the workflow applies viacz bump --increment - the Oak Semantic Release Bot GitHub App (a
main-ruleset bypass actor) authenticates viaactions/create-github-app-token(secretsRELEASE_APP_CLIENT_IDandRELEASE_APP_PRIVATE_KEY); the workflow bumpspyproject.toml,uv.lock, andCHANGELOG.md, commits + tagsvX.Y.Z, pushes straight tomain, thenuv builds and creates the GitHub Release with the wheel + sdist attached - the bump commit is marked
[skip ci]so pushing it tomaindoes not re-trigger CI → Release (an infinite loop) - there is no manual release trigger (no
workflow_dispatch): releases originate only from a merge tomain, whichaudit_release_workflowenforces - major releases are not automated: a
!/BREAKING CHANGEmarker makes the auto-release stand down, and theprevent-accidental-majorcommit-msg hook (tools/prevent_accidental_major.py) rejects the marker at commit time so it cannot land by accident and silently halt releases. The rare major is cut by a human engineer outside this repo's automation - the version stays committed in the tree; releases publish to GitHub Releases
only (no PyPI).
audit_release_workflowkeeps the workflow (trigger,cz bump, the increment tool, the[skip ci]loop guard) and the bump policy honest - gotcha: because the loop guard relies on GitHub's CI-skip marker, a feature
commit or PR title/body that contains that literal marker in prose will make a
squash-merge skip CI too — so no
workflow_runfires and no release is cut. Keep the literal marker out of feature commit/PR messages; only the bump commit should carry it
When hydrating or extending another Python repo with this Practice:
- keep the canonical repo-local
uv run python -m ...gate surface truthful - use Python-native separators such as dashes rather than colon aliases
- preserve stronger existing local contracts where they already meet or exceed this stack
- update docs, hooks, and audits in the same landing as command-surface changes