【GitHub】Pythonでリポジトリの情報を取得するCloud Functionsを作る

2806 語
14 分
【GitHub】Pythonでリポジトリの情報を取得するCloud Functionsを作る

はじまり#

リサちゃん avatar
リサちゃん
リポジトリ増えてきたな・・・
135ml avatar
135ml
一目で情報を見られるようにするか。

リポジトリを一目で見渡したい#

自分の GitHub アカウントにあるリポジトリが増えてくると、一体どこのリポジトリが今どうなっているのか、どんなリポジトリが自分のアカウント上にあるのか、ふと気になってきました。 そこで、 GitHub で管理されているリポジトリのデータを瞬時に確認するために、 GitHub API を利用して必要なデータを抽出する仕組みを作っていきたいと思います。

開発に使ったライブラリなど#

ライブラリおよびツール一覧#

今回使ったライブラリやツールは下記です。

  • Python 3.10
  • PyGithub(Python から GitHub API にアクセスしやすくなる。)
  • Pytest(Python コードをテストする。)
  • Pytest Coverage Comment(Pytest のカバレッジをREADME.mdに表示するための GitHub Actions)
  • Cloud Shell Editor (Google Cloud で無料で使える VSCode ライクのエディタ。 Eclipse Theia というエディタがベースらしい。週で利用可能時間が決まっている。)

今回、主に使用する Python のライブラリはPyGithubです。PyGithubは、 GitHub REST API v3 を簡単に利用できるようにするライブラリで、 GitHub のリポジトリやユーザ情報を簡単に取得できます。

pipで当時の最新バージョンをインストールします。

Terminal window
pip install PyGithub>=1.55

しかし、 Cloud Shell 上ででpip installしたらエラーになりました・・・詳細は下記で。

Cloud Shell Editorについて#

今回、 Google Cloud から提供されている「Cloud Shell」で利用できる「Cloud Shell Editor」を利用していきたいと思います。

このエディタに関しては、解説記事を以下のページで書いています。

Cloud Shell Editorを使う場合、pipのバージョンがヤヴァい#

そんな Cloud Shell を触り始める時、pip installを実行するとこんなエラーメッセージが出てきます。

pipのバージョンが古いらしい。

DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.

Python 2 だと・・・!?

なんか知りませんが、 Python 2 でも利用できるように、pipのバージョンが古いんですかね・・・?

というわけで、 Cloud Shell から Python のパッケージを更新したい場合はpipをアップグレードしましょう。

Terminal window
sudo python3 -m pip install --upgrade pip

これで、 Pygithub をインストールすることが出来ました。

Terminal window
pip install PyGithub>=1.55

PythonでGitHubから情報を取得する#

基本構造#

PyGithubから GitHub API にアクセスします。

情報を取得する Python スクリプトの基本的な構造は以下の通りです。まず、 GitHub からアクセストークンを発行して、 API にアクセスします。

from github import Github
# アクセストークンを設定
g = Github("my_access_token")
# リポジトリの情報を取得
repo = g.get_repo("my_username/my_repository")
print(repo.name)
print(repo.stargazers_count)

アクセストークンのスコープは、repoを全チェックにしました。

リポジトリの色々な情報を取得する#

たった1つのリポジトリの中にも沢山の情報が詰め込まれています。

リポジトリ名とその説明、URL、サイズ、Issueの数、フォークされた数などなど、多種多様な情報をGitHub APIから取得することが可能です。これほどの量とは、驚いた・・・

今回取得した情報は以下のような感じです。リポジトリのサイズとか、記述した言語とかは統計してみたかったのですかさず取得します。日付はISO-8601準拠にします。

def get_repo_info_in_format(repo) -> Repo_format:
"""
Retrieves and formats repository data into a dictionary conforming to Repo_format.
:param repo: The repository object to format.
:return: A dictionary containing formatted repository data.
:rtype: Repo_format
"""
state_of_pulls = "all"
created_at = repo.created_at
updated_at = repo.updated_at
created_at = f"{created_at.year:04}-{created_at.month:02}-{created_at.day:02}T{created_at.hour:02}:{created_at.minute:02}:{created_at.second:02}Z"
updated_at = f"{updated_at.year:04}-{updated_at.month:02}-{updated_at.day:02}T{updated_at.hour:02}:{updated_at.minute:02}:{updated_at.second:02}Z"
obj = {
"name": repo.name,
"description": repo.description,
"is_private": repo.private,
"html_url": repo.html_url,
"issues_count": repo.open_issues_count,
"forks_count": repo.forks_count,
"stargazers_count": repo.stargazers_count,
"subscribers_count": repo.subscribers_count,
"size": repo.size,
"is_archived": repo.archived,
"created_at": created_at,
"updated_at": updated_at,
"language": repo.language,
"languages": repo.get_languages(),
"pulls_count": repo.get_pulls(state=state_of_pulls).totalCount
}
return obj

処理をマルチスレッド化する#

今回取得する情報の中には、リポジトリのプルリクのカウント、また取得するプログラミング言語は複数となっています。

そのように情報を取得する場合には、GitHub APIへのリクエストは一回だけでは足りません。上記の2種類の情報を取得するために更に2回リクエストを行う必要があります。(そして、更に2回リクエストを行うリポジトリの数が100個近くあるため、200回のGETリクエストを行います・・・)

それだけリクエストの数が多いと流石に処理時間が長すぎるので、マルチスレッドにして処理していきます。

今回使ったのは、threadingというライブラリです。

import threading
from pprint import pprint
from typing import TypedDict, Final
import datetime
import json
import requests
import os
from github import Github
from config import get_config, get_github_token, get_github_username, get_env_variable
from memory_profiler import profile
def fetch_repositories(github, fetch_type: str, username: str | None):
"""
Fetches a list of repositories for a specified user or the authenticated user if no username is provided.
:param github: The GitHub session object.
:param fetch_type: The type of repositories to fetch ('all', 'owner', 'public', 'private', 'forks', etc.).
:param username: The username of the GitHub user whose repositories are to be fetched. If None, fetches repositories of the authenticated user.
:type username: str, optional
:return: A list of Repository objects.
:rtype: PaginatedList[Repository]
:raises ValueError: if fetch_type is an empty string.
"""
if fetch_type not in ["all", "owner", "public", "private", "forks"]:
raise ValueError(f"'fetch_type' is not support '{fetch_type}'")
if username == None:
user = github.get_user()
else:
user = github.get_user(username)
repos = user.get_repos(type=fetch_type)
return repos
def store_repo_info(repo, results: list) -> None:
"""
Processes and stores information about a repository in a list, intended for multi-threaded operations.
:param repo: The repository object to process information from.
:param results: The list where processed information will be stored.
"""
obj: Repo_format = get_repo_info_in_format(repo)
# gh_info.append(obj)
results.append(obj)
# @profile
def get_repo_info(github, is_threading: bool = False, username: str | None = None) -> list[Repo_format]:
"""
Starts multiple threads to process detailed information for 'owner' type repositories for a given user.
:param github: The GitHub session object.
:param username: The username of the GitHub user. If None, processes repositories for the authenticated user.
:type username: str, optional
:return: A list of processed repository information.
:rtype: list[Repo_format]
"""
repos = fetch_repositories(github, "owner", username)
results = []
if is_threading:
threads = []
for repo in repos:
thread = threading.Thread(
target=store_repo_info, args=(repo, results))
threads.append(thread)
thread.start()
pprint(threads.__sizeof__())
pprint("threads.__sizeof__()------------------------------------")
for thread in threads:
thread.join()
else:
for repo in repos:
info: Repo_format = get_repo_info_in_format(repo)
results.append(info)
return results

この処理の内容は、ざっとこんな感じです。

  • thread = threading.Thread(target=store_repo_info, args=(repo, results))で、追加するスレッドを設定する。
  • store_repo_info()内のget_repo_info_in_format()で、更に2回リクエストを行って、リストの中に情報を入れる。
  • thread.start()で、スレッドを追加する。
  • thread.join()で、追加したスレッドが終了するまでget_repo_info関数が終了しないようにする。

こうすることで、200回のリクエスト処理を並行化することが出来ました。かなり処理時間を減らせました。

そしたら、出来たコードをテストします。

Pytestでテストして、カバレッジをREADME.mdに表示する#

有名なリポジトリではよく、テストのカバレッジをREADMEの冒頭で表示してたりしますよね。アレを、やってみたいと思います。

今回使うのは、「Pytest Coverage Comment」および「Dynamic Badges」いうGitHub Actionsです。

「Pytest Coverage Comment」でバッジの情報を作って、「Dynamic Badges」でバッジを作ります。

今回使用したGitHub Actionsのワークフローはこちら。

name: pytest-integration
on:
push:
permissions: write-all
env:
PYTHON_VERSION: '3.10'
PYTHON_VERSION_JSON: 'python-version.json'
LICENSE: 'Apache'
LICENSE_JSON: 'license.json'
TEST_DIR: 'pytest_results'
GIST_ID: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
PYTHON_COVERAGE_COMMENT_JSON: 'pytest-coverage-comment.json'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: 'x64'
- name: Get Python version
run: python -V
- name: Install dependencies
run: pip install --no-cache-dir -r requirements/dev.txt
- name: Set environment variables
run: |
echo 'PYTHONPATH=./' >> .env
mkdir -p ${{ env.TEST_DIR }}
- name: Run pytest
run: |
python -m pytest -n auto --cov=src --cov-branch --cov-report=term-missing:skip-covered --tb=short --junitxml=./${{ env.TEST_DIR }}/junit.xml | tee ./${{ env.TEST_DIR }}/coverage.txt
- name: Create Coverage Comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
pytest-coverage-path: ./${{ env.TEST_DIR }}/coverage.txt
junitxml-path: ./${{ env.TEST_DIR }}/junit.xml
- name: Create Coverage Badge
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.ACCESS_BADGE_IN_GIST }}
gistID: ${{ env.GIST_ID }}
filename: ${{ env.PYTHON_COVERAGE_COMMENT_JSON }}
label: Coverage
message: ${{ steps.coverageComment.outputs.coverage }}
color: ${{ steps.coverageComment.outputs.color }}
namedLogo: pytest
- name: Generate Python version json
uses: jsdaniell/create-json@v1.2.3
with:
name: ${{ env.PYTHON_VERSION_JSON }}
json: '{"label":"Python","message":"${{ env.PYTHON_VERSION }}","schemaVersion":1,"color":"blue","namedLogo":"python","style":"flat"}'
dir: './'
- name: Deploy Python version json to Gist
uses: exuanbo/actions-deploy-gist@v1.1.4
with:
token: ${{ secrets.ACCESS_BADGE_IN_GIST }}
gist_id: ${{ env.GIST_ID }}
file_path: ${{ env.PYTHON_VERSION_JSON }}
file_type: text
- name: Generate License json
uses: jsdaniell/create-json@v1.2.3
with:
name: ${{ env.LICENSE_JSON }}
json: '{"label":"license","message":"${{ env.LICENSE }}","schemaVersion":1,"color":"skyblue","namedLogo":"apache","style":"flat"}'
dir: './'
- name: Deploy License json to Gist
uses: exuanbo/actions-deploy-gist@v1.1.4
with:
token: ${{ secrets.ACCESS_BADGE_IN_GIST }}
gist_id: ${{ env.GIST_ID }}
file_path: ${{ env.LICENSE_JSON }}
file_type: text

Pytest のカバレッジ以外にも、 Python のバージョンおよびライセンスの情報もバッジとして表示できるようにしています。その場合、「Pytest Coverage Comment」を使わないで JSON ファイルを作る必要があるので、jsdaniell/create-jsonの GitHub Actions を利用しています。そして、その JSON 形式の情報をexuanbo/actions-deploy-gistの GitHub Actions を利用して Gist にアップロードする。。。(色々な Actions を使わせてもらいました。ありがたい。)

面倒なので、 push した時に動くようにしてしまっています。

バッジがちゃんと追加できているとこんな感じ。

Cloud Functionsにデプロイする#

Python で GitHub 情報を取得して、テストも行えたならば、 Cloud Functions として本処理をデプロイしていきたいと思います。(今回取得する情報は、 GAS (Google Apps Script)から取って Google スプレッドシートに入れたかったのです。)

Google Cloud のコンソールからデプロイしていきたいと思います。

import functions_framework
@functions_framework.http
def retrieve_github_repo_info_by_request(request) -> str:
"""
Handles an HTTP request to retrieve GitHub repository information based on the provided token.
:param request: The HTTP request object containing parameters and JSON data.
:return: A JSON formatted string of repository information or a greeting message if no token is provided.
:rtype: str
"""
request_json = request.get_json(silent=True)
request_args = request.args
info: list[Repo_format] | str = []
if request_json and "token" in request_json:
token: str = request_json["token"]
info = retrieve_github_repo_info(token, False)
elif request_args and "token" in request_args:
info = request_args["token"]
else:
info = "Hello, World!!!"
res_obj = {"data": info}
return json.dumps(res_obj, sort_keys=True, ensure_ascii=False)

Google Cloud では、 Python ランタイムを使用する場合に、コンソール画面でデプロイ直前の事前テストを行うことが出来ます。

便利なのですが、環境変数を使ったテストは出来なかったりします。その部分は、別記事でまとめています。

デプロイ後の格闘#

デプロイ後に直面するのが、 API の様々な閾値に起因するバグです。

GAS からリクエストをした時に、こんなエラーメッセージが返ってきたりしました。

Service Unavailable
{"object":"error","status":503,"code":"service_unavailable","message":"Public API service is temporarily unavailable, please try again later.","request_id":"xxxxxxxc-xxxx-4d07-bcc3-xxxxxxxxx"}

このメッセージはレスポンスコード503なので、 Cloud Functions 上でバグっているみたいです。 最初はタイムアウトすることが原因だったのですが、何回かリクエストしていると、メモリ不足で起きるエラーも発生していました。 なので、デプロイした関数に割り当てるメモリ量やタイムアウト閾値を増やします。

一旦、これで動くようになりました! Google スプレッドシートに入れたらこんな感じ。

これで、 Python で GitHub 上のリポジトリの情報を取得する Cloud Functions が出来ました!

おっ・・・?

まとめ#

この記事では、 GitHub のリポジトリ情報を Python と Cloud Functions を使用して取得する手順を解説しました。

  1. GitHub API を利用してリポジトリ情報を抽出しました。
  2. ライブラリとツール: PyGithub、Pytest、Cloud Shell Editor など。
  3. マルチスレッド処理: 情報取得処理を効率化しました。
  4. Pytestでのテストおよびカバレッジの表示: GitHub Actions を活用して自動化しました。
  5. Cloud Functionsによるデプロイ: Google スプレッドシートに保存できるようにAPI化しました。

GAS だと API を叩きやすくするライブラリがあまり無いので、今回は Python で実装しました。 Cloud Functions は、 1 日に 1 リクエストくらいであれば月に 5 円も掛からないので、これからも利用していきたいです。

おしまい#

リサちゃん avatar
リサちゃん
なんかメモリ食いすぎじゃね?
135ml avatar
135ml
うーん確かに・・・

以上になります!

記事を共有

この記事が役に立ったなら、ぜひ他の人と共有してください!

【GitHub】Pythonでリポジトリの情報を取得するCloud Functionsを作る
https://endorphinbath.com/posts/cloudfunctions-to-get-github-repo-with-python/
著者
kinkinbeer135ml
公開日
2024-05-06
ライセンス
CC BY-NC-SA 4.0
関連記事 スマート
1
【GitHub】Goでリポジトリの情報を取得するCloud Functionsを作って、Pythonと比較する
Code Go言語でGitHubのリポジトリ情報を取得するCloud Functionsを開発し、Pythonで作成した同様の機能と比較した記事です。使ったツールや、並行処理の比較も行っています。
2
【GitHub】PythonとGitHub ActionsでProjectsにIssuesを作る作業を自動化する(前編:Pythonのソース)
Code 日々のタスクをGitHub IssuesおよびGitHub Projectsで管理する時に登録する作業が面倒くさいと思います。いちいちオプションを選択して入力する手間が面倒くさいと思います。本記事では、その作業を省略したツールを紹介します。
3
【GitHub】PythonとGitHub ActionsでProjectsにIssuesを作る作業を自動化する(後編:GitHub Actionの内容)
Code 日々のタスクをGitHub IssuesおよびGitHub Projectsで管理する時に登録する作業が面倒くさいと思います。いちいちオプションを選択して入力する手間が面倒くさいと思います。本記事では、その作業を省略したツールを紹介します。
4
【Google Cloud】GitHub Actionsで認証するためのシェル関数を作る
Code Google Cloud上のリソースを使ってGitHub ActionsでCI/CDするためにシェル関数を構築します。その関数ではサービスアカウントにWorkload Identity連携をして処理の途中に通知を行ったりもします。
5
【Heroku】Pythonで作成したDiscord用のボットをGitHubリポジトリからデプロイするやり方
Software HerokuでPythonで書いたDiscordアプリをGitHubリポジトリからデプロイします。記載した手順で特に躓くことなく実施できたので、ご参考ください。
ランダム記事 ランダム
Profile Image of the Author
kinkinbeer135ml
SIerをやめて、プログラミングを勉強しています。※Amazonアソシエイトに参加しています。
お知らせ
私のブログへようこそ!これはサンプルのお知らせです。
音楽
カバー

音楽

再生中なし

0:00 0:00
歌詞なし
カテゴリ
タグ
サイト統計
記事
287
カテゴリー
8
タグ
93
総文字数
486,174
運用日数
0
最終活動
0 日前

目次