heinzel-github 0.9.1__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 (32) hide show
  1. heinzel_github-0.9.1/.gitignore +15 -0
  2. heinzel_github-0.9.1/CHANGELOG.md +8 -0
  3. heinzel_github-0.9.1/LICENSE +21 -0
  4. heinzel_github-0.9.1/PKG-INFO +137 -0
  5. heinzel_github-0.9.1/README.md +124 -0
  6. heinzel_github-0.9.1/plugin-lock.json +40 -0
  7. heinzel_github-0.9.1/pyproject.toml +131 -0
  8. heinzel_github-0.9.1/requirements.lock +1154 -0
  9. heinzel_github-0.9.1/src/heinzel_github/__init__.py +70 -0
  10. heinzel_github-0.9.1/src/heinzel_github/_adapter.py +176 -0
  11. heinzel_github-0.9.1/src/heinzel_github/_client.py +202 -0
  12. heinzel_github-0.9.1/src/heinzel_github/_gh.py +349 -0
  13. heinzel_github-0.9.1/src/heinzel_github/_models.py +87 -0
  14. heinzel_github-0.9.1/src/heinzel_github/_module.py +5 -0
  15. heinzel_github-0.9.1/src/heinzel_github/_service.py +155 -0
  16. heinzel_github-0.9.1/src/heinzel_github/py.typed +0 -0
  17. heinzel_github-0.9.1/src/heinzel_github/skills/review-pull-request/SKILL.md +25 -0
  18. heinzel_github-0.9.1/src/heinzel_github/skills/review-pull-request/references/checklist.md +11 -0
  19. heinzel_github-0.9.1/tests/conftest.py +287 -0
  20. heinzel_github-0.9.1/tests/fake_gh.py +57 -0
  21. heinzel_github-0.9.1/tests/fixtures/issues.json +49 -0
  22. heinzel_github-0.9.1/tests/fixtures/pulls.json +54 -0
  23. heinzel_github-0.9.1/tests/integration/capability_test.py +225 -0
  24. heinzel_github-0.9.1/tests/integration/cli_test.py +59 -0
  25. heinzel_github-0.9.1/tests/integration/connector_test.py +92 -0
  26. heinzel_github-0.9.1/tests/integration/gh_test.py +244 -0
  27. heinzel_github-0.9.1/tests/unit/adapter_test.py +248 -0
  28. heinzel_github-0.9.1/tests/unit/client_test.py +184 -0
  29. heinzel_github-0.9.1/tests/unit/contract_test.py +36 -0
  30. heinzel_github-0.9.1/tests/unit/gh_test.py +61 -0
  31. heinzel_github-0.9.1/tests/unit/models_test.py +57 -0
  32. heinzel_github-0.9.1/tests/unit/service_test.py +149 -0
@@ -0,0 +1,15 @@
1
+ .venv/
2
+ .tools/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .pyright/
7
+ .coverage*
8
+ htmlcov/
9
+ node_modules/
10
+ dist/
11
+ release-dist/
12
+ *.egg-info/
13
+ .acceptance/
14
+ .preview/
15
+ .candidates/
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.9.1
4
+
5
+ Move this plugin to its independent repository project for the Heinzel 0.9.1 plugin contract.
6
+ Own its dependencies, tests and artifacts; consume public contracts without concrete provider dependencies.
7
+
8
+ Earlier history is preserved in the source repository named in README.md.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Helmut Hoffer von Ankershoffen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: heinzel-github
3
+ Version: 0.9.1
4
+ Summary: Read pull requests and issues from GitHub: the code-host and tracker capabilities.
5
+ Project-URL: Source, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/tree/main/plugins/heinzel-github
6
+ Project-URL: Changelog, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/blob/main/plugins/heinzel-github/CHANGELOG.md
7
+ Project-URL: Issues, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: heinzel<0.10,>=0.9.1
12
+ Description-Content-Type: text/markdown
13
+
14
+ # heinzel-github
15
+
16
+ Read pull requests and issues from GitHub, the `code-host` and `tracker` capabilities ([ADR 0028](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0028-integrations-are-capability-adapters.md)): a first-party
17
+ [Heinzelmännchen](https://github.com/helmut-hoffer-von-ankershoffen/heinzel) plugin. It is independently released and installed explicitly:
18
+
19
+ ```bash
20
+ hz system plugin add github --confirm # a compatible plugin release; run it again to upgrade
21
+ hz system plugin remove heinzel-github
22
+ ```
23
+
24
+ ## Set up
25
+
26
+ The adapter talks to GitHub only through the [GitHub CLI](https://cli.github.com) `gh` (2.40.0 or later), which owns
27
+ authentication ([ADR 0035](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0035-integrations-go-through-vendor-clis-as-an-ordinary-employee.md)). It reads what you can read, as you:
28
+ no app to register, no token to create or store. Install `gh` and log in once:
29
+
30
+ ```bash
31
+ gh auth login # or: gh auth login --hostname ghe.example.com
32
+ ```
33
+
34
+ The adapter acts with whatever that login may do, which is usually more than reading; it only ever sends GET
35
+ requests. As for any `gh` command, a `GH_TOKEN` or `GITHUB_TOKEN` (`GH_ENTERPRISE_TOKEN` for a GitHub Enterprise
36
+ Server) in your environment takes precedence over the stored login: it is gh's own variable, passed to gh and never
37
+ read by Heinzelmännchen. If you stored a token for an earlier version, remove it:
38
+ `hz system credential remove github-token --service github` (or unset `HZ_CREDENTIAL_GITHUB_GITHUB_TOKEN`).
39
+
40
+ Bind the adapter in your user configuration, never in a project `hz.toml`, which Heinzelmännchen ignores for bindings:
41
+
42
+ ```bash
43
+ hz system config set capabilities.code_host github
44
+ hz system config set capabilities.tracker github
45
+ ```
46
+
47
+ which writes
48
+
49
+ ```toml
50
+ [capabilities]
51
+ code_host = "github"
52
+ tracker = "github"
53
+ ```
54
+
55
+ Or per shell: `HZ_CAPABILITIES__CODE_HOST=github`, `HZ_CAPABILITIES__TRACKER=github`.
56
+
57
+ For GitHub Enterprise Server, name its host in your user configuration or `HZ_GITHUB__HOST`; the adapter passes it to
58
+ `gh` as `--hostname` and `GH_HOST`, so `gh` uses its login for that host:
59
+
60
+ ```bash
61
+ hz system config set github.host ghe.example.com
62
+ ```
63
+
64
+ or in the file (`api_url` from earlier versions is refused; `host` replaced it):
65
+
66
+ ```toml
67
+ [github]
68
+ host = "ghe.example.com"
69
+ gh = "gh" # the GitHub CLI: a name on PATH (absolute entries only) or an absolute path
70
+ timeout = 60 # seconds per gh call
71
+ ```
72
+
73
+ `host`, `gh` and `timeout` are ignored in a project `hz.toml`, so a repository can't send your login elsewhere or pick
74
+ the program that runs. The adapter's hosts come
75
+ from it: `hz system capability list` shows `api.github.com`, or your GitHub Enterprise Server's host.
76
+
77
+ ### Is it connected?
78
+
79
+ Bound automatically while it is the only installed adapter of a capability (usually `code-host`; `tracker` once another
80
+ tracker is installed needs a choice), [ADR
81
+ 0055](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0055-connectors-show-their-sign-in-test-live-on-demand-and-bind-themselves-when-alone.md). `hz
82
+ system connector list` (or Settings → Connectors in `hz ui`) shows whether `gh` is logged in and as whom, read from gh's
83
+ hosts file without asking GitHub; `hz system connector test github` asks GitHub who gh is (`gh api user`, 15 s at most),
84
+ and only when you run it.
85
+
86
+ ## Use
87
+
88
+ Workflows ask for `code-host` and `tracker`, never for GitHub:
89
+
90
+ ```python
91
+ from datetime import datetime
92
+
93
+ from heinzel import CodeHost, Module, PullRequest, Window
94
+
95
+ module = Module("digest", "Weekly digest.", requires=["code-host:read", "tracker:read"])
96
+
97
+
98
+ @module.operation
99
+ def merged(code_host: CodeHost, since: datetime, until: datetime) -> list[PullRequest]:
100
+ """Pull requests merged this week."""
101
+ return code_host.merged_pull_requests("acme/widgets", Window(since=since, until=until))
102
+ ```
103
+
104
+ | Method | Capability | `gh api` call |
105
+ | --- | --- | --- |
106
+ | `merged_pull_requests(repository, window)` | `code-host:read` | `repos/{owner}/{repo}/pulls?state=closed&sort=updated&direction=desc`, kept when `merged_at` is in the window; paging stops before the window |
107
+ | `issues_updated(project, window)` | `tracker:read` | `repos/{owner}/{repo}/issues?state=all&since=...&sort=updated&direction=asc`, without pull requests |
108
+
109
+ Repositories and projects are written `owner/repo`; windows are half-open `[since, until)` with timezone-aware times;
110
+ results are oldest first. The adapter pages with the API's `page` and `per_page` parameters rather than
111
+ `gh api --paginate`, so it can stop as soon as the window is covered. A call reads at most 100 pages of 100 (10,000
112
+ items), 50 MiB in all and ten times `github.timeout`, then fails and says what to ask for instead: merged pull requests page through every pull request updated since `since`, even after `until`, so they need
113
+ a window that starts more recently; issues stop at `until`, so they need a shorter window. Why the pulls list and not
114
+ the search API: [ADR 0033](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0033-github-rest-api-over-urllib.md); why `gh`:
115
+ [ADR 0035](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0035-integrations-go-through-vendor-clis-as-an-ordinary-employee.md).
116
+
117
+ ```bash
118
+ hz github info # this plugin, its version and the GitHub host it reads
119
+ hz system capability list # who reads what, through which adapter and hosts
120
+ hz system doctor # gh on PATH, its version and login, the bindings; never contacts GitHub
121
+ ```
122
+
123
+ Errors say what to do: no `gh`, or one too old, names where to get it; no login, or a login GitHub rejects, names
124
+ `gh auth login --hostname <host>`; a rate limit says to wait and how to see when it resets; a missing or forbidden
125
+ repository says to check the spelling and what your account can see; a slow call names the timeout setting.
126
+
127
+ How `gh` is run: with an argument list, never a shell, from your home directory, in its own process group, stopped
128
+ (SIGTERM, then SIGKILL) at the timeout, with its output capped, and with an allowlisted environment: never
129
+ Heinzelmännchen's `HZ_CREDENTIAL_*` variables or debug switches such as `GH_DEBUG`, which would print request
130
+ headers. Doctor runs only `gh --version` and looks for the host in gh's hosts file (or a token variable gh reads); it
131
+ never runs a command that prints a token. Everything GitHub or `gh` says is redacted of anything shaped like a GitHub
132
+ token, stripped of control and bidirectional characters, and only then cut to length; every string the adapter
133
+ returns is cleaned too.
134
+
135
+ Development and release commands: [repository guide](../../README.md).
136
+
137
+ Migration provenance: heinzel, before the 0.9 plugin-boundary migration.
@@ -0,0 +1,124 @@
1
+ # heinzel-github
2
+
3
+ Read pull requests and issues from GitHub, the `code-host` and `tracker` capabilities ([ADR 0028](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0028-integrations-are-capability-adapters.md)): a first-party
4
+ [Heinzelmännchen](https://github.com/helmut-hoffer-von-ankershoffen/heinzel) plugin. It is independently released and installed explicitly:
5
+
6
+ ```bash
7
+ hz system plugin add github --confirm # a compatible plugin release; run it again to upgrade
8
+ hz system plugin remove heinzel-github
9
+ ```
10
+
11
+ ## Set up
12
+
13
+ The adapter talks to GitHub only through the [GitHub CLI](https://cli.github.com) `gh` (2.40.0 or later), which owns
14
+ authentication ([ADR 0035](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0035-integrations-go-through-vendor-clis-as-an-ordinary-employee.md)). It reads what you can read, as you:
15
+ no app to register, no token to create or store. Install `gh` and log in once:
16
+
17
+ ```bash
18
+ gh auth login # or: gh auth login --hostname ghe.example.com
19
+ ```
20
+
21
+ The adapter acts with whatever that login may do, which is usually more than reading; it only ever sends GET
22
+ requests. As for any `gh` command, a `GH_TOKEN` or `GITHUB_TOKEN` (`GH_ENTERPRISE_TOKEN` for a GitHub Enterprise
23
+ Server) in your environment takes precedence over the stored login: it is gh's own variable, passed to gh and never
24
+ read by Heinzelmännchen. If you stored a token for an earlier version, remove it:
25
+ `hz system credential remove github-token --service github` (or unset `HZ_CREDENTIAL_GITHUB_GITHUB_TOKEN`).
26
+
27
+ Bind the adapter in your user configuration, never in a project `hz.toml`, which Heinzelmännchen ignores for bindings:
28
+
29
+ ```bash
30
+ hz system config set capabilities.code_host github
31
+ hz system config set capabilities.tracker github
32
+ ```
33
+
34
+ which writes
35
+
36
+ ```toml
37
+ [capabilities]
38
+ code_host = "github"
39
+ tracker = "github"
40
+ ```
41
+
42
+ Or per shell: `HZ_CAPABILITIES__CODE_HOST=github`, `HZ_CAPABILITIES__TRACKER=github`.
43
+
44
+ For GitHub Enterprise Server, name its host in your user configuration or `HZ_GITHUB__HOST`; the adapter passes it to
45
+ `gh` as `--hostname` and `GH_HOST`, so `gh` uses its login for that host:
46
+
47
+ ```bash
48
+ hz system config set github.host ghe.example.com
49
+ ```
50
+
51
+ or in the file (`api_url` from earlier versions is refused; `host` replaced it):
52
+
53
+ ```toml
54
+ [github]
55
+ host = "ghe.example.com"
56
+ gh = "gh" # the GitHub CLI: a name on PATH (absolute entries only) or an absolute path
57
+ timeout = 60 # seconds per gh call
58
+ ```
59
+
60
+ `host`, `gh` and `timeout` are ignored in a project `hz.toml`, so a repository can't send your login elsewhere or pick
61
+ the program that runs. The adapter's hosts come
62
+ from it: `hz system capability list` shows `api.github.com`, or your GitHub Enterprise Server's host.
63
+
64
+ ### Is it connected?
65
+
66
+ Bound automatically while it is the only installed adapter of a capability (usually `code-host`; `tracker` once another
67
+ tracker is installed needs a choice), [ADR
68
+ 0055](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0055-connectors-show-their-sign-in-test-live-on-demand-and-bind-themselves-when-alone.md). `hz
69
+ system connector list` (or Settings → Connectors in `hz ui`) shows whether `gh` is logged in and as whom, read from gh's
70
+ hosts file without asking GitHub; `hz system connector test github` asks GitHub who gh is (`gh api user`, 15 s at most),
71
+ and only when you run it.
72
+
73
+ ## Use
74
+
75
+ Workflows ask for `code-host` and `tracker`, never for GitHub:
76
+
77
+ ```python
78
+ from datetime import datetime
79
+
80
+ from heinzel import CodeHost, Module, PullRequest, Window
81
+
82
+ module = Module("digest", "Weekly digest.", requires=["code-host:read", "tracker:read"])
83
+
84
+
85
+ @module.operation
86
+ def merged(code_host: CodeHost, since: datetime, until: datetime) -> list[PullRequest]:
87
+ """Pull requests merged this week."""
88
+ return code_host.merged_pull_requests("acme/widgets", Window(since=since, until=until))
89
+ ```
90
+
91
+ | Method | Capability | `gh api` call |
92
+ | --- | --- | --- |
93
+ | `merged_pull_requests(repository, window)` | `code-host:read` | `repos/{owner}/{repo}/pulls?state=closed&sort=updated&direction=desc`, kept when `merged_at` is in the window; paging stops before the window |
94
+ | `issues_updated(project, window)` | `tracker:read` | `repos/{owner}/{repo}/issues?state=all&since=...&sort=updated&direction=asc`, without pull requests |
95
+
96
+ Repositories and projects are written `owner/repo`; windows are half-open `[since, until)` with timezone-aware times;
97
+ results are oldest first. The adapter pages with the API's `page` and `per_page` parameters rather than
98
+ `gh api --paginate`, so it can stop as soon as the window is covered. A call reads at most 100 pages of 100 (10,000
99
+ items), 50 MiB in all and ten times `github.timeout`, then fails and says what to ask for instead: merged pull requests page through every pull request updated since `since`, even after `until`, so they need
100
+ a window that starts more recently; issues stop at `until`, so they need a shorter window. Why the pulls list and not
101
+ the search API: [ADR 0033](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0033-github-rest-api-over-urllib.md); why `gh`:
102
+ [ADR 0035](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/adr/0035-integrations-go-through-vendor-clis-as-an-ordinary-employee.md).
103
+
104
+ ```bash
105
+ hz github info # this plugin, its version and the GitHub host it reads
106
+ hz system capability list # who reads what, through which adapter and hosts
107
+ hz system doctor # gh on PATH, its version and login, the bindings; never contacts GitHub
108
+ ```
109
+
110
+ Errors say what to do: no `gh`, or one too old, names where to get it; no login, or a login GitHub rejects, names
111
+ `gh auth login --hostname <host>`; a rate limit says to wait and how to see when it resets; a missing or forbidden
112
+ repository says to check the spelling and what your account can see; a slow call names the timeout setting.
113
+
114
+ How `gh` is run: with an argument list, never a shell, from your home directory, in its own process group, stopped
115
+ (SIGTERM, then SIGKILL) at the timeout, with its output capped, and with an allowlisted environment: never
116
+ Heinzelmännchen's `HZ_CREDENTIAL_*` variables or debug switches such as `GH_DEBUG`, which would print request
117
+ headers. Doctor runs only `gh --version` and looks for the host in gh's hosts file (or a token variable gh reads); it
118
+ never runs a command that prints a token. Everything GitHub or `gh` says is redacted of anything shaped like a GitHub
119
+ token, stripped of control and bidirectional characters, and only then cut to length; every string the adapter
120
+ returns is cleaned too.
121
+
122
+ Development and release commands: [repository guide](../../README.md).
123
+
124
+ Migration provenance: heinzel, before the 0.9 plugin-boundary migration.
@@ -0,0 +1,40 @@
1
+ {
2
+ "format": 1,
3
+ "sdk": 1,
4
+ "inputs": [
5
+ "coverage>=7.12",
6
+ "editables>=0.5",
7
+ "hatchling>=1.29.0",
8
+ "heinzel>=0.9.1,<0.10",
9
+ "mypy>=1.19.0,<3",
10
+ "pyright[nodejs]==1.1.414",
11
+ "pytest-timeout>=2.4.0,<3",
12
+ "pytest-xdist>=3.8",
13
+ "pytest>=9.0.3,<10",
14
+ "ruff==0.17.2"
15
+ ],
16
+ "core": {
17
+ "version": "0.9.1",
18
+ "dependencies": [
19
+ "croniter>=6.2.4",
20
+ "fastapi>=0.161",
21
+ "keyring>=25.6",
22
+ "mcp>=2.3",
23
+ "packaging>=25.0",
24
+ "platformdirs>=4.5",
25
+ "pluggy<2,>=1.6.0",
26
+ "psutil==7.2.2",
27
+ "pydantic-ai-slim==2.61.0",
28
+ "pydantic-ai-slim[anthropic]==2.61.0; extra == \"anthropic\"",
29
+ "pydantic-ai-slim[openai]==2.61.0; extra == \"openai\"",
30
+ "pydantic-settings<3,>=2.11.0",
31
+ "pydantic<3,>=2.12.0",
32
+ "pyyaml<7,>=6.0.2",
33
+ "rich>=14.2.0",
34
+ "tomlkit>=0.13",
35
+ "typer>=0.23.0",
36
+ "uvicorn>=0.54",
37
+ "wsproto>=1.3"
38
+ ]
39
+ }
40
+ }
@@ -0,0 +1,131 @@
1
+ [project]
2
+ name = "heinzel-github"
3
+ version = "0.9.1"
4
+ description = "Read pull requests and issues from GitHub: the code-host and tracker capabilities."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ dependencies = ["heinzel>=0.9.1,<0.10"]
10
+
11
+ [project.urls]
12
+ Source = "https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/tree/main/plugins/heinzel-github"
13
+ Changelog = "https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/blob/main/plugins/heinzel-github/CHANGELOG.md"
14
+ Issues = "https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/issues"
15
+
16
+ [project.entry-points."heinzel.modules"]
17
+ github = "heinzel_github"
18
+
19
+ [build-system]
20
+ requires = ["hatchling>=1.29.0"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [dependency-groups]
24
+ test = ["editables>=0.5", "pytest>=9.0.3,<10", "pytest-timeout>=2.4.0,<3", "pytest-xdist>=3.8", "ruff==0.17.2", "mypy>=1.19.0,<3", "coverage>=7.12", "pyright[nodejs]==1.1.414"]
25
+
26
+
27
+ [tool.ruff]
28
+ line-length = 120
29
+ fix = true
30
+ extend-exclude = ["template"] # rendered and checked by `make template-test`
31
+
32
+ [tool.ruff.lint]
33
+ select = ["ALL"]
34
+ # pydoclint (DOC): docstring sections must match what the code raises, returns and yields. Preview rules, opted into
35
+ # one by one so no other preview rule sneaks in.
36
+ preview = true
37
+ explicit-preview-rules = true
38
+ extend-select = ["DOC201", "DOC202", "DOC402", "DOC403", "DOC501", "DOC502"]
39
+ ignore = [
40
+ "COM812", # conflicts with the ruff formatter
41
+ "CPY001", # no copyright header in OSS files
42
+ "D203", # incompatible with D211
43
+ "DOC201", # a return value is documented by the type and the summary line; Returns: only when it adds meaning
44
+ "DOC502", # documenting exceptions raised by callees is useful for callers
45
+ "D213", # incompatible with D212
46
+ "FBT001", # boolean positional arguments are idiomatic for Typer options
47
+ "FBT002", # same as FBT001
48
+ "ISC001", # conflicts with the ruff formatter
49
+ "TD003", # todos don't need an issue link
50
+ "FIX002", # todos are allowed
51
+ ]
52
+
53
+ [tool.ruff.lint.per-file-ignores]
54
+ ".claude/hooks/*.py" = [
55
+ "D103", # hook entry points are documented by the module docstring
56
+ "INP001", # hooks are scripts, not a package
57
+ "S603", # subprocess calls with fixed argument lists
58
+ "S607", # tools (uv, git, make) are resolved from PATH on purpose
59
+ "T201", # hooks talk to the agent through stdout/stderr
60
+ ]
61
+ "fuzz/*.py" = [
62
+ "INP001", # fuzz targets are scripts, not a package
63
+ ]
64
+ "hatch_build.py" = [
65
+ "INP001", # the build hook hatch loads by path, not a package
66
+ ]
67
+ "scripts/*.py" = [
68
+ "INP001", # scripts are not a package
69
+ "T201", # scripts report on stdout/stderr
70
+ ]
71
+ "**/tests/**/*.py" = [
72
+ "ANN", # fixtures and tests don't need full annotations
73
+ "DOC", # test docstrings describe behaviour, not interfaces
74
+ "ARG", # fixtures are used for their side effects
75
+ "D", # test names are the documentation
76
+ "INP001", # tests are not a package
77
+ "PLC0415", # local imports keep fixtures isolated
78
+ "PLR2004", # magic values are fine in assertions
79
+ "S101", # asserts are what tests do
80
+ "S105", # fake tokens and passwords are test data (credentials, adapters for tracker and document services)
81
+ "S106", # same as S105, passed as arguments
82
+ "S603", # subprocess calls with trusted input
83
+ "S607", # partial executable paths (uv, hz) are resolved from PATH on purpose
84
+ "SLF001", # private access is needed for solitary unit tests
85
+ ]
86
+
87
+ [tool.ruff.lint.pydocstyle]
88
+ convention = "google"
89
+
90
+ [tool.ruff.format]
91
+ docstring-code-format = true
92
+
93
+ [tool.mypy]
94
+ strict = true
95
+ mypy_path = ["src"]
96
+ files = ["src"]
97
+ plugins = ["pydantic.mypy"]
98
+
99
+ [tool.pydantic-mypy]
100
+ init_forbid_extra = true
101
+ init_typed = true
102
+ warn_required_dynamic_aliases = true
103
+
104
+ [tool.pytest]
105
+ minversion = "9.0"
106
+ strict = true
107
+ testpaths = ["tests"]
108
+ addopts = ["-ra", "--import-mode=importlib"]
109
+ filterwarnings = ["error"]
110
+ markers = ["unit: Offline isolated behavior.", "integration: Offline public-contract integration.", "e2e: Installed artifacts or real external services.", "perf: Explicit timing tests.", "long_running: Extended end-to-end budget."]
111
+
112
+ [tool.heinzel.plugin]
113
+ profiles = []
114
+ requires = ["core:plugin-api@1"]
115
+ provides = ["code-host:read", "tracker:read"]
116
+
117
+ [tool.coverage.run]
118
+ source = ["heinzel_github"]
119
+ branch = true
120
+
121
+ [tool.coverage.report]
122
+ fail_under = 90
123
+ show_missing = true
124
+
125
+ [tool.pyright]
126
+ typeCheckingMode = "strict"
127
+ include = ["src"]
128
+ pythonVersion = "3.12"
129
+
130
+ [tool.hatch.build.targets.sdist]
131
+ exclude = [".venv", ".acceptance", ".preview", "dist", "web/node_modules", "**/__pycache__", ".pytest_cache", ".mypy_cache", ".coverage*"]