ghtraffic 0.0.2__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.
@@ -0,0 +1 @@
1
+ *.ipynb linguist-documentation
@@ -0,0 +1,120 @@
1
+ .claude/handoffs/
2
+ .claude/scratch/
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+
10
+ .DS_Store
11
+ # C extensions
12
+ *.so
13
+
14
+ # TLS certificates
15
+ ## Ignore all PEM files anywhere
16
+ *.pem
17
+ ## Also ignore any certs directory
18
+ certs/
19
+
20
+ # Distribution / packaging
21
+ .Python
22
+ build/
23
+ develop-eggs/
24
+ dist/
25
+ downloads/
26
+ eggs/
27
+ .eggs/
28
+ lib/
29
+ lib64/
30
+ parts/
31
+ sdist/
32
+ var/
33
+ wheels/
34
+ node_modules/
35
+ *.egg-info/
36
+ .installed.cfg
37
+ *.egg
38
+ MANIFEST
39
+ _build
40
+
41
+ # PyInstaller
42
+ # Usually these files are written by a python script from a template
43
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
44
+ *.manifest
45
+ *.spec
46
+
47
+ # Installer logs
48
+ pip-log.txt
49
+ pip-delete-this-directory.txt
50
+
51
+ # Unit test / coverage reports
52
+ htmlcov/
53
+ .tox/
54
+ .coverage
55
+ .coverage.*
56
+ .cache
57
+ nosetests.xml
58
+ coverage.xml
59
+ *.cover
60
+ .hypothesis/
61
+ .pytest_cache/
62
+
63
+ # Translations
64
+ *.mo
65
+ *.pot
66
+
67
+ # Django stuff:
68
+ *.log
69
+ local_settings.py
70
+ db.sqlite3
71
+
72
+ # Flask stuff:
73
+ instance/
74
+ .webassets-cache
75
+
76
+ # Scrapy stuff:
77
+ .scrapy
78
+
79
+ # Sphinx documentation
80
+ docs/_build/
81
+
82
+ # PyBuilder
83
+ target/
84
+
85
+ # Jupyter Notebook
86
+ .ipynb_checkpoints
87
+
88
+ # pyenv
89
+ .python-version
90
+
91
+ # celery beat schedule file
92
+ celerybeat-schedule
93
+
94
+ # SageMath parsed files
95
+ *.sage.py
96
+
97
+ # Environments
98
+ .env
99
+ .venv
100
+ env/
101
+ venv/
102
+ ENV/
103
+ env.bak/
104
+ venv.bak/
105
+
106
+ # Spyder project settings
107
+ .spyderproject
108
+ .spyproject
109
+
110
+ # Rope project settings
111
+ .ropeproject
112
+
113
+ # mkdocs documentation
114
+ /site
115
+
116
+ # mypy
117
+ .mypy_cache/
118
+
119
+ # PyCharm
120
+ .idea
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thor Whalen
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,131 @@
1
+ Metadata-Version: 2.5
2
+ Name: ghtraffic
3
+ Version: 0.0.2
4
+ Summary: Collect, accumulate and explore GitHub traffic and insights (views, clones, referrers, paths, stars) across all your repos and orgs, with a GitHub Action and a static dashboard.
5
+ Project-URL: Homepage, https://github.com/thorwhalen/ghtraffic
6
+ Project-URL: Repository, https://github.com/thorwhalen/ghtraffic
7
+ Project-URL: Dashboard, https://thorwhalen.github.io/ghtraffic/
8
+ Author: Thor Whalen
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: analytics,dashboard,github,github-actions,insights,traffic
12
+ Requires-Python: >=3.11
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
15
+ Requires-Dist: pytest>=7.0; extra == 'dev'
16
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
17
+ Provides-Extra: docs
18
+ Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
19
+ Requires-Dist: sphinx>=6.0; extra == 'docs'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # ghtraffic
23
+
24
+ Keep your GitHub traffic for longer than 14 days, across all your repos and orgs, and explore it in a dashboard.
25
+
26
+ GitHub shows a repo's views, clones, referrers and popular pages for the last 14 days only, one repo at a time. `ghtraffic` collects them for every repo you can push to, on a schedule, and merges each run into a growing history of small CSV files in a git repository you own. Star, fork and watcher counts come along, and star history is rebuilt back to each repo's creation. A static dashboard reads that history.
27
+
28
+ **Dashboard (demo data):** https://thorwhalen.github.io/ghtraffic/
29
+
30
+ ## Quick start: collect with a GitHub Action
31
+
32
+ 1. Create a **private** repository for the data (it will hold the traffic of your private repos too), and add the workflow and config to it:
33
+
34
+ ```bash
35
+ pip install ghtraffic
36
+ gh repo create my-traffic-data --private --clone && cd my-traffic-data
37
+ ghtraffic init . --owner @me # or: --owner me --owner my-org ...
38
+ git add -A && git commit -m "ghtraffic" && git push -u origin main
39
+ ```
40
+
41
+ `ghtraffic init` writes two files: `ghtraffic.toml` (what to collect) and `.github/workflows/ghtraffic.yml`, which is all the workflow needs:
42
+
43
+ ```yaml
44
+ - uses: actions/checkout@v4
45
+ - uses: thorwhalen/ghtraffic@0.0.2 # the release you installed; pin it
46
+ with:
47
+ token: ${{ secrets.GHTRAFFIC_TOKEN }}
48
+ ```
49
+
50
+ The Action is pinned to a release tag (or pin a commit SHA): the token it receives can read every repo it covers, so it should run code you chose, not whatever lands on `main` later. It refuses to commit private repos' data into a public repository (input `allow-public: true` overrides).
51
+
52
+ `"@me"` means you **and every organization you belong to**, including client or employer organizations whose repo names and traffic may not be yours to copy. List owners explicitly, or use `exclude = ["client-org/*"]`, when that matters.
53
+
54
+ 2. Create a token with push access to the repos you want: a [classic token with the `repo` and `read:org` scopes](https://github.com/settings/tokens/new?scopes=repo,read:org&description=ghtraffic) covers every owner you can push to (a fine-grained token is limited to one owner). Save it as the data repo's `GHTRAFFIC_TOKEN` secret:
55
+
56
+ ```bash
57
+ gh secret set GHTRAFFIC_TOKEN
58
+ ```
59
+
60
+ 3. Run it once by hand (`gh workflow run ghtraffic.yml`), and it will run on its own on Mondays and Thursdays from then on. The first run is the cold start: it takes the 14 days GitHub still has, plus each repo's star history. Every later run merges into what is there; re-running is always safe.
61
+
62
+ ## Look at it
63
+
64
+ Open the dashboard at https://thorwhalen.github.io/ghtraffic/ and point it at your data repo, or use `#repo=<owner>/<data-repo>` in the URL. Paste a [fine-grained token](https://github.com/settings/personal-access-tokens/new) that can only **read the contents of that one repo**. Your browser fetches the data straight from the GitHub API with it. The page itself is public, holds no data, talks to no host but GitHub, and refuses to run inside a frame. The token stays in memory unless you tick "remember"; a remembered token is readable by any page on the same `<user>.github.io` origin, which is one more reason to give it read access to the data repo only. `?data=<url>` reads a public data directory from `raw.githubusercontent.com` instead.
65
+
66
+ It shows totals and growth over any time range, by owner and repo, views or clones, totals or uniques, a sortable leaderboard with sparklines, top referrers and pages, and stars and forks over time.
67
+
68
+ ## Collect from your own computer
69
+
70
+ ```bash
71
+ ghtraffic collect --owner @me # into ~/.local/share/ghtraffic/data
72
+ ghtraffic collect --data-dir ./my-traffic-data # into a checkout of your data repo
73
+ ```
74
+
75
+ The token comes from `GHTRAFFIC_TOKEN`, `GH_TOKEN` or `GITHUB_TOKEN`, or from the GitHub CLI (`gh auth login`).
76
+
77
+ From Python:
78
+
79
+ ```python
80
+ from ghtraffic import collect, load_settings, Files
81
+
82
+ report = collect(load_settings(owners=["@me"], exclude=["my-org/sandbox-*"]), store=Files("~/traffic"))
83
+ print(report.summary())
84
+ ```
85
+
86
+ `store` is any `MutableMapping[str, str]` (relative path to text): a folder by default, or any [dol](https://github.com/i2mint/dol) store (S3, a database, ...). `rest=` and `graphql=` replace the HTTP transport.
87
+
88
+ ## Configuration
89
+
90
+ `ghtraffic.toml`, at the data directory's root. Every key is optional.
91
+
92
+ | key | default | meaning |
93
+ |---|---|---|
94
+ | `owners` | `["@me"]` | user and org logins; `"@me"` is you and every org you belong to |
95
+ | `exclude` | `[]` | `owner/repo` glob patterns to skip (`"my-org/*"` skips an owner) |
96
+ | `include_forks` | `true` | collect forks too |
97
+ | `include_archived` | `true` | collect archived repos too |
98
+ | `snapshot_every_days` | `14` | days between snapshots of referrers, paths and 14-day totals |
99
+ | `backfill_stars` | `true` | rebuild a repo's star history the first time it is seen |
100
+ | `max_workers` | `8` | concurrent requests |
101
+
102
+ Unknown keys are refused (a typo never silently falls back to a default). Every key is also a `ghtraffic collect` flag (`--no-include-forks`, `--snapshot-every-days 7`, ...).
103
+
104
+ Repos you cannot push to have no readable traffic: they are counted and listed in `last_run.json`, never an error. An organization policy refusing the token (SAML SSO, a ban on classic tokens) is an error, listed with the repo, so a whole org never silently reads as zero traffic. The Action log carries counts only; names and messages go to `last_run.json`.
105
+
106
+ ## Data layout
107
+
108
+ Plain CSV, one table per kind, split by year, sorted by date so each run adds lines at the end of the current year's file. `manifest.json` lists every file and its columns.
109
+
110
+ | file | rows |
111
+ |---|---|
112
+ | `views/YYYY.csv`, `clones/YYYY.csv` | `date,repo,count,uniques`: one per repo and day with any traffic (no row means zero) |
113
+ | `windows/YYYY.csv` | `fetched,repo,kind,count,uniques`: 14-day totals at each snapshot, the only true 14-day unique counts |
114
+ | `referrers/YYYY.csv`, `paths/YYYY.csv` | the top 10 over the 14 days before `fetched`; snapshots are 14 days apart, so they never overlap |
115
+ | `stars.csv` | `date,repo,stars,forks,watchers`: a row only when something changed |
116
+ | `repos.csv` | current metadata of every repo seen |
117
+ | `backfills.csv` | when each repo's star history was rebuilt (a failed backfill is retried next run) |
118
+ | `runs.csv`, `last_run.json` | one row per run; the last run's details (repos without access, errors) |
119
+
120
+ A day seen by several runs keeps the largest value of each field, so overlapping windows, re-runs and partial days all merge to the same result. Why this layout, and the alternatives: [docs/adr/0001-storage.md](docs/adr/0001-storage.md).
121
+
122
+ ## Cost
123
+
124
+ One job per run, about 1 minute (a few minutes for the cold start with star history). At two runs a week that is about 10 to 20 Actions minutes a month on a private repository, and two missed runs in a row still lose nothing. Data grows by a few MB a year for a few hundred repos.
125
+
126
+ ## Limits
127
+
128
+ - GitHub keeps traffic for 14 days. Nothing earlier can be recovered, except star history.
129
+ - Star history counts current stargazers by the date they starred; unstars are invisible.
130
+ - Referrers and paths are GitHub's top 10 per repo only. Snapshots are 14 days apart so they can be added up; a missed snapshot run loses the referrer and path data of the days it slipped by (daily views and clones are unaffected).
131
+ - One token for every owner means a classic token (fine-grained tokens are per owner).
@@ -0,0 +1,110 @@
1
+ # ghtraffic
2
+
3
+ Keep your GitHub traffic for longer than 14 days, across all your repos and orgs, and explore it in a dashboard.
4
+
5
+ GitHub shows a repo's views, clones, referrers and popular pages for the last 14 days only, one repo at a time. `ghtraffic` collects them for every repo you can push to, on a schedule, and merges each run into a growing history of small CSV files in a git repository you own. Star, fork and watcher counts come along, and star history is rebuilt back to each repo's creation. A static dashboard reads that history.
6
+
7
+ **Dashboard (demo data):** https://thorwhalen.github.io/ghtraffic/
8
+
9
+ ## Quick start: collect with a GitHub Action
10
+
11
+ 1. Create a **private** repository for the data (it will hold the traffic of your private repos too), and add the workflow and config to it:
12
+
13
+ ```bash
14
+ pip install ghtraffic
15
+ gh repo create my-traffic-data --private --clone && cd my-traffic-data
16
+ ghtraffic init . --owner @me # or: --owner me --owner my-org ...
17
+ git add -A && git commit -m "ghtraffic" && git push -u origin main
18
+ ```
19
+
20
+ `ghtraffic init` writes two files: `ghtraffic.toml` (what to collect) and `.github/workflows/ghtraffic.yml`, which is all the workflow needs:
21
+
22
+ ```yaml
23
+ - uses: actions/checkout@v4
24
+ - uses: thorwhalen/ghtraffic@0.0.2 # the release you installed; pin it
25
+ with:
26
+ token: ${{ secrets.GHTRAFFIC_TOKEN }}
27
+ ```
28
+
29
+ The Action is pinned to a release tag (or pin a commit SHA): the token it receives can read every repo it covers, so it should run code you chose, not whatever lands on `main` later. It refuses to commit private repos' data into a public repository (input `allow-public: true` overrides).
30
+
31
+ `"@me"` means you **and every organization you belong to**, including client or employer organizations whose repo names and traffic may not be yours to copy. List owners explicitly, or use `exclude = ["client-org/*"]`, when that matters.
32
+
33
+ 2. Create a token with push access to the repos you want: a [classic token with the `repo` and `read:org` scopes](https://github.com/settings/tokens/new?scopes=repo,read:org&description=ghtraffic) covers every owner you can push to (a fine-grained token is limited to one owner). Save it as the data repo's `GHTRAFFIC_TOKEN` secret:
34
+
35
+ ```bash
36
+ gh secret set GHTRAFFIC_TOKEN
37
+ ```
38
+
39
+ 3. Run it once by hand (`gh workflow run ghtraffic.yml`), and it will run on its own on Mondays and Thursdays from then on. The first run is the cold start: it takes the 14 days GitHub still has, plus each repo's star history. Every later run merges into what is there; re-running is always safe.
40
+
41
+ ## Look at it
42
+
43
+ Open the dashboard at https://thorwhalen.github.io/ghtraffic/ and point it at your data repo, or use `#repo=<owner>/<data-repo>` in the URL. Paste a [fine-grained token](https://github.com/settings/personal-access-tokens/new) that can only **read the contents of that one repo**. Your browser fetches the data straight from the GitHub API with it. The page itself is public, holds no data, talks to no host but GitHub, and refuses to run inside a frame. The token stays in memory unless you tick "remember"; a remembered token is readable by any page on the same `<user>.github.io` origin, which is one more reason to give it read access to the data repo only. `?data=<url>` reads a public data directory from `raw.githubusercontent.com` instead.
44
+
45
+ It shows totals and growth over any time range, by owner and repo, views or clones, totals or uniques, a sortable leaderboard with sparklines, top referrers and pages, and stars and forks over time.
46
+
47
+ ## Collect from your own computer
48
+
49
+ ```bash
50
+ ghtraffic collect --owner @me # into ~/.local/share/ghtraffic/data
51
+ ghtraffic collect --data-dir ./my-traffic-data # into a checkout of your data repo
52
+ ```
53
+
54
+ The token comes from `GHTRAFFIC_TOKEN`, `GH_TOKEN` or `GITHUB_TOKEN`, or from the GitHub CLI (`gh auth login`).
55
+
56
+ From Python:
57
+
58
+ ```python
59
+ from ghtraffic import collect, load_settings, Files
60
+
61
+ report = collect(load_settings(owners=["@me"], exclude=["my-org/sandbox-*"]), store=Files("~/traffic"))
62
+ print(report.summary())
63
+ ```
64
+
65
+ `store` is any `MutableMapping[str, str]` (relative path to text): a folder by default, or any [dol](https://github.com/i2mint/dol) store (S3, a database, ...). `rest=` and `graphql=` replace the HTTP transport.
66
+
67
+ ## Configuration
68
+
69
+ `ghtraffic.toml`, at the data directory's root. Every key is optional.
70
+
71
+ | key | default | meaning |
72
+ |---|---|---|
73
+ | `owners` | `["@me"]` | user and org logins; `"@me"` is you and every org you belong to |
74
+ | `exclude` | `[]` | `owner/repo` glob patterns to skip (`"my-org/*"` skips an owner) |
75
+ | `include_forks` | `true` | collect forks too |
76
+ | `include_archived` | `true` | collect archived repos too |
77
+ | `snapshot_every_days` | `14` | days between snapshots of referrers, paths and 14-day totals |
78
+ | `backfill_stars` | `true` | rebuild a repo's star history the first time it is seen |
79
+ | `max_workers` | `8` | concurrent requests |
80
+
81
+ Unknown keys are refused (a typo never silently falls back to a default). Every key is also a `ghtraffic collect` flag (`--no-include-forks`, `--snapshot-every-days 7`, ...).
82
+
83
+ Repos you cannot push to have no readable traffic: they are counted and listed in `last_run.json`, never an error. An organization policy refusing the token (SAML SSO, a ban on classic tokens) is an error, listed with the repo, so a whole org never silently reads as zero traffic. The Action log carries counts only; names and messages go to `last_run.json`.
84
+
85
+ ## Data layout
86
+
87
+ Plain CSV, one table per kind, split by year, sorted by date so each run adds lines at the end of the current year's file. `manifest.json` lists every file and its columns.
88
+
89
+ | file | rows |
90
+ |---|---|
91
+ | `views/YYYY.csv`, `clones/YYYY.csv` | `date,repo,count,uniques`: one per repo and day with any traffic (no row means zero) |
92
+ | `windows/YYYY.csv` | `fetched,repo,kind,count,uniques`: 14-day totals at each snapshot, the only true 14-day unique counts |
93
+ | `referrers/YYYY.csv`, `paths/YYYY.csv` | the top 10 over the 14 days before `fetched`; snapshots are 14 days apart, so they never overlap |
94
+ | `stars.csv` | `date,repo,stars,forks,watchers`: a row only when something changed |
95
+ | `repos.csv` | current metadata of every repo seen |
96
+ | `backfills.csv` | when each repo's star history was rebuilt (a failed backfill is retried next run) |
97
+ | `runs.csv`, `last_run.json` | one row per run; the last run's details (repos without access, errors) |
98
+
99
+ A day seen by several runs keeps the largest value of each field, so overlapping windows, re-runs and partial days all merge to the same result. Why this layout, and the alternatives: [docs/adr/0001-storage.md](docs/adr/0001-storage.md).
100
+
101
+ ## Cost
102
+
103
+ One job per run, about 1 minute (a few minutes for the cold start with star history). At two runs a week that is about 10 to 20 Actions minutes a month on a private repository, and two missed runs in a row still lose nothing. Data grows by a few MB a year for a few hundred repos.
104
+
105
+ ## Limits
106
+
107
+ - GitHub keeps traffic for 14 days. Nothing earlier can be recovered, except star history.
108
+ - Star history counts current stargazers by the date they starred; unstars are invisible.
109
+ - Referrers and paths are GitHub's top 10 per repo only. Snapshots are 14 days apart so they can be added up; a missed snapshot run loses the referrer and path data of the days it slipped by (daily views and clones are unaffected).
110
+ - One token for every owner means a classic token (fine-grained tokens are per owner).
@@ -0,0 +1,72 @@
1
+ name: ghtraffic
2
+ description: Collect GitHub traffic (views, clones, referrers, paths, stars) across your repos and orgs, and commit the accumulated history.
3
+ branding:
4
+ icon: bar-chart-2
5
+ color: blue
6
+ inputs:
7
+ token:
8
+ description: A token with push access to the repos to collect (classic, scopes `repo` + `read:org`, for several owners).
9
+ required: true
10
+ data-dir:
11
+ description: Where the history lives in the checked-out repository.
12
+ default: "."
13
+ config:
14
+ description: Path to ghtraffic.toml (default <data-dir>/ghtraffic.toml).
15
+ default: ""
16
+ commit:
17
+ description: Commit and push the updated history.
18
+ default: "true"
19
+ allow-public:
20
+ description: Allow committing private repos' data into a PUBLIC data repository (refused by default).
21
+ default: "false"
22
+ fail-on-errors:
23
+ description: Fail the job when some repo or owner errored (never for missing access).
24
+ default: "false"
25
+ runs:
26
+ using: composite
27
+ steps:
28
+ - name: Collect and commit
29
+ shell: bash
30
+ env:
31
+ GHTRAFFIC_TOKEN: ${{ inputs.token }}
32
+ PYTHONPATH: ${{ github.action_path }}
33
+ DATA_DIR: ${{ inputs.data-dir }}
34
+ CONFIG: ${{ inputs.config }}
35
+ COMMIT: ${{ inputs.commit }}
36
+ ALLOW_PUBLIC: ${{ inputs.allow-public }}
37
+ FAIL_ON_ERRORS: ${{ inputs.fail-on-errors }}
38
+ run: |
39
+ set -euo pipefail
40
+ # ghtraffic needs only the standard library, but Python 3.11+ (tomllib).
41
+ python3 -c 'import sys; sys.exit(sys.version_info < (3, 11))' || {
42
+ echo "::error::ghtraffic needs Python 3.11+; add actions/setup-python before this step."; exit 1; }
43
+
44
+ args=(collect --data-dir "$DATA_DIR")
45
+ [ -n "$CONFIG" ] && args+=(--config "$CONFIG")
46
+ [ "$FAIL_ON_ERRORS" = "true" ] && args+=(--fail-on-errors)
47
+ status=0
48
+ python3 -m ghtraffic "${args[@]}" || status=$?
49
+ [ "$COMMIT" = "true" ] || exit $status
50
+
51
+ # Never put private repos' data into a public repository.
52
+ if [ "$ALLOW_PUBLIC" != "true" ]; then
53
+ python3 -m ghtraffic guard --data-dir "$DATA_DIR" --repo "$GITHUB_REPOSITORY"
54
+ fi
55
+
56
+ git config user.name "github-actions[bot]"
57
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
58
+ commit() {
59
+ git add -A "$DATA_DIR"
60
+ git diff --cached --quiet || git commit -q -m "ghtraffic: collect $(date -u +%F)"
61
+ }
62
+ commit
63
+ # On a push race, start from the remote and collect again: merges are
64
+ # idempotent, so re-collecting loses nothing and never conflicts.
65
+ for attempt in 1 2 3; do
66
+ git push -q && exit $status
67
+ git fetch -q origin "$GITHUB_REF_NAME"
68
+ git reset -q --hard "origin/$GITHUB_REF_NAME"
69
+ python3 -m ghtraffic "${args[@]}" || status=$?
70
+ commit
71
+ done
72
+ echo "::error::could not push after 3 attempts"; exit 1
@@ -0,0 +1,41 @@
1
+ """Accumulate GitHub traffic and insights across all your repos and orgs.
2
+
3
+ GitHub keeps repository traffic (views, clones, referrers, popular paths) for
4
+ 14 days only. ``ghtraffic`` collects it on a schedule and merges it into a
5
+ growing, git-friendly history of CSV files, together with star, fork and
6
+ watcher counts, and ships a static dashboard to explore it.
7
+
8
+ >>> from ghtraffic import collect, load_settings, Files # doctest: +SKIP
9
+ >>> collect(load_settings(owners=["@me"]), store=Files("~/traffic")) # doctest: +SKIP
10
+
11
+ Main pieces:
12
+
13
+ - :func:`collect`: one idempotent run (cold start and update are the same call).
14
+ - :func:`load_settings` / :class:`Settings`: which owners and repos.
15
+ - :data:`TABLES`: the on-disk schema; :class:`Files`: the default store.
16
+ - :func:`init_data_repo`: write the workflow + config into a data repository.
17
+ """
18
+
19
+ from ghtraffic.collect import RunReport, collect, write_manifest
20
+ from ghtraffic.config import Settings, load_settings
21
+ from ghtraffic.github import GitHubError, make_graphql, make_rest, resolve_token
22
+ from ghtraffic.scaffold import init_data_repo
23
+ from ghtraffic.tables import TABLES, Files, TableSpec, merge_rows, read_table
24
+
25
+ __all__ = [
26
+ "Files",
27
+ "GitHubError",
28
+ "RunReport",
29
+ "Settings",
30
+ "TABLES",
31
+ "TableSpec",
32
+ "collect",
33
+ "init_data_repo",
34
+ "load_settings",
35
+ "make_graphql",
36
+ "make_rest",
37
+ "merge_rows",
38
+ "read_table",
39
+ "resolve_token",
40
+ "write_manifest",
41
+ ]
@@ -0,0 +1,134 @@
1
+ """Command line: ``ghtraffic collect | init | guard | demo | manifest``.
2
+
3
+ ``python -m ghtraffic collect --data-dir .`` is what the GitHub Action runs.
4
+ Without ``--data-dir``, data goes to ``~/.local/share/ghtraffic/data`` (never
5
+ the current directory, so a local run cannot end up committed by accident).
6
+ Every :class:`~ghtraffic.config.Settings` field is also a ``collect`` flag.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+ import typing
14
+ from dataclasses import fields
15
+ from pathlib import Path
16
+
17
+ from ghtraffic.collect import collect, has_private_data, write_manifest
18
+ from ghtraffic.config import (
19
+ DFLT_CONFIG_FILENAME,
20
+ DFLT_DATA_DIR,
21
+ Settings,
22
+ load_settings,
23
+ )
24
+ from ghtraffic.scaffold import DFLT_CRON, default_action_ref, init_data_repo
25
+ from ghtraffic.tables import Files
26
+
27
+
28
+ def _add_settings_flags(parser: argparse.ArgumentParser) -> None:
29
+ """One flag per Settings field, so a new setting needs no CLI edit."""
30
+ hints = typing.get_type_hints(Settings)
31
+ for f in fields(Settings):
32
+ flag = "--" + f.name.replace("_", "-")
33
+ kind = hints[f.name]
34
+ if kind is bool:
35
+ parser.add_argument(flag, action=argparse.BooleanOptionalAction)
36
+ elif typing.get_origin(kind) is tuple:
37
+ parser.add_argument(flag, action="append", help="repeatable")
38
+ else:
39
+ parser.add_argument(flag, type=kind)
40
+
41
+
42
+ def _parser() -> argparse.ArgumentParser:
43
+ p = argparse.ArgumentParser(prog="ghtraffic", description=__doc__.splitlines()[0])
44
+ sub = p.add_subparsers(dest="cmd", required=True)
45
+
46
+ c = sub.add_parser("collect", help="fetch traffic and merge it into the data dir")
47
+ c.add_argument("--data-dir", default=str(DFLT_DATA_DIR))
48
+ c.add_argument("--config", help=f"default: <data-dir>/{DFLT_CONFIG_FILENAME}")
49
+ c.add_argument(
50
+ "--fail-on-errors",
51
+ action="store_true",
52
+ help="exit 1 when some repo or owner failed (never for missing access)",
53
+ )
54
+ _add_settings_flags(c)
55
+
56
+ i = sub.add_parser("init", help="write the workflow and config into a data repo")
57
+ i.add_argument("dir", nargs="?", default=".")
58
+ i.add_argument("--owner", action="append", dest="owners")
59
+ i.add_argument("--cron", default=DFLT_CRON)
60
+ i.add_argument("--action-ref", help="default: this ghtraffic's release tag")
61
+ i.add_argument("--overwrite", action="store_true")
62
+
63
+ g = sub.add_parser(
64
+ "guard", help="exit 1 if private data would be committed to a public repo"
65
+ )
66
+ g.add_argument("--data-dir", default=".")
67
+ g.add_argument("--repo", required=True, help="owner/name of the data repo")
68
+
69
+ d = sub.add_parser(
70
+ "demo", help="write a synthetic data dir (for the demo dashboard)"
71
+ )
72
+ d.add_argument("--data-dir", required=True)
73
+ d.add_argument("--days", type=int, default=420)
74
+
75
+ m = sub.add_parser("manifest", help="rewrite manifest.json from the files present")
76
+ m.add_argument("--data-dir", default=str(DFLT_DATA_DIR))
77
+ return p
78
+
79
+
80
+ def _guard(data_dir: str, repo: str) -> int:
81
+ from ghtraffic.github import make_rest
82
+
83
+ if not make_rest()(f"/repos/{repo}").get("private", False) and has_private_data(
84
+ Files(data_dir)
85
+ ):
86
+ print(
87
+ f"ghtraffic: REFUSING to commit: {repo} is public and the history "
88
+ "holds private repos. Make the data repo private, or exclude private "
89
+ "repos, or set the action input allow-public: true.",
90
+ file=sys.stderr,
91
+ )
92
+ return 1
93
+ return 0
94
+
95
+
96
+ def main(argv=None) -> int:
97
+ """Entry point of the ``ghtraffic`` command."""
98
+ args = _parser().parse_args(argv)
99
+ if args.cmd == "collect":
100
+ data_dir = Path(args.data_dir)
101
+ overrides = {f.name: getattr(args, f.name) for f in fields(Settings)}
102
+ settings = load_settings(
103
+ args.config or data_dir / DFLT_CONFIG_FILENAME, **overrides
104
+ )
105
+ report = collect(settings, store=Files(data_dir), log=print)
106
+ failed = report.errors or report.owner_errors
107
+ return 1 if (args.fail_on_errors and failed) else 0
108
+ if args.cmd == "init":
109
+ written = init_data_repo(
110
+ args.dir,
111
+ owners=args.owners or ("@me",),
112
+ cron=args.cron,
113
+ action_ref=args.action_ref or default_action_ref(),
114
+ overwrite=args.overwrite,
115
+ )
116
+ print("\n".join(written) or "nothing written (files exist; --overwrite)")
117
+ return 0
118
+ if args.cmd == "guard":
119
+ return _guard(args.data_dir, args.repo)
120
+ if args.cmd == "demo":
121
+ from ghtraffic.demo import make_demo_data
122
+
123
+ make_demo_data(Files(args.data_dir), n_days=args.days)
124
+ return 0
125
+ if args.cmd == "manifest":
126
+ from datetime import date
127
+
128
+ write_manifest(Files(args.data_dir), updated=date.today().isoformat())
129
+ return 0
130
+ return 2 # pragma: no cover
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())