diff --git a/.gitignore b/.gitignore
index 110bbdd..c93f595 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,9 @@ __pycache__/
.vscode
received_events/
*.html
+src/.github_token
+.codacy/
+
+
+#Ignore vscode AI rules
+.github/instructions/codacy.instructions.md
diff --git a/requirements.txt b/requirements.txt
index 7fcb1ad..625748f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
colorama==0.4.6
PyQt5==5.15.11
PyQt5_sip==12.15.0
-Requests==2.32.3
+Requests==2.33.0
webview==0.1.5
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..6ecb098
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[pydocstyle]
+add-select = D203,D213
+ignore = D211,D212
\ No newline at end of file
diff --git a/src/core/utils.py b/src/core/utils.py
index 89f0a5b..208786d 100644
--- a/src/core/utils.py
+++ b/src/core/utils.py
@@ -1,11 +1,15 @@
+"""Shared helpers for fetching GitHub data and rendering the HTML report."""
+import base64
import os
import platform
import requests
from colorama import Fore, Style
-from requests.exceptions import HTTPError
+from requests.exceptions import HTTPError, RequestException
-class colors:
+class colors: # pylint: disable=invalid-name,too-few-public-methods # noqa: D203,D211
+ """ANSI color constants used for terminal output."""
+
HEADER = Fore.MAGENTA
BLUE = Fore.BLUE
CYAN = Fore.CYAN
@@ -18,14 +22,43 @@ class colors:
def clear():
- os.system("cls" if platform.system() == "Windows" else "clear")
+ """Clear the terminal screen without spawning a shell."""
+ if platform.system() == "Windows":
+ print("\033[2J\033[H", end="")
+ else:
+ print("\033c", end="")
+
+
+def get_auth_headers():
+ """Return the Authorization header dict if a GitHub token is set."""
+ token = os.environ.get("GITHUB_TOKEN", "").strip()
+ if token:
+ return {"Authorization": f"Bearer {token}"}
+ return {}
+
+
+def fetch_as_data_uri(url): # noqa: D212,D213
+ """Fetch a remote image and return it as a base64 data URI.
+
+ Embeds the image directly in the HTML file (needed when the page is served
+ via file:// and WebKit would otherwise block external requests).
+ """
+ try:
+ resp = requests.get(url, timeout=15)
+ resp.raise_for_status()
+ content_type = resp.headers.get("Content-Type", "image/svg+xml").split(";")[0].strip()
+ encoded = base64.b64encode(resp.content).decode("ascii")
+ return f"data:{content_type};base64,{encoded}"
+ except (RequestException, ValueError, TypeError):
+ return url # fall back to original URL on any failure
def fetch_and_print_data(username):
+ """Fetch the GitHub user profile and print each field to the terminal."""
print(f"Fetching data for user: {colors.FAIL}{username}{colors.ENDC}")
url = f"https://api.github.com/users/{username}"
try:
- response = requests.get(url, timeout=10)
+ response = requests.get(url, headers=get_auth_headers(), timeout=10)
response.raise_for_status()
data = response.json()
print(f"\n{colors.WARNING}User Data:{colors.ENDC}")
@@ -35,17 +68,20 @@ def fetch_and_print_data(username):
except HTTPError as http_err:
print(f"{colors.FAIL}HTTP error occurred: {http_err}{colors.ENDC}")
- except Exception as err:
+ except (RequestException, ValueError) as err:
print(f"{colors.FAIL}Unexpected error: {err}{colors.ENDC}")
-def show_events_and_graphs(urls):
+def show_events_and_graphs():
+ """Print a confirmation that graphs are available in the report."""
print(
f"\nGraphs available in Received Events [{colors.GREEN}✓{colors.ENDC}]"
)
-def generate_html_event_row(avatar, login, event_type, repo_name, repo_url, badge_class, action_text):
+def generate_html_event_row( # pylint: disable=too-many-arguments,too-many-positional-arguments
+ avatar, login, repo_name, repo_url, badge_class, action_text):
+ """Return the HTML markup for a single received-event row."""
return f"""

@@ -57,46 +93,7 @@ def generate_html_event_row(avatar, login, event_type, repo_name, repo_url, badg
"""
-def create_and_display_html_user_events(username, urls):
- events_url = f"https://api.github.com/users/{username}/received_events"
- html_path = ".temp/index.html"
-
- print(f"Generating HTML report... [{colors.GREEN}✓{colors.ENDC}]\n")
-
- url = f"https://api.github.com/users/{username}"
- try:
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- user_data = response.json()
-
- login = user_data.get("login", "")
- name = user_data.get("name", "")
- location = user_data.get("location", "")
- html_url = user_data.get("html_url", "")
- avatar_url = user_data.get("avatar_url", "")
- bio = user_data.get("bio", "")
- followers = user_data.get("followers", 0)
- following = user_data.get("following", 0)
-
- except HTTPError as http_err:
- print(f"{colors.FAIL}HTTP error occurred: {http_err}{colors.ENDC}")
- return
- except Exception as err:
- print(f"{colors.FAIL}Unexpected error: {err}{colors.ENDC}")
- return
-
- os.makedirs(os.path.dirname(html_path), exist_ok=True)
- if os.path.exists(html_path):
- os.remove(html_path)
-
- try:
- response = requests.get(events_url, timeout=10)
- response.raise_for_status()
- events = response.json()
-
- with open(html_path, "w", encoding="utf_8") as f:
- f.write(f"""
-
+HTML_REPORT_TEMPLATE = """
@@ -207,71 +204,147 @@ def create_and_display_html_user_events(username, urls):
Username: {login}
About: {bio}
Followers: {followers}, Following: {following}
- Location: {location}
+ Location: {location}
View Profile
-
Received Events
""")
-
- for event in events:
- event_type = event.get("type")
- actor = event.get("actor", {})
- payload = event.get("payload", {})
- repo = event.get("repo", {})
- login = actor.get("login", "")
- avatar = actor.get("avatar_url", "")
- repo_name = repo.get("name", "")
- repo_url = f"https://github.com/{repo_name}"
-
- badge_class = ""
- action_text = ""
-
- if event_type == "ForkEvent":
- badge_class = "text-danger"
- action_text = "Forked a repository"
- repo_url = payload.get("forkee", {}).get("html_url", "#")
-
- elif event_type == "WatchEvent":
- badge_class = "text-warning"
- action_text = "Watch/Starred a repository"
-
- elif event_type == "CreateEvent":
- badge_class = "text-success"
- action_text = "Created a repository"
-
- elif event_type == "PublicEvent":
- badge_class = "text-primary"
- action_text = "Published a repository"
-
- elif event_type == "ReleaseEvent":
- badge_class = "text-primary"
- action_text = "Released a repository"
- repo_url = payload.get("release", {}).get("html_url", "#")
-
- if action_text: # Only write if action_text is set
- f.write(generate_html_event_row(
- avatar, login, event_type, repo_name,
- repo_url, badge_class, action_text))
-
- f.write(f"""
+ Received Events
+{events_html}
+
-""")
+"""
+
+
+EVENT_ACTIONS = {
+ "WatchEvent": ("text-warning", "Watch/Starred a repository", None),
+ "CreateEvent": ("text-success", "Created a repository", None),
+ "PublicEvent": ("text-primary", "Published a repository", None),
+ "ForkEvent": ("text-danger", "Forked a repository", "forkee"),
+ "ReleaseEvent": ("text-primary", "Released a repository", "release"),
+}
+
+
+def _resolve_event_action(event_type, payload, repo_url):
+ """Return badge/action text and final URL for a GitHub event."""
+ action = EVENT_ACTIONS.get(event_type)
+ if not action:
+ return "", "", repo_url
+
+ badge_class, action_text, payload_key = action
+ if payload_key:
+ repo_url = payload.get(payload_key, {}).get("html_url", "#")
+
+ return badge_class, action_text, repo_url
+
+
+def _build_events_html(events):
+ """Return HTML rows for supported received event types."""
+ rows = []
+ for event in events:
+ event_type = event.get("type")
+ actor = event.get("actor", {})
+ payload = event.get("payload", {})
+ repo = event.get("repo", {})
+ login = actor.get("login", "")
+ avatar = actor.get("avatar_url", "")
+ repo_name = repo.get("name", "")
+ repo_url = f"https://github.com/{repo_name}"
+ badge_class, action_text, repo_url = _resolve_event_action(event_type, payload, repo_url)
+ if not action_text:
+ continue
+ rows.append(generate_html_event_row(
+ avatar,
+ login,
+ repo_name,
+ repo_url,
+ badge_class,
+ action_text,
+ ))
+ return "".join(rows)
+
+
+def _build_report_html(user_data, events_html, urls):
+ """Render the final HTML report document as a single string."""
+ return HTML_REPORT_TEMPLATE.format(
+ login=user_data.get("login", ""),
+ name=user_data.get("name", ""),
+ location=user_data.get("location", ""),
+ html_url=user_data.get("html_url", ""),
+ avatar_url=user_data.get("avatar_url", ""),
+ bio=user_data.get("bio", ""),
+ followers=user_data.get("followers", 0),
+ following=user_data.get("following", 0),
+ events_html=events_html,
+ langs_src=fetch_as_data_uri(urls["mostUsedLanguages"]),
+ stats_src=fetch_as_data_uri(urls["githubStats"]),
+ streak_src=fetch_as_data_uri(urls["streakContributionsLS"]),
+ )
+
+
+def _fetch_json(url):
+ """Fetch JSON from a URL and return parsed data, or None on failure."""
+ try:
+ response = requests.get(url, headers=get_auth_headers(), timeout=10)
+ response.raise_for_status()
+ return response.json()
except HTTPError as http_err:
print(f"{colors.FAIL}HTTP error occurred: {http_err}{colors.ENDC}")
- except Exception as err:
+ except (RequestException, ValueError) as err:
+ print(f"{colors.FAIL}Unexpected error: {err}{colors.ENDC}")
+ return None
+
+
+def _prepare_output_path(html_path):
+ """Create output directory and remove any existing report file."""
+ os.makedirs(os.path.dirname(html_path), exist_ok=True)
+ if os.path.exists(html_path):
+ os.remove(html_path)
+
+
+def _write_report_file(html_path, report_html):
+ """Write report HTML to disk, returning True on success."""
+ try:
+ with open(html_path, "w", encoding="utf_8") as file_obj:
+ file_obj.write(report_html)
+ return True
+ except OSError as err:
print(f"{colors.FAIL}Unexpected error: {err}{colors.ENDC}")
+ return False
+
+
+def create_and_display_html_user_events(username, urls):
+ """Build the user's HTML report from profile data and received events."""
+ user_url = f"https://api.github.com/users/{username}"
+ events_url = f"{user_url}/received_events"
+ html_path = ".temp/index.html"
+
+ print(f"Generating HTML report... [{colors.GREEN}✓{colors.ENDC}]\n")
+
+ user_data = _fetch_json(user_url)
+ if user_data is None:
+ return
+
+ events = _fetch_json(events_url)
+ if events is None:
+ return
+
+ _prepare_output_path(html_path)
+
+ events_html = _build_events_html(events)
+ report_html = _build_report_html(user_data, events_html, urls)
+ _write_report_file(html_path, report_html)
diff --git a/src/htmlviewers/linux.py b/src/htmlviewers/linux.py
index 47bd51d..9fd676c 100644
--- a/src/htmlviewers/linux.py
+++ b/src/htmlviewers/linux.py
@@ -1,13 +1,19 @@
-import webview # type: ignore
+"""Linux HTML viewer backed by pywebview/WebKit."""
import os
import sys
+import webview # type: ignore
+
+# Suppress dconf "no database" warnings from GTK/WebKit
+os.environ.setdefault("DCONF_PROFILE", "/dev/null")
+# Suppress MESA ZINK (Vulkan-backed OpenGL) errors — fall back to software renderer
+os.environ.setdefault("GALLIUM_DRIVER", "llvmpipe")
-def showHTMLLinux():
- # Functions & Global Variables
+def show_html_linux():
+ """Open the generated HTML report in a pywebview window."""
app_name = "GitHubUserDataExtractor - HTML Viewer"
html_file = os.path.abspath(os.path.join(
- "Data", "ReceivedEvents", "index.html"))
+ ".temp", "index.html"))
# Check if the file exists
if not os.path.exists(html_file):
diff --git a/src/htmlviewers/win.py b/src/htmlviewers/win.py
index b92a9a6..bc8c7aa 100644
--- a/src/htmlviewers/win.py
+++ b/src/htmlviewers/win.py
@@ -1,12 +1,18 @@
+"""Windows HTML viewer backed by PyQt5 QWebEngineView."""
import sys
import os
from PyQt5.QtCore import QUrl
-from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QMessageBox, QDesktopWidget
+from PyQt5.QtWidgets import (
+ QApplication, QMainWindow, QVBoxLayout, QWidget, QMessageBox, QDesktopWidget
+)
from PyQt5.QtWebEngineWidgets import QWebEngineView
-class HTMLViewer(QMainWindow):
+class HTMLViewer(QMainWindow): # pylint: disable=too-few-public-methods # noqa: D203,D211
+ """Main window that renders the generated HTML report."""
+
def __init__(self, html_path):
+ """Initialize the viewer window for the given HTML file path."""
super().__init__()
self.setWindowTitle("GitHubUserDataExtractor - HTML Viewer")
@@ -45,8 +51,8 @@ def center_on_screen(self):
self.move(frame_geometry.topLeft())
-def showHTMLWindow():
- """Launches the PyQt5 application to display the HTML content."""
+def show_html_window():
+ """Launch the PyQt5 application and display the HTML content."""
app = QApplication(sys.argv)
# Absolute path to the HTML file
@@ -56,13 +62,16 @@ def showHTMLWindow():
browser = HTMLViewer(html_file_path)
browser.show()
- # Execute the application and ensure proper exit
- sys.exit(app.exec_())
+ # Execute the application and capture the exit code so cleanup can run.
+ exit_code = app.exec_()
# Delete the HTML file after closing the viewer
try:
os.remove(html_file_path)
except FileNotFoundError:
print(f"Warning: HTML file '{html_file_path}' not found to delete.")
- except Exception as e:
+ except OSError as e:
print(f"Error deleting HTML file: {e}")
+
+ # Exit with the same code returned by Qt.
+ sys.exit(exit_code)
diff --git a/src/main.py b/src/main.py
index f4a6fd9..8b1ec1a 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,16 +1,14 @@
+"""Entry point for the GitHub User Data Extractor CLI."""
import os
+import sys
import platform
import core.utils as session
from core.utils import colors
-if platform.system() == "Windows":
- import htmlviewers.win as windows
-else:
- import htmlviewers.linux as linux
-
def display_banner():
- os.system("cls" if platform.system() == "Windows" else "clear")
+ """Print the application banner and developer credits."""
+ session.clear()
banner = r'''
____ _ _ _ _ _ _ _ ____ _
/ ___(_) |_| | | |_ _| |__ | | | |___ ___ _ __ | _ \ __ _| |_ __ _
@@ -24,40 +22,110 @@ def display_banner():
f"Website : {colors.CYAN}https://quantumbytestudios.in{colors.ENDC}\n")
+TOKEN_DIR = os.path.join(os.path.expanduser("~"), ".config", "githubuserdataextractor")
+TOKEN_FILE = os.path.join(TOKEN_DIR, "github_token")
+LEGACY_TOKEN_FILE = os.path.join(os.path.dirname(__file__), ".github_token")
+
+
+def load_saved_token():
+ """Return the GitHub token persisted on disk, or an empty string."""
+ try:
+ with open(TOKEN_FILE, "r", encoding="utf-8") as f:
+ return f.read().strip()
+ except FileNotFoundError:
+ try:
+ with open(LEGACY_TOKEN_FILE, "r", encoding="utf-8") as f:
+ token = f.read().strip()
+ if token:
+ save_token(token)
+ return token
+ except FileNotFoundError:
+ return ""
+
+
+def save_token(token):
+ """Persist the supplied GitHub token to the token file."""
+ os.makedirs(TOKEN_DIR, exist_ok=True)
+ with open(TOKEN_FILE, "w", encoding="utf-8") as f:
+ f.write(token)
+
+
+def prompt_for_token():
+ """Load a saved token or prompt the user for a new one."""
+ saved = load_saved_token()
+ if saved:
+ os.environ["GITHUB_TOKEN"] = saved
+ print(f"{colors.GREEN}Saved token loaded — "
+ f"authenticated rate limit active (5,000 req/hr).{colors.ENDC}\n")
+ return
+
+ token = input(
+ f"Enter a GitHub Access Token (leave blank to use unauthenticated): {colors.WARNING}"
+ ).strip()
+ print(colors.ENDC, end="")
+ if token:
+ os.environ["GITHUB_TOKEN"] = token
+ save_token(token)
+ print(f"{colors.GREEN}Token set and saved — "
+ f"authenticated rate limit active (5,000 req/hr).{colors.ENDC}\n")
+ else:
+ print(f"{colors.WARNING}No token provided — "
+ f"unauthenticated rate limit applies (60 req/hr).{colors.ENDC}\n")
+
+
def get_username():
+ """Prompt for and return a non-empty GitHub username."""
username = input(
f"Enter a GitHub Username: {colors.WARNING}").strip().lower()
print(colors.ENDC, end="")
if not username:
print(f"{colors.RED}Username cannot be empty!{colors.ENDC}")
- exit(1)
+ sys.exit(1)
if username == "exit":
print(colors.RED + 'Bye.' + colors.ENDC)
- exit(0)
+ sys.exit(0)
return username
def get_stat_urls(username):
+ """Return the mapping of stat-card URLs for the given username."""
return {
- "mostUsedLanguages": f"https://github-readme-stats.vercel.app/api/top-langs?username={username}&langs_count=8",
- "githubStats": f"https://github-readme-stats.vercel.app/api?username={username}&show_icons=true&locale=en",
+ "mostUsedLanguages": (
+ "https://github-profile-summary-cards.vercel.app/api/cards/repos-per-language"
+ f"?username={username}&theme=dark"
+ ),
+ "githubStats": (
+ "https://github-profile-summary-cards.vercel.app/api/cards/stats"
+ f"?username={username}&theme=dark"
+ ),
"streakContributionsLS": f"https://streak-stats.demolab.com/?user={username}",
- "contributorGraphOne": f"https://github-readme-activity-graph.vercel.app/graph?username={username}&bg_color=000000&color=ffffff&line=ffffff&point=ffffff&area=true&hide_border=true",
- "contributorGraphTwo": f"https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username={username}&theme=dark"
+ "contributorGraphOne": (
+ "https://github-readme-activity-graph.vercel.app/graph"
+ f"?username={username}"
+ "&bg_color=000000&color=ffffff&line=ffffff"
+ "&point=ffffff&area=true&hide_border=true"
+ ),
+ "contributorGraphTwo": (
+ "https://github-profile-summary-cards.vercel.app/api/cards/profile-details"
+ f"?username={username}&theme=dark"
+ ),
}
def open_html_viewer():
+ """Open the generated HTML report in the platform-specific viewer."""
html_file = os.path.join(".temp", "index.html")
if platform.system() == "Linux":
try:
- linux.showHTMLLinux()
+ from htmlviewers import linux # pylint: disable=import-outside-toplevel
+ linux.show_html_linux()
except ImportError:
print(
f"{colors.RED}Error: HTMLViewer_Linux module not found.{colors.ENDC}")
elif platform.system() == "Windows":
try:
- windows.showHTMLWindow()
+ from htmlviewers import win as windows # pylint: disable=import-outside-toplevel
+ windows.show_html_window()
except ImportError:
print(
f"{colors.RED}Error: HTMLViewer_Windows module not found.{colors.ENDC}")
@@ -73,12 +141,14 @@ def open_html_viewer():
def main():
+ """Run the full extraction workflow end-to-end."""
display_banner()
+ prompt_for_token()
username = get_username()
urls = get_stat_urls(username)
session.fetch_and_print_data(username)
- session.show_events_and_graphs(urls)
+ session.show_events_and_graphs()
session.create_and_display_html_user_events(username, urls)
open_html_viewer()