Skip to content

Commit

Permalink
add github-stats example (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
masenf authored Oct 5, 2023
1 parent 88f95e9 commit db832e0
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 0 deletions.
4 changes: 4 additions & 0 deletions github-stats/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.db
*.py[cod]
.web
__pycache__/
Binary file added github-stats/assets/favicon.ico
Binary file not shown.
Empty file.
93 changes: 93 additions & 0 deletions github-stats/github_stats/fetchers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import json
import os

import httpx


# GraphQL queries (borrowed from https://github.com/anuraghazra/github-readme-stats/blob/master/src/fetchers/stats-fetcher.js).
GRAPHQL_STATS_QUERY = """
query userInfo($login: String!) {
user(login: $login) {
name
login
contributionsCollection {
totalCommitContributions,
totalPullRequestReviewContributions
}
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
pullRequests(first: 1) {
totalCount
}
mergedPullRequests: pullRequests(states: MERGED) {
totalCount
}
openIssues: issues(states: OPEN) {
totalCount
}
closedIssues: issues(states: CLOSED) {
totalCount
}
followers {
totalCount
}
repositoryDiscussions {
totalCount
}
repositoryDiscussionComments(onlyAnswers: true) {
totalCount
}
}
}
"""

# Set your GitHub personal access token (Replace with your actual token)
GITHUB_API_TOKEN = os.environ.get("GITHUB_API_TOKEN")

# Set the GitHub GraphQL API URL
GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"


async def user_stats(username: str):
if GITHUB_API_TOKEN is None:
raise RuntimeError("Set GITHUB_API_TOKEN in the environment")

# Create a dictionary for the request headers with the Authorization header
headers = {
"Authorization": f"Bearer {GITHUB_API_TOKEN}",
"Content-Type": "application/json",
}

# Create a dictionary for the request payload
payload = {
"query": GRAPHQL_STATS_QUERY,
"variables": {
"login": username,
},
}

async with httpx.AsyncClient() as client:
response = await client.post(GITHUB_GRAPHQL_URL, headers=headers, json=payload)

response.raise_for_status()

print(f"Fetched stats for {username}")

if response.status_code == 200:
data = response.json()
user_data = data["data"]["user"]
if user_data is None:
print(f"User {username} not found")
return None
for key, value in user_data.items():
if isinstance(value, dict) and "totalCount" in value:
user_data[key] = value["totalCount"]
return user_data


# Run the asynchronous function
if __name__ == "__main__":
import asyncio

asyncio.run(user_stats("masenf"))
172 changes: 172 additions & 0 deletions github-stats/github_stats/github_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import json

import reflex as rx

from .fetchers import user_stats


TEAM = [
"masenf",
"Lendemor",
"picklelo",
"martinxu9",
"ElijahAhianyo",
"Alek99",
"tgberkeley",
"jackie-pc",
]


class State(rx.State):
selected_users: list[str]
user_stats: list[dict] = []
fetching: bool = False
selected_users_json: str = rx.LocalStorage()
user_stats_json: str = rx.LocalStorage()

def on_load(self):
if self.selected_users_json:
self.selected_users = json.loads(self.selected_users_json)
if self.user_stats_json:
self.user_stats = json.loads(self.user_stats_json)
return State.fetch_missing_stats

def _save_selected_users(self):
self.selected_users_json = json.dumps(self.get_value(self.selected_users))

def _save_user_stats(self):
self.user_stats_json = json.dumps(self.get_value(self.user_stats))

def _selected_users_lower(self):
return [u.lower() for u in self.selected_users]

def _already_fetched_users(self):
return set(u["login"].lower() for u in self.user_stats)

def _remove_data_for_deselected_users(self):
selected_users_lower = self._selected_users_lower()
remove_data = []
for user_data in self.user_stats:
if user_data["login"].lower() not in selected_users_lower:
remove_data.append(user_data)
for user_data in remove_data:
self.user_stats.remove(user_data)

@rx.background
async def fetch_missing_stats(self):
async with self:
if self.fetching:
return
self._remove_data_for_deselected_users()
already_fetched_users = self._already_fetched_users()
self.fetching = True
try:
for user in self._selected_users_lower():
if user in already_fetched_users:
continue
user_data = await user_stats(user)
if user_data is None:
continue
async with self:
if user_data["login"].lower() in set(self._selected_users_lower()):
self.user_stats.append(user_data)
finally:
async with self:
self._save_user_stats()
self.fetching = False
async with self:
# check if any users were added while fetching
currently_fetched_users = self._already_fetched_users()
if any(
user.lower() not in currently_fetched_users
for user in self.selected_users
):
return State.fetch_missing_stats

def remove_user(self, user: str):
self.selected_users.remove(user)
self._save_selected_users()
self._remove_data_for_deselected_users()

def add_user(self, user: str):
self.selected_users.append(user)
self._save_selected_users()
return State.fetch_missing_stats

def handle_form(self, form_data: dict):
username = form_data.get("username")
if username and username not in self.selected_users:
yield State.add_user(username)
yield rx.set_value("username", "")

@rx.cached_var
def data_pretty(self) -> str:
return json.dumps(self.user_stats, indent=2)


def index() -> rx.Component:
return rx.fragment(
rx.color_mode_button(rx.color_mode_icon(), float="right"),
rx.vstack(
rx.heading("Github Stats", font_size="2em"),
rx.flex(
rx.foreach(
State.selected_users,
lambda user: rx.box(
user,
rx.button(
"X",
size="xs",
on_click=State.remove_user(user),
margin_left="5px",
),
border="1px solid black",
margin="10px",
padding="5px",
),
),
direction="row",
wrap="wrap",
),
rx.cond(
State.fetching,
rx.vstack(
rx.text("Fetching Data..."),
rx.progress(is_indeterminate=True, width="50vw"),
),
),
rx.form(
rx.hstack(
rx.input(placeholder="Github Username", id="username"),
rx.button("Get Stats", type_="submit"),
),
on_submit=State.handle_form,
),
rx.box(
rx.bar_chart(
rx.graphing_tooltip(cursor=False),
rx.bar(
data_key="repositoriesContributedTo",
stroke="#8884d8",
fill="#8884d8",
),
rx.bar(data_key="mergedPullRequests", stroke="#82ca9d", fill="#82ca9d"),
rx.bar(data_key="openIssues", stroke="#ffc658", fill="#ffc658"),
rx.bar(data_key="closedIssues", stroke="#ff0000", fill="#ff0000"),
rx.x_axis(data_key="login"),
rx.y_axis(),
data=State.user_stats,
),
width="100%",
height="15em",
),
rx.text_area(
value=State.data_pretty,
),
),
)


app = rx.App()
app.add_page(index, on_load=State.on_load)
app.compile()
1 change: 1 addition & 0 deletions github-stats/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
reflex>=0.3.0a1
5 changes: 5 additions & 0 deletions github-stats/rxconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import reflex as rx

config = rx.Config(
app_name="github_stats",
)

0 comments on commit db832e0

Please sign in to comment.