Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
110a7b2
Add token auth persistence and fix Linux HTML stats rendering
SpiritGun91 May 10, 2026
38854ab
Fix docstring formatting per PEP 257
SpiritGun91 May 10, 2026
56c12e1
Fix docstring: opening quotes and summary on separate lines
SpiritGun91 May 10, 2026
3667083
Fix docstring: summary on first line per PEP 257
SpiritGun91 May 10, 2026
dc543d1
Appease Codacy D213: summary on line 2
SpiritGun91 May 10, 2026
b31046c
Fix Codacy issues: avoid shell clear and narrow broad exceptions
SpiritGun91 May 10, 2026
87795c9
Fix D212 docstring summary placement in utils
SpiritGun91 May 10, 2026
e9d2900
Fix D213: multi-line docstring summary on second line
SpiritGun91 May 10, 2026
4d0214d
Add module/class/function docstrings to satisfy pydocstyle (D100/D101…
SpiritGun91 May 10, 2026
448d915
Fix Codacy pydocstyle: D212 summary placement and D203 blank line bef…
SpiritGun91 May 10, 2026
2f75769
Fix Codacy docstyle: D211 class docstrings and D213 summary placement
SpiritGun91 May 10, 2026
46e398b
Fix remaining Codacy warnings and ignore local Codacy cache
SpiritGun91 May 10, 2026
704cf2f
fix: resolve pydocstyle D203/D212 docstring issues
SpiritGun91 May 10, 2026
8699118
fix: remove blank line before class docstrings to satisfy D211
SpiritGun91 May 10, 2026
7e14d85
fix: complete pylint cleanup and split report generation helpers
SpiritGun91 May 10, 2026
fd215af
fix: address Codacy security findings for token storage and requests
SpiritGun91 May 10, 2026
c830d02
fix: align docstrings and URL formatting with Codacy rules
SpiritGun91 May 10, 2026
7f43665
fix: resolve remaining Codacy docstring rule mismatches
SpiritGun91 May 10, 2026
5b9719a
fix: match Codacy D203 D213 docstring expectations
SpiritGun91 May 10, 2026
b2279d5
fix: satisfy Codacy D211 and D212 docstring rules
SpiritGun91 May 10, 2026
c830ac6
fix: restore Codacy D203 and D213 docstring style
SpiritGun91 May 10, 2026
a6aa8d8
fix: commit pending Codacy D211 and D212 docstrings
SpiritGun91 May 10, 2026
0c16837
fix: satisfy Codacy D203 and D213 docstring rules
SpiritGun91 May 10, 2026
ea882d8
chore: pin pydocstyle rules to Codacy docstring configuration
SpiritGun91 May 10, 2026
4398ad9
fix: satisfy Codacy D211 and D212 docstring issues
SpiritGun91 May 10, 2026
5f87b01
fix: neutralize conflicting pydocstyle rules with targeted noqa
SpiritGun91 May 10, 2026
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ __pycache__/
.vscode
received_events/
*.html
src/.github_token
.codacy/


#Ignore vscode AI rules
.github/instructions/codacy.instructions.md
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pydocstyle]
add-select = D203,D213
ignore = D211,D212
265 changes: 169 additions & 96 deletions src/core/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}")
Expand All @@ -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"""
<div class="event-row d-flex align-items-center shadow-sm">
<img src="{avatar}" class="profile-picture me-3" alt="Avatar of {login}">
Expand All @@ -57,46 +93,7 @@ def generate_html_event_row(avatar, login, event_type, repo_name, repo_url, badg
</div>"""


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"""
<!DOCTYPE html>
HTML_REPORT_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
Expand Down Expand Up @@ -207,71 +204,147 @@ def create_and_display_html_user_events(username, urls):
Username: {login} <br>
About: {bio} <br>
Followers: {followers}, Following: {following} <br>
Location: {location}
Location: {location}
</p>
<a href="{html_url}" class="btn btn-light" target="_blank">View Profile</a>
</div>
</div>
</div>
<div class="col-sm-12 col-md-8 col-lg-8">
<h4 class="mb-4">Received Events</h4>""")

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"""</div>
<h4 class="mb-4">Received Events</h4>
{events_html}
</div>
<div class="col-sm-12 col-md-4 col-lg-4">
<h4 class="mb-4">Contribution Insights</h4>
<div class="row justify-content-center align-items-left graph-container">
<div class="col-12">
<img class="img-fluid w-100" src="{urls['mostUsedLanguages']}" alt="Top Languages">
<img class="img-fluid w-100" src="{urls['githubStats']}" alt="GitHub Stats">
<img class="img-fluid w-100" src="{urls['streakContributionsLS']}" alt="Streak Stats">
<img class="img-fluid w-100" src="{langs_src}" alt="Top Languages">
<img class="img-fluid w-100" src="{stats_src}" alt="GitHub Stats">
<img class="img-fluid w-100" src="{streak_src}" alt="Streak Stats">
</div>
</div>
</div>
</div>
</body>
</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)
14 changes: 10 additions & 4 deletions src/htmlviewers/linux.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
Loading