Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 6 additions & 24 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,18 @@ jobs:
# Sequence of tasks that will be executed as part of the job
steps:
# Checks-out repository under $GITHUB_WORKSPACE, so the job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v6

# Run using Python 3.8 for consistency and aiohttp
- name: Set up Python 3.12
uses: actions/setup-python@v5
# Set up UV to manage Python versions
- name: Install uv and set python version
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
python-version: '3.12'
architecture: 'x64'

# Cache dependencies. From:
# https://github.com/actions/cache/blob/master/examples.md#python---pip
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-


# Install dependencies with `pip`
- name: Install requirements
run: |
python3 -m pip install --upgrade pip setuptools wheel
python3 -m pip install -r requirements.txt

# Generate all statistics images
- name: Generate images
run: |
python3 --version
python3 generate_images.py
run: uv run generate_images.py
env:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ anyone may be able to see the name of one or more private repositories.
1. Create a personal access token (not the default GitHub Actions token) using
the instructions
[here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
Personal access token must have permissions: `read:user` and `repo`. Copy
Personal access token must have permissions: `repo:status`, `public_repo` and `read:user`. Copy
the access token when it is generated – if you lose it, you will have to
regenerate the token.

Expand All @@ -37,17 +37,17 @@ anyone may be able to see the name of one or more private repositories.
Otherwise, go to the "Settings" tab of the newly-created repository and go
to the "Secrets" page (bottom left).

![](https://raw.githubusercontent.com/rahul-jha98/github-stats-transparent/main/readme_images/Actions.png)
![](/readme_images/Actions.png)

4. Create a new secret with the name `ACCESS_TOKEN` and paste the copied
personal access token as the value.

<img src='https://raw.githubusercontent.com/rahul-jha98/github-stats-transparent/main/readme_images/Token.png' height='250px'/>
<img src='/readme_images/Token.png' height='250px'/>

5. If you want to ignore certain repos, add them (separated by commas) to a new
secret—created as before—called `EXCLUDED`.

<img src='https://raw.githubusercontent.com/rahul-jha98/github-stats-transparent/main/readme_images/Exclude.png' height='250px'/>
<img src='/readme_images/Exclude.png' height='250px'/>

6. If you want to ignore certain languages, add them (separated by commas) to a new secret called
`EXCLUDED_LANGS`.
Expand All @@ -57,7 +57,7 @@ anyone may be able to see the name of one or more private repositories.
forked repositories also you can do so by creating a new secret called `COUNT_STATS_FROM_FORKS`.
For the value you can put any random value because the action only checks if the secret is set or not.

<img src='https://raw.githubusercontent.com/rahul-jha98/github-stats-transparent/main/readme_images/Forks.png' height='250px'/>
<img src='/readme_images/Forks.png' height='250px'/>

8. Go to the [Actions Page](../../actions?query=workflow%3A"Generate+Stats+Images") and press "Run
Workflow" on the right side of the screen to generate images for the first
Expand Down
67 changes: 33 additions & 34 deletions github_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

import aiohttp
import requests
import uvloop

asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # use uvloop

###############################################################################
# Main Classes
Expand Down Expand Up @@ -37,17 +39,19 @@ async def query(self, generated_query: str) -> Dict:
}
try:
async with self.semaphore:
r = await self.session.post("https://api.github.com/graphql",
headers=headers,
json={"query": generated_query})
r = await self.session.post(
"https://api.github.com/graphql",
headers=headers,
json={"query": generated_query}
)
return await r.json()
except:
print("aiohttp failed for GraphQL query")
# Fall back on non-async requests
async with self.semaphore:
r = requests.post("https://api.github.com/graphql",
headers=headers,
json={"query": generated_query})
r = requests.post(
"https://api.github.com/graphql",
headers=headers,
json={"query": generated_query}
)
return r.json()

async def query_rest(self, path: str, params: Optional[Dict] = None) -> Dict:
Expand All @@ -58,21 +62,21 @@ async def query_rest(self, path: str, params: Optional[Dict] = None) -> Dict:
:return: deserialized REST JSON output
"""

for _ in range(60):
headers = {
"Authorization": f"token {self.access_token}",
headers = {
"Authorization": f"token {self.access_token}",
}
if params is None:
params = dict()
if path.startswith("/"):
path = path[1:]
if params is None:
params = dict()
if path.startswith("/"):
path = path[1:]

for _ in range(60):
try:
async with self.semaphore:
r = await self.session.get(f"https://api.github.com/{path}",
headers=headers,
params=tuple(params.items()))
if r.status == 202:
# print(f"{path} returned 202. Retrying...")
print(f"A path returned 202. Retrying...")
await asyncio.sleep(2)
continue
Expand Down Expand Up @@ -466,24 +470,23 @@ async def lines_changed(self) -> Tuple[int, int]:
"""
if self._lines_changed is not None:
return self._lines_changed
additions = 0
deletions = 0
for repo in await self.all_repos:

async def fetch(repo):
additions, deletions = 0, 0
r = await self.queries.query_rest(f"/repos/{repo}/stats/contributors")
for author_obj in r:
# Handle malformed response from the API by skipping this repo
if (not isinstance(author_obj, dict)
or not isinstance(author_obj.get("author", {}), dict)):
if not isinstance(author_obj, dict) or not isinstance(author_obj.get("author", {}), dict):
continue
author = author_obj.get("author", {}).get("login", "")
if author != self.username:
if author_obj.get("author", {}).get("login", "") != self.username:
continue

for week in author_obj.get("weeks", []):
additions += week.get("a", 0)
deletions += week.get("d", 0)
self._lines_changed = (additions, deletions)
return additions, deletions

self._lines_changed = (additions, deletions)
results = await asyncio.gather(*[fetch(repo) for repo in await self.all_repos])
self._lines_changed = (sum(r[0] for r in results), sum(r[1] for r in results))
return self._lines_changed

@property
Expand All @@ -495,15 +498,12 @@ async def views(self) -> int:
if self._views is not None:
return self._views

total = 0
for repo in await self.repos:
async def fetch(repo):
r = await self.queries.query_rest(f"/repos/{repo}/traffic/views")
for view in r.get("views", []):
total += view.get("count", 0)

self._views = total
return total
return sum(view.get("count", 0) for view in r.get("views", []))

self._views = sum(await asyncio.gather(*[fetch(repo) for repo in await self.repos]))
return self._views

###############################################################################
# Main Function
Expand All @@ -519,6 +519,5 @@ async def main() -> None:
s = Stats(user, access_token, session)
print(await s.to_str())


if __name__ == "__main__":
asyncio.run(main())
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[project]
name = "github-stats-transparent"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"aiohttp>=3.13.2",
"requests>=2.32.5",
"uvloop>=0.22.1",
]
2 changes: 0 additions & 2 deletions requirements.txt

This file was deleted.

Loading