Skip to content

Commit fc92bb4

Browse files
committed
Modularize & refactor the source code
1 parent 6d90cfb commit fc92bb4

File tree

10 files changed

+237
-224
lines changed

10 files changed

+237
-224
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=9.5 $(git ls-files '*.py')
26+
venv/bin/pylint --fail-under=9.5 src/*.py

.github/workflows/unittests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ jobs:
2525
pip install -r requirements.txt
2626
- name: Run the tests
2727
run: |
28-
venv/bin/python -m unittest discover tests
28+
venv/bin/python -m unittest discover .

Dockerfile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ FROM python:3.9-slim-buster AS build
66

77
WORKDIR /app
88

9-
COPY requirements.txt ./
9+
COPY src/requirements.txt ./
1010

1111
RUN pip install --no-cache-dir -r requirements.txt
1212

@@ -17,8 +17,7 @@ RUN groupadd -r app && useradd --no-log-init -r -g app app
1717
WORKDIR /app
1818

1919
COPY --from=build /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
20-
COPY app.py ./
21-
COPY CHANGELOG.md ./
20+
COPY src/* ./
2221

2322
RUN chown -R app:app /app
2423

src/app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""App Module"""
2+
from waitress import serve
3+
from routes import app
4+
5+
if __name__ == '__main__':
6+
serve(app, host='0.0.0.0', port=8000)

src/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Config Module"""
2+
import os
3+
4+
API_BASE_URL = "https://api.github.com"
5+
MAX_WORKERS = 10
6+
TIMEOUT = 10
7+
8+
BASIC_AUTH_USERNAME = os.environ.get("BASIC_AUTH_USERNAME")
9+
BASIC_AUTH_PASSWORD = os.environ.get("BASIC_AUTH_PASSWORD")
10+
11+
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
12+
APP_AUTH_ID = os.environ.get("APP_AUTH_ID")
13+
APP_AUTH_PRIVATE_KEY = os.environ.get("APP_AUTH_PRIVATE_KEY")
14+
APP_AUTH_INSTALLATION_ID = os.environ.get("APP_AUTH_INSTALLATION_ID")
15+
APP_AUTH_BASE_URL = "https://api.github.com"

src/helpers.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Helpers Module"""
2+
import datetime
3+
import argparse
4+
from concurrent.futures import ThreadPoolExecutor
5+
import jwt
6+
import requests
7+
from flask import request
8+
from config import (
9+
API_BASE_URL, MAX_WORKERS, TIMEOUT, GITHUB_TOKEN,
10+
APP_AUTH_ID, APP_AUTH_BASE_URL,
11+
APP_AUTH_INSTALLATION_ID, APP_AUTH_PRIVATE_KEY
12+
)
13+
14+
def get_token():
15+
"""Sets the GitHub API token based on the selected mode
16+
17+
Returns:
18+
token: Either the personal access token or the GitHub App access token
19+
"""
20+
parser = argparse.ArgumentParser()
21+
parser.add_argument(
22+
"--mode",
23+
choices=[
24+
"pat-auth",
25+
"app-auth"],
26+
default="pat-auth",
27+
help="Authentication mode")
28+
args = parser.parse_args()
29+
30+
token = request.args.get("token")
31+
32+
if token:
33+
return token
34+
elif args.mode == "pat-auth":
35+
token = GITHUB_TOKEN
36+
elif args.mode == "app-auth":
37+
app_auth_id = APP_AUTH_ID
38+
app_auth_private_key = APP_AUTH_PRIVATE_KEY
39+
app_auth_installation_id = APP_AUTH_INSTALLATION_ID
40+
app_auth_base_url = APP_AUTH_BASE_URL
41+
42+
now = datetime.datetime.utcnow()
43+
iat = int((now - datetime.datetime(1970, 1, 1)).total_seconds())
44+
exp = iat + 600
45+
payload = {
46+
"iat": iat,
47+
"exp": exp,
48+
"iss": app_auth_id
49+
}
50+
encoded_jwt = jwt.encode(
51+
payload,
52+
app_auth_private_key,
53+
algorithm="RS256")
54+
headers = {
55+
"Authorization": f"Bearer {encoded_jwt}",
56+
"Accept": "application/vnd.github.v3+json"
57+
}
58+
response = requests.post(
59+
f"{app_auth_base_url}/app/installations/{app_auth_installation_id}/access_tokens",
60+
headers=headers,
61+
timeout=TIMEOUT)
62+
63+
if response.status_code == 201:
64+
token = response.json()["token"]
65+
else:
66+
raise Exception(
67+
f"Failed to obtain access token: {response.status_code} {response.text}")
68+
69+
return token
70+
71+
def get_workflows(owner, repo, headers):
72+
"""Get the workflows for a given owner and repo from the GitHub API.
73+
74+
Args:
75+
owner (str): The owner of the repository.
76+
repo (str): The repository name.
77+
headers (dict): HTTP headers to be sent with the request.
78+
79+
Returns:
80+
list: A list of workflows for the given repository.
81+
82+
Raises:
83+
requests.HTTPError: If the request to the GitHub API fails.
84+
"""
85+
endpoint = f"{API_BASE_URL}/repos/{owner}/{repo}/actions/workflows"
86+
response = requests.get(endpoint, headers=headers, timeout=TIMEOUT)
87+
response.raise_for_status()
88+
return response.json()["workflows"]
89+
90+
91+
def get_workflow_runs(workflow, headers):
92+
"""Get the workflow runs for a specific workflow from the GitHub API.
93+
94+
Args:
95+
workflow (dict): The workflow information.
96+
headers (dict): HTTP headers to be sent with the request.
97+
98+
Returns:
99+
list: A list of workflow runs for the given workflow.
100+
101+
Raises:
102+
requests.HTTPError: If the request to the GitHub API fails.
103+
"""
104+
url = f"{workflow['url']}/runs"
105+
response = requests.get(url, headers=headers, timeout=TIMEOUT)
106+
response.raise_for_status()
107+
return response.json()["workflow_runs"]
108+
109+
110+
def get_all_workflow_runs(owner, repo, token):
111+
"""Get all workflow runs for a given owner, repo, and token.
112+
113+
Args:
114+
owner (str): The owner of the repository.
115+
repo (str): The repository name.
116+
token (str): The GitHub token for authentication.
117+
118+
Returns:
119+
list: A list of all workflow runs for the given repository.
120+
121+
Raises:
122+
requests.HTTPError: If the request to the GitHub API fails.
123+
"""
124+
headers = {
125+
"Authorization": f"Bearer {token}",
126+
"Accept": "application/vnd.github.v3+json"
127+
}
128+
129+
workflows = get_workflows(owner, repo, headers)
130+
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
131+
futures = [
132+
executor.submit(
133+
get_workflow_runs,
134+
workflow,
135+
headers) for workflow in workflows]
136+
137+
results = []
138+
for future in futures:
139+
data = future.result()
140+
results += data
141+
142+
return results
File renamed without changes.
Lines changed: 7 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,23 @@
1-
"""App Module"""
2-
import os
1+
"""Routes Module"""
32
import re
43
import logging
5-
import argparse
6-
import datetime
74
import xml.etree.ElementTree as ET
8-
from concurrent.futures import ThreadPoolExecutor
9-
10-
import jwt
5+
import datetime
116
import requests
127
from flask import Flask, request, make_response, jsonify
138
from flask_basicauth import BasicAuth
9+
from helpers import get_token, get_all_workflow_runs
10+
from config import BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD, TIMEOUT
1411

1512
logging.basicConfig(level=logging.INFO)
1613
logger = logging.getLogger(__name__)
1714

1815
app = Flask('github-cctray')
19-
app.config['BASIC_AUTH_USERNAME'] = os.environ.get("BASIC_AUTH_USERNAME")
20-
app.config['BASIC_AUTH_PASSWORD'] = os.environ.get("BASIC_AUTH_PASSWORD")
16+
app.config['BASIC_AUTH_USERNAME'] = BASIC_AUTH_USERNAME
17+
app.config['BASIC_AUTH_PASSWORD'] = BASIC_AUTH_PASSWORD
2118

2219
basic_auth = BasicAuth(app)
2320

24-
API_BASE_URL = "https://api.github.com"
25-
MAX_WORKERS = 10
26-
TIMEOUT = 10
27-
28-
def get_token():
29-
"""Sets the GitHub API token based on the selected mode
30-
31-
Returns:
32-
token: Either the personal access token or the GitHub App access token
33-
"""
34-
parser = argparse.ArgumentParser()
35-
parser.add_argument(
36-
"--mode",
37-
choices=[
38-
"pat-auth",
39-
"app-auth"],
40-
default="pat-auth",
41-
help="Authentication mode")
42-
args = parser.parse_args()
43-
44-
token = request.args.get("token")
45-
46-
if token:
47-
return token
48-
elif args.mode == "pat-auth":
49-
token = os.environ.get("GITHUB_TOKEN")
50-
elif args.mode == "app-auth":
51-
app_auth_id = os.environ.get("APP_AUTH_ID")
52-
app_auth_private_key = os.environ.get("APP_AUTH_PRIVATE_KEY")
53-
app_auth_installation_id = os.environ.get("APP_AUTH_INSTALLATION_ID")
54-
app_auth_base_url = "https://api.github.com"
55-
56-
now = datetime.datetime.utcnow()
57-
iat = int((now - datetime.datetime(1970, 1, 1)).total_seconds())
58-
exp = iat + 600
59-
payload = {
60-
"iat": iat,
61-
"exp": exp,
62-
"iss": app_auth_id
63-
}
64-
encoded_jwt = jwt.encode(
65-
payload,
66-
app_auth_private_key,
67-
algorithm="RS256")
68-
headers = {
69-
"Authorization": f"Bearer {encoded_jwt}",
70-
"Accept": "application/vnd.github.v3+json"
71-
}
72-
response = requests.post(
73-
f"{app_auth_base_url}/app/installations/{app_auth_installation_id}/access_tokens",
74-
headers=headers,
75-
timeout=TIMEOUT)
76-
77-
if response.status_code == 201:
78-
token = response.json()["token"]
79-
else:
80-
raise Exception(
81-
f"Failed to obtain access token: {response.status_code} {response.text}")
82-
83-
return token
84-
85-
86-
def get_workflows(owner, repo, headers):
87-
"""Get the workflows for a given owner and repo from the GitHub API.
88-
89-
Args:
90-
owner (str): The owner of the repository.
91-
repo (str): The repository name.
92-
headers (dict): HTTP headers to be sent with the request.
93-
94-
Returns:
95-
list: A list of workflows for the given repository.
96-
97-
Raises:
98-
requests.HTTPError: If the request to the GitHub API fails.
99-
"""
100-
endpoint = f"{API_BASE_URL}/repos/{owner}/{repo}/actions/workflows"
101-
response = requests.get(endpoint, headers=headers, timeout=TIMEOUT)
102-
response.raise_for_status()
103-
return response.json()["workflows"]
104-
105-
106-
def get_workflow_runs(workflow, headers):
107-
"""Get the workflow runs for a specific workflow from the GitHub API.
108-
109-
Args:
110-
workflow (dict): The workflow information.
111-
headers (dict): HTTP headers to be sent with the request.
112-
113-
Returns:
114-
list: A list of workflow runs for the given workflow.
115-
116-
Raises:
117-
requests.HTTPError: If the request to the GitHub API fails.
118-
"""
119-
url = f"{workflow['url']}/runs"
120-
response = requests.get(url, headers=headers, timeout=TIMEOUT)
121-
response.raise_for_status()
122-
return response.json()["workflow_runs"]
123-
124-
125-
def get_all_workflow_runs(owner, repo, token):
126-
"""Get all workflow runs for a given owner, repo, and token.
127-
128-
Args:
129-
owner (str): The owner of the repository.
130-
repo (str): The repository name.
131-
token (str): The GitHub token for authentication.
132-
133-
Returns:
134-
list: A list of all workflow runs for the given repository.
135-
136-
Raises:
137-
requests.HTTPError: If the request to the GitHub API fails.
138-
"""
139-
headers = {
140-
"Authorization": f"Bearer {token}",
141-
"Accept": "application/vnd.github.v3+json"
142-
}
143-
144-
workflows = get_workflows(owner, repo, headers)
145-
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
146-
futures = [
147-
executor.submit(
148-
get_workflow_runs,
149-
workflow,
150-
headers) for workflow in workflows]
151-
152-
results = []
153-
for future in futures:
154-
data = future.result()
155-
results += data
156-
157-
return results
158-
159-
16021
@app.route('/')
16122
@basic_auth.required
16223
def index():
@@ -225,7 +86,7 @@ def health():
22586
Returns:
22687
flask.Response: JSON response containing the status and version.
22788
"""
228-
with open('CHANGELOG.md', 'r', encoding='utf-8') as changelog_file:
89+
with open('../CHANGELOG.md', 'r', encoding='utf-8') as changelog_file:
22990
changelog_content = changelog_file.read()
23091

23192
latest_version_match = re.search(
@@ -296,9 +157,3 @@ def handle_error(exception):
296157
logger.error("An error occurred: %s", str(exception))
297158
error_message = f"An error occurred: {str(exception)}"
298159
return error_message, 500
299-
300-
301-
if __name__ == '__main__':
302-
from waitress import serve
303-
304-
serve(app, host='0.0.0.0', port=8000)

tests/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import os
2+
import sys
3+
PROJECT_PATH = os.getcwd()
4+
SOURCE_PATH = os.path.join(
5+
PROJECT_PATH,"src"
6+
)
7+
sys.path.append(SOURCE_PATH)

0 commit comments

Comments
 (0)