ghinsight 0.2.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 (50) hide show
  1. ghinsight-0.2.0/.gitattributes +2 -0
  2. ghinsight-0.2.0/.github/dependabot.yml +9 -0
  3. ghinsight-0.2.0/.github/workflows/ci.yml +149 -0
  4. ghinsight-0.2.0/.github/workflows/release.yml +39 -0
  5. ghinsight-0.2.0/.gitignore +14 -0
  6. ghinsight-0.2.0/AGENTS.md +47 -0
  7. ghinsight-0.2.0/LICENSE +29 -0
  8. ghinsight-0.2.0/MAINTAINER.md +88 -0
  9. ghinsight-0.2.0/PKG-INFO +249 -0
  10. ghinsight-0.2.0/README.md +222 -0
  11. ghinsight-0.2.0/bin/recreate.sh +104 -0
  12. ghinsight-0.2.0/bin/verify.sh +28 -0
  13. ghinsight-0.2.0/docs/conf.py +58 -0
  14. ghinsight-0.2.0/docs/design.md +161 -0
  15. ghinsight-0.2.0/docs/index.md +53 -0
  16. ghinsight-0.2.0/docs/tutorials/activity.md +55 -0
  17. ghinsight-0.2.0/docs/tutorials/cli.md +132 -0
  18. ghinsight-0.2.0/docs/tutorials/python_analysis.py +188 -0
  19. ghinsight-0.2.0/examples/analysis.toml +25 -0
  20. ghinsight-0.2.0/pixi.lock +3606 -0
  21. ghinsight-0.2.0/pyproject.toml +93 -0
  22. ghinsight-0.2.0/src/ghinsight/__init__.py +32 -0
  23. ghinsight-0.2.0/src/ghinsight/actions/__init__.py +41 -0
  24. ghinsight-0.2.0/src/ghinsight/actions/analysis.py +201 -0
  25. ghinsight-0.2.0/src/ghinsight/actions/billing.py +195 -0
  26. ghinsight-0.2.0/src/ghinsight/actions/charts.py +439 -0
  27. ghinsight-0.2.0/src/ghinsight/actions/cli.py +283 -0
  28. ghinsight-0.2.0/src/ghinsight/actions/compare.py +323 -0
  29. ghinsight-0.2.0/src/ghinsight/actions/config.py +189 -0
  30. ghinsight-0.2.0/src/ghinsight/actions/csv_export.py +60 -0
  31. ghinsight-0.2.0/src/ghinsight/actions/dataset.py +271 -0
  32. ghinsight-0.2.0/src/ghinsight/actions/domain.py +360 -0
  33. ghinsight-0.2.0/src/ghinsight/actions/fetch.py +317 -0
  34. ghinsight-0.2.0/src/ghinsight/actions/metrics.py +287 -0
  35. ghinsight-0.2.0/src/ghinsight/actions/report.py +655 -0
  36. ghinsight-0.2.0/src/ghinsight/activity.py +589 -0
  37. ghinsight-0.2.0/src/ghinsight/cli.py +122 -0
  38. ghinsight-0.2.0/src/ghinsight/client.py +321 -0
  39. ghinsight-0.2.0/src/ghinsight/repos.py +104 -0
  40. ghinsight-0.2.0/tests/__init__.py +0 -0
  41. ghinsight-0.2.0/tests/conftest.py +21 -0
  42. ghinsight-0.2.0/tests/synthetic.py +139 -0
  43. ghinsight-0.2.0/tests/test_activity.py +259 -0
  44. ghinsight-0.2.0/tests/test_billing.py +202 -0
  45. ghinsight-0.2.0/tests/test_charts.py +34 -0
  46. ghinsight-0.2.0/tests/test_client.py +309 -0
  47. ghinsight-0.2.0/tests/test_domain.py +109 -0
  48. ghinsight-0.2.0/tests/test_fetch.py +52 -0
  49. ghinsight-0.2.0/tests/test_pipeline.py +259 -0
  50. ghinsight-0.2.0/uv.lock +2563 -0
@@ -0,0 +1,2 @@
1
+ # SCM syntax highlighting & preventing 3-way merges
2
+ pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff
@@ -0,0 +1,9 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ groups:
8
+ github-actions:
9
+ patterns: ["*"]
@@ -0,0 +1,149 @@
1
+ name: ci
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v7
13
+ - name: Install uv
14
+ uses: astral-sh/setup-uv@v7
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v7
17
+ with:
18
+ python-version: "3.12"
19
+ - name: Install dependencies
20
+ run: uv sync --group dev
21
+ - name: Run tests
22
+ run: uv run pytest
23
+ - name: Build package
24
+ run: uv build
25
+
26
+ docs:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v7
30
+ - name: Install uv
31
+ uses: astral-sh/setup-uv@v7
32
+ - name: Set up Python
33
+ uses: actions/setup-python@v7
34
+ with:
35
+ python-version: "3.12"
36
+ - name: Install docs dependencies
37
+ run: uv sync --group docs
38
+ - name: Build docs
39
+ run: uv run --group docs sphinx-build -b html -W docs docs/_build/html
40
+
41
+ online-smoke:
42
+ # End-to-end test against *real* GitHub Actions data, using the built-in
43
+ # GITHUB_TOKEN to read this repository's own runs/jobs. This exercises the
44
+ # network fetch path that cannot run in an offline dev sandbox.
45
+ runs-on: ubuntu-latest
46
+ permissions:
47
+ contents: read
48
+ actions: read
49
+ env:
50
+ ORG: ${{ github.repository_owner }}
51
+ REPO: ${{ github.event.repository.name }}
52
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53
+ steps:
54
+ - uses: actions/checkout@v7
55
+ - name: Install uv
56
+ uses: astral-sh/setup-uv@v7
57
+ - name: Set up Python
58
+ uses: actions/setup-python@v7
59
+ with:
60
+ python-version: "3.12"
61
+ - name: Install dependencies
62
+ run: uv sync --group dev
63
+ - name: Fetch real Actions data (single repo) and build report
64
+ run: |
65
+ uv run ghinsight actions fetch \
66
+ --org "$ORG" --repo "$REPO" --period last-year --cache-dir cache
67
+ uv run ghinsight actions report \
68
+ --org "$ORG" --cache-dir cache --output-dir reports
69
+ uv run ghinsight actions dashboard \
70
+ --org "$ORG" --cache-dir cache \
71
+ --output docs/index.html --summary docs/summary.json
72
+ - name: Assert the pipeline produced non-empty results
73
+ run: |
74
+ uv run python - <<'PY'
75
+ import json
76
+ from pathlib import Path
77
+
78
+ from ghinsight.actions.dataset import ActionsDataset
79
+
80
+ org = __import__("os").environ["ORG"]
81
+ dataset = ActionsDataset.from_cache(org, Path("cache"))
82
+ assert not dataset.is_empty, "no completed jobs were fetched"
83
+ print(f"OK: {len(dataset.jobs)} jobs, {len(dataset.runs)} runs")
84
+
85
+ for csv in ["repositories.csv", "workflows.csv"]:
86
+ path = Path("reports/actions-usage-metrics") / csv
87
+ assert path.exists() and path.stat().st_size > 0, f"missing/empty {path}"
88
+
89
+ summary = json.loads(Path("docs/summary.json").read_text())
90
+ assert summary["raw_total_minutes_period"] > 0, "zero raw minutes"
91
+ print("summary:", summary["raw_total_minutes_period"], "raw minutes")
92
+ PY
93
+ - name: Upload generated dashboard
94
+ uses: actions/upload-artifact@v7
95
+ with:
96
+ name: dashboard
97
+ path: |
98
+ docs/index.html
99
+ docs/summary.json
100
+ reports/
101
+
102
+ online-full-account:
103
+ # Full-account end-to-end test across *all* repos of an account. The built-in
104
+ # GITHUB_TOKEN is scoped to a single repo, so this needs a Personal Access
105
+ # Token with read access to the target account (classic: repo + read:org;
106
+ # fine-grained: Actions read across repos). Configure repository secrets
107
+ # GH_PAT and (optionally) GH_ACCOUNT to enable it; the job skips cleanly when
108
+ # GH_PAT is absent, so forks and unconfigured repos stay green.
109
+ # Its output is deliberately not uploaded: this repository is public, so an
110
+ # artifact would expose the account's private repository names and usage.
111
+ runs-on: ubuntu-latest
112
+ env:
113
+ GH_PAT: ${{ secrets.GH_PAT }}
114
+ GH_ACCOUNT: ${{ secrets.GH_ACCOUNT || github.repository_owner }}
115
+ steps:
116
+ - uses: actions/checkout@v7
117
+ - name: Install uv
118
+ uses: astral-sh/setup-uv@v7
119
+ - name: Set up Python
120
+ uses: actions/setup-python@v7
121
+ with:
122
+ python-version: "3.12"
123
+ - name: Install dependencies
124
+ run: uv sync --group dev
125
+ - name: Fetch the whole account and build the report
126
+ run: |
127
+ if [ -z "$GH_PAT" ]; then
128
+ echo "GH_PAT secret not set — skipping full-account test."
129
+ exit 0
130
+ fi
131
+ export GITHUB_TOKEN="$GH_PAT"
132
+ uv run ghinsight actions fetch \
133
+ --org "$GH_ACCOUNT" --period last-3-months --cache-dir cache
134
+ uv run ghinsight actions report \
135
+ --org "$GH_ACCOUNT" --cache-dir cache --output-dir reports
136
+ uv run ghinsight actions dashboard \
137
+ --org "$GH_ACCOUNT" --cache-dir cache \
138
+ --output docs/index.html --summary docs/summary.json
139
+ uv run python - <<'PY'
140
+ import os
141
+ from pathlib import Path
142
+
143
+ from ghinsight.actions.dataset import ActionsDataset
144
+
145
+ dataset = ActionsDataset.from_cache(os.environ["GH_ACCOUNT"], Path("cache"))
146
+ repos = {job.repo for job in dataset.jobs}
147
+ print(f"OK: {len(dataset.jobs)} jobs across {len(repos)} repos")
148
+ assert not dataset.is_empty, "no jobs fetched across the account"
149
+ PY
@@ -0,0 +1,39 @@
1
+ name: release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v7
12
+ - name: Install uv
13
+ uses: astral-sh/setup-uv@v7
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v7
16
+ with:
17
+ python-version: "3.12"
18
+ - name: Build distributions
19
+ run: uv build
20
+ - name: Upload distributions
21
+ uses: actions/upload-artifact@v7
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+
26
+ publish:
27
+ needs: build
28
+ runs-on: ubuntu-latest
29
+ environment: pypi
30
+ permissions:
31
+ id-token: write
32
+ steps:
33
+ - name: Download distributions
34
+ uses: actions/download-artifact@v8
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ - name: Publish to PyPI
39
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ .DS_Store
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .pixi/
9
+ .env
10
+ .ruff_cache/
11
+ .pytest_cache/
12
+ docs/_build/
13
+ cache/
14
+ reports/
@@ -0,0 +1,47 @@
1
+ # Repository notes for future agents
2
+
3
+ - Use `uv` for PyPI packaging and release checks in this repository.
4
+ - The repository also defines a `pixi` environment in `pyproject.toml`; keep
5
+ package runtime dependencies mirrored in `[project.dependencies]` and
6
+ `[tool.pixi.dependencies]` so uv resolves from PyPI while pixi resolves from
7
+ conda-forge.
8
+ - Prefer commands of the form:
9
+
10
+ ```bash
11
+ uv run python ...
12
+ uv run ghinsight ...
13
+ pixi run check
14
+ ```
15
+
16
+ - Do not run bare `python` for project tasks unless the user explicitly asks for
17
+ it.
18
+ - Keep this package organization-agnostic. Organization names, private data
19
+ paths, billing assumptions, and one-off repository quirks should live in a
20
+ consuming repository's config, not in this library.
21
+
22
+ ## Architecture (post-refactor)
23
+
24
+ - Layout: the core (`client.py`, `repos.py`, `activity.py`, `cli.py`) needs
25
+ only httpx2 and typer. `ghinsight.actions` (the Actions pipeline and its
26
+ dashboard) needs the `actions` extra (pandas, plotly, rich); nothing in the
27
+ core may import it at module level. Keep `[project.dependencies]`, the
28
+ `actions` extra and `[tool.pixi.dependencies]` in step.
29
+ - All GitHub API access goes through `client.py` (httpx2; `GitHubClient`,
30
+ `GitHubError`, `resolve_token`) and `repos.py` (`RepoInfo`, account repo
31
+ listing). Keep these two modules light and stable: other packages (e.g.
32
+ repo-secret-scan) depend on them, so they must not import pandas/plotly.
33
+ - `ghinsight.actions` has three functional stages: `fetch` (network) → `dataset` (the canonical tidy
34
+ IR, `ActionsDataset`) → analysis (`metrics`, `analysis`, `charts`, `report`).
35
+ - The intermediate representation is a tidy per-job table, not GitHub's
36
+ aggregate CSVs. CSVs are an export (`csv_export`) for `compare` only.
37
+ - Prefer the functional style already in place: frozen dataclasses, pure
38
+ functions, type hints on signatures, declarative specs over imperative loops.
39
+ - Derived measures (billed minutes, durations, OS) are defined once, as
40
+ properties on the `Job`/`Run` records in `domain.py`.
41
+
42
+ ## Testing & docs
43
+
44
+ - `uv run pytest` runs the offline suite against `tests/synthetic.py` (no token
45
+ or network needed). Add fixtures there rather than depending on a live org.
46
+ - `uv run --group docs sphinx-build -b html -W docs docs/_build/html` builds the
47
+ docs (AutoAPI + MyST-NB tutorials); CI builds with `-W`, so keep it warning-free.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Kolen Cheung
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,88 @@
1
+ # Maintainer Guide
2
+
3
+ This package is intended to be published to PyPI as `ghinsight`.
4
+
5
+ ## Local Release Checks
6
+
7
+ Use `uv` from the repository root:
8
+
9
+ ```bash
10
+ uv sync
11
+ uv build
12
+ uv run twine check dist/*
13
+ ```
14
+
15
+ Also check the pixi environment after dependency changes:
16
+
17
+ ```bash
18
+ pixi lock
19
+ pixi run check
20
+ pixi run help
21
+ ```
22
+
23
+ Inspect the contents before uploading:
24
+
25
+ ```bash
26
+ tar -tzf dist/ghinsight-*.tar.gz | sort | head -50
27
+ unzip -l dist/ghinsight-*.whl | sort | head -50
28
+ ```
29
+
30
+ ## First PyPI Release
31
+
32
+ 1. Confirm the package name in `pyproject.toml` is available on PyPI. If
33
+ `ghinsight` is already taken, rename the project before the first
34
+ release.
35
+ 2. Confirm `README.md`, `LICENSE`, project URLs, dependencies, and the console
36
+ script metadata are correct.
37
+ 3. Create a PyPI account if needed and enable 2FA.
38
+ 4. Configure a PyPI Trusted Publisher for GitHub Actions.
39
+ - Project name: `ghinsight`
40
+ - Owner: the GitHub repository owner
41
+ - Repository: the GitHub repository name
42
+ - Workflow filename: `release.yml`
43
+ - Environment: `pypi`
44
+ 5. Push the repository to GitHub.
45
+ 6. Create a GitHub Release for the version in `pyproject.toml`.
46
+ 7. The `release.yml` workflow will build the sdist/wheel with `uv build` and
47
+ publish with `pypa/gh-action-pypi-publish`.
48
+
49
+ Trusted Publishing avoids storing a long-lived PyPI token in GitHub secrets.
50
+ The workflow needs `permissions: id-token: write`, and the `pypi` environment
51
+ should match the environment configured on PyPI.
52
+
53
+ ## Ongoing Releases
54
+
55
+ 1. Update `version` in `pyproject.toml`.
56
+ 2. Update the README or changelog if user-facing behavior changed.
57
+ 3. If runtime dependencies changed, update both `[project.dependencies]` and
58
+ `[tool.pixi.dependencies]` in `pyproject.toml`, then regenerate `uv.lock`
59
+ and `pixi.lock`.
60
+ 4. Run local checks:
61
+
62
+ ```bash
63
+ uv sync
64
+
65
+ uv build
66
+ uv run twine check dist/*
67
+ pixi lock
68
+ pixi run check
69
+ ```
70
+
71
+ 5. Commit the version/docs changes.
72
+ 6. Tag or create a GitHub Release for the same version, for example `v0.1.1`.
73
+ 7. Confirm the GitHub Actions release workflow publishes successfully.
74
+ 8. Verify the package page and install path:
75
+
76
+ ```bash
77
+ uv tool install --force 'ghinsight[actions]'
78
+ ghinsight --help
79
+ ```
80
+
81
+ ## Useful References
82
+
83
+ - PyPI Trusted Publishing:
84
+ https://docs.pypi.org/trusted-publishers/
85
+ - Publishing with a Trusted Publisher:
86
+ https://docs.pypi.org/trusted-publishers/using-a-publisher/
87
+ - Python Packaging User Guide for GitHub Actions publishing:
88
+ https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.5
2
+ Name: ghinsight
3
+ Version: 0.2.0
4
+ Summary: Reusable tools for GitHub analysis: Actions usage, issue and PR activity, and a small API client
5
+ Project-URL: Homepage, https://github.com/ickc/ghinsight
6
+ Project-URL: Repository, https://github.com/ickc/ghinsight
7
+ Project-URL: Issues, https://github.com/ickc/ghinsight/issues
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Keywords: actions,activity,analysis,dashboard,github,metrics
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: httpx2>=2.16
21
+ Requires-Dist: typer>=0.12
22
+ Provides-Extra: actions
23
+ Requires-Dist: pandas>=2.0; extra == 'actions'
24
+ Requires-Dist: plotly>=6.0; extra == 'actions'
25
+ Requires-Dist: rich>=13.0; extra == 'actions'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # ghinsight
29
+
30
+ *Pronounced "gin-sight": the h is silent.*
31
+
32
+ Reusable tools for analysing GitHub organisations and user accounts:
33
+
34
+ | Part | Install | What it does |
35
+ | ---- | ------- | ------------ |
36
+ | `ghinsight activity` | core | Markdown table of a user's issue and PR activity over a date range |
37
+ | `ghinsight actions` | `[actions]` extra | GitHub Actions usage and performance: cached fetch, metric CSVs, HTML dashboard |
38
+ | `ghinsight.client`, `ghinsight.repos` | core | A small GitHub REST/GraphQL client and repository listing, for other tools to build on |
39
+
40
+ The core depends only on [httpx2](https://github.com/pydantic/httpx2) and
41
+ [typer](https://typer.tiangolo.com); the `actions` extra adds pandas, plotly and
42
+ rich. Everything is organisation-agnostic: names, paths and billing assumptions
43
+ are arguments or config.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ uv tool install ghinsight # core: activity
49
+ uv tool install 'ghinsight[actions]' # also: actions
50
+ ghinsight --help
51
+ ```
52
+
53
+ For local development from a checkout:
54
+
55
+ ```bash
56
+ uv sync --group dev # includes the actions extra
57
+ uv run ghinsight --help
58
+ ```
59
+
60
+ A conda-forge `pixi` environment is also provided:
61
+
62
+ ```bash
63
+ pixi run help
64
+ pixi run check
65
+ pixi run test
66
+ ```
67
+
68
+ ## Authentication
69
+
70
+ Every command needs a GitHub token. For `ghinsight actions`, it must be able to
71
+ read Actions metadata for the target account. Use either:
72
+
73
+ ```bash
74
+ export GH_TOKEN=... # or GITHUB_TOKEN
75
+ ```
76
+
77
+ or login with the GitHub CLI (`gh auth login`): if neither variable is set, the
78
+ token comes from `gh auth token`, wherever `gh` stores it. `GITHUB_API_URL` (set
79
+ inside GitHub Actions) selects a GitHub Enterprise Server API.
80
+
81
+ ## Issue and PR activity
82
+
83
+ ```bash
84
+ ghinsight activity --repo my-org/some-repo --user octocat --from 2026-05-20 --to 2026-05-27
85
+ ghinsight activity --org my-org --user octocat --from 2026-09-16 --to 2026-09-24 --output week.md
86
+ ```
87
+
88
+ With `--org`, every repository of the organisation (or user) is searched and
89
+ items are labelled `repo#123`. An issue or PR is listed when, within the range,
90
+ it was:
91
+
92
+ - opened by `--user`;
93
+ - closed or merged, and either authored or closed/merged by `--user`;
94
+ - authored by `--user` earlier and updated by them (comments, commits,
95
+ force-pushes, draft/ready changes, review requests), even if still open;
96
+ - reviewed by `--user` (someone else's PR; the type shows approved, changes
97
+ requested or reviewed).
98
+
99
+ `--authored-only` drops the last category and PRs closed/merged by `--user`
100
+ that they did not author. Date ranges with more than 1,000 search results are
101
+ split automatically; long ranges across a large organisation are slower, since
102
+ every item closed in the range is fetched to check who closed it.
103
+
104
+ ## GitHub Actions usage and performance
105
+
106
+ `ghinsight actions` fetches workflow-run and job data from the GitHub REST API,
107
+ caches the raw JSON locally, parses it into a single typed **intermediate
108
+ representation**, and derives every report — GitHub-style metric tables,
109
+ headline KPIs, and a static HTML dashboard — as a pure projection of that
110
+ representation.
111
+
112
+ ### Architecture
113
+
114
+ A three-stage, functional pipeline (see the [design docs](docs/design.md) for
115
+ the full rationale):
116
+
117
+ | Stage | Module | Output |
118
+ | ----- | ------ | ------ |
119
+ | 1. Fetch | `ghinsight.actions.fetch` | cached raw JSON (verbatim API responses) |
120
+ | 2. Dataset (the IR) | `ghinsight.actions.dataset` | `ActionsDataset` — a tidy per-job / per-run table |
121
+ | 3a. Metric tables | `ghinsight.actions.metrics` | usage / performance DataFrames |
122
+ | 3b. Insights | `ghinsight.actions.analysis` | `UsageSummary`, monthly time-series |
123
+ | 3c. Report | `ghinsight.actions.report` | HTML dashboard + JSON summary |
124
+
125
+ The canonical intermediate representation is a **tidy per-job table** (one row
126
+ per job, with all derived measures and dimensions), not GitHub's aggregate CSV
127
+ exports. Aggregate exports drop per-job timestamps, so they cannot support
128
+ time-series views; the tidy table can, and every other view derives from it.
129
+ GitHub-compatible CSVs are still produced as an *export*
130
+ (`ghinsight.actions.csv_export`) for diffing against GitHub's own metrics.
131
+
132
+ ### Quick start
133
+
134
+ ```bash
135
+ mkdir github-actions-report && cd github-actions-report
136
+ $EDITOR analysis.toml # see examples/analysis.toml
137
+ ghinsight actions recreate --config analysis.toml
138
+ ```
139
+
140
+ `recreate` runs fetch → CSV export → dashboard end to end. The dashboard is
141
+ written to `docs/index.html` and machine-readable metadata to
142
+ `docs/summary.json`. See the [CLI tutorial](docs/tutorials/cli.md) for the
143
+ step-by-step version and the [Python tutorial](docs/tutorials/python_analysis.py)
144
+ for the programmatic analysis API.
145
+
146
+ ### Other commands
147
+
148
+ ```bash
149
+ # Fetch, then export CSVs, separately:
150
+ ghinsight actions fetch --org example-org --period last-year --cache-dir cache
151
+ ghinsight actions report --org example-org --cache-dir cache --output-dir reports
152
+
153
+ # Print a table to the terminal:
154
+ ghinsight actions show usage repositories --org example-org --cache-dir cache
155
+
156
+ # Diff generated CSVs against a reference snapshot exported from GitHub:
157
+ ghinsight actions compare reference-data reports
158
+ ```
159
+
160
+ ### Config reference
161
+
162
+ See [examples/analysis.toml](examples/analysis.toml). `period` accepts
163
+ `last-year`, `last-6-months`, `last-3-months`, `last-month`, or
164
+ `YYYY-MM-DD..YYYY-MM-DD`.
165
+
166
+ `[analysis]`: `org`, `period`, `cache_dir`, `reports_dir`, `data_dir`,
167
+ `output_html`, `summary_json`, `billing` (optional; see below).
168
+
169
+ `[dashboard]`: `title`, `period_label`, `plan_minutes` (optional monthly cap),
170
+ `extra_insights_html`, `extra_limitations_html`.
171
+
172
+ `[os_multipliers]`: runtime OS billing multipliers for billed-equivalent
173
+ estimates (defaults: linux 1, windows 2, macos 10).
174
+
175
+ ### Private-repository minutes and the billing report
176
+
177
+ A plan's included minutes only apply to private (and internal) repositories:
178
+ public repositories on standard GitHub-hosted runners are free. `fetch` records
179
+ each repository's visibility (`repo.json` in the cache), and the dashboard's
180
+ minute charts get an **All repositories / Private repositories only** toggle,
181
+ with private-only KPIs and a `private_only` block in `summary.json`.
182
+ Visibility is as of the fetch, so a repository whose visibility changed during
183
+ the period is classified by its current visibility throughout. Caches written
184
+ before visibility was recorded render without the toggle; re-run `fetch` to add
185
+ it.
186
+
187
+ Optionally, `fetch --billing` (or `billing = true` in the config) also caches
188
+ GitHub's [billing usage report](https://docs.github.com/en/rest/billing/usage),
189
+ which gives the minutes GitHub actually billed and the net charge. The dashboard
190
+ then adds a billed-usage section and a `billing_report` block in `summary.json`.
191
+ The API appears to admit **organisation owners only**: billing managers get a
192
+ 404 even with a classic token carrying `admin:org` (for a user account, the
193
+ token needs the `user` scope). Without access the fetch logs a warning and
194
+ caches nothing, and the dashboard notes that all figures are estimates.
195
+
196
+ Billing managers can instead download the usage report CSV from the
197
+ organisation's billing usage page (the *summarized* report covers up to a year
198
+ and breaks usage down by repository) and import it:
199
+
200
+ ```bash
201
+ ghinsight actions import-billing usage-report.csv --org example-org --cache-dir cache
202
+ ```
203
+
204
+ This writes the same cache files as the API fetch, which then reuses them.
205
+
206
+ The billing report appears to list only usage that counted towards the
207
+ quota: in testing, a public repository's usage while public was not billed at
208
+ all, while a repository billed for months was made public only afterwards.
209
+ The dashboard therefore shows billed minutes as they are, with no visibility
210
+ filter; being as-billed, they also reflect each repository's visibility *at the
211
+ time*, which the private-only estimates cannot.
212
+
213
+ ## Using the client from Python
214
+
215
+ ```python
216
+ from ghinsight import GitHubClient, list_repos
217
+
218
+ with GitHubClient() as gh: # token from GH_TOKEN, GITHUB_TOKEN or gh
219
+ repos = list_repos("my-org", gh) # RepoInfo: slug, visibility, fork, archived, ...
220
+ runs = gh.paginate("/repos/my-org/some-repo/actions/runs", response_key="workflow_runs")
221
+ viewer = gh.graphql("{ viewer { login } }")
222
+ ```
223
+
224
+ `GitHubClient` waits out primary and secondary rate limits (up to
225
+ `max_rate_limit_wait` seconds), retries server and network errors with backoff,
226
+ and raises `GitHubError` (with `.status`) otherwise. For the authenticated user,
227
+ `list_repos` includes private repositories, which `/users/{user}/repos` omits.
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ uv sync --group dev
233
+ uv run pytest # offline test suite (synthetic fixtures)
234
+ uv build # build the wheel/sdist
235
+ ```
236
+
237
+ The test suite runs fully offline against a synthetic cache (`tests/synthetic.py`),
238
+ so no token or network is required.
239
+
240
+ ## Documentation
241
+
242
+ ```bash
243
+ uv sync --group docs
244
+ uv run --group docs sphinx-build -b html docs docs/_build/html
245
+ ```
246
+
247
+ The site uses Sphinx with AutoAPI (API reference from source), a design page,
248
+ and CLI + Python tutorials (the Python tutorial is an executable jupytext
249
+ notebook rendered via MyST-NB).