Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions .github/workflows/update_mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: update_mkdocs
on:
push:
branches:
- master
- main
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install mkdocs-material
- run: mkdocs gh-deploy --force
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kieran.pritchard@ons.gov.uk
2 changes: 0 additions & 2 deletions README.md

This file was deleted.

13 changes: 13 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# GitHub API Package

## Overview

This project is a supporting package for tools which need to access the GitHub RESTful API. It includes a function to authenticate requests as a GitHub App installation, and a class which allows Get, Patch and Post request to be made easily.

This package is primarily used by:

- [GitHub Repository Archive Tool](https://github.com/ONS-Innovation/code-repo-archive-tool)
- [GitHub CoPilot Usage Dashboard](https://github.com/ONS-Innovation/code-github-copilot-usage-audit)
- [GitHub Policy/Audit Dashboard](https://github.com/ONS-Innovation/code-github-audit-dashboard)

For more information, please see the [README.md](./readme.md).
49 changes: 49 additions & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# GitHub API Package
A Python package used to interact with the GitHub RESTful API.

## Installation
This package can be installed using the GitHub repository URL.

### PIP

```bash
pip install git+https://github.com/ONS-Innovation/code-github-api-package.git
```

### Poetry

```bash
poetry add git+https://github.com/ONS-Innovation/code-github-api-package.git
```

## Usage
This package can be imported as a normal Python package.

Import whole module:
```python
import github_api_toolkit
```

Import part of the module:
```python
from github_api_toolkit import github_interface
```

## Development

This project uses pip for package management. Poetry was avoided to keep the package size small.

To develop/test the project locally, clone the repository then navigate to its root directory and run the following:

```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

This will create a virtual environment, activate it and install all the project dependancies into it.

To deactivate the virtual environment, use:
```bash
deactivate
```
157 changes: 157 additions & 0 deletions github_api_toolkit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import jwt
import time
import requests

def get_token_as_installation(org: str, pem_contents: str, app_client_id: str) -> tuple | Exception:
"""Get an access token for a GitHub App installed in an organization.

Generates an encoded JSON Web Token (JWT) using the GitHub app client ID and the private key (pem_contents).
The JWT is used to get the installation ID of the GitHub App in the organization.
The installation ID is then used to get an access token for the GitHub App.
The access token is returned along with the expiration time.

Args:
org (str): The GitHub organization name which the GitHub App is installed in.
pem_contents (str): The contents of the private key file for the GitHub App.
app_client_id (str): The GitHub App Client ID.

Returns:
A tuple containing the access token and the expiration time.
If an error occurs, an Exception object is returned to be handled by the importing program.
"""

# Generate JSON Web Token
issue_time = time.time()
expiration_time = issue_time + 600

try:
signing_key = jwt.jwk_from_pem(pem_contents.encode())
except jwt.exceptions.UnsupportedKeyTypeError as err:
return(err)

payload = {
# Issued at time
"iat": int(issue_time),
# Expiration time
"exp": int(expiration_time),
# Github App CLient ID
"iss": app_client_id
}

jwt_instance = jwt.JWT()
encoded_jwt = jwt_instance.encode(payload, signing_key, alg="RS256")

# Get Installation ID
header = {"Authorization": f"Bearer {encoded_jwt}"}

response = requests.get(url=f"https://api.github.com/orgs/{org}/installation", headers=header)

try:
response = requests.get(url=f"https://api.github.com/orgs/{org}/installation", headers=header)

response.raise_for_status()

installation_json = response.json()
installation_id = installation_json["id"]

# Get Access Token
response = requests.post(url=f"https://api.github.com/app/installations/{installation_id}/access_tokens", headers=header)
access_token = response.json()
return (access_token["token"], access_token["expires_at"])

except requests.exceptions.HTTPError as errh:
return(errh)
except requests.exceptions.ConnectionError as errc:
return(errc)
except requests.exceptions.Timeout as errt:
return(errt)
except requests.exceptions.RequestException as err:
return(err)


class github_interface():
"""A class used to interact with the Github API.

The class can perform authenticated get, patch and post requests to the GitHub API using the requests library.
"""

def __init__(self, token: str) -> None:
"""Creates the header attribute containing the Personal Access token to make auth'd API requests.
"""
self.headers = {"Authorization": "token " + token}


def handle_response(self, response: requests.Response) -> requests.Response | Exception:
"""Checks the passed response for errors and returns the response or an Exception object.

Args:
response (requests.Response): The response to be checked for errors.

Returns:
The response from the API endpoint.
If an error occurs, an Exception object is returned to be handled by the importing program.
"""

try:
response.raise_for_status()
return response
except requests.exceptions.HTTPError as errh:
return(errh)
except requests.exceptions.ConnectionError as errc:
return(errc)
except requests.exceptions.Timeout as errt:
return(errt)
except requests.exceptions.RequestException as err:
return(err)


def get(self, url: str, params: dict = {}, add_prefix: bool = True) -> requests.Response | Exception:
"""Performs a get request using the passed url.

Args:
url (str): The url endpoint of the request.
params (dict): A Dictionary containing any Query Parameters.
add_prefix (bool): A Boolean determining whether to add the "https://api.github.com" prefix
to the beginning of the passed url.

Returns:
The response from the API endpoint.
If an error occurs, an Exception object is returned to be handled by the importing program.
"""
if add_prefix:
url = "https://api.github.com" + url
return self.handle_response(requests.get(url=url, headers=self.headers, params=params))

def patch(self, url: str, params: dict = {}, add_prefix: bool = True) -> requests.Response | Exception:
"""Performs a patch request using the passed url.

Args:
url (str): The url endpoint of the request.
params (dict): A Dictionary containing any Query Parameters.
add_prefix (bool): A Boolean determining whether to add the "https://api.github.com" prefix
to the beginning of the passed url.

Returns:
The response from the API endpoint.
If an error occurs, an Exception object is returned to be handled by the importing program.
"""
if add_prefix:
url = "https://api.github.com" + url
return self.handle_response(requests.patch(url=url, headers=self.headers, params=params))

def post(self, url: str, params: dict = {}, add_prefix: bool = True) -> requests.Response | Exception:
"""Performs a post request using the passed url.

Args:
url (str): The url endpoint of the request.
params (dict): A Dictionary containing any Query Parameters.
add_prefix (bool): A Boolean determining whether to add the "https://api.github.com" prefix
to the beginning of the passed url.

Returns:
The response from the API endpoint.
If an error occurs, an Exception object is returned to be handled by the importing program.
"""
if add_prefix:
url = "https://api.github.com" + url
return self.handle_response(requests.post(url=url, headers=self.headers, params=params))
20 changes: 20 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
site_name: GitHub API Package

nav:
- Home: 'index.md'
- Getting Started: 'readme.md'
theme:
name: material
palette:
scheme: slate
features:
- content.code.copy

markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
9 changes: 9 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
certifi==2024.7.4
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==42.0.8
idna==3.7
jwt==1.3.1
pycparser==2.22
requests==2.32.3
urllib3==2.2.2
13 changes: 13 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from setuptools import setup

setup(
name='github-api-toolkit',
version='0.1.0',
description='A toolkit for interacting with the GitHub API',
url='https://github.com/ONS-Innovation/code-github-api-package',
author='Kieran Pritchard',
author_email='kieran.pritchard@ons.gov.uk',
license='MIT',
packages=['github_api_toolkit'],
zip_safe=False
)