diff --git a/.github/linters/.mypy.ini b/.github/linters/.mypy.ini index f0d4703..3e79b06 100644 --- a/.github/linters/.mypy.ini +++ b/.github/linters/.mypy.ini @@ -1,5 +1,5 @@ [mypy] disable_error_code = attr-defined, import-not-found -[mypy-github3.*] +[mypy-github.*] ignore_missing_imports = True diff --git a/.github/linters/.shellcheckrc b/.github/linters/.shellcheckrc index 8d3c10c..57624c5 100644 --- a/.github/linters/.shellcheckrc +++ b/.github/linters/.shellcheckrc @@ -1,2 +1,2 @@ -# Don't suggest [ -n "$VAR" ] over [ ! -z "$VAR" ] +# Don't suggest [ -n "$VAR" ] over [ ! -z "$VAR" ] disable=SC2129 diff --git a/auth.py b/auth.py index a5e551a..23702a6 100644 --- a/auth.py +++ b/auth.py @@ -1,7 +1,7 @@ """This is the module that contains functions related to authenticating to GitHub with a personal access token.""" -import github3 import requests +from github import Auth, Github, GithubException, GithubIntegration def auth_to_github( @@ -11,7 +11,7 @@ def auth_to_github( gh_app_private_key_bytes: bytes, ghe: str, gh_app_enterprise_only: bool, -) -> github3.GitHub: +) -> Github: """ Connect to GitHub.com or GitHub Enterprise, depending on env variables. @@ -24,29 +24,25 @@ def auth_to_github( gh_app_enterprise_only (bool): Set this to true if the GH APP is created on GHE and needs to communicate with GHE api only Returns: - github3.GitHub: the GitHub connection object + Github: the GitHub connection object """ if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id: + app_auth = Auth.AppAuth(int(gh_app_id), gh_app_private_key_bytes.decode()) + installation_auth = app_auth.get_installation_auth(int(gh_app_installation_id)) if ghe and gh_app_enterprise_only: - gh = github3.github.GitHubEnterprise(url=ghe) + github_connection = Github(base_url=f"{ghe}/api/v3", auth=installation_auth) else: - gh = github3.github.GitHub() - gh.login_as_app_installation( - gh_app_private_key_bytes, str(gh_app_id), gh_app_installation_id - ) - github_connection = gh + github_connection = Github(auth=installation_auth) elif ghe and token: - github_connection = github3.github.GitHubEnterprise(url=ghe, token=token) + github_connection = Github(base_url=f"{ghe}/api/v3", auth=Auth.Token(token)) elif token: - github_connection = github3.login(token=token) + github_connection = Github(auth=Auth.Token(token)) else: raise ValueError( "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set" ) - if not github_connection: - raise ValueError("Unable to authenticate to GitHub") - return github_connection # type: ignore + return github_connection def get_github_app_installation_token( @@ -68,14 +64,14 @@ def get_github_app_installation_token( Returns: str: the GitHub App token """ - jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id) - api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com" - url = f"{api_endpoint}/app/installations/{gh_app_installation_id}/access_tokens" - + app_auth = Auth.AppAuth(int(gh_app_id), gh_app_private_key_bytes.decode()) + if ghe: + gi = GithubIntegration(base_url=f"{ghe}/api/v3", auth=app_auth) + else: + gi = GithubIntegration(auth=app_auth) try: - response = requests.post(url, headers=jwt_headers, json=None, timeout=5) - response.raise_for_status() - except requests.exceptions.RequestException as e: + installation_token = gi.get_access_token(int(gh_app_installation_id)) + except (GithubException, requests.exceptions.RequestException) as e: print(f"Request to get GitHub App Installation Token failed: {e}") return None - return response.json().get("token") + return installation_token.token diff --git a/contributors.py b/contributors.py index 70c8cda..c71683c 100644 --- a/contributors.py +++ b/contributors.py @@ -1,6 +1,7 @@ # pylint: disable=broad-exception-caught """This file contains the main() and other functions needed to get contributor information from the organization or repository""" +from datetime import datetime from typing import List import auth @@ -122,12 +123,11 @@ def get_all_contributors( """ repos = [] if organization: - repos = github_connection.organization(organization).repositories() + repos = github_connection.get_organization(organization).get_repos() else: repos = [] for repo in repository_list: - owner, repo_name = repo.split("/") - repository_obj = github_connection.repository(owner, repo_name) + repository_obj = github_connection.get_repo(repo) repos.append(repository_obj) all_contributors = [] @@ -165,9 +165,10 @@ def get_contributors(repo: object, start_date: str, end_date: str, ghe: str): # on large repositories. avatars: dict[str, str] = {} counts: dict[str, int] = {} - for commit in repo.commits(since=start_date, until=end_date): - # github3.py leaves commit.author as a raw dict (not a ShortUser) - # when the API returns an author payload without a matching GitHub user. + for commit in repo.get_commits( + since=datetime.fromisoformat(start_date), + until=datetime.fromisoformat(end_date), + ): login = getattr(commit.author, "login", None) if not login or "[bot]" in login: continue @@ -187,7 +188,7 @@ def get_contributors(repo: object, start_date: str, end_date: str, ghe: str): ) contributors.append(contributor) else: - for user in repo.contributors(): + for user in repo.get_contributors(): login = getattr(user, "login", None) if not login or "[bot]" in login: continue @@ -195,7 +196,7 @@ def get_contributors(repo: object, start_date: str, end_date: str, ghe: str): contributor = contributor_stats.ContributorStats( login, getattr(user, "avatar_url", "") or "", - getattr(user, "contributions_count", 0) or 0, + getattr(user, "contributions", 0) or 0, commit_url, "", ) diff --git a/pyproject.toml b/pyproject.toml index 65e28df..e690f5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "2.0.0" description = "GitHub Action that produces information about contributors over a specified time period." requires-python = ">=3.11" dependencies = [ - "github3-py==4.0.1", + "PyGithub==2.9.1", "python-dotenv==1.2.2", "requests==2.34.2", ] diff --git a/test_auth.py b/test_auth.py index a7ed25b..9b907a6 100644 --- a/test_auth.py +++ b/test_auth.py @@ -5,6 +5,7 @@ import auth import requests +from github import GithubException class TestAuth(unittest.TestCase): @@ -12,16 +13,22 @@ class TestAuth(unittest.TestCase): Test case for the auth module. """ - @patch("github3.login") - def test_auth_to_github_with_token(self, mock_login): + @patch("auth.Auth") + @patch("auth.Github") + def test_auth_to_github_with_token(self, mock_github_cls, mock_auth_cls): """ Test the auth_to_github function when the token is provided. """ - mock_login.return_value = "Authenticated to GitHub.com" + mock_token = MagicMock() + mock_auth_cls.Token.return_value = mock_token + mock_github = MagicMock() + mock_github_cls.return_value = mock_github result = auth.auth_to_github("token", "", "", b"", "", False) - self.assertEqual(result, "Authenticated to GitHub.com") + mock_auth_cls.Token.assert_called_once_with("token") + mock_github_cls.assert_called_once_with(auth=mock_token) + self.assertEqual(result, mock_github) def test_auth_to_github_without_token(self): """ @@ -36,112 +43,197 @@ def test_auth_to_github_without_token(self): "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set", ) - @patch("github3.github.GitHubEnterprise") - def test_auth_to_github_with_ghe(self, mock_ghe): + @patch("auth.Auth") + @patch("auth.Github") + def test_auth_to_github_with_ghe(self, mock_github_cls, mock_auth_cls): """ Test the auth_to_github function when the GitHub Enterprise URL is provided. """ - mock_ghe.return_value = "Authenticated to GitHub Enterprise" + mock_token = MagicMock() + mock_auth_cls.Token.return_value = mock_token + mock_github = MagicMock() + mock_github_cls.return_value = mock_github + result = auth.auth_to_github( "token", "", "", b"", "https://github.example.com", False ) - self.assertEqual(result, "Authenticated to GitHub Enterprise") + mock_auth_cls.Token.assert_called_once_with("token") + mock_github_cls.assert_called_once_with( + base_url="https://github.example.com/api/v3", auth=mock_token + ) + self.assertEqual(result, mock_github) - @patch("github3.github.GitHubEnterprise") - def test_auth_to_github_with_ghe_and_ghe_app(self, mock_ghe): + @patch("auth.Auth") + @patch("auth.Github") + def test_auth_to_github_with_ghe_and_ghe_app(self, mock_github_cls, mock_auth_cls): """ Test the auth_to_github function when the GitHub Enterprise URL is provided and the app was created in GitHub Enterprise URL. """ - mock = mock_ghe.return_value - mock.login_as_app_installation = MagicMock(return_value=True) + mock_app_auth = MagicMock() + mock_installation_auth = MagicMock() + mock_auth_cls.AppAuth.return_value = mock_app_auth + mock_app_auth.get_installation_auth.return_value = mock_installation_auth + mock_github = MagicMock() + mock_github_cls.return_value = mock_github + result = auth.auth_to_github( "", "123", "123", b"123", "https://github.example.com", True ) - mock.login_as_app_installation.assert_called_once_with(b"123", "123", "123") - self.assertEqual(result, mock) - @patch("github3.github.GitHub") - def test_auth_to_github_with_app(self, mock_gh): + mock_auth_cls.AppAuth.assert_called_once_with(123, "123") + mock_app_auth.get_installation_auth.assert_called_once_with(123) + mock_github_cls.assert_called_once_with( + base_url="https://github.example.com/api/v3", auth=mock_installation_auth + ) + self.assertEqual(result, mock_github) + + @patch("auth.Auth") + @patch("auth.Github") + def test_auth_to_github_with_app(self, mock_github_cls, mock_auth_cls): """ Test the auth_to_github function when app credentials are provided """ - mock = mock_gh.return_value - mock.login_as_app_installation = MagicMock(return_value=True) + mock_app_auth = MagicMock() + mock_installation_auth = MagicMock() + mock_auth_cls.AppAuth.return_value = mock_app_auth + mock_app_auth.get_installation_auth.return_value = mock_installation_auth + mock_github = MagicMock() + mock_github_cls.return_value = mock_github + result = auth.auth_to_github( "", "123", "123", b"123", "https://github.example.com", False ) - mock.login_as_app_installation.assert_called_once_with(b"123", "123", "123") - self.assertEqual(result, mock) - @patch("github3.github.GitHub") - def test_auth_to_github_with_app_int_app_id(self, mock_gh): + mock_auth_cls.AppAuth.assert_called_once_with(123, "123") + mock_app_auth.get_installation_auth.assert_called_once_with(123) + mock_github_cls.assert_called_once_with(auth=mock_installation_auth) + self.assertEqual(result, mock_github) + + @patch("auth.Auth") + @patch("auth.Github") + def test_auth_to_github_with_app_int_app_id(self, mock_github_cls, mock_auth_cls): """ - Test that an integer app_id is converted to a string before passing - to login_as_app_installation, to avoid PyJWT TypeError on the iss claim. + Test that an integer app_id is converted properly for Auth.AppAuth. """ - mock = mock_gh.return_value - mock.login_as_app_installation = MagicMock(return_value=True) + mock_app_auth = MagicMock() + mock_installation_auth = MagicMock() + mock_auth_cls.AppAuth.return_value = mock_app_auth + mock_app_auth.get_installation_auth.return_value = mock_installation_auth + mock_github = MagicMock() + mock_github_cls.return_value = mock_github + result = auth.auth_to_github("", 123, 456, b"private_key", "", False) - mock.login_as_app_installation.assert_called_once_with( - b"private_key", "123", 456 - ) - self.assertEqual(result, mock) - @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) - @patch("requests.post") - def test_get_github_app_installation_token(self, mock_post): + mock_auth_cls.AppAuth.assert_called_once_with(123, "private_key") + mock_app_auth.get_installation_auth.assert_called_once_with(456) + mock_github_cls.assert_called_once_with(auth=mock_installation_auth) + self.assertEqual(result, mock_github) + + @patch("auth.Auth") + @patch("auth.GithubIntegration") + def test_get_github_app_installation_token( + self, mock_integration_cls, mock_auth_cls + ): """ Test the get_github_app_installation_token function. """ dummy_token = "dummytoken" - mock_response = MagicMock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"token": dummy_token} - mock_post.return_value = mock_response + mock_app_auth = MagicMock() + mock_auth_cls.AppAuth.return_value = mock_app_auth + mock_integration = MagicMock() + mock_integration_cls.return_value = mock_integration + mock_access_token = MagicMock() + mock_access_token.token = dummy_token + mock_integration.get_access_token.return_value = mock_access_token result = auth.get_github_app_installation_token( - b"ghe", "gh_private_token", "gh_app_id", "gh_installation_id" + "", "123", b"gh_private_key", "456" ) + mock_auth_cls.AppAuth.assert_called_once_with(123, "gh_private_key") + mock_integration_cls.assert_called_once_with(auth=mock_app_auth) + mock_integration.get_access_token.assert_called_once_with(456) self.assertEqual(result, dummy_token) - @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) - @patch("auth.requests.post") - def test_get_github_app_installation_token_request_failure(self, mock_post): + @patch("auth.Auth") + @patch("auth.GithubIntegration") + def test_get_github_app_installation_token_request_failure( + self, mock_integration_cls, mock_auth_cls + ): """ Test the get_github_app_installation_token function returns None when the request fails. """ - # Mock the post request to raise a RequestException - mock_post.side_effect = requests.exceptions.RequestException("Request failed") + mock_app_auth = MagicMock() + mock_auth_cls.AppAuth.return_value = mock_app_auth + mock_integration = MagicMock() + mock_integration_cls.return_value = mock_integration + mock_integration.get_access_token.side_effect = GithubException( + 500, "Request failed", None + ) - # Call the function with test data result = auth.get_github_app_installation_token( ghe="https://api.github.com", - gh_app_id=12345, + gh_app_id="12345", gh_app_private_key_bytes=b"private_key", - gh_app_installation_id=678910, + gh_app_installation_id="678910", ) - # Assert that the result is None self.assertIsNone(result) - @patch("github3.login") - def test_auth_to_github_invalid_credentials(self, mock_login): + @patch("auth.Auth") + @patch("auth.GithubIntegration") + def test_get_github_app_installation_token_network_failure( + self, mock_integration_cls, mock_auth_cls + ): """ - Test the auth_to_github function raises correct ValueError - when credentials are present but incorrect. + Test the get_github_app_installation_token function returns None on network errors. """ - mock_login.return_value = None - with self.assertRaises(ValueError) as context_manager: - auth.auth_to_github("not_a_valid_token", "", "", b"", "", False) + mock_app_auth = MagicMock() + mock_auth_cls.AppAuth.return_value = mock_app_auth + mock_integration = MagicMock() + mock_integration_cls.return_value = mock_integration + mock_integration.get_access_token.side_effect = ( + requests.exceptions.ConnectionError("Network unreachable") + ) - the_exception = context_manager.exception - self.assertEqual( - str(the_exception), - "Unable to authenticate to GitHub", + result = auth.get_github_app_installation_token( + ghe="", + gh_app_id="12345", + gh_app_private_key_bytes=b"private_key", + gh_app_installation_id="678910", ) + self.assertIsNone(result) + + @patch("auth.Auth") + @patch("auth.GithubIntegration") + def test_get_github_app_installation_token_with_ghe( + self, mock_integration_cls, mock_auth_cls + ): + """ + Test the get_github_app_installation_token function with a GHE URL. + """ + dummy_token = "dummytoken" + mock_app_auth = MagicMock() + mock_auth_cls.AppAuth.return_value = mock_app_auth + mock_integration = MagicMock() + mock_integration_cls.return_value = mock_integration + mock_access_token = MagicMock() + mock_access_token.token = dummy_token + mock_integration.get_access_token.return_value = mock_access_token + + result = auth.get_github_app_installation_token( + "https://github.example.com", "123", b"gh_private_key", "456" + ) + + mock_auth_cls.AppAuth.assert_called_once_with(123, "gh_private_key") + mock_integration_cls.assert_called_once_with( + base_url="https://github.example.com/api/v3", auth=mock_app_auth + ) + mock_integration.get_access_token.assert_called_once_with(456) + self.assertEqual(result, dummy_token) + if __name__ == "__main__": unittest.main() diff --git a/test_contributors.py b/test_contributors.py index 4d6ebc5..f15fd3f 100644 --- a/test_contributors.py +++ b/test_contributors.py @@ -2,6 +2,7 @@ import runpy import unittest +from datetime import datetime from unittest.mock import MagicMock, patch import contributors as contributors_module @@ -25,15 +26,15 @@ def test_get_contributors(self, mock_contributor_stats): "https://avatars.githubusercontent.com/u/12345678?v=4" ) mock_repo.full_name = "owner/repo" - mock_repo.commits.return_value = iter([mock_commit]) + mock_repo.get_commits.return_value = iter([mock_commit]) result = contributors_module.get_contributors( mock_repo, "2022-01-01", "2022-12-31", "" ) self.assertEqual(len(result), 1) - mock_repo.commits.assert_called_once_with( - since="2022-01-01", until="2022-12-31" + mock_repo.get_commits.assert_called_once_with( + since=datetime(2022, 1, 1), until=datetime(2022, 12, 31) ) mock_contributor_stats.assert_called_once_with( "user", @@ -49,7 +50,7 @@ def test_get_all_contributors_with_organization(self, mock_get_contributors): Test the get_all_contributors function when an organization is provided. """ mock_github_connection = MagicMock() - mock_github_connection.organization().repositories.return_value = [ + mock_github_connection.get_organization().get_repos.return_value = [ "repo1", "repo2", ] @@ -89,7 +90,7 @@ def test_get_all_contributors_with_repository(self, mock_get_contributors): Test the get_all_contributors function when a repository is provided. """ mock_github_connection = MagicMock() - mock_github_connection.repository.return_value = "repo" + mock_github_connection.get_repo.return_value = "repo" mock_get_contributors.return_value = [ ContributorStats( "user", @@ -134,7 +135,7 @@ def test_get_contributors_with_single_commit(self, mock_contributor_stats): ) mock_repo.full_name = "owner/repo" - mock_repo.commits.return_value = iter([mock_commit]) + mock_repo.get_commits.return_value = iter([mock_commit]) ghe = "" result = contributors_module.get_contributors( @@ -142,8 +143,8 @@ def test_get_contributors_with_single_commit(self, mock_contributor_stats): ) self.assertEqual(len(result), 1) - mock_repo.commits.assert_called_once_with( - since="2022-01-01", until="2022-12-31" + mock_repo.get_commits.assert_called_once_with( + since=datetime(2022, 1, 1), until=datetime(2022, 12, 31) ) mock_contributor_stats.assert_called_once_with( "user", @@ -166,7 +167,7 @@ def test_get_contributors_skip_bot(self, mock_contributor_stats): ) mock_repo.full_name = "owner/repo" - mock_repo.commits.return_value = iter([mock_commit]) + mock_repo.get_commits.return_value = iter([mock_commit]) ghe = "" result = contributors_module.get_contributors( @@ -186,15 +187,15 @@ def test_get_contributors_no_commit_end_date(self, mock_contributor_stats): mock_user = MagicMock() mock_user.login = "user" mock_user.avatar_url = "https://avatars.githubusercontent.com/u/12345678?v=4" - mock_user.contributions_count = 100 + mock_user.contributions = 100 - mock_repo.contributors.return_value = [mock_user] + mock_repo.get_contributors.return_value = [mock_user] mock_repo.full_name = "owner/repo" ghe = "" contributors_module.get_contributors(mock_repo, "2022-01-01", "", ghe) - mock_repo.commits.assert_not_called() + mock_repo.get_commits.assert_not_called() mock_contributor_stats.assert_called_once_with( "user", "https://avatars.githubusercontent.com/u/12345678?v=4", @@ -210,9 +211,9 @@ def test_get_contributors_no_end_date_skips_bot(self, mock_contributor_stats): mock_bot = MagicMock() mock_bot.login = "dependabot[bot]" mock_bot.avatar_url = "https://avatars.githubusercontent.com/u/0?v=4" - mock_bot.contributions_count = 50 + mock_bot.contributions = 50 - mock_repo.contributors.return_value = [mock_bot] + mock_repo.get_contributors.return_value = [mock_bot] mock_repo.full_name = "owner/repo" ghe = "" @@ -225,7 +226,7 @@ def test_get_contributors_skips_when_no_commits_in_range(self): """Test get_contributors returns empty list when no commits in the date range.""" mock_repo = MagicMock() mock_repo.full_name = "owner/repo" - mock_repo.commits.return_value = iter([]) + mock_repo.get_commits.return_value = iter([]) result = contributors_module.get_contributors( mock_repo, "2022-01-01", "2022-12-31", "" @@ -239,7 +240,7 @@ def test_get_contributors_skips_none_author(self): mock_repo.full_name = "owner/repo" mock_commit = MagicMock() mock_commit.author = None - mock_repo.commits.return_value = iter([mock_commit]) + mock_repo.get_commits.return_value = iter([mock_commit]) result = contributors_module.get_contributors( mock_repo, "2022-01-01", "2022-12-31", "" @@ -248,12 +249,12 @@ def test_get_contributors_skips_none_author(self): self.assertEqual(result, []) def test_get_contributors_skips_dict_author(self): - """Skip commits where github3.py left author as a raw dict instead of a ShortUser.""" + """Skip commits where the API returns an author payload without a matching GitHub user.""" mock_repo = MagicMock() mock_repo.full_name = "owner/repo" mock_commit = MagicMock() mock_commit.author = {"name": "Detached Author", "email": "x@example.com"} - mock_repo.commits.return_value = iter([mock_commit]) + mock_repo.get_commits.return_value = iter([mock_commit]) with patch("builtins.print") as mock_print: result = contributors_module.get_contributors( @@ -271,8 +272,8 @@ def test_get_contributors_no_commit_end_date_skips_user_without_login(self): good_user = MagicMock() good_user.login = "real" good_user.avatar_url = "https://avatars.githubusercontent.com/u/1" - good_user.contributions_count = 5 - mock_repo.contributors.return_value = [bad_user, good_user] + good_user.contributions = 5 + mock_repo.get_contributors.return_value = [bad_user, good_user] with patch("builtins.print") as mock_print: result = contributors_module.get_contributors(mock_repo, "", "", "") @@ -291,7 +292,7 @@ def test_get_contributors_aggregates_multiple_commits(self): mock_commit2 = MagicMock() mock_commit2.author.login = "user" mock_commit2.author.avatar_url = "https://avatars.githubusercontent.com/u/1" - mock_repo.commits.return_value = iter([mock_commit1, mock_commit2]) + mock_repo.get_commits.return_value = iter([mock_commit1, mock_commit2]) result = contributors_module.get_contributors( mock_repo, "2022-01-01", "2022-12-31", "" @@ -311,7 +312,7 @@ def __iter__(self): mock_repo = MagicMock() mock_repo.full_name = "owner/repo" - mock_repo.commits.return_value = BoomIterable() + mock_repo.get_commits.return_value = BoomIterable() with patch("builtins.print") as mock_print: result = contributors_module.get_contributors( @@ -351,8 +352,8 @@ def test_main_runs_under_main_guard(self): mock_auth = MagicMock() mock_github = MagicMock() mock_org = MagicMock() - mock_org.repositories.return_value = [] - mock_github.organization.return_value = mock_org + mock_org.get_repos.return_value = [] + mock_github.get_organization.return_value = mock_org mock_auth.auth_to_github.return_value = mock_github mock_auth.get_github_app_installation_token.return_value = "token" diff --git a/uv.lock b/uv.lock index 23b2aa5..ef8b63d 100644 --- a/uv.lock +++ b/uv.lock @@ -271,7 +271,7 @@ name = "contributors" version = "2.0.0" source = { virtual = "." } dependencies = [ - { name = "github3-py" }, + { name = "pygithub" }, { name = "python-dotenv" }, { name = "requests" }, ] @@ -290,7 +290,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "github3-py", specifier = "==4.0.1" }, + { name = "pygithub", specifier = ">=2.6.0" }, { name = "python-dotenv", specifier = "==1.2.2" }, { name = "requests", specifier = "==2.34.2" }, ] @@ -493,21 +493,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] -[[package]] -name = "github3-py" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-dateutil" }, - { name = "requests" }, - { name = "uritemplate" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/91/603bcaf8cd1b3927de64bf56c3a8915f6653ea7281919140c5bcff2bfe7b/github3.py-4.0.1.tar.gz", hash = "sha256:30d571076753efc389edc7f9aaef338a4fcb24b54d8968d5f39b1342f45ddd36", size = 36214038, upload-time = "2023-04-26T17:56:37.677Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/2394d4fb542574678b0ba342daf734d4d811768da3c2ee0c84d509dcb26c/github3.py-4.0.1-py3-none-any.whl", hash = "sha256:a89af7de25650612d1da2f0609622bcdeb07ee8a45a1c06b2d16a05e4234e753", size = 151800, upload-time = "2023-04-26T17:56:25.015Z" }, -] - [[package]] name = "idna" version = "3.15" @@ -740,6 +725,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, ] +[[package]] +name = "pygithub" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/c3/8465a311197e16cf5ab68789fe689535e90f6b61ab524cc32a39e67237ae/pygithub-2.9.1.tar.gz", hash = "sha256:59771d7ff63d54d427be2e7d0dad2208dfffc2b0a045fec959263787739b611c", size = 2594989, upload-time = "2026-04-14T07:26:13.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/aa/81a5506f089a26338bff17535e4339b3b22049ebd1bcdeff756c4d7a7559/pygithub-2.9.1-py3-none-any.whl", hash = "sha256:2ec78fca30092d51a42d76f4ddb02131b6f0c666a35dfdf364cf302cdda115b9", size = 449710, upload-time = "2026-04-14T07:26:12.382Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -781,6 +782,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/da/acb2e7d4dbd2dfb792d38c0d850481f29ad7049b356d23f56c687d35203b/pylint-4.0.6-py3-none-any.whl", hash = "sha256:d11a0e1fdb7b1cd46ec5d6fc78fee8b95f28695b2d6140e5809925f61e32ea54", size = 538389, upload-time = "2026-06-14T14:43:24.873Z" }, ] +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -811,18 +847,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - [[package]] name = "python-dotenv" version = "1.2.2" @@ -881,15 +905,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - [[package]] name = "tomli" version = "2.4.0" @@ -974,15 +989,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] -[[package]] -name = "uritemplate" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, -] - [[package]] name = "urllib3" version = "2.7.0"