gitlytics 0.1.4__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.
Files changed (48) hide show
  1. {gitlytics-0.1.4 → gitlytics-0.1.6}/LICENSE +19 -5
  2. {gitlytics-0.1.4 → gitlytics-0.1.6}/PKG-INFO +44 -22
  3. {gitlytics-0.1.4 → gitlytics-0.1.6}/README.md +43 -20
  4. {gitlytics-0.1.4 → gitlytics-0.1.6}/pyproject.toml +1 -2
  5. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics/__init__.py +28 -13
  6. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics/api.py +72 -31
  7. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics/automation.py +14 -11
  8. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics/cli.py +7 -3
  9. gitlytics-0.1.6/src/gitlytics/core.py +491 -0
  10. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics/process.py +77 -49
  11. gitlytics-0.1.6/src/gitlytics/static/android-chrome-192x192.png +0 -0
  12. gitlytics-0.1.6/src/gitlytics/static/android-chrome-512x512.png +0 -0
  13. gitlytics-0.1.6/src/gitlytics/static/apple-touch-icon.png +0 -0
  14. gitlytics-0.1.6/src/gitlytics/static/assets/html2canvas-pro.esm-C9_j7xg5.js +10 -0
  15. gitlytics-0.1.6/src/gitlytics/static/assets/html2canvas.esm-QH1iLAAe.js +22 -0
  16. gitlytics-0.1.6/src/gitlytics/static/assets/index-D6vJCUrl.js +504 -0
  17. gitlytics-0.1.6/src/gitlytics/static/assets/index-hl2LPOqz.css +1 -0
  18. gitlytics-0.1.6/src/gitlytics/static/assets/index.es-BM2mGRzK.js +18 -0
  19. gitlytics-0.1.6/src/gitlytics/static/assets/jspdf.es.min-cihQsb1K.js +170 -0
  20. gitlytics-0.1.6/src/gitlytics/static/assets/purify.es-Csrj9YNg.js +3 -0
  21. gitlytics-0.1.6/src/gitlytics/static/favicon-16x16.png +0 -0
  22. gitlytics-0.1.6/src/gitlytics/static/favicon-32x32.png +0 -0
  23. gitlytics-0.1.6/src/gitlytics/static/favicon.ico +0 -0
  24. gitlytics-0.1.6/src/gitlytics/static/gitlytics-logo.png +0 -0
  25. gitlytics-0.1.6/src/gitlytics/static/index.html +19 -0
  26. gitlytics-0.1.6/src/gitlytics/static/octocat.png +0 -0
  27. gitlytics-0.1.6/src/gitlytics/static/robots.txt +5 -0
  28. gitlytics-0.1.6/src/gitlytics/static/sitemap.xml +13 -0
  29. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics.egg-info/PKG-INFO +44 -22
  30. gitlytics-0.1.6/src/gitlytics.egg-info/SOURCES.txt +40 -0
  31. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics.egg-info/requires.txt +0 -1
  32. {gitlytics-0.1.4 → gitlytics-0.1.6}/tests/test_api.py +6 -3
  33. {gitlytics-0.1.4 → gitlytics-0.1.6}/tests/test_core.py +1 -1
  34. {gitlytics-0.1.4 → gitlytics-0.1.6}/tests/test_process.py +3 -0
  35. gitlytics-0.1.6/tests/test_username.py +335 -0
  36. gitlytics-0.1.4/src/gitlytics/core.py +0 -288
  37. gitlytics-0.1.4/src/gitlytics/static/assets/index-CRiPPhX0.js +0 -44
  38. gitlytics-0.1.4/src/gitlytics/static/assets/index-i_G_CLdY.css +0 -2
  39. gitlytics-0.1.4/src/gitlytics/static/favicon.svg +0 -4
  40. gitlytics-0.1.4/src/gitlytics/static/index.html +0 -14
  41. gitlytics-0.1.4/src/gitlytics.egg-info/SOURCES.txt +0 -25
  42. {gitlytics-0.1.4 → gitlytics-0.1.6}/setup.cfg +0 -0
  43. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics/__main__.py +0 -0
  44. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics.egg-info/dependency_links.txt +0 -0
  45. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics.egg-info/entry_points.txt +0 -0
  46. {gitlytics-0.1.4 → gitlytics-0.1.6}/src/gitlytics.egg-info/top_level.txt +0 -0
  47. {gitlytics-0.1.4 → gitlytics-0.1.6}/tests/test_automation.py +0 -0
  48. {gitlytics-0.1.4 → 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
- 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.
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.4
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,13 +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
- # 📊 Gitlytics
35
+ <img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/logo.png" alt="Gitlytics Logo" width="150" />
36
+
37
+ # Gitlytics
37
38
  ### GitHub Traffic Analytics & Automation
38
39
 
39
40
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
@@ -46,6 +47,8 @@ Dynamic: license-file
46
47
  [![Live](https://img.shields.io/badge/Live%20Demo-dashboard.gitlytics.dev-success)](https://dashboard.gitlytics.dev)
47
48
  [![Docs](https://img.shields.io/badge/Docs-docs.gitlytics.dev-success)](https://docs.gitlytics.dev)
48
49
 
50
+ <br/>Please consider giving this project a ⭐ if you find it helpful! <br/>
51
+
49
52
  **Beautiful GitHub traffic analytics for all your repositories — public and private.** <br/> Track views, clones, referrers, and popular paths indefinitely.
50
53
 
51
54
  ✨ **[Try the live dashboard at dashboard.gitlytics.dev](https://dashboard.gitlytics.dev)** ✨
@@ -55,22 +58,16 @@ Dynamic: license-file
55
58
 
56
59
  > **🐍 Native Python API**
57
60
  >
58
- > You can import Gitlytics natively into your own Python applications to build custom integrations, run custom cron workflows, or serve the dashboard programmatically on your own cloud servers.
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.
59
62
  >
60
63
  > 📚 **[Read the Full API Documentation](https://docs.gitlytics.dev)**
61
64
 
62
- Please consider giving this project a ⭐ if you find it helpful!
63
-
64
65
  </div>
65
66
 
66
67
  ---
67
68
 
68
69
  <div align="center">
69
- <img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_1.png" width="49%" />
70
- <img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_2.png" width="49%" />
71
- </div>
72
- <div align="center">
73
- <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%" />
74
71
  </div>
75
72
 
76
73
  ---
@@ -99,7 +96,7 @@ Please consider giving this project a ⭐ if you find it helpful!
99
96
 
100
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:
101
98
 
102
- - 🌐 **[Gitlytics Web Ecosystem](https://github.com/ameyac11/gitlytics-deployement)**: The production homepage, React Dashboard, and VitePress documentation site.
99
+ - **[Gitlytics Web Ecosystem](https://github.com/ameyac11/gitlytics-deployement)**: The production landing page, React Dashboard, and React Documentation site.
103
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.
104
101
 
105
102
  ---
@@ -145,6 +142,9 @@ Gitlytics is powered by 3 massive command-line tools. You can run them anywhere
145
142
  Fetch your live 14-day traffic and print a beautiful ASCII table directly in your console.
146
143
  ```bash
147
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
148
148
  ```
149
149
 
150
150
  ### 2️⃣ `gitlytics sync` (Background Database Cron)
@@ -153,6 +153,9 @@ Tired of losing data? Use `sync` to permanently append today's traffic to a CSV
153
153
  # Sync once
154
154
  gitlytics sync --token ghp_your_token --data-dir ./data
155
155
 
156
+ # Sync specific metrics only
157
+ gitlytics sync --token ghp_your_token --data-dir ./data --metrics views clones
158
+
156
159
  # Run permanently in the background as a cron job (runs at 11:00 PM every day)
157
160
  gitlytics sync --token ghp_your_token --data-dir ./data --schedule-cron "0 23 * * *"
158
161
  ```
@@ -210,6 +213,7 @@ gitlytics.fetch_traffic(
210
213
  | `print_table` | `bool` | `False` | If `True`, formats and prints a detailed ASCII traffic table to the console. |
211
214
  | `return_format` | `str` | `"dataframe"` | The format of returned data: `"dataframe"` (Pandas DataFrame), `"timeseries"` (chart-ready nested dict), or `"summary"` (per-repo totals dict). |
212
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"]`). |
213
217
 
214
218
  ---
215
219
 
@@ -246,6 +250,7 @@ gitlytics.sync(
246
250
  | `schedule_cron` | `str` | `None` | Optional cron expression (e.g., `"*/15 * * * *"`). If set, runs an infinite scheduler loop. |
247
251
  | `export_json` | `str` | `None` | Optional. Path to compile and export a consolidated history JSON for the frontend. |
248
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"]`). |
249
254
 
250
255
  ---
251
256
 
@@ -277,16 +282,33 @@ gitlytics.serve_dashboard(
277
282
 
278
283
  ## 📊 CSV Output Columns
279
284
 
280
- When you sync data, the local CSV databases track 13 detailed metrics:
281
-
282
- | Column | Description | Column | Description |
283
- |---|---|---|---|
284
- | `repository` | Full repo name (`user/repo`) | `stars` | Current star count |
285
- | `is_private` | `True` / `False` | `forks` | Current fork count |
286
- | `views` | Page views today | `unique_visitors` | Unique visitors today |
287
- | `clones` | Clone count today | `unique_cloners` | Unique cloners today |
288
- | `top_referrer` | Highest-traffic referral source | `top_referrer_views` | Views from top referrer |
289
- | `top_path` | Most visited path | `top_path_views` | Views for top path |
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. |
290
312
 
291
313
  ---
292
314
 
@@ -1,6 +1,8 @@
1
1
  <div align="center">
2
2
 
3
- # 📊 Gitlytics
3
+ <img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/logo.png" alt="Gitlytics Logo" width="150" />
4
+
5
+ # Gitlytics
4
6
  ### GitHub Traffic Analytics & Automation
5
7
 
6
8
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
@@ -13,6 +15,8 @@
13
15
  [![Live](https://img.shields.io/badge/Live%20Demo-dashboard.gitlytics.dev-success)](https://dashboard.gitlytics.dev)
14
16
  [![Docs](https://img.shields.io/badge/Docs-docs.gitlytics.dev-success)](https://docs.gitlytics.dev)
15
17
 
18
+ <br/>Please consider giving this project a ⭐ if you find it helpful! <br/>
19
+
16
20
  **Beautiful GitHub traffic analytics for all your repositories — public and private.** <br/> Track views, clones, referrers, and popular paths indefinitely.
17
21
 
18
22
  ✨ **[Try the live dashboard at dashboard.gitlytics.dev](https://dashboard.gitlytics.dev)** ✨
@@ -22,22 +26,16 @@
22
26
 
23
27
  > **🐍 Native Python API**
24
28
  >
25
- > You can import Gitlytics natively into your own Python applications to build custom integrations, run custom cron workflows, or serve the dashboard programmatically on your own cloud servers.
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.
26
30
  >
27
31
  > 📚 **[Read the Full API Documentation](https://docs.gitlytics.dev)**
28
32
 
29
- Please consider giving this project a ⭐ if you find it helpful!
30
-
31
33
  </div>
32
34
 
33
35
  ---
34
36
 
35
37
  <div align="center">
36
- <img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_1.png" width="49%" />
37
- <img src="https://raw.githubusercontent.com/ameyac11/gitlytics/main/assets/gitlytics_thumbnail_2.png" width="49%" />
38
- </div>
39
- <div align="center">
40
- <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%" />
41
39
  </div>
42
40
 
43
41
  ---
@@ -66,7 +64,7 @@ Please consider giving this project a ⭐ if you find it helpful!
66
64
 
67
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:
68
66
 
69
- - 🌐 **[Gitlytics Web Ecosystem](https://github.com/ameyac11/gitlytics-deployement)**: The production homepage, React Dashboard, and VitePress documentation site.
67
+ - **[Gitlytics Web Ecosystem](https://github.com/ameyac11/gitlytics-deployement)**: The production landing page, React Dashboard, and React Documentation site.
70
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.
71
69
 
72
70
  ---
@@ -112,6 +110,9 @@ Gitlytics is powered by 3 massive command-line tools. You can run them anywhere
112
110
  Fetch your live 14-day traffic and print a beautiful ASCII table directly in your console.
113
111
  ```bash
114
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
115
116
  ```
116
117
 
117
118
  ### 2️⃣ `gitlytics sync` (Background Database Cron)
@@ -120,6 +121,9 @@ Tired of losing data? Use `sync` to permanently append today's traffic to a CSV
120
121
  # Sync once
121
122
  gitlytics sync --token ghp_your_token --data-dir ./data
122
123
 
124
+ # Sync specific metrics only
125
+ gitlytics sync --token ghp_your_token --data-dir ./data --metrics views clones
126
+
123
127
  # Run permanently in the background as a cron job (runs at 11:00 PM every day)
124
128
  gitlytics sync --token ghp_your_token --data-dir ./data --schedule-cron "0 23 * * *"
125
129
  ```
@@ -177,6 +181,7 @@ gitlytics.fetch_traffic(
177
181
  | `print_table` | `bool` | `False` | If `True`, formats and prints a detailed ASCII traffic table to the console. |
178
182
  | `return_format` | `str` | `"dataframe"` | The format of returned data: `"dataframe"` (Pandas DataFrame), `"timeseries"` (chart-ready nested dict), or `"summary"` (per-repo totals dict). |
179
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"]`). |
180
185
 
181
186
  ---
182
187
 
@@ -213,6 +218,7 @@ gitlytics.sync(
213
218
  | `schedule_cron` | `str` | `None` | Optional cron expression (e.g., `"*/15 * * * *"`). If set, runs an infinite scheduler loop. |
214
219
  | `export_json` | `str` | `None` | Optional. Path to compile and export a consolidated history JSON for the frontend. |
215
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"]`). |
216
222
 
217
223
  ---
218
224
 
@@ -244,16 +250,33 @@ gitlytics.serve_dashboard(
244
250
 
245
251
  ## 📊 CSV Output Columns
246
252
 
247
- When you sync data, the local CSV databases track 13 detailed metrics:
248
-
249
- | Column | Description | Column | Description |
250
- |---|---|---|---|
251
- | `repository` | Full repo name (`user/repo`) | `stars` | Current star count |
252
- | `is_private` | `True` / `False` | `forks` | Current fork count |
253
- | `views` | Page views today | `unique_visitors` | Unique visitors today |
254
- | `clones` | Clone count today | `unique_cloners` | Unique cloners today |
255
- | `top_referrer` | Highest-traffic referral source | `top_referrer_views` | Views from top referrer |
256
- | `top_path` | Most visited path | `top_path_views` | Views for top path |
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. |
257
280
 
258
281
  ---
259
282
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitlytics"
7
- version = "0.1.4"
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.4"
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
- # Pass the token and data folder to the FastAPI app via environment variables
134
- if token:
135
- os.environ["GITLYTICS_TOKEN"] = token
136
- if data_dir:
137
- os.environ["GITLYTICS_DATA_DIR"] = os.path.abspath(data_dir)
138
-
139
- # Start the web server — it won't return until the user presses Ctrl+C
140
- uvicorn.run("gitlytics.api:app", host=host, port=port, reload=False)
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 validate_token, get_user_profile, fetch_traffic_data
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 — this dashboard is never deployed publicly
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
- # Use the token from the request body, or fall back to the one set in the environment
45
- return token or os.environ.get("GITLYTICS_TOKEN")
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, # Real display name, e.g. "Ameya Chopade"
77
- "avatar_url": profile["avatar_url"], # Real GitHub 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, _ = validate_token(active_token)
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
- # Transform the DataFrame into the array of objects the React app expects
118
- payload = build_react_payload(df)
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 and convert it to the same format as the API response
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
- # ── Static file serving ───────────────────────────────────────────────────────
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")