gitlytics 0.1.5__tar.gz → 0.1.6__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.
- {gitlytics-0.1.5 → gitlytics-0.1.6}/LICENSE +19 -5
- {gitlytics-0.1.5 → gitlytics-0.1.6}/PKG-INFO +43 -23
- {gitlytics-0.1.5 → gitlytics-0.1.6}/README.md +42 -21
- {gitlytics-0.1.5 → gitlytics-0.1.6}/pyproject.toml +1 -2
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics/__init__.py +28 -13
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics/api.py +72 -31
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics/automation.py +14 -11
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics/cli.py +7 -3
- gitlytics-0.1.6/src/gitlytics/core.py +491 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics/process.py +77 -49
- gitlytics-0.1.6/src/gitlytics/static/android-chrome-192x192.png +0 -0
- gitlytics-0.1.6/src/gitlytics/static/android-chrome-512x512.png +0 -0
- gitlytics-0.1.6/src/gitlytics/static/apple-touch-icon.png +0 -0
- gitlytics-0.1.6/src/gitlytics/static/assets/html2canvas-pro.esm-C9_j7xg5.js +10 -0
- gitlytics-0.1.6/src/gitlytics/static/assets/html2canvas.esm-QH1iLAAe.js +22 -0
- gitlytics-0.1.6/src/gitlytics/static/assets/index-D6vJCUrl.js +504 -0
- gitlytics-0.1.6/src/gitlytics/static/assets/index-hl2LPOqz.css +1 -0
- gitlytics-0.1.6/src/gitlytics/static/assets/index.es-BM2mGRzK.js +18 -0
- gitlytics-0.1.6/src/gitlytics/static/assets/jspdf.es.min-cihQsb1K.js +170 -0
- gitlytics-0.1.6/src/gitlytics/static/assets/purify.es-Csrj9YNg.js +3 -0
- gitlytics-0.1.6/src/gitlytics/static/favicon-16x16.png +0 -0
- gitlytics-0.1.6/src/gitlytics/static/favicon-32x32.png +0 -0
- gitlytics-0.1.6/src/gitlytics/static/favicon.ico +0 -0
- gitlytics-0.1.6/src/gitlytics/static/gitlytics-logo.png +0 -0
- gitlytics-0.1.6/src/gitlytics/static/index.html +19 -0
- gitlytics-0.1.6/src/gitlytics/static/octocat.png +0 -0
- gitlytics-0.1.6/src/gitlytics/static/robots.txt +5 -0
- gitlytics-0.1.6/src/gitlytics/static/sitemap.xml +13 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics.egg-info/PKG-INFO +43 -23
- gitlytics-0.1.6/src/gitlytics.egg-info/SOURCES.txt +40 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics.egg-info/requires.txt +0 -1
- {gitlytics-0.1.5 → gitlytics-0.1.6}/tests/test_api.py +6 -3
- {gitlytics-0.1.5 → gitlytics-0.1.6}/tests/test_core.py +1 -1
- {gitlytics-0.1.5 → gitlytics-0.1.6}/tests/test_process.py +3 -0
- gitlytics-0.1.6/tests/test_username.py +335 -0
- gitlytics-0.1.5/src/gitlytics/core.py +0 -288
- gitlytics-0.1.5/src/gitlytics/static/assets/index-CJlQrbYd.js +0 -44
- gitlytics-0.1.5/src/gitlytics/static/assets/index-Dpkz0yGK.css +0 -2
- gitlytics-0.1.5/src/gitlytics/static/index.html +0 -14
- gitlytics-0.1.5/src/gitlytics/static/logo.png +0 -0
- gitlytics-0.1.5/src/gitlytics.egg-info/SOURCES.txt +0 -25
- {gitlytics-0.1.5 → gitlytics-0.1.6}/setup.cfg +0 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics/__main__.py +0 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics.egg-info/dependency_links.txt +0 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics.egg-info/entry_points.txt +0 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/src/gitlytics.egg-info/top_level.txt +0 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/tests/test_automation.py +0 -0
- {gitlytics-0.1.5 → gitlytics-0.1.6}/tests/test_cli.py +0 -0
|
@@ -194,8 +194,22 @@
|
|
|
194
194
|
|
|
195
195
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
202
|
+
|
|
203
|
+
-------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
"Commons Clause" License Condition v1.0
|
|
206
|
+
|
|
207
|
+
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
|
208
|
+
|
|
209
|
+
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
|
|
210
|
+
|
|
211
|
+
For purposes of the foregoing, "Sell" means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
|
|
212
|
+
|
|
213
|
+
Software: Gitlytics
|
|
214
|
+
License: Apache 2.0
|
|
215
|
+
Licensor: Ameya Sanjay Chopade
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlytics
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Monitor and automate your GitHub repository traffic analytics.
|
|
5
5
|
Author-email: Ameya Chopade <ameyaccod171@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -27,15 +27,14 @@ Provides-Extra: dev
|
|
|
27
27
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
28
28
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
29
29
|
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
30
|
-
Requires-Dist: Faker>=20.0.0; extra == "dev"
|
|
31
30
|
Requires-Dist: anyio[trio]>=4.0.0; extra == "dev"
|
|
32
31
|
Dynamic: license-file
|
|
33
32
|
|
|
34
33
|
<div align="center">
|
|
35
34
|
|
|
36
|
-
<img src="
|
|
35
|
+
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/logo.png" alt="Gitlytics Logo" width="150" />
|
|
37
36
|
|
|
38
|
-
#
|
|
37
|
+
# Gitlytics
|
|
39
38
|
### GitHub Traffic Analytics & Automation
|
|
40
39
|
|
|
41
40
|
[](LICENSE)
|
|
@@ -48,6 +47,8 @@ Dynamic: license-file
|
|
|
48
47
|
[](https://dashboard.gitlytics.dev)
|
|
49
48
|
[](https://docs.gitlytics.dev)
|
|
50
49
|
|
|
50
|
+
<br/>Please consider giving this project a ⭐ if you find it helpful! <br/>
|
|
51
|
+
|
|
51
52
|
**Beautiful GitHub traffic analytics for all your repositories — public and private.** <br/> Track views, clones, referrers, and popular paths indefinitely.
|
|
52
53
|
|
|
53
54
|
✨ **[Try the live dashboard at dashboard.gitlytics.dev](https://dashboard.gitlytics.dev)** ✨
|
|
@@ -57,22 +58,16 @@ Dynamic: license-file
|
|
|
57
58
|
|
|
58
59
|
> **🐍 Native Python API**
|
|
59
60
|
>
|
|
60
|
-
> You can import Gitlytics natively into your own Python applications to
|
|
61
|
+
> You can import Gitlytics natively into your own Python applications to fetch live repository data like views, clones, stars, and referrers. Build custom integrations, run custom cron workflows, or serve the dashboard programmatically on your own cloud servers.
|
|
61
62
|
>
|
|
62
63
|
> 📚 **[Read the Full API Documentation](https://docs.gitlytics.dev)**
|
|
63
64
|
|
|
64
|
-
Please consider giving this project a ⭐ if you find it helpful!
|
|
65
|
-
|
|
66
65
|
</div>
|
|
67
66
|
|
|
68
67
|
---
|
|
69
68
|
|
|
70
69
|
<div align="center">
|
|
71
|
-
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_1.png" width="
|
|
72
|
-
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_2.png" width="49%" />
|
|
73
|
-
</div>
|
|
74
|
-
<div align="center">
|
|
75
|
-
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_3.png" width="98.5%" />
|
|
70
|
+
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_1.png" width="100%" />
|
|
76
71
|
</div>
|
|
77
72
|
|
|
78
73
|
---
|
|
@@ -101,7 +96,7 @@ Please consider giving this project a ⭐ if you find it helpful!
|
|
|
101
96
|
|
|
102
97
|
The full Gitlytics ecosystem spans across a few repositories. If you are looking for the live web dashboard or the automation cron job, check out the links below:
|
|
103
98
|
|
|
104
|
-
- **[
|
|
99
|
+
- **[Gitlytics Web Ecosystem](https://github.com/ameyac11/gitlytics-deployement)**: The production landing page, React Dashboard, and React Documentation site.
|
|
105
100
|
- ⚙️ **[Gitlytics Automation](https://github.com/ameyac11/gitlytics-github-traffic-automation)**: The GitHub Action companion tool that automates fetching and saving to defeat GitHub's 14-day traffic limit.
|
|
106
101
|
|
|
107
102
|
---
|
|
@@ -147,6 +142,9 @@ Gitlytics is powered by 3 massive command-line tools. You can run them anywhere
|
|
|
147
142
|
Fetch your live 14-day traffic and print a beautiful ASCII table directly in your console.
|
|
148
143
|
```bash
|
|
149
144
|
gitlytics fetch --token ghp_your_token_here --print-table
|
|
145
|
+
|
|
146
|
+
# Fetch specific metrics only (e.g., views and clones)
|
|
147
|
+
gitlytics fetch --token ghp_your_token_here --print-table --metrics views clones
|
|
150
148
|
```
|
|
151
149
|
|
|
152
150
|
### 2️⃣ `gitlytics sync` (Background Database Cron)
|
|
@@ -155,6 +153,9 @@ Tired of losing data? Use `sync` to permanently append today's traffic to a CSV
|
|
|
155
153
|
# Sync once
|
|
156
154
|
gitlytics sync --token ghp_your_token --data-dir ./data
|
|
157
155
|
|
|
156
|
+
# Sync specific metrics only
|
|
157
|
+
gitlytics sync --token ghp_your_token --data-dir ./data --metrics views clones
|
|
158
|
+
|
|
158
159
|
# Run permanently in the background as a cron job (runs at 11:00 PM every day)
|
|
159
160
|
gitlytics sync --token ghp_your_token --data-dir ./data --schedule-cron "0 23 * * *"
|
|
160
161
|
```
|
|
@@ -212,6 +213,7 @@ gitlytics.fetch_traffic(
|
|
|
212
213
|
| `print_table` | `bool` | `False` | If `True`, formats and prints a detailed ASCII traffic table to the console. |
|
|
213
214
|
| `return_format` | `str` | `"dataframe"` | The format of returned data: `"dataframe"` (Pandas DataFrame), `"timeseries"` (chart-ready nested dict), or `"summary"` (per-repo totals dict). |
|
|
214
215
|
| `save_file` | `str` | `None` | Optional. File path where the fetched data will be saved (CSV or JSON). |
|
|
216
|
+
| `metrics` | `list` | `None` | Optional. List of metrics to fetch (e.g., `["views", "clones"]`). |
|
|
215
217
|
|
|
216
218
|
---
|
|
217
219
|
|
|
@@ -248,6 +250,7 @@ gitlytics.sync(
|
|
|
248
250
|
| `schedule_cron` | `str` | `None` | Optional cron expression (e.g., `"*/15 * * * *"`). If set, runs an infinite scheduler loop. |
|
|
249
251
|
| `export_json` | `str` | `None` | Optional. Path to compile and export a consolidated history JSON for the frontend. |
|
|
250
252
|
| `export_public_only` | `bool` | `True` | Security firewall: if `True`, strips private repository data from the compiled `export_json`. |
|
|
253
|
+
| `metrics` | `list` | `None` | Optional. List of metrics to sync (e.g., `["views", "clones"]`). |
|
|
251
254
|
|
|
252
255
|
---
|
|
253
256
|
|
|
@@ -279,16 +282,33 @@ gitlytics.serve_dashboard(
|
|
|
279
282
|
|
|
280
283
|
## 📊 CSV Output Columns
|
|
281
284
|
|
|
282
|
-
When you sync data, the local CSV databases track
|
|
283
|
-
|
|
284
|
-
| Column |
|
|
285
|
-
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
| `
|
|
285
|
+
When you sync data, the local CSV databases track 23 detailed metrics by default. If you customize the metrics using the `--metrics` CLI flag or `metrics` Python parameter, the CSV columns will dynamically include only the columns corresponding to your selection (along with the default `date`, `repository`, and `is_private` identification columns).
|
|
286
|
+
|
|
287
|
+
| Column | Type | Description |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `date` | `str` | ISO date (`YYYY-MM-DD`) for this day's traffic snapshot. |
|
|
290
|
+
| `repository` | `str` | Full GitHub repository name (`owner/repo`). |
|
|
291
|
+
| `is_private` | `bool` | `True` if repository is private, `False` otherwise. |
|
|
292
|
+
| `views` | `int` | Total page views on this day. |
|
|
293
|
+
| `unique_visitors` | `int` | Unique visitors on this day. |
|
|
294
|
+
| `clones` | `int` | Total git clone operations on this day. |
|
|
295
|
+
| `unique_cloners` | `int` | Unique clone clients on this day. |
|
|
296
|
+
| `stars` | `int` | Current total star count snapshot. |
|
|
297
|
+
| `forks` | `int` | Current total fork count snapshot. |
|
|
298
|
+
| `language` | `str` | Primary programming language of the repository. |
|
|
299
|
+
| `topics` | `str` | JSON array containing repository tags/topics. |
|
|
300
|
+
| `watchers_count` | `int` | Total watchers of the repository. |
|
|
301
|
+
| `pushed_at` | `str` | Last push ISO timestamp. |
|
|
302
|
+
| `created_at` | `str` | Repository creation ISO timestamp. |
|
|
303
|
+
| `open_issues_count` | `int` | Total number of open issues. |
|
|
304
|
+
| `top_referrer` | `str` | Top external traffic referral source (14-day rolling window). |
|
|
305
|
+
| `top_referrer_views` | `int` | Views sent by the top referrer. |
|
|
306
|
+
| `top_referrer_uniques` | `int` | Uniques sent by the top referrer. |
|
|
307
|
+
| `_raw_referrers` | `str` | Raw JSON array of all referral sources. |
|
|
308
|
+
| `top_path` | `str` | Most visited repository file path (14-day rolling window). |
|
|
309
|
+
| `top_path_views` | `int` | Views for the top path. |
|
|
310
|
+
| `top_path_uniques` | `int` | Uniques for the top path. |
|
|
311
|
+
| `_raw_paths` | `str` | Raw JSON array of all popular paths. |
|
|
292
312
|
|
|
293
313
|
---
|
|
294
314
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/logo.png" alt="Gitlytics Logo" width="150" />
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Gitlytics
|
|
6
6
|
### GitHub Traffic Analytics & Automation
|
|
7
7
|
|
|
8
8
|
[](LICENSE)
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
[](https://dashboard.gitlytics.dev)
|
|
16
16
|
[](https://docs.gitlytics.dev)
|
|
17
17
|
|
|
18
|
+
<br/>Please consider giving this project a ⭐ if you find it helpful! <br/>
|
|
19
|
+
|
|
18
20
|
**Beautiful GitHub traffic analytics for all your repositories — public and private.** <br/> Track views, clones, referrers, and popular paths indefinitely.
|
|
19
21
|
|
|
20
22
|
✨ **[Try the live dashboard at dashboard.gitlytics.dev](https://dashboard.gitlytics.dev)** ✨
|
|
@@ -24,22 +26,16 @@
|
|
|
24
26
|
|
|
25
27
|
> **🐍 Native Python API**
|
|
26
28
|
>
|
|
27
|
-
> You can import Gitlytics natively into your own Python applications to
|
|
29
|
+
> You can import Gitlytics natively into your own Python applications to fetch live repository data like views, clones, stars, and referrers. Build custom integrations, run custom cron workflows, or serve the dashboard programmatically on your own cloud servers.
|
|
28
30
|
>
|
|
29
31
|
> 📚 **[Read the Full API Documentation](https://docs.gitlytics.dev)**
|
|
30
32
|
|
|
31
|
-
Please consider giving this project a ⭐ if you find it helpful!
|
|
32
|
-
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
37
|
<div align="center">
|
|
38
|
-
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_1.png" width="
|
|
39
|
-
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_2.png" width="49%" />
|
|
40
|
-
</div>
|
|
41
|
-
<div align="center">
|
|
42
|
-
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_3.png" width="98.5%" />
|
|
38
|
+
<img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_1.png" width="100%" />
|
|
43
39
|
</div>
|
|
44
40
|
|
|
45
41
|
---
|
|
@@ -68,7 +64,7 @@ Please consider giving this project a ⭐ if you find it helpful!
|
|
|
68
64
|
|
|
69
65
|
The full Gitlytics ecosystem spans across a few repositories. If you are looking for the live web dashboard or the automation cron job, check out the links below:
|
|
70
66
|
|
|
71
|
-
- **[
|
|
67
|
+
- **[Gitlytics Web Ecosystem](https://github.com/ameyac11/gitlytics-deployement)**: The production landing page, React Dashboard, and React Documentation site.
|
|
72
68
|
- ⚙️ **[Gitlytics Automation](https://github.com/ameyac11/gitlytics-github-traffic-automation)**: The GitHub Action companion tool that automates fetching and saving to defeat GitHub's 14-day traffic limit.
|
|
73
69
|
|
|
74
70
|
---
|
|
@@ -114,6 +110,9 @@ Gitlytics is powered by 3 massive command-line tools. You can run them anywhere
|
|
|
114
110
|
Fetch your live 14-day traffic and print a beautiful ASCII table directly in your console.
|
|
115
111
|
```bash
|
|
116
112
|
gitlytics fetch --token ghp_your_token_here --print-table
|
|
113
|
+
|
|
114
|
+
# Fetch specific metrics only (e.g., views and clones)
|
|
115
|
+
gitlytics fetch --token ghp_your_token_here --print-table --metrics views clones
|
|
117
116
|
```
|
|
118
117
|
|
|
119
118
|
### 2️⃣ `gitlytics sync` (Background Database Cron)
|
|
@@ -122,6 +121,9 @@ Tired of losing data? Use `sync` to permanently append today's traffic to a CSV
|
|
|
122
121
|
# Sync once
|
|
123
122
|
gitlytics sync --token ghp_your_token --data-dir ./data
|
|
124
123
|
|
|
124
|
+
# Sync specific metrics only
|
|
125
|
+
gitlytics sync --token ghp_your_token --data-dir ./data --metrics views clones
|
|
126
|
+
|
|
125
127
|
# Run permanently in the background as a cron job (runs at 11:00 PM every day)
|
|
126
128
|
gitlytics sync --token ghp_your_token --data-dir ./data --schedule-cron "0 23 * * *"
|
|
127
129
|
```
|
|
@@ -179,6 +181,7 @@ gitlytics.fetch_traffic(
|
|
|
179
181
|
| `print_table` | `bool` | `False` | If `True`, formats and prints a detailed ASCII traffic table to the console. |
|
|
180
182
|
| `return_format` | `str` | `"dataframe"` | The format of returned data: `"dataframe"` (Pandas DataFrame), `"timeseries"` (chart-ready nested dict), or `"summary"` (per-repo totals dict). |
|
|
181
183
|
| `save_file` | `str` | `None` | Optional. File path where the fetched data will be saved (CSV or JSON). |
|
|
184
|
+
| `metrics` | `list` | `None` | Optional. List of metrics to fetch (e.g., `["views", "clones"]`). |
|
|
182
185
|
|
|
183
186
|
---
|
|
184
187
|
|
|
@@ -215,6 +218,7 @@ gitlytics.sync(
|
|
|
215
218
|
| `schedule_cron` | `str` | `None` | Optional cron expression (e.g., `"*/15 * * * *"`). If set, runs an infinite scheduler loop. |
|
|
216
219
|
| `export_json` | `str` | `None` | Optional. Path to compile and export a consolidated history JSON for the frontend. |
|
|
217
220
|
| `export_public_only` | `bool` | `True` | Security firewall: if `True`, strips private repository data from the compiled `export_json`. |
|
|
221
|
+
| `metrics` | `list` | `None` | Optional. List of metrics to sync (e.g., `["views", "clones"]`). |
|
|
218
222
|
|
|
219
223
|
---
|
|
220
224
|
|
|
@@ -246,16 +250,33 @@ gitlytics.serve_dashboard(
|
|
|
246
250
|
|
|
247
251
|
## 📊 CSV Output Columns
|
|
248
252
|
|
|
249
|
-
When you sync data, the local CSV databases track
|
|
250
|
-
|
|
251
|
-
| Column |
|
|
252
|
-
|
|
253
|
-
| `
|
|
254
|
-
| `
|
|
255
|
-
| `
|
|
256
|
-
| `
|
|
257
|
-
| `
|
|
258
|
-
| `
|
|
253
|
+
When you sync data, the local CSV databases track 23 detailed metrics by default. If you customize the metrics using the `--metrics` CLI flag or `metrics` Python parameter, the CSV columns will dynamically include only the columns corresponding to your selection (along with the default `date`, `repository`, and `is_private` identification columns).
|
|
254
|
+
|
|
255
|
+
| Column | Type | Description |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| `date` | `str` | ISO date (`YYYY-MM-DD`) for this day's traffic snapshot. |
|
|
258
|
+
| `repository` | `str` | Full GitHub repository name (`owner/repo`). |
|
|
259
|
+
| `is_private` | `bool` | `True` if repository is private, `False` otherwise. |
|
|
260
|
+
| `views` | `int` | Total page views on this day. |
|
|
261
|
+
| `unique_visitors` | `int` | Unique visitors on this day. |
|
|
262
|
+
| `clones` | `int` | Total git clone operations on this day. |
|
|
263
|
+
| `unique_cloners` | `int` | Unique clone clients on this day. |
|
|
264
|
+
| `stars` | `int` | Current total star count snapshot. |
|
|
265
|
+
| `forks` | `int` | Current total fork count snapshot. |
|
|
266
|
+
| `language` | `str` | Primary programming language of the repository. |
|
|
267
|
+
| `topics` | `str` | JSON array containing repository tags/topics. |
|
|
268
|
+
| `watchers_count` | `int` | Total watchers of the repository. |
|
|
269
|
+
| `pushed_at` | `str` | Last push ISO timestamp. |
|
|
270
|
+
| `created_at` | `str` | Repository creation ISO timestamp. |
|
|
271
|
+
| `open_issues_count` | `int` | Total number of open issues. |
|
|
272
|
+
| `top_referrer` | `str` | Top external traffic referral source (14-day rolling window). |
|
|
273
|
+
| `top_referrer_views` | `int` | Views sent by the top referrer. |
|
|
274
|
+
| `top_referrer_uniques` | `int` | Uniques sent by the top referrer. |
|
|
275
|
+
| `_raw_referrers` | `str` | Raw JSON array of all referral sources. |
|
|
276
|
+
| `top_path` | `str` | Most visited repository file path (14-day rolling window). |
|
|
277
|
+
| `top_path_views` | `int` | Views for the top path. |
|
|
278
|
+
| `top_path_uniques` | `int` | Uniques for the top path. |
|
|
279
|
+
| `_raw_paths` | `str` | Raw JSON array of all popular paths. |
|
|
259
280
|
|
|
260
281
|
---
|
|
261
282
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitlytics"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "Monitor and automate your GitHub repository traffic analytics."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -51,7 +51,6 @@ dev = [
|
|
|
51
51
|
"pytest>=8.0.0",
|
|
52
52
|
"pytest-cov>=5.0.0",
|
|
53
53
|
"httpx>=0.27.0", # required by FastAPI TestClient (used in test_api.py)
|
|
54
|
-
"Faker>=20.0.0", # used by pytest-faker plugin for generating test data
|
|
55
54
|
"anyio[trio]>=4.0.0" # async test support
|
|
56
55
|
]
|
|
57
56
|
|
|
@@ -8,7 +8,9 @@ import json
|
|
|
8
8
|
|
|
9
9
|
# Single source of truth for the package version.
|
|
10
10
|
# Mirrors the version in pyproject.toml — keep them in sync.
|
|
11
|
-
__version__ = "0.1.
|
|
11
|
+
__version__ = "0.1.6"
|
|
12
|
+
|
|
13
|
+
__all__ = ["fetch_traffic", "sync", "serve_dashboard", "__version__"]
|
|
12
14
|
|
|
13
15
|
# Import the internal building blocks — users never call these directly
|
|
14
16
|
from .core import fetch_traffic_data, print_repo_table
|
|
@@ -20,7 +22,7 @@ logger = logging.getLogger(__name__)
|
|
|
20
22
|
logger.addHandler(logging.NullHandler())
|
|
21
23
|
|
|
22
24
|
|
|
23
|
-
def fetch_traffic(token: str, repo_name=None, print_table: bool = False, return_format: str = "dataframe", save_file: str = None):
|
|
25
|
+
def fetch_traffic(token: str, repo_name=None, print_table: bool = False, return_format: str = "dataframe", save_file: str = None, metrics: list = None):
|
|
24
26
|
"""
|
|
25
27
|
Fetches the last 14 days of traffic data for one or all repositories.
|
|
26
28
|
|
|
@@ -35,13 +37,14 @@ def fetch_traffic(token: str, repo_name=None, print_table: bool = False, return_
|
|
|
35
37
|
``"summary"`` — returns a per-repo totals dict.
|
|
36
38
|
save_file: Optional path to save the output. Extension determines
|
|
37
39
|
format: ``.json`` writes JSON, anything else writes CSV.
|
|
40
|
+
metrics: Optional list of metrics to fetch (e.g., ``["views", "clones"]``).
|
|
38
41
|
|
|
39
42
|
Returns:
|
|
40
43
|
A ``pandas.DataFrame`` when ``return_format="dataframe"``, otherwise
|
|
41
44
|
a ``dict`` matching the requested format.
|
|
42
45
|
"""
|
|
43
46
|
# Hit the GitHub API and get back a tidy DataFrame (one row per day per repo)
|
|
44
|
-
df = fetch_traffic_data(token, repo_name)
|
|
47
|
+
df = fetch_traffic_data(token, repo_name, metrics)
|
|
45
48
|
|
|
46
49
|
# Print the ASCII table to the console if the user asked for it
|
|
47
50
|
if print_table:
|
|
@@ -79,7 +82,7 @@ def fetch_traffic(token: str, repo_name=None, print_table: bool = False, return_
|
|
|
79
82
|
return payload
|
|
80
83
|
|
|
81
84
|
|
|
82
|
-
def sync(token: str, repo_name=None, data_dir: str = "./data", output_mode: str = "monthly", schedule_cron: str = None, export_json: str = None, export_public_only: bool = True):
|
|
85
|
+
def sync(token: str, repo_name=None, data_dir: str = "./data", output_mode: str = "monthly", schedule_cron: str = None, export_json: str = None, export_public_only: bool = True, metrics: list = None):
|
|
83
86
|
"""
|
|
84
87
|
Fetches data and appends it to a local CSV database, optionally running as a permanent background daemon.
|
|
85
88
|
|
|
@@ -93,6 +96,7 @@ def sync(token: str, repo_name=None, data_dir: str = "./data", output_mode: str
|
|
|
93
96
|
export_json: Path to export the merged historical database as a JSON file.
|
|
94
97
|
export_public_only: If ``True`` (default), strips private repos from the
|
|
95
98
|
exported JSON — acts as a security firewall.
|
|
99
|
+
metrics: Optional list of metrics to fetch (e.g., ``["views", "clones"]``).
|
|
96
100
|
"""
|
|
97
101
|
# Hand off to the automation engine — it handles deduplication and schema migration
|
|
98
102
|
run_sync(
|
|
@@ -102,7 +106,8 @@ def sync(token: str, repo_name=None, data_dir: str = "./data", output_mode: str
|
|
|
102
106
|
output_mode=output_mode,
|
|
103
107
|
schedule_cron=schedule_cron,
|
|
104
108
|
export_json=export_json,
|
|
105
|
-
export_public_only=export_public_only
|
|
109
|
+
export_public_only=export_public_only,
|
|
110
|
+
metrics=metrics
|
|
106
111
|
)
|
|
107
112
|
|
|
108
113
|
|
|
@@ -130,11 +135,21 @@ def serve_dashboard(host: str = "127.0.0.1", port: int = 8000, token: str = None
|
|
|
130
135
|
"Install them with: pip install \"gitlytics[dashboard]\""
|
|
131
136
|
)
|
|
132
137
|
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
# M-7: save original values so they are restored when the server stops
|
|
139
|
+
_orig_token = os.environ.get("GITLYTICS_TOKEN")
|
|
140
|
+
_orig_data_dir = os.environ.get("GITLYTICS_DATA_DIR")
|
|
141
|
+
try:
|
|
142
|
+
if token:
|
|
143
|
+
os.environ["GITLYTICS_TOKEN"] = token
|
|
144
|
+
if data_dir:
|
|
145
|
+
os.environ["GITLYTICS_DATA_DIR"] = os.path.abspath(data_dir)
|
|
146
|
+
uvicorn.run("gitlytics.api:app", host=host, port=port, reload=False)
|
|
147
|
+
finally:
|
|
148
|
+
if _orig_token is None:
|
|
149
|
+
os.environ.pop("GITLYTICS_TOKEN", None)
|
|
150
|
+
else:
|
|
151
|
+
os.environ["GITLYTICS_TOKEN"] = _orig_token
|
|
152
|
+
if _orig_data_dir is None:
|
|
153
|
+
os.environ.pop("GITLYTICS_DATA_DIR", None)
|
|
154
|
+
else:
|
|
155
|
+
os.environ["GITLYTICS_DATA_DIR"] = _orig_data_dir
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
gitlytics/api.py
|
|
3
3
|
Powers the FastAPI backend — serves traffic data and the React dashboard to the browser.
|
|
4
4
|
"""
|
|
5
|
+
import hashlib
|
|
5
6
|
import logging
|
|
6
7
|
import os
|
|
8
|
+
import time as _time
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
|
|
9
11
|
import pandas as pd
|
|
@@ -12,14 +14,21 @@ from fastapi.responses import FileResponse, JSONResponse
|
|
|
12
14
|
from fastapi.middleware.cors import CORSMiddleware
|
|
13
15
|
from fastapi.staticfiles import StaticFiles
|
|
14
16
|
|
|
15
|
-
from gitlytics.core import
|
|
17
|
+
from gitlytics.core import (
|
|
18
|
+
validate_token,
|
|
19
|
+
get_user_profile,
|
|
20
|
+
get_public_user,
|
|
21
|
+
get_public_repos,
|
|
22
|
+
fetch_traffic_data,
|
|
23
|
+
fetch_deep_stats_for_top,
|
|
24
|
+
)
|
|
16
25
|
from gitlytics.process import process_uploaded_csv, build_react_payload
|
|
17
26
|
|
|
18
27
|
logger = logging.getLogger(__name__)
|
|
19
28
|
|
|
20
29
|
app = FastAPI(title="GitHub Traffic API")
|
|
21
30
|
|
|
22
|
-
# Only allow requests from localhost —
|
|
31
|
+
# Only allow requests from localhost — never deployed publicly
|
|
23
32
|
_ALLOWED_ORIGINS = [
|
|
24
33
|
"http://localhost",
|
|
25
34
|
"http://localhost:3000",
|
|
@@ -40,14 +49,33 @@ app.add_middleware(
|
|
|
40
49
|
)
|
|
41
50
|
|
|
42
51
|
|
|
52
|
+
_auth_cache: dict = {} # sha256_prefix -> (valid, username, expires_at)
|
|
53
|
+
_AUTH_CACHE_TTL = 300 # 5 minutes
|
|
54
|
+
|
|
55
|
+
|
|
43
56
|
def _get_token(token: str = None) -> str:
|
|
44
|
-
#
|
|
45
|
-
|
|
57
|
+
# C-2: explicit empty string must not fall through to the env token
|
|
58
|
+
if token and token.strip():
|
|
59
|
+
return token.strip()
|
|
60
|
+
return os.environ.get("GITLYTICS_TOKEN")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _validate_token_cached(token: str):
|
|
64
|
+
# M-1: cache validation results to avoid a double HTTP round-trip on every /api/traffic call
|
|
65
|
+
key = hashlib.sha256(token.encode()).hexdigest()[:16]
|
|
66
|
+
now = _time.time()
|
|
67
|
+
if key in _auth_cache:
|
|
68
|
+
valid, username, expires = _auth_cache[key]
|
|
69
|
+
if now < expires:
|
|
70
|
+
return valid, username
|
|
71
|
+
from gitlytics.core import validate_token
|
|
72
|
+
valid, username = validate_token(token)
|
|
73
|
+
_auth_cache[key] = (valid, username, now + _AUTH_CACHE_TTL)
|
|
74
|
+
return valid, username
|
|
46
75
|
|
|
47
76
|
|
|
48
77
|
@app.get("/api/config")
|
|
49
78
|
def get_config():
|
|
50
|
-
# Lets the frontend know if it's running in headless/TV mode with a pre-set token
|
|
51
79
|
return {
|
|
52
80
|
"has_token": bool(os.environ.get("GITLYTICS_TOKEN")),
|
|
53
81
|
"has_data_dir": bool(os.environ.get("GITLYTICS_DATA_DIR"))
|
|
@@ -63,21 +91,39 @@ def auth(token: str = Body("", embed=True)):
|
|
|
63
91
|
|
|
64
92
|
ok, username = validate_token(active_token)
|
|
65
93
|
if not ok:
|
|
66
|
-
# Log a warning without echoing the token value into logs
|
|
67
94
|
logger.warning("Authentication attempt failed for a provided token.")
|
|
68
95
|
raise HTTPException(status_code=401, detail=username)
|
|
69
96
|
|
|
70
|
-
# Fetch the real display name and avatar URL — validate_token only gives us the login
|
|
71
97
|
profile = get_user_profile(active_token)
|
|
72
98
|
|
|
73
99
|
return {
|
|
74
100
|
"authenticated": True,
|
|
75
101
|
"username": profile["login"] or username,
|
|
76
|
-
"name": profile["name"] or username,
|
|
77
|
-
"avatar_url": profile["avatar_url"],
|
|
102
|
+
"name": profile["name"] or username,
|
|
103
|
+
"avatar_url": profile["avatar_url"],
|
|
104
|
+
"bio": profile.get("bio"),
|
|
105
|
+
"location": profile.get("location"),
|
|
106
|
+
"followers": profile.get("followers", 0),
|
|
107
|
+
"following": profile.get("following", 0),
|
|
78
108
|
}
|
|
79
109
|
|
|
80
110
|
|
|
111
|
+
@app.post("/api/username")
|
|
112
|
+
def get_username_data(username: str = Body("", embed=True)):
|
|
113
|
+
"""Fetches public profile and repos for any GitHub username — no token required."""
|
|
114
|
+
if not username or not username.strip():
|
|
115
|
+
raise HTTPException(status_code=400, detail="Username is required.")
|
|
116
|
+
try:
|
|
117
|
+
profile = get_public_user(username.strip())
|
|
118
|
+
repos = get_public_repos(username.strip())
|
|
119
|
+
return {"profile": profile, "repos": repos}
|
|
120
|
+
except ValueError as exc:
|
|
121
|
+
raise HTTPException(status_code=404, detail=str(exc))
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
logger.warning(f"Username fetch failed for {username}: {exc}")
|
|
124
|
+
raise HTTPException(status_code=500, detail="Failed to fetch GitHub data.")
|
|
125
|
+
|
|
126
|
+
|
|
81
127
|
@app.post("/api/traffic")
|
|
82
128
|
def get_traffic(token: str = Body("", embed=True)):
|
|
83
129
|
# Serve traffic data — either from the historical CSV database or live from GitHub
|
|
@@ -85,13 +131,11 @@ def get_traffic(token: str = Body("", embed=True)):
|
|
|
85
131
|
if not active_token:
|
|
86
132
|
raise HTTPException(status_code=401, detail="No token provided")
|
|
87
133
|
|
|
88
|
-
ok, _ =
|
|
134
|
+
ok, _ = _validate_token_cached(active_token)
|
|
89
135
|
if not ok:
|
|
90
136
|
raise HTTPException(status_code=401, detail="Invalid token")
|
|
91
|
-
|
|
92
137
|
data_dir = os.environ.get("GITLYTICS_DATA_DIR")
|
|
93
138
|
if data_dir:
|
|
94
|
-
# Load from the historical CSV database (headless/TV mode)
|
|
95
139
|
data_dir_path = Path(data_dir)
|
|
96
140
|
csv_files = list(data_dir_path.glob("traffic_*.csv")) if data_dir_path.exists() else []
|
|
97
141
|
dfs = []
|
|
@@ -102,43 +146,47 @@ def get_traffic(token: str = Body("", embed=True)):
|
|
|
102
146
|
logger.warning(f"Skipping unreadable CSV '{f}': {exc}")
|
|
103
147
|
if dfs:
|
|
104
148
|
df = pd.concat(dfs, ignore_index=True)
|
|
105
|
-
# Clean up any duplicate day-repo rows that crept in somehow
|
|
106
149
|
df = df.drop_duplicates(subset=["date", "repository"], keep="last")
|
|
107
150
|
else:
|
|
108
|
-
# No CSVs found — fall through to a live fetch
|
|
109
151
|
df = fetch_traffic_data(active_token)
|
|
110
152
|
else:
|
|
111
|
-
# Default: hit GitHub and get the live 14-day window
|
|
112
153
|
df = fetch_traffic_data(active_token)
|
|
113
154
|
|
|
114
|
-
# Replace any infinity or NaN values before JSON serialisation
|
|
115
155
|
df = df.replace([float('inf'), float('-inf')], None).where(pd.notnull(df), None)
|
|
116
156
|
|
|
117
|
-
#
|
|
118
|
-
|
|
157
|
+
# Build a quick view-sum map to find the top 20 repos for deep fetching
|
|
158
|
+
repos_with_views = []
|
|
159
|
+
if not df.empty and "repository" in df.columns and "views" in df.columns:
|
|
160
|
+
for repo_name, group in df.groupby("repository"):
|
|
161
|
+
repos_with_views.append({"repository": repo_name, "total_views": int(group["views"].sum())})
|
|
162
|
+
|
|
163
|
+
# Fetch deep stats concurrently for the top 20 most-viewed repos
|
|
164
|
+
deep_stats = {}
|
|
165
|
+
if repos_with_views and active_token:
|
|
166
|
+
deep_stats = fetch_deep_stats_for_top(active_token, repos_with_views, top_n=20)
|
|
167
|
+
|
|
168
|
+
payload = build_react_payload(df, deep_stats=deep_stats)
|
|
119
169
|
return payload
|
|
120
170
|
|
|
121
171
|
|
|
122
172
|
@app.post("/api/upload-csv")
|
|
123
173
|
def upload_csv(file: UploadFile = File(...)):
|
|
124
|
-
# Accept a user-uploaded CSV
|
|
174
|
+
# Accept a user-uploaded CSV — deep stats not available in CSV mode
|
|
125
175
|
try:
|
|
126
176
|
df = process_uploaded_csv(file.file)
|
|
127
177
|
df = df.replace([float('inf'), float('-inf')], None).where(pd.notnull(df), None)
|
|
128
|
-
payload = build_react_payload(df)
|
|
178
|
+
payload = build_react_payload(df, deep_stats=None)
|
|
129
179
|
return payload
|
|
130
180
|
except Exception as e:
|
|
131
181
|
raise HTTPException(status_code=400, detail=str(e))
|
|
132
182
|
|
|
133
183
|
|
|
134
|
-
#
|
|
135
|
-
# The React build output lands in gitlytics/static/ after `npm run build`
|
|
184
|
+
# Static file serving
|
|
136
185
|
frontend_dir = Path(__file__).parent / "static"
|
|
137
186
|
|
|
138
187
|
|
|
139
188
|
@app.get("/")
|
|
140
189
|
def serve_index():
|
|
141
|
-
# Serve the React app's index.html for the root URL
|
|
142
190
|
index_file = frontend_dir / "index.html"
|
|
143
191
|
if index_file.exists():
|
|
144
192
|
return FileResponse(index_file)
|
|
@@ -150,17 +198,11 @@ def serve_index():
|
|
|
150
198
|
|
|
151
199
|
@app.get("/{full_path:path}")
|
|
152
200
|
def serve_spa_fallback(full_path: str):
|
|
153
|
-
"""
|
|
154
|
-
SPA catch-all — any URL that doesn't match an API route returns index.html
|
|
155
|
-
so React Router can handle client-side navigation on hard refresh.
|
|
156
|
-
Real static assets (JS/CSS) are served by the StaticFiles mount first.
|
|
157
|
-
"""
|
|
158
|
-
# Serve the actual file if it exists (e.g. a JS or CSS asset)
|
|
201
|
+
"""SPA catch-all — returns index.html so React Router handles navigation."""
|
|
159
202
|
asset_file = frontend_dir / full_path
|
|
160
203
|
if asset_file.exists() and asset_file.is_file():
|
|
161
204
|
return FileResponse(asset_file)
|
|
162
205
|
|
|
163
|
-
# For everything else (like /repos/my-repo), hand control to React Router
|
|
164
206
|
index_file = frontend_dir / "index.html"
|
|
165
207
|
if index_file.exists():
|
|
166
208
|
return FileResponse(index_file)
|
|
@@ -171,7 +213,6 @@ def serve_spa_fallback(full_path: str):
|
|
|
171
213
|
)
|
|
172
214
|
|
|
173
215
|
|
|
174
|
-
# Mount the /assets directory for compiled JS and CSS — must come after route definitions
|
|
175
216
|
assets_dir = frontend_dir / "assets"
|
|
176
217
|
if assets_dir.exists():
|
|
177
218
|
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|