Skip to content

Commit b7bb40a

Browse files
committed
Add a new method, extract and re-use
* Refactor and reduce duplicacy * Use reponse raise for status for raising HTTP Errors * Add docstrings and improve code liniting to 10/10 * Update unit tests as per refactoring
1 parent 9883dd8 commit b7bb40a

File tree

3 files changed

+108
-50
lines changed

3 files changed

+108
-50
lines changed

.github/workflows/pylint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ jobs:
2323
pip install pylint
2424
- name: Analysing the code with pylint
2525
run: |
26-
venv/bin/pylint --fail-under=8 $(git ls-files '*.py')
26+
venv/bin/pylint --fail-under=9.5 $(git ls-files '*.py')

app.py

Lines changed: 86 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,99 @@
11
"""App Module"""
22
import os
3-
import xml.etree.ElementTree as ET
43
import logging
54
from concurrent.futures import ThreadPoolExecutor
6-
import requests as requests
5+
6+
import xml.etree.ElementTree as ET
7+
import requests
78
from flask import Flask, request, make_response
89

10+
logging.basicConfig(level=logging.INFO)
11+
logger = logging.getLogger(__name__)
12+
913
app = Flask('github-cctray')
1014

11-
# Configure logging
12-
logging.basicConfig(level=logging.INFO)
13-
logger = app.logger
15+
API_BASE_URL = "https://api.github.com"
16+
MAX_WORKERS = 10
17+
TIMEOUT = 10
18+
19+
20+
def get_workflows(owner, repo, headers):
21+
"""Get the workflows for a given owner and repo from the GitHub API.
22+
23+
Args:
24+
owner (str): The owner of the repository.
25+
repo (str): The repository name.
26+
headers (dict): HTTP headers to be sent with the request.
27+
28+
Returns:
29+
list: A list of workflows for the given repository.
30+
31+
Raises:
32+
requests.HTTPError: If the request to the GitHub API fails.
33+
"""
34+
endpoint = f"{API_BASE_URL}/repos/{owner}/{repo}/actions/workflows"
35+
response = requests.get(endpoint, headers=headers, timeout=TIMEOUT)
36+
response.raise_for_status()
37+
return response.json()["workflows"]
38+
39+
40+
def get_workflow_runs(workflow, headers):
41+
"""Get the workflow runs for a specific workflow from the GitHub API.
42+
43+
Args:
44+
workflow (dict): The workflow information.
45+
headers (dict): HTTP headers to be sent with the request.
1446
47+
Returns:
48+
list: A list of workflow runs for the given workflow.
1549
16-
def get_workflow_runs(owner, repo, token):
17-
endpoint = f"https://api.github.com/repos/{owner}/{repo}/actions/workflows"
50+
Raises:
51+
requests.HTTPError: If the request to the GitHub API fails.
52+
"""
53+
url = f"{workflow['url']}/runs"
54+
response = requests.get(url, headers=headers, timeout=TIMEOUT)
55+
response.raise_for_status()
56+
return response.json()["workflow_runs"]
57+
58+
59+
def get_all_workflow_runs(owner, repo, token):
60+
"""Get all workflow runs for a given owner, repo, and token.
61+
62+
Args:
63+
owner (str): The owner of the repository.
64+
repo (str): The repository name.
65+
token (str): The GitHub token for authentication.
66+
67+
Returns:
68+
list: A list of all workflow runs for the given repository.
69+
70+
Raises:
71+
requests.HTTPError: If the request to the GitHub API fails.
72+
"""
1873
headers = {
1974
"Authorization": f"Bearer {token}",
2075
"Accept": "application/vnd.github.v3+json"
2176
}
22-
results = []
2377

24-
response = requests.get(endpoint, headers=headers, timeout=10)
25-
if response.status_code != 200:
26-
logger.error("GitHub API returned status code %d", response.status_code)
27-
else:
28-
workflows = response.json()["workflows"]
29-
with ThreadPoolExecutor(max_workers=10) as executor:
30-
futures = []
31-
for workflow in workflows:
32-
future = executor.submit(requests.get, f"{workflow['url']}/runs", headers=headers, timeout=10)
33-
futures.append(future)
34-
for future in futures:
35-
response = future.result()
36-
if response is None:
37-
logger.error("Failed to get response from GitHub API")
38-
elif response.status_code != 200:
39-
logger.error("GitHub API returned status code %d", response.status_code)
40-
else:
41-
data = response.json()
42-
results += data["workflow_runs"]
78+
workflows = get_workflows(owner, repo, headers)
79+
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
80+
futures = [executor.submit(get_workflow_runs, workflow, headers) for workflow in workflows]
81+
82+
results = []
83+
for future in futures:
84+
data = future.result()
85+
results += data
4386

4487
return results
4588

4689

4790
@app.route('/')
4891
def index():
92+
"""Endpoint for generating the CCTray XML.
93+
94+
Returns:
95+
flask.Response: The XML response containing the project information.
96+
"""
4997
owner = request.args.get("owner") or request.form.get('owner')
5098
repo = request.args.get("repo") or request.form.get('repo')
5199
token = os.environ.get("GITHUB_TOKEN")
@@ -54,9 +102,8 @@ def index():
54102
logger.warning("Missing parameter(s) or Environment Variable")
55103
return make_response("Missing parameter(s)", 400)
56104

57-
data = get_workflow_runs(owner, repo, token)
105+
data = get_all_workflow_runs(owner, repo, token)
58106

59-
# Sort workflow runs by 'updated_at' timestamp in descending order
60107
workflow_runs = sorted(data, key=lambda run: run["updated_at"], reverse=True)
61108

62109
root = ET.Element("Projects")
@@ -69,7 +116,7 @@ def index():
69116
project = ET.SubElement(root, "Project")
70117
project.set("name", project_name)
71118

72-
# Map 'status' field to 'activity'
119+
# Map 'Github' attributes to 'CCTray'
73120
if run["status"] == "completed":
74121
if run["conclusion"] == "success":
75122
project.set("activity", "Sleeping")
@@ -89,7 +136,6 @@ def index():
89136
response = make_response(ET.tostring(root).decode())
90137
response.headers['Content-Type'] = 'application/xml'
91138

92-
# Log request URI and response code
93139
logger.info("Request URI: %s Response Code: %d", request.path, response.status_code)
94140

95141

@@ -98,12 +144,19 @@ def index():
98144

99145
@app.errorhandler(Exception)
100146
def handle_error(exception):
101-
# Log the error
102-
logger.error("An error occurred: %s", str(exception))
147+
"""Error handler for handling exceptions raised in the application.
103148
149+
Args:
150+
exception (Exception): The exception object.
151+
152+
Returns:
153+
str: The error message response.
154+
"""
155+
logger.error("An error occurred: %s", str(exception))
104156
return "An error occurred.", 500
105157

106158

107159
if __name__ == '__main__':
108160
from waitress import serve
161+
109162
serve(app, host='0.0.0.0', port=8000)

tests/test_app.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
1+
"""Teat Module for the App"""
12
import unittest
23
from unittest.mock import patch
34
from app import app
45

56

67
class TestApp(unittest.TestCase):
8+
"""Test cases for the App module."""
79

810
def setUp(self):
11+
"""Set up the test environment."""
912
app.testing = True
1013
self.client = app.test_client()
1114

1215
def test_index_missing_parameters(self):
16+
"""Test handling missing parameters in the index route."""
1317
response = self.client.get('/')
1418
self.assertEqual(response.status_code, 400)
1519
self.assertIn(b"Missing parameter(s)", response.data)
1620

17-
@patch('app.get_workflow_runs')
18-
def test_index_successful_response(self, mock_workflow_runs):
19-
mock_workflow_runs.return_value = [
21+
@patch('app.get_all_workflow_runs')
22+
def test_index_successful_response(self, mock_get_all_workflow_runs):
23+
"""Test successful response in the index route."""
24+
mock_get_all_workflow_runs.return_value = [
2025
{
2126
"name": "CI",
2227
"status": "completed",
23-
"conclusion": "success",
24-
"updated_at": "2021-09-20T18:25:34Z",
25-
"html_url": "https://github.com/owner/repo/actions/runs/1234"
28+
"conclusion": "success",
29+
"updated_at": "2021-09-20T18:25:34Z",
30+
"html_url": "https://github.com/owner/repo/actions/runs/1234"
2631
}
2732
]
2833
response = self.client.get('/?owner=owner&repo=repo')
@@ -33,10 +38,10 @@ def test_index_successful_response(self, mock_workflow_runs):
3338
self.assertIn(b'lastBuildStatus="Success"', response.data)
3439
self.assertIn(b'webUrl="https://github.com/owner/repo/actions/runs/1234"', response.data)
3540

36-
37-
@patch('app.get_workflow_runs')
38-
def test_index_unknown_build_status(self, mock_workflow_runs):
39-
mock_workflow_runs.return_value = [
41+
@patch('app.get_all_workflow_runs')
42+
def test_index_unknown_build_status(self, mock_get_all_workflow_runs):
43+
"""Test unknown build status in the index route."""
44+
mock_get_all_workflow_runs.return_value = [
4045
{
4146
"name": "CI",
4247
"status": "in_progress",
@@ -50,13 +55,13 @@ def test_index_unknown_build_status(self, mock_workflow_runs):
5055
self.assertIn(b'<Project', response.data)
5156
self.assertIn(b'lastBuildStatus="Unknown"', response.data)
5257

53-
54-
@patch('app.get_workflow_runs')
55-
def test_index_failed_response(self, mock_workflow_runs):
56-
mock_workflow_runs.return_value = None
58+
@patch('app.get_all_workflow_runs')
59+
def test_index_failed_response(self, mock_get_all_workflow_runs):
60+
"""Test failed response in the index route."""
61+
mock_get_all_workflow_runs.return_value = []
5762
response = self.client.get('/?owner=owner&repo=repo')
58-
self.assertEqual(response.status_code, 500)
59-
self.assertIn(b"An error occurred.", response.data)
63+
self.assertEqual(response.status_code, 200)
64+
self.assertIn(b'<Projects />', response.data)
6065

6166

6267
if __name__ == '__main__':

0 commit comments

Comments
 (0)