gitlytics 0.1.2__tar.gz → 0.1.5__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.2/src/gitlytics.egg-info → gitlytics-0.1.5}/PKG-INFO +47 -20
- gitlytics-0.1.2/PKG-INFO → gitlytics-0.1.5/README.md +31 -37
- gitlytics-0.1.5/pyproject.toml +75 -0
- gitlytics-0.1.5/src/gitlytics/__init__.py +140 -0
- gitlytics-0.1.5/src/gitlytics/__main__.py +9 -0
- gitlytics-0.1.5/src/gitlytics/api.py +177 -0
- gitlytics-0.1.5/src/gitlytics/automation.py +217 -0
- {gitlytics-0.1.2 → gitlytics-0.1.5}/src/gitlytics/cli.py +35 -7
- gitlytics-0.1.5/src/gitlytics/core.py +288 -0
- gitlytics-0.1.5/src/gitlytics/process.py +237 -0
- gitlytics-0.1.5/src/gitlytics/static/assets/index-CJlQrbYd.js +44 -0
- gitlytics-0.1.5/src/gitlytics/static/assets/index-Dpkz0yGK.css +2 -0
- {gitlytics-0.1.2 → gitlytics-0.1.5}/src/gitlytics/static/index.html +2 -2
- gitlytics-0.1.5/src/gitlytics/static/logo.png +0 -0
- gitlytics-0.1.2/README.md → gitlytics-0.1.5/src/gitlytics.egg-info/PKG-INFO +64 -10
- {gitlytics-0.1.2 → gitlytics-0.1.5}/src/gitlytics.egg-info/SOURCES.txt +7 -5
- gitlytics-0.1.5/src/gitlytics.egg-info/requires.txt +16 -0
- gitlytics-0.1.5/tests/test_api.py +264 -0
- gitlytics-0.1.5/tests/test_automation.py +178 -0
- gitlytics-0.1.5/tests/test_cli.py +130 -0
- gitlytics-0.1.5/tests/test_core.py +226 -0
- gitlytics-0.1.5/tests/test_process.py +321 -0
- gitlytics-0.1.2/pyproject.toml +0 -52
- gitlytics-0.1.2/src/gitlytics/__init__.py +0 -73
- gitlytics-0.1.2/src/gitlytics/api.py +0 -112
- gitlytics-0.1.2/src/gitlytics/automation.py +0 -153
- gitlytics-0.1.2/src/gitlytics/core.py +0 -188
- gitlytics-0.1.2/src/gitlytics/process.py +0 -117
- gitlytics-0.1.2/src/gitlytics/static/assets/index-BPCIBQz4.js +0 -44
- gitlytics-0.1.2/src/gitlytics/static/assets/index-DytQw1pB.css +0 -2
- gitlytics-0.1.2/src/gitlytics/static/favicon.svg +0 -4
- gitlytics-0.1.2/src/gitlytics/static/icons.svg +0 -24
- gitlytics-0.1.2/src/gitlytics.egg-info/requires.txt +0 -13
- gitlytics-0.1.2/tests/test_automation.py +0 -72
- gitlytics-0.1.2/tests/test_cli.py +0 -56
- gitlytics-0.1.2/tests/test_core.py +0 -55
- {gitlytics-0.1.2 → gitlytics-0.1.5}/LICENSE +0 -0
- {gitlytics-0.1.2 → gitlytics-0.1.5}/setup.cfg +0 -0
- {gitlytics-0.1.2 → gitlytics-0.1.5}/src/gitlytics.egg-info/dependency_links.txt +0 -0
- {gitlytics-0.1.2 → gitlytics-0.1.5}/src/gitlytics.egg-info/entry_points.txt +0 -0
- {gitlytics-0.1.2 → gitlytics-0.1.5}/src/gitlytics.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlytics
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Monitor and automate your GitHub repository traffic analytics.
|
|
5
|
-
Author: Ameya Chopade
|
|
5
|
+
Author-email: Ameya Chopade <ameyaccod171@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://gitlytics.dev
|
|
8
|
+
Project-URL: Documentation, https://docs.gitlytics.dev
|
|
9
|
+
Project-URL: Repository, https://github.com/ameyac11/gitlytics
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/ameyac11/gitlytics/issues
|
|
8
11
|
Keywords: github,traffic,analytics,automation,cli
|
|
9
12
|
Classifier: Programming Language :: Python :: 3
|
|
10
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -12,22 +15,27 @@ Classifier: Operating System :: OS Independent
|
|
|
12
15
|
Requires-Python: >=3.9
|
|
13
16
|
Description-Content-Type: text/markdown
|
|
14
17
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: requests
|
|
16
|
-
Requires-Dist: pandas
|
|
17
|
-
Requires-Dist: python-dotenv
|
|
18
|
-
Requires-Dist: croniter
|
|
18
|
+
Requires-Dist: requests>=2.32.0
|
|
19
|
+
Requires-Dist: pandas>=2.2.0
|
|
20
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
21
|
+
Requires-Dist: croniter>=2.0.0
|
|
19
22
|
Provides-Extra: dashboard
|
|
20
|
-
Requires-Dist: fastapi; extra == "dashboard"
|
|
21
|
-
Requires-Dist: uvicorn; extra == "dashboard"
|
|
22
|
-
Requires-Dist: python-multipart; extra == "dashboard"
|
|
23
|
+
Requires-Dist: fastapi>=0.111.0; extra == "dashboard"
|
|
24
|
+
Requires-Dist: uvicorn>=0.30.0; extra == "dashboard"
|
|
25
|
+
Requires-Dist: python-multipart>=0.0.9; extra == "dashboard"
|
|
23
26
|
Provides-Extra: dev
|
|
24
27
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
25
28
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
30
|
+
Requires-Dist: Faker>=20.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: anyio[trio]>=4.0.0; extra == "dev"
|
|
26
32
|
Dynamic: license-file
|
|
27
33
|
|
|
28
34
|
<div align="center">
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
<img src="./assets/logo.png" alt="Gitlytics Logo" width="150" />
|
|
37
|
+
|
|
38
|
+
# <span style="color: #F05032">Git</span>lytics
|
|
31
39
|
### GitHub Traffic Analytics & Automation
|
|
32
40
|
|
|
33
41
|
[](LICENSE)
|
|
@@ -36,17 +44,26 @@ Dynamic: license-file
|
|
|
36
44
|
[](https://react.dev/)
|
|
37
45
|
[](https://fastapi.tiangolo.com/)
|
|
38
46
|
[](https://github.com/ameyac11/gitlytics-github-traffic-automation)
|
|
47
|
+
[](https://gitlytics.dev)
|
|
48
|
+
[](https://dashboard.gitlytics.dev)
|
|
49
|
+
[](https://docs.gitlytics.dev)
|
|
39
50
|
|
|
40
51
|
**Beautiful GitHub traffic analytics for all your repositories — public and private.** <br/> Track views, clones, referrers, and popular paths indefinitely.
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
✨ **[Try the live dashboard at dashboard.gitlytics.dev](https://dashboard.gitlytics.dev)** ✨
|
|
54
|
+
📚 **[Read the documentation at docs.gitlytics.dev](https://docs.gitlytics.dev)**
|
|
43
55
|
|
|
44
|
-
|
|
56
|
+
<br/>
|
|
45
57
|
|
|
46
|
-
|
|
58
|
+
> **🐍 Native Python API**
|
|
59
|
+
>
|
|
60
|
+
> 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
|
+
>
|
|
62
|
+
> 📚 **[Read the Full API Documentation](https://docs.gitlytics.dev)**
|
|
63
|
+
|
|
64
|
+
Please consider giving this project a ⭐ if you find it helpful!
|
|
47
65
|
|
|
48
|
-
>
|
|
49
|
-
> Formerly `github-traffic-monitor`, we have officially rebranded to **`gitlytics`**! We completely migrated away from Streamlit. The dashboard is now a **React + Vite** SPA, powered by a **FastAPI** backend!
|
|
66
|
+
</div>
|
|
50
67
|
|
|
51
68
|
---
|
|
52
69
|
|
|
@@ -62,7 +79,8 @@ Please consider giving this project a ⭐ if you find it helpful!
|
|
|
62
79
|
|
|
63
80
|
## 📌 Table of Contents
|
|
64
81
|
|
|
65
|
-
- [
|
|
82
|
+
- [🔗 The Gitlytics Ecosystem](#-the-gitlytics-ecosystem)
|
|
83
|
+
- [🚨 The 14-Day Catch (And How We Fix It)](#-the-14-day-catch-and-how-we-fix-it)
|
|
66
84
|
- [🛠️ Installation](#installation)
|
|
67
85
|
- [🔑 Generating a GitHub Personal Access Token](#generating-a-github-personal-access-token)
|
|
68
86
|
- [⌨️ The 3 Core CLI Commands](#the-3-core-cli-commands)
|
|
@@ -79,6 +97,15 @@ Please consider giving this project a ⭐ if you find it helpful!
|
|
|
79
97
|
|
|
80
98
|
---
|
|
81
99
|
|
|
100
|
+
## 🔗 The Gitlytics Ecosystem
|
|
101
|
+
|
|
102
|
+
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
|
+
|
|
104
|
+
- **[<span style="color: #F05032">Git</span>lytics Web Ecosystem](https://github.com/ameyac11/gitlytics-deployement)**: The production homepage, React Dashboard, and VitePress documentation site.
|
|
105
|
+
- ⚙️ **[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
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
82
109
|
## 🚨 The 14-Day Catch (And How We Fix It)
|
|
83
110
|
|
|
84
111
|
> **⚠️ Did you know?** GitHub normally **only saves your repository traffic data for 14 days**. After two weeks, your valuable views and clones data is permanently deleted.
|
|
@@ -148,7 +175,7 @@ gitlytics dashboard
|
|
|
148
175
|
|
|
149
176
|
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.
|
|
150
177
|
|
|
151
|
-
📚 **[Read the Full API Documentation](docs
|
|
178
|
+
📚 **[Read the Full API Documentation](https://docs.gitlytics.dev)**
|
|
152
179
|
|
|
153
180
|
### 1️⃣ `gitlytics.fetch_traffic()`
|
|
154
181
|
Fetches the last 14 days of traffic data (views, clones, referrers, paths) for one or more repositories.
|
|
@@ -159,7 +186,7 @@ import gitlytics
|
|
|
159
186
|
# Fetch traffic for all repositories accessible by the token
|
|
160
187
|
df = gitlytics.fetch_traffic(
|
|
161
188
|
token="ghp_your_token",
|
|
162
|
-
return_format="dataframe" # Options: "dataframe" (Pandas), "timeseries" (
|
|
189
|
+
return_format="dataframe" # Options: "dataframe" (Pandas), "timeseries" (chart-ready dict), or "summary" (per-repo totals dict)
|
|
163
190
|
)
|
|
164
191
|
|
|
165
192
|
# Fetch traffic for a single specific repository and print the table to stdout
|
|
@@ -183,7 +210,7 @@ gitlytics.fetch_traffic(
|
|
|
183
210
|
| `token` | `str` | *Required* | GitHub Personal Access Token with `repo` scope enabled. |
|
|
184
211
|
| `repo_name` | `str` | `None` | Specific repository name (e.g. `"user/repo"`). If `None`, fetches all repositories. |
|
|
185
212
|
| `print_table` | `bool` | `False` | If `True`, formats and prints a detailed ASCII traffic table to the console. |
|
|
186
|
-
| `return_format` | `str` | `"dataframe"` | The format of returned data: `"dataframe"` (Pandas DataFrame), `"timeseries"
|
|
213
|
+
| `return_format` | `str` | `"dataframe"` | The format of returned data: `"dataframe"` (Pandas DataFrame), `"timeseries"` (chart-ready nested dict), or `"summary"` (per-repo totals dict). |
|
|
187
214
|
| `save_file` | `str` | `None` | Optional. File path where the fetched data will be saved (CSV or JSON). |
|
|
188
215
|
|
|
189
216
|
---
|
|
@@ -1,33 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: gitlytics
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: Monitor and automate your GitHub repository traffic analytics.
|
|
5
|
-
Author: Ameya Chopade
|
|
6
|
-
License: Apache-2.0
|
|
7
|
-
Project-URL: Homepage, https://github.com/ameyac11/gitlytics
|
|
8
|
-
Keywords: github,traffic,analytics,automation,cli
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.9
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
Requires-Dist: requests
|
|
16
|
-
Requires-Dist: pandas
|
|
17
|
-
Requires-Dist: python-dotenv
|
|
18
|
-
Requires-Dist: croniter
|
|
19
|
-
Provides-Extra: dashboard
|
|
20
|
-
Requires-Dist: fastapi; extra == "dashboard"
|
|
21
|
-
Requires-Dist: uvicorn; extra == "dashboard"
|
|
22
|
-
Requires-Dist: python-multipart; extra == "dashboard"
|
|
23
|
-
Provides-Extra: dev
|
|
24
|
-
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
25
|
-
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
26
|
-
Dynamic: license-file
|
|
27
|
-
|
|
28
1
|
<div align="center">
|
|
29
2
|
|
|
30
|
-
|
|
3
|
+
<img src="./assets/logo.png" alt="Gitlytics Logo" width="150" />
|
|
4
|
+
|
|
5
|
+
# <span style="color: #F05032">Git</span>lytics
|
|
31
6
|
### GitHub Traffic Analytics & Automation
|
|
32
7
|
|
|
33
8
|
[](LICENSE)
|
|
@@ -36,17 +11,26 @@ Dynamic: license-file
|
|
|
36
11
|
[](https://react.dev/)
|
|
37
12
|
[](https://fastapi.tiangolo.com/)
|
|
38
13
|
[](https://github.com/ameyac11/gitlytics-github-traffic-automation)
|
|
14
|
+
[](https://gitlytics.dev)
|
|
15
|
+
[](https://dashboard.gitlytics.dev)
|
|
16
|
+
[](https://docs.gitlytics.dev)
|
|
39
17
|
|
|
40
18
|
**Beautiful GitHub traffic analytics for all your repositories — public and private.** <br/> Track views, clones, referrers, and popular paths indefinitely.
|
|
41
19
|
|
|
42
|
-
|
|
20
|
+
✨ **[Try the live dashboard at dashboard.gitlytics.dev](https://dashboard.gitlytics.dev)** ✨
|
|
21
|
+
📚 **[Read the documentation at docs.gitlytics.dev](https://docs.gitlytics.dev)**
|
|
43
22
|
|
|
44
|
-
|
|
23
|
+
<br/>
|
|
45
24
|
|
|
46
|
-
|
|
25
|
+
> **🐍 Native Python API**
|
|
26
|
+
>
|
|
27
|
+
> 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.
|
|
28
|
+
>
|
|
29
|
+
> 📚 **[Read the Full API Documentation](https://docs.gitlytics.dev)**
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
|
|
31
|
+
Please consider giving this project a ⭐ if you find it helpful!
|
|
32
|
+
|
|
33
|
+
</div>
|
|
50
34
|
|
|
51
35
|
---
|
|
52
36
|
|
|
@@ -62,7 +46,8 @@ Please consider giving this project a ⭐ if you find it helpful!
|
|
|
62
46
|
|
|
63
47
|
## 📌 Table of Contents
|
|
64
48
|
|
|
65
|
-
- [
|
|
49
|
+
- [🔗 The Gitlytics Ecosystem](#-the-gitlytics-ecosystem)
|
|
50
|
+
- [🚨 The 14-Day Catch (And How We Fix It)](#-the-14-day-catch-and-how-we-fix-it)
|
|
66
51
|
- [🛠️ Installation](#installation)
|
|
67
52
|
- [🔑 Generating a GitHub Personal Access Token](#generating-a-github-personal-access-token)
|
|
68
53
|
- [⌨️ The 3 Core CLI Commands](#the-3-core-cli-commands)
|
|
@@ -79,6 +64,15 @@ Please consider giving this project a ⭐ if you find it helpful!
|
|
|
79
64
|
|
|
80
65
|
---
|
|
81
66
|
|
|
67
|
+
## 🔗 The Gitlytics Ecosystem
|
|
68
|
+
|
|
69
|
+
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
|
+
|
|
71
|
+
- **[<span style="color: #F05032">Git</span>lytics Web Ecosystem](https://github.com/ameyac11/gitlytics-deployement)**: The production homepage, React Dashboard, and VitePress documentation site.
|
|
72
|
+
- ⚙️ **[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
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
82
76
|
## 🚨 The 14-Day Catch (And How We Fix It)
|
|
83
77
|
|
|
84
78
|
> **⚠️ Did you know?** GitHub normally **only saves your repository traffic data for 14 days**. After two weeks, your valuable views and clones data is permanently deleted.
|
|
@@ -148,7 +142,7 @@ gitlytics dashboard
|
|
|
148
142
|
|
|
149
143
|
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.
|
|
150
144
|
|
|
151
|
-
📚 **[Read the Full API Documentation](docs
|
|
145
|
+
📚 **[Read the Full API Documentation](https://docs.gitlytics.dev)**
|
|
152
146
|
|
|
153
147
|
### 1️⃣ `gitlytics.fetch_traffic()`
|
|
154
148
|
Fetches the last 14 days of traffic data (views, clones, referrers, paths) for one or more repositories.
|
|
@@ -159,7 +153,7 @@ import gitlytics
|
|
|
159
153
|
# Fetch traffic for all repositories accessible by the token
|
|
160
154
|
df = gitlytics.fetch_traffic(
|
|
161
155
|
token="ghp_your_token",
|
|
162
|
-
return_format="dataframe" # Options: "dataframe" (Pandas), "timeseries" (
|
|
156
|
+
return_format="dataframe" # Options: "dataframe" (Pandas), "timeseries" (chart-ready dict), or "summary" (per-repo totals dict)
|
|
163
157
|
)
|
|
164
158
|
|
|
165
159
|
# Fetch traffic for a single specific repository and print the table to stdout
|
|
@@ -183,7 +177,7 @@ gitlytics.fetch_traffic(
|
|
|
183
177
|
| `token` | `str` | *Required* | GitHub Personal Access Token with `repo` scope enabled. |
|
|
184
178
|
| `repo_name` | `str` | `None` | Specific repository name (e.g. `"user/repo"`). If `None`, fetches all repositories. |
|
|
185
179
|
| `print_table` | `bool` | `False` | If `True`, formats and prints a detailed ASCII traffic table to the console. |
|
|
186
|
-
| `return_format` | `str` | `"dataframe"` | The format of returned data: `"dataframe"` (Pandas DataFrame), `"timeseries"
|
|
180
|
+
| `return_format` | `str` | `"dataframe"` | The format of returned data: `"dataframe"` (Pandas DataFrame), `"timeseries"` (chart-ready nested dict), or `"summary"` (per-repo totals dict). |
|
|
187
181
|
| `save_file` | `str` | `None` | Optional. File path where the fetched data will be saved (CSV or JSON). |
|
|
188
182
|
|
|
189
183
|
---
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gitlytics"
|
|
7
|
+
version = "0.1.5"
|
|
8
|
+
description = "Monitor and automate your GitHub repository traffic analytics."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Ameya Chopade", email = "ameyaccod171@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
# Fix #20: Add version lower-bounds to prevent silent breakage on old environments.
|
|
16
|
+
# These match the constraints already documented in requirements.txt.
|
|
17
|
+
dependencies = [
|
|
18
|
+
"requests>=2.32.0",
|
|
19
|
+
"pandas>=2.2.0",
|
|
20
|
+
"python-dotenv>=1.0.1",
|
|
21
|
+
"croniter>=2.0.0"
|
|
22
|
+
]
|
|
23
|
+
keywords = [
|
|
24
|
+
"github",
|
|
25
|
+
"traffic",
|
|
26
|
+
"analytics",
|
|
27
|
+
"automation",
|
|
28
|
+
"cli"
|
|
29
|
+
]
|
|
30
|
+
classifiers = [
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"License :: OSI Approved :: Apache Software License",
|
|
33
|
+
"Operating System :: OS Independent",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://gitlytics.dev"
|
|
38
|
+
Documentation = "https://docs.gitlytics.dev"
|
|
39
|
+
Repository = "https://github.com/ameyac11/gitlytics"
|
|
40
|
+
"Bug Tracker" = "https://github.com/ameyac11/gitlytics/issues"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
# Fix #20: dashboard extras are optional — base package works without them.
|
|
44
|
+
# requirements.txt was including them as mandatory; that was wrong.
|
|
45
|
+
dashboard = [
|
|
46
|
+
"fastapi>=0.111.0",
|
|
47
|
+
"uvicorn>=0.30.0",
|
|
48
|
+
"python-multipart>=0.0.9"
|
|
49
|
+
]
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=8.0.0",
|
|
52
|
+
"pytest-cov>=5.0.0",
|
|
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
|
+
"anyio[trio]>=4.0.0" # async test support
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
[project.scripts]
|
|
60
|
+
gitlytics = "gitlytics.cli:main"
|
|
61
|
+
|
|
62
|
+
[tool.setuptools.packages.find]
|
|
63
|
+
where = ["src"]
|
|
64
|
+
include = ["gitlytics*"]
|
|
65
|
+
|
|
66
|
+
[tool.setuptools.package-data]
|
|
67
|
+
"gitlytics" = ["static/**/*", "static/*"]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
# Only collect files that match the offline test naming convention.
|
|
71
|
+
# live_*.py scripts are excluded — they need a real GitHub token.
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
python_files = ["test_*.py"]
|
|
74
|
+
python_classes = ["Test*"]
|
|
75
|
+
python_functions = ["test_*"]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gitlytics/__init__.py
|
|
3
|
+
The public API for the gitlytics package.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
# Single source of truth for the package version.
|
|
10
|
+
# Mirrors the version in pyproject.toml — keep them in sync.
|
|
11
|
+
__version__ = "0.1.5"
|
|
12
|
+
|
|
13
|
+
# Import the internal building blocks — users never call these directly
|
|
14
|
+
from .core import fetch_traffic_data, print_repo_table
|
|
15
|
+
from .automation import run_sync
|
|
16
|
+
from .process import build_json_payload
|
|
17
|
+
|
|
18
|
+
# Set up a silent logger so gitlytics never messes with your app's logging
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
logger.addHandler(logging.NullHandler())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fetch_traffic(token: str, repo_name=None, print_table: bool = False, return_format: str = "dataframe", save_file: str = None):
|
|
24
|
+
"""
|
|
25
|
+
Fetches the last 14 days of traffic data for one or all repositories.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
token: GitHub Personal Access Token with `repo` scope.
|
|
29
|
+
repo_name: Specific repo name (e.g. ``"user/repo"``) or list of names.
|
|
30
|
+
If ``None``, fetches all repositories accessible by the token.
|
|
31
|
+
print_table: If ``True``, prints an ASCII summary table to stdout.
|
|
32
|
+
return_format: Shape of the returned data. One of:
|
|
33
|
+
``"dataframe"`` (default) — returns a ``pandas.DataFrame``.
|
|
34
|
+
``"timeseries"`` — returns a nested JSON-serialisable dict.
|
|
35
|
+
``"summary"`` — returns a per-repo totals dict.
|
|
36
|
+
save_file: Optional path to save the output. Extension determines
|
|
37
|
+
format: ``.json`` writes JSON, anything else writes CSV.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A ``pandas.DataFrame`` when ``return_format="dataframe"``, otherwise
|
|
41
|
+
a ``dict`` matching the requested format.
|
|
42
|
+
"""
|
|
43
|
+
# Hit the GitHub API and get back a tidy DataFrame (one row per day per repo)
|
|
44
|
+
df = fetch_traffic_data(token, repo_name)
|
|
45
|
+
|
|
46
|
+
# Print the ASCII table to the console if the user asked for it
|
|
47
|
+
if print_table:
|
|
48
|
+
print_repo_table(df)
|
|
49
|
+
|
|
50
|
+
# --- dataframe mode: just return the raw DataFrame, optionally save it ---
|
|
51
|
+
if return_format == "dataframe":
|
|
52
|
+
if save_file:
|
|
53
|
+
if save_file.endswith(".json"):
|
|
54
|
+
# Save as a chart-ready JSON file
|
|
55
|
+
payload = build_json_payload(df, return_format="timeseries", export_public_only=True)
|
|
56
|
+
with open(save_file, "w", encoding="utf-8") as f:
|
|
57
|
+
json.dump(payload, f, indent=2)
|
|
58
|
+
else:
|
|
59
|
+
# Save as a standard CSV file
|
|
60
|
+
df.to_csv(save_file, index=False)
|
|
61
|
+
return df
|
|
62
|
+
|
|
63
|
+
# Reject anything that isn't a known format before doing more work
|
|
64
|
+
valid_formats = {"timeseries", "summary"}
|
|
65
|
+
if return_format not in valid_formats:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Invalid return_format={return_format!r}. "
|
|
68
|
+
f"Choose one of: 'dataframe', 'timeseries', 'summary'."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Build the JSON-serialisable payload in the requested shape
|
|
72
|
+
payload = build_json_payload(df, return_format=return_format, export_public_only=False)
|
|
73
|
+
|
|
74
|
+
# Save to disk if the user gave us a file path
|
|
75
|
+
if save_file:
|
|
76
|
+
with open(save_file, "w", encoding="utf-8") as f:
|
|
77
|
+
json.dump(payload, f, indent=2)
|
|
78
|
+
|
|
79
|
+
return payload
|
|
80
|
+
|
|
81
|
+
|
|
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):
|
|
83
|
+
"""
|
|
84
|
+
Fetches data and appends it to a local CSV database, optionally running as a permanent background daemon.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
token: GitHub Personal Access Token.
|
|
88
|
+
repo_name: Specific repository name(s) to sync. If ``None``, syncs all.
|
|
89
|
+
data_dir: Directory where CSV files are stored.
|
|
90
|
+
output_mode: ``"monthly"`` (``traffic_YYYY-MM.csv``) or ``"yearly"`` (``traffic_YYYY.csv``).
|
|
91
|
+
schedule_cron: Standard cron expression (e.g. ``"0 23 * * *"``). If set,
|
|
92
|
+
runs an infinite scheduler loop.
|
|
93
|
+
export_json: Path to export the merged historical database as a JSON file.
|
|
94
|
+
export_public_only: If ``True`` (default), strips private repos from the
|
|
95
|
+
exported JSON — acts as a security firewall.
|
|
96
|
+
"""
|
|
97
|
+
# Hand off to the automation engine — it handles deduplication and schema migration
|
|
98
|
+
run_sync(
|
|
99
|
+
token=token,
|
|
100
|
+
repo_names=repo_name,
|
|
101
|
+
data_dir=data_dir,
|
|
102
|
+
output_mode=output_mode,
|
|
103
|
+
schedule_cron=schedule_cron,
|
|
104
|
+
export_json=export_json,
|
|
105
|
+
export_public_only=export_public_only
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def serve_dashboard(host: str = "127.0.0.1", port: int = 8000, token: str = None, data_dir: str = None):
|
|
110
|
+
"""
|
|
111
|
+
Starts the React + FastAPI dashboard server.
|
|
112
|
+
|
|
113
|
+
``uvicorn`` and ``fastapi`` are optional dependencies installed via::
|
|
114
|
+
|
|
115
|
+
pip install "gitlytics[dashboard]"
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
host: Host IP to bind. Use ``"0.0.0.0"`` to listen on all interfaces.
|
|
119
|
+
port: Port number (default ``8000``).
|
|
120
|
+
token: Optional GitHub token — pre-authenticates the dashboard session.
|
|
121
|
+
data_dir: Optional path to the historical CSV database directory.
|
|
122
|
+
"""
|
|
123
|
+
# Only import uvicorn when the user actually calls serve_dashboard,
|
|
124
|
+
# so the base `pip install gitlytics` doesn't crash without it
|
|
125
|
+
try:
|
|
126
|
+
import uvicorn
|
|
127
|
+
except ImportError:
|
|
128
|
+
raise ImportError(
|
|
129
|
+
"The dashboard requires additional dependencies. "
|
|
130
|
+
"Install them with: pip install \"gitlytics[dashboard]\""
|
|
131
|
+
)
|
|
132
|
+
|
|
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)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gitlytics/__main__.py
|
|
3
|
+
Makes `python -m gitlytics` work identically to the `gitlytics` console command.
|
|
4
|
+
This is the entry point Python calls when the package is run with -m.
|
|
5
|
+
"""
|
|
6
|
+
from gitlytics.cli import main
|
|
7
|
+
|
|
8
|
+
# Run the CLI when invoked as `python -m gitlytics`
|
|
9
|
+
main()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gitlytics/api.py
|
|
3
|
+
Powers the FastAPI backend — serves traffic data and the React dashboard to the browser.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from fastapi import FastAPI, HTTPException, Body, File, UploadFile
|
|
11
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
12
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
13
|
+
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
|
|
15
|
+
from gitlytics.core import validate_token, get_user_profile, fetch_traffic_data
|
|
16
|
+
from gitlytics.process import process_uploaded_csv, build_react_payload
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
app = FastAPI(title="GitHub Traffic API")
|
|
21
|
+
|
|
22
|
+
# Only allow requests from localhost — this dashboard is never deployed publicly
|
|
23
|
+
_ALLOWED_ORIGINS = [
|
|
24
|
+
"http://localhost",
|
|
25
|
+
"http://localhost:3000",
|
|
26
|
+
"http://localhost:5173",
|
|
27
|
+
"http://localhost:8000",
|
|
28
|
+
"http://127.0.0.1",
|
|
29
|
+
"http://127.0.0.1:3000",
|
|
30
|
+
"http://127.0.0.1:5173",
|
|
31
|
+
"http://127.0.0.1:8000",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
app.add_middleware(
|
|
35
|
+
CORSMiddleware,
|
|
36
|
+
allow_origins=_ALLOWED_ORIGINS,
|
|
37
|
+
allow_credentials=True,
|
|
38
|
+
allow_methods=["GET", "POST"],
|
|
39
|
+
allow_headers=["Content-Type"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
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")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.get("/api/config")
|
|
49
|
+
def get_config():
|
|
50
|
+
# Lets the frontend know if it's running in headless/TV mode with a pre-set token
|
|
51
|
+
return {
|
|
52
|
+
"has_token": bool(os.environ.get("GITLYTICS_TOKEN")),
|
|
53
|
+
"has_data_dir": bool(os.environ.get("GITLYTICS_DATA_DIR"))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.post("/api/auth")
|
|
58
|
+
def auth(token: str = Body("", embed=True)):
|
|
59
|
+
# Validate the token and return the user's GitHub profile info
|
|
60
|
+
active_token = _get_token(token)
|
|
61
|
+
if not active_token:
|
|
62
|
+
raise HTTPException(status_code=401, detail="No token provided and no environment token found.")
|
|
63
|
+
|
|
64
|
+
ok, username = validate_token(active_token)
|
|
65
|
+
if not ok:
|
|
66
|
+
# Log a warning without echoing the token value into logs
|
|
67
|
+
logger.warning("Authentication attempt failed for a provided token.")
|
|
68
|
+
raise HTTPException(status_code=401, detail=username)
|
|
69
|
+
|
|
70
|
+
# Fetch the real display name and avatar URL — validate_token only gives us the login
|
|
71
|
+
profile = get_user_profile(active_token)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"authenticated": True,
|
|
75
|
+
"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
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.post("/api/traffic")
|
|
82
|
+
def get_traffic(token: str = Body("", embed=True)):
|
|
83
|
+
# Serve traffic data — either from the historical CSV database or live from GitHub
|
|
84
|
+
active_token = _get_token(token)
|
|
85
|
+
if not active_token:
|
|
86
|
+
raise HTTPException(status_code=401, detail="No token provided")
|
|
87
|
+
|
|
88
|
+
ok, _ = validate_token(active_token)
|
|
89
|
+
if not ok:
|
|
90
|
+
raise HTTPException(status_code=401, detail="Invalid token")
|
|
91
|
+
|
|
92
|
+
data_dir = os.environ.get("GITLYTICS_DATA_DIR")
|
|
93
|
+
if data_dir:
|
|
94
|
+
# Load from the historical CSV database (headless/TV mode)
|
|
95
|
+
data_dir_path = Path(data_dir)
|
|
96
|
+
csv_files = list(data_dir_path.glob("traffic_*.csv")) if data_dir_path.exists() else []
|
|
97
|
+
dfs = []
|
|
98
|
+
for f in csv_files:
|
|
99
|
+
try:
|
|
100
|
+
dfs.append(pd.read_csv(f))
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
logger.warning(f"Skipping unreadable CSV '{f}': {exc}")
|
|
103
|
+
if dfs:
|
|
104
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
105
|
+
# Clean up any duplicate day-repo rows that crept in somehow
|
|
106
|
+
df = df.drop_duplicates(subset=["date", "repository"], keep="last")
|
|
107
|
+
else:
|
|
108
|
+
# No CSVs found — fall through to a live fetch
|
|
109
|
+
df = fetch_traffic_data(active_token)
|
|
110
|
+
else:
|
|
111
|
+
# Default: hit GitHub and get the live 14-day window
|
|
112
|
+
df = fetch_traffic_data(active_token)
|
|
113
|
+
|
|
114
|
+
# Replace any infinity or NaN values before JSON serialisation
|
|
115
|
+
df = df.replace([float('inf'), float('-inf')], None).where(pd.notnull(df), None)
|
|
116
|
+
|
|
117
|
+
# Transform the DataFrame into the array of objects the React app expects
|
|
118
|
+
payload = build_react_payload(df)
|
|
119
|
+
return payload
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.post("/api/upload-csv")
|
|
123
|
+
def upload_csv(file: UploadFile = File(...)):
|
|
124
|
+
# Accept a user-uploaded CSV and convert it to the same format as the API response
|
|
125
|
+
try:
|
|
126
|
+
df = process_uploaded_csv(file.file)
|
|
127
|
+
df = df.replace([float('inf'), float('-inf')], None).where(pd.notnull(df), None)
|
|
128
|
+
payload = build_react_payload(df)
|
|
129
|
+
return payload
|
|
130
|
+
except Exception as e:
|
|
131
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── Static file serving ───────────────────────────────────────────────────────
|
|
135
|
+
# The React build output lands in gitlytics/static/ after `npm run build`
|
|
136
|
+
frontend_dir = Path(__file__).parent / "static"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.get("/")
|
|
140
|
+
def serve_index():
|
|
141
|
+
# Serve the React app's index.html for the root URL
|
|
142
|
+
index_file = frontend_dir / "index.html"
|
|
143
|
+
if index_file.exists():
|
|
144
|
+
return FileResponse(index_file)
|
|
145
|
+
return JSONResponse(
|
|
146
|
+
status_code=503,
|
|
147
|
+
content={"error": "Dashboard not found. Run 'npm run build' in the dashboard directory."}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@app.get("/{full_path:path}")
|
|
152
|
+
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)
|
|
159
|
+
asset_file = frontend_dir / full_path
|
|
160
|
+
if asset_file.exists() and asset_file.is_file():
|
|
161
|
+
return FileResponse(asset_file)
|
|
162
|
+
|
|
163
|
+
# For everything else (like /repos/my-repo), hand control to React Router
|
|
164
|
+
index_file = frontend_dir / "index.html"
|
|
165
|
+
if index_file.exists():
|
|
166
|
+
return FileResponse(index_file)
|
|
167
|
+
|
|
168
|
+
return JSONResponse(
|
|
169
|
+
status_code=503,
|
|
170
|
+
content={"error": "Dashboard not found. Run 'npm run build' in the dashboard directory."}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Mount the /assets directory for compiled JS and CSS — must come after route definitions
|
|
175
|
+
assets_dir = frontend_dir / "assets"
|
|
176
|
+
if assets_dir.exists():
|
|
177
|
+
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|