Skip to content

Commit a19230a

Browse files
feat: Update requirements and add GitHub client for organization and repository management (#5)
feat: Update child pages for each github org to include details about each org for searchability
1 parent a217c2f commit a19230a

File tree

4 files changed

+201
-58
lines changed

4 files changed

+201
-58
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Generates dynamic documentation of all our GitHub Organizations and their descri
2121
The application requires three environment variables to be set. You can create a `.env` file in the root directory of the project with the following content:
2222

2323
```env
24-
export GITHUB_TOKEN=<Uses a PAT> #Assumes that all orgs a user is part of is an ORG that we own.
24+
export GITHUB_TOKEN=<Uses a PAT> #Assumes that all orgs a user is part of is an ORG that we own. Permissions required: **full** `repo` scope and org `read:org` scope
2525
export GETOUTLINE_DOCUMENT_ID=<This is usually at the end of the document in the URL of the document you want to update>
2626
export GETOUTLINE_API_TOKEN=<Token is tied to a user account in GETOUTLINE>
2727
```

app/github.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import requests
2+
import os
3+
import glueops.setup_logging
4+
5+
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
6+
logger = glueops.setup_logging.configure(level=LOG_LEVEL)
7+
8+
class GitHubClient:
9+
def __init__(self, github_token, github_api_url):
10+
self.github_token = github_token
11+
self.github_api_url = github_api_url
12+
13+
def get_organizations(self):
14+
logger.debug("Fetching organizations from GitHub API.")
15+
headers = {
16+
'Authorization': f'token {self.github_token}',
17+
'Accept': 'application/vnd.github.v3+json',
18+
}
19+
organizations = []
20+
url = f"{self.github_api_url}/user/orgs"
21+
22+
try:
23+
while url:
24+
response = requests.get(url, headers=headers)
25+
response.raise_for_status()
26+
organizations.extend(response.json())
27+
logger.debug(f"Fetched {len(response.json())} organizations.")
28+
29+
# Check for pagination
30+
links = response.headers.get('Link')
31+
if links:
32+
next_link = None
33+
for link in links.split(','):
34+
if 'rel="next"' in link:
35+
next_link = link[link.find('<') + 1:link.find('>')]
36+
break
37+
url = next_link
38+
else:
39+
url = None
40+
41+
logger.debug("All organizations fetched successfully.")
42+
return organizations
43+
except requests.exceptions.RequestException as e:
44+
logger.error(f"Error fetching organizations: {e}")
45+
raise
46+
47+
48+
def generate_markdown(github_orgs):
49+
logger.debug("Generating markdown for organizations.")
50+
markdown_content = "> This page is automatically generated. Any manual changes will be lost. See: https://github.com/GlueOps/getoutline-docs-update-github \n\n"
51+
markdown_content = "# Full list of GitHub Organizations\n\n"
52+
markdown_content += "| Organization Name | Description |\n"
53+
markdown_content += "|-------------------|-------------|\n"
54+
55+
for org in github_orgs:
56+
name = org['login']
57+
url = f"https://github.com/{org['login']}"
58+
description = org.get('description', 'No description available.')
59+
markdown_content += f"| [{name}]({url}) | {description} |\n"
60+
61+
logger.debug("Markdown generation completed.")
62+
return markdown_content
63+
64+
65+
def get_repositories(self, org_login):
66+
logger.debug(f"Fetching repositories for organization: {org_login}")
67+
headers = {
68+
'Authorization': f'token {self.github_token}',
69+
'Accept': 'application/vnd.github.v3+json',
70+
}
71+
repositories = []
72+
url = f"{self.github_api_url}/orgs/{org_login}/repos"
73+
74+
try:
75+
while url:
76+
response = requests.get(url, headers=headers)
77+
response.raise_for_status()
78+
repositories.extend(response.json())
79+
logger.debug(f"Fetched {len(response.json())} repositories for organization: {org_login}")
80+
81+
# Check for pagination
82+
links = response.headers.get('Link')
83+
if links:
84+
next_link = None
85+
for link in links.split(','):
86+
if 'rel="next"' in link:
87+
next_link = link[link.find('<') + 1:link.find('>')]
88+
break
89+
url = next_link
90+
else:
91+
url = None
92+
93+
return repositories
94+
except requests.exceptions.RequestException as e:
95+
logger.error(f"Error fetching repositories for organization {org_login}: {e}")
96+
raise
97+
98+
def get_repository_topics(self, org_login, repo_name):
99+
logger.debug(f"Fetching topics for repository: {org_login}/{repo_name}")
100+
headers = {
101+
'Authorization': f'token {self.github_token}',
102+
'Accept': 'application/vnd.github.mercy-preview+json',
103+
}
104+
url = f"{self.github_api_url}/repos/{org_login}/{repo_name}/topics"
105+
106+
try:
107+
response = requests.get(url, headers=headers)
108+
response.raise_for_status()
109+
topics = response.json().get('names', [])
110+
logger.debug(f"Fetched topics for repository: {org_login}/{repo_name}")
111+
return topics
112+
except requests.exceptions.RequestException as e:
113+
logger.error(f"Error fetching topics for repository {org_login}/{repo_name}: {e}")
114+
raise
115+
116+
def generate_markdown_for_org(self, org_login):
117+
logger.debug(f"Generating markdown for organization: {org_login}")
118+
markdown_content = f"# Repositories for {org_login}\n\n"
119+
markdown_content += "| Repository | Description | Topics |\n"
120+
markdown_content += "|------------|-------------|--------|\n"
121+
122+
org_repos = self.get_repositories(org_login)
123+
for repo in org_repos:
124+
repo_name = repo['name']
125+
repo_description = repo.get('description', 'No description available.')
126+
repo_topics = self.get_repository_topics(org_login, repo_name)
127+
topics_str = ', '.join(repo_topics)
128+
markdown_content += f"| [{repo_name}](https://github.com/{org_login}/{repo_name}) | {repo_description} | {topics_str} |\n"
129+
130+
logger.debug(f"Markdown generation completed for organization: {org_login}")
131+
return markdown_content

app/main.py

Lines changed: 67 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import requests
22
import os
33
import glueops.setup_logging
4-
5-
4+
import glueops.getoutline
5+
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
6+
from github import GitHubClient
67

78
GITHUB_API_URL = "https://api.github.com"
9+
GETOUTLINE_API_URL = "https://app.getoutline.com"
10+
811
# Environment Variables
912
REQUIRED_ENV_VARS = [
1013
"GITHUB_TOKEN",
@@ -18,7 +21,14 @@
1821
}
1922

2023
def get_env_variable(var_name: str, default=None):
21-
"""Retrieve environment variable or return default if not set."""
24+
"""
25+
Retrieve environment variable or return default if not set.
26+
27+
:param var_name: Name of the environment variable.
28+
:param default: Default value if the environment variable is not set.
29+
:return: Value of the environment variable or default.
30+
:raises EnvironmentError: If a required environment variable is not set.
31+
"""
2232
value = os.getenv(var_name, default)
2333
if var_name in REQUIRED_ENV_VARS and value is None:
2434
logger.error(f"Environment variable '{var_name}' is not set.")
@@ -47,64 +57,65 @@ def get_env_variable(var_name: str, default=None):
4757
logger.critical(f"Environment setup failed: {env_err}")
4858
raise
4959

50-
def get_organizations():
51-
logger.debug("Fetching organizations from GitHub API.")
52-
headers = {
53-
'Authorization': f'token {GITHUB_TOKEN}',
54-
'Accept': 'application/vnd.github.v3+json',
55-
}
56-
try:
57-
response = requests.get(f"{GITHUB_API_URL}/user/orgs", headers=headers)
58-
response.raise_for_status()
59-
logger.debug("Organizations fetched successfully.")
60-
return response.json()
61-
except requests.exceptions.RequestException as e:
62-
logger.error(f"Error fetching organizations: {e}")
63-
return []
60+
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=60, max=120), retry=retry_if_exception_type(requests.exceptions.RequestException))
61+
def retry_create_document(client, parent_id, title, text):
62+
"""
63+
Retry creating a document in Outline.
64+
65+
:param client: GetOutlineClient instance.
66+
:param parent_id: Parent document ID.
67+
:param title: Title of the new document.
68+
:param text: Content of the new document.
69+
:return: Result of the create_document method.
70+
"""
71+
return client.create_document(parent_id, title, text)
6472

65-
def generate_markdown(orgs):
66-
logger.debug("Generating markdown for organizations.")
67-
markdown_content = "> This page is automatically generated. Any manual changes will be lost. See: https://github.com/GlueOps/getoutline-docs-update-github \n\n"
68-
markdown_content = "# Full list of GitHub Organizations\n\n"
69-
markdown_content += "| Organization Name | Description |\n"
70-
markdown_content += "|-------------------|-------------|\n"
71-
72-
for org in orgs:
73-
name = org['login']
74-
url = f"https://github.com/{org['login']}"
75-
description = org.get('description', 'No description available.')
76-
markdown_content += f"| [{name}]({url}) | {description} |\n"
77-
78-
logger.debug("Markdown generation completed.")
79-
return markdown_content
73+
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=60, max=120), retry=retry_if_exception_type(requests.exceptions.RequestException))
74+
def retry_update_document(client, text):
75+
"""
76+
Retry updating a document in Outline.
8077
81-
def update_document(markdown_text):
82-
logger.debug("Updating document on Outline.")
83-
url = "https://app.getoutline.com/api/documents.update"
84-
payload = {
85-
"id": GETOUTLINE_DOCUMENT_ID,
86-
"text": markdown_text
87-
}
88-
headers = {
89-
"Content-Type": "application/json",
90-
"Authorization": f"Bearer {GETOUTLINE_API_TOKEN}"
91-
}
92-
try:
93-
response = requests.post(url, json=payload, headers=headers)
94-
response.raise_for_status()
95-
logger.info(f"Document update response code: {response.status_code}")
96-
except requests.exceptions.RequestException as e:
97-
logger.error(f"Error updating document: {e}")
78+
:param client: GetOutlineClient instance.
79+
:param text: New content for the document.
80+
:return: Result of the update_document method.
81+
"""
82+
return client.update_document(text)
83+
84+
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=60, max=120), retry=retry_if_exception_type(requests.exceptions.RequestException))
85+
def retry_generate_markdown_for_org(client, github_org_name):
86+
"""
87+
Retry generating markdown content for a GitHub organization.
88+
89+
:param client: GitHubClient instance.
90+
:param github_org_name: Name of the GitHub organization.
91+
:return: Markdown content as a string.
92+
"""
93+
return client.generate_markdown_for_org(github_org_name)
9894

9995
def main():
100-
logger.info("Starting.")
101-
organizations = get_organizations()
96+
"""
97+
Main function to update GitHub organizations documentation in Outline.
98+
"""
99+
logger.info("Starting GitHub Doc Updates.")
100+
GetOutlineClient = glueops.getoutline.GetOutlineClient(GETOUTLINE_API_URL, GETOUTLINE_DOCUMENT_ID, GETOUTLINE_API_TOKEN)
101+
github_client = GitHubClient(GITHUB_TOKEN, GITHUB_API_URL)
102+
organizations = github_client.get_organizations()
102103
if organizations:
103-
markdown = generate_markdown(organizations)
104-
update_document(markdown)
104+
logger.info(f"Updating document letting folks know we are updating the list of organizations.")
105+
retry_update_document(GetOutlineClient, "# UPDATING..... \n\n # check back shortly.....\n\n\n")
106+
parent_id = GetOutlineClient.get_document_uuid()
107+
children = GetOutlineClient.get_children_documents_to_delete(parent_id)
108+
for id in children:
109+
GetOutlineClient.delete_document(id)
110+
for org in organizations:
111+
org_specific_markdown_content = retry_generate_markdown_for_org(github_client, org["login"])
112+
retry_create_document(GetOutlineClient, parent_id, org["login"], org_specific_markdown_content)
113+
markdown = GitHubClient.generate_markdown(organizations)
114+
retry_update_document(GetOutlineClient, markdown)
115+
116+
logger.info("Finished GitHub Doc Updates.")
105117
else:
106118
logger.warning("No organizations found.")
107-
logger.info("Finished.")
108-
119+
109120
if __name__ == "__main__":
110-
main()
121+
main()

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
requests==2.32.3
2-
glueops-helpers @ https://github.com/GlueOps/python-glueops-helpers-library/archive/refs/tags/v0.4.1.zip
2+
glueops-helpers @ https://github.com/GlueOps/python-glueops-helpers-library/archive/refs/tags/v0.6.0.zip
3+
tenacity==9.0.0

0 commit comments

Comments
 (0)