From b698bec48b94ff2c939e03b374c810f7d1cf78a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:12:32 +0000 Subject: [PATCH 1/2] feat: improve error handling and env parsing Agent-Logs-Url: https://github.com/devfle/readme-level-up/sessions/808ea229-c7e1-4ebf-b971-7695f68295e5 Co-authored-by: devfle <52854338+devfle@users.noreply.github.com> --- main.py | 43 +++++++++++++++++++++----------- readme_level.py | 51 ++++++++++++++++++++++++-------------- tests/test_main.py | 19 ++++++++++++++ tests/test_readme_level.py | 33 +++++++++++++++++++++++- 4 files changed, 112 insertions(+), 34 deletions(-) diff --git a/main.py b/main.py index a028a9d..96720d5 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,8 @@ from logging import basicConfig, INFO from readme_level import ReadmeLevel +DEFAULT_PROGRESS_BAR_CHAR_LENGTH: int = 30 + # set default config for application logging basicConfig( level=INFO, @@ -13,29 +15,42 @@ ) +def _get_progress_bar_length() -> int: + """Gets and validates the progress bar length from environment variables.""" + progress_bar_length: str | None = getenv("INPUT_PROGRESS_BAR_CHAR_LENGTH") + + if not progress_bar_length: + return DEFAULT_PROGRESS_BAR_CHAR_LENGTH + + try: + value: int = int(progress_bar_length) + except ValueError: + return DEFAULT_PROGRESS_BAR_CHAR_LENGTH + + return value if value > 0 else DEFAULT_PROGRESS_BAR_CHAR_LENGTH + + +def _env_is_truthy(var_name: str) -> bool: + """Converts common environment variable values to a boolean.""" + return getenv(var_name, "").lower() in {"1", "true", "yes", "on"} + + def draw_progress_bar(current_progress: float | int) -> str: """Draws the progress bar""" - progress_bar_length: int = int(getenv("INPUT_PROGRESS_BAR_CHAR_LENGTH")) + progress_bar_length: int = _get_progress_bar_length() + current_progress = max(0.0, min(100.0, float(current_progress))) progress_bar_content = { "empty_bar": getenv("INPUT_EMPTY_BAR"), "filled_bar": getenv("INPUT_FILLED_BAR") } - progress_bar: str = "" filled_progress: int = round( progress_bar_length * (current_progress / 100), 0) - - for index in range(progress_bar_length): - - # because the index starts at 0 we reduce filled_progress - # we should define our own index - if index <= filled_progress - 1: - progress_bar += progress_bar_content["filled_bar"] - - if index > filled_progress - 1: - progress_bar += progress_bar_content["empty_bar"] - + progress_bar: str = ( + progress_bar_content["filled_bar"] * int(filled_progress) + + progress_bar_content["empty_bar"] * (progress_bar_length - int(filled_progress)) + ) return progress_bar @@ -58,7 +73,7 @@ def generate_content(readme_instance: ReadmeLevel, start_section: str, end_secti f"{ getenv('INPUT_CARD_TITLE') if getenv('INPUT_CARD_TITLE') else '' } \n" f"
level: { user_level } \
{ draw_progress_bar(to_next_lvl) } {round(to_next_lvl, 2)}%\n"
- f"{ ep_information if getenv('INPUT_SHOW_EP_INFO') else '' }"
+ f"{ ep_information if _env_is_truthy('INPUT_SHOW_EP_INFO') else '' }"
f"{end_section}")
diff --git a/readme_level.py b/readme_level.py
index 81f82da..1490d2d 100644
--- a/readme_level.py
+++ b/readme_level.py
@@ -1,8 +1,9 @@
"""Module that contains all the logic about the levelsystem."""
from os import getenv
from datetime import datetime
-from logging import exception, info
+from logging import exception, info, error
from requests import post
+from requests.exceptions import RequestException
from graphql_query import REQUEST_QUERY
from readme_data import ReadmeLevelData
@@ -22,22 +23,32 @@ def __init__(self) -> None:
def fetch_user_data(self) -> dict[str, int] | None:
"""Fetches the user data from github api"""
- if not getenv("INPUT_GITHUB_TOKEN"):
- exception("an error with the github token occurred")
+ github_token: str | None = getenv("INPUT_GITHUB_TOKEN")
+ if not github_token:
+ error("missing github token")
+ return None
- auth_header = {"Authorization": "Bearer " +
- getenv("INPUT_GITHUB_TOKEN")}
- response = post("https://api.github.com/graphql",
- json={"query": REQUEST_QUERY}, headers=auth_header, timeout=2)
+ auth_header = {"Authorization": "Bearer " + github_token}
- if response.status_code == 200:
- info("request to github api was successfull")
+ try:
+ response = post("https://api.github.com/graphql",
+ json={"query": REQUEST_QUERY}, headers=auth_header, timeout=2)
+ except RequestException:
+ exception("request to github api failed")
+ return None
- response_data = response.json()
+ if response.status_code != 200:
+ error("request to github api failed with status code %s", response.status_code)
+ return None
- current_year = datetime.now().year
- total_contribution = []
+ info("request to github api was successfull")
+ response_data = response.json()
+
+ current_year = datetime.now().year
+ total_contribution = []
+
+ try:
while current_year >= 2015:
contribution_count = (response_data["data"]["user"]
["_" + str(current_year)]["contributionCalendar"]["totalContributions"])
@@ -45,7 +56,6 @@ def fetch_user_data(self) -> dict[str, int] | None:
total_contribution.append(contribution_count)
current_year -= 1
-
user_data = {}
user_data["totalContributions"] = sum(total_contribution)
@@ -54,18 +64,21 @@ def fetch_user_data(self) -> dict[str, int] | None:
["followers"]["totalCount"])
user_data["totalRepositories"] = (response_data["data"]["user"]
- ["repositories"]["totalCount"])
-
- return user_data
+ ["repositories"]["totalCount"])
+ except (KeyError, TypeError):
+ exception("github api response is malformed")
+ return None
- exception("request to github api failed")
- return None
+ return user_data
def calc_current_ep(self) -> int:
"""Calculates the current user experience points"""
# get user stats
- user_stats: dict[str, int] = self.fetch_user_data()
+ user_stats: dict[str, int] | None = self.fetch_user_data()
+ if user_stats is None:
+ self.current_ep = 0
+ return self.current_ep
# calc the current experience points
self.current_ep = (
diff --git a/tests/test_main.py b/tests/test_main.py
index ae8bf0b..2d3972c 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -63,6 +63,25 @@ def test_generate_content(self) -> None:
# it realy difficult to check for isEqual
self.assertIsInstance(replace_str, str)
+ @patch.dict(os.environ, {'INPUT_SHOW_EP_INFO': 'false'}, clear=True)
+ def test_generate_content_without_ep_info(self) -> None:
+ """Tests generated content without ep information."""
+ mock_readme_instance = MagicMock()
+ mock_readme_instance.get_current_level.return_value = {
+ "current_level": "10", "percentage_level": 20}
+ mock_readme_instance.get_contribution_ep = 20
+ mock_readme_instance.get_follower_ep = 20
+ mock_readme_instance.get_project_ep = 5
+
+ with patch('main.draw_progress_bar', return_value="███"):
+ replace_str: str = generate_content(
+ mock_readme_instance,
+ "",
+ ""
+ )
+
+ self.assertNotIn("experience points", replace_str)
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/test_readme_level.py b/tests/test_readme_level.py
index af7b756..6d9ef21 100644
--- a/tests/test_readme_level.py
+++ b/tests/test_readme_level.py
@@ -2,8 +2,9 @@
Unit Tests for readme_level.py
"""
+import os
import unittest
-from unittest.mock import patch
+from unittest.mock import patch, MagicMock
from readme_level import ReadmeLevel
class TestLevelSystem(unittest.TestCase):
@@ -37,6 +38,36 @@ def test_calc_current_ep(self, mock_fetch_user_data):
readme_instance.calc_current_ep()
self.assertEqual(readme_instance.current_ep, 2725)
+ @patch('readme_level.ReadmeLevel.fetch_user_data')
+ def test_calc_current_ep_without_user_data(self, mock_fetch_user_data):
+ """Tests ep calc with missing user data."""
+ mock_fetch_user_data.return_value = None
+
+ readme_instance: ReadmeLevel = ReadmeLevel()
+ readme_instance.calc_current_ep()
+ self.assertEqual(readme_instance.current_ep, 0)
+
+ @patch.dict(os.environ, {}, clear=True)
+ @patch('readme_level.post')
+ def test_fetch_user_data_without_token(self, mock_post):
+ """Tests fetch_user_data returns None if token is missing."""
+ readme_instance: ReadmeLevel = ReadmeLevel()
+ user_data = readme_instance.fetch_user_data()
+
+ self.assertIsNone(user_data)
+ mock_post.assert_not_called()
+
+ @patch.dict(os.environ, {"INPUT_GITHUB_TOKEN": "test-token"}, clear=True)
+ @patch('readme_level.post')
+ def test_fetch_user_data_unsuccessful_response(self, mock_post):
+ """Tests fetch_user_data handles non 200 responses."""
+ mock_post.return_value = MagicMock(status_code=500)
+
+ readme_instance: ReadmeLevel = ReadmeLevel()
+ user_data = readme_instance.fetch_user_data()
+
+ self.assertIsNone(user_data)
+
if __name__ == '__main__':
unittest.main()
From 4b24581368e11e58fffa7a8a1381566aa9864432 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Apr 2026 20:13:39 +0000
Subject: [PATCH 2/2] test: refine wording and log message text
Agent-Logs-Url: https://github.com/devfle/readme-level-up/sessions/808ea229-c7e1-4ebf-b971-7695f68295e5
Co-authored-by: devfle <52854338+devfle@users.noreply.github.com>
---
readme_level.py | 2 +-
tests/test_main.py | 2 +-
tests/test_readme_level.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/readme_level.py b/readme_level.py
index 1490d2d..8b5bbfe 100644
--- a/readme_level.py
+++ b/readme_level.py
@@ -41,7 +41,7 @@ def fetch_user_data(self) -> dict[str, int] | None:
error("request to github api failed with status code %s", response.status_code)
return None
- info("request to github api was successfull")
+ info("request to github api was successful")
response_data = response.json()
diff --git a/tests/test_main.py b/tests/test_main.py
index 2d3972c..271292c 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -60,7 +60,7 @@ def test_generate_content(self) -> None:
mock_readme_instance, start_section, end_section)
# we check only if return value is from type str because the spaces makes
- # it realy difficult to check for isEqual
+ # it really difficult to check for isEqual
self.assertIsInstance(replace_str, str)
@patch.dict(os.environ, {'INPUT_SHOW_EP_INFO': 'false'}, clear=True)
diff --git a/tests/test_readme_level.py b/tests/test_readme_level.py
index 6d9ef21..2094f3b 100644
--- a/tests/test_readme_level.py
+++ b/tests/test_readme_level.py
@@ -60,7 +60,7 @@ def test_fetch_user_data_without_token(self, mock_post):
@patch.dict(os.environ, {"INPUT_GITHUB_TOKEN": "test-token"}, clear=True)
@patch('readme_level.post')
def test_fetch_user_data_unsuccessful_response(self, mock_post):
- """Tests fetch_user_data handles non 200 responses."""
+ """Tests fetch_user_data handles non-200 responses."""
mock_post.return_value = MagicMock(status_code=500)
readme_instance: ReadmeLevel = ReadmeLevel()