Sync GithubIntegration __init__ arguments with github.Github (#2556)

Github and GithubIntegration now both support the same (full) set of Requester arguments.
Creating a Github instance for a Github App Installation coming from GithubIntegration uses the same
Requester arguments (except for auth).
This commit is contained in:
Enrico Minack
2023-06-21 09:04:19 +02:00
committed by GitHub
parent f4e9dcb341
commit ea45237d3b
16 changed files with 333 additions and 27 deletions
+9
View File
@@ -51,6 +51,15 @@ when authenticated as a Github App:
... installation.id
'1234567'
Get a ``github.Github`` instance authenticated as an App installation:
.. code-block:: python
>>> installation = gi.get_installations()[0]
>>> g = installation.get_github_for_installation()
>>> g.get_repo("user/repo").name
'repo'
App installation authentication
-------------------------------
+1
View File
@@ -134,6 +134,7 @@ repoVisibilityPreview = "application/vnd.github.nebula-preview+json"
DEFAULT_BASE_URL = "https://api.github.com"
DEFAULT_STATUS_URL = "https://status.github.com"
DEFAULT_USER_AGENT = "PyGithub/Python"
# As of 2018-05-17, Github imposes a 10s limit for completion of API requests.
# Thus, the timeout should be slightly > 10s to account for network/front-end
# latency.
+39 -6
View File
@@ -1,7 +1,9 @@
import warnings
import deprecated
import urllib3
import github
from github import Consts
from github.Auth import AppAuth
from github.GithubApp import GithubApp
@@ -17,6 +19,7 @@ class GithubIntegration:
Main class to obtain tokens for a GitHub integration.
"""
# keep non-deprecated arguments in-sync with Requester
# v2: remove integration_id, private_key, jwt_expiry, jwt_issued_at and jwt_algorithm
# v2: move auth to the front of arguments
# v2: add * before first argument so all arguments must be named,
@@ -26,6 +29,12 @@ class GithubIntegration:
integration_id=None,
private_key=None,
base_url=Consts.DEFAULT_BASE_URL,
timeout=Consts.DEFAULT_TIMEOUT,
user_agent=Consts.DEFAULT_USER_AGENT,
per_page=Consts.DEFAULT_PER_PAGE,
verify=True,
retry=None,
pool_size=None,
jwt_expiry=Consts.DEFAULT_JWT_EXPIRY,
jwt_issued_at=Consts.DEFAULT_JWT_ISSUED_AT,
jwt_algorithm=Consts.DEFAULT_JWT_ALGORITHM,
@@ -35,8 +44,15 @@ class GithubIntegration:
:param integration_id: int deprecated, use auth=github.Auth.AppAuth(...) instead
:param private_key: string deprecated, use auth=github.Auth.AppAuth(...) instead
:param base_url: string
:param timeout: integer
:param user_agent: string
:param per_page: int
:param verify: boolean or string
:param retry: int or urllib3.util.retry.Retry object
:param pool_size: int
:param jwt_expiry: int deprecated, use auth=github.Auth.AppAuth(...) instead
:param jwt_issued_at: int deprecated, use auth=github.Auth.AppAuth(...) instead
:param jwt_algorithm: string deprecated, use auth=github.Auth.AppAuth(...) instead
:param auth: authentication method
"""
if integration_id is not None:
@@ -46,6 +62,16 @@ class GithubIntegration:
private_key, str
), "supplied private key should be a string"
assert isinstance(base_url, str), base_url
assert isinstance(timeout, int), timeout
assert user_agent is None or isinstance(user_agent, str), user_agent
assert isinstance(per_page, int), per_page
assert isinstance(verify, (bool, str)), verify
assert (
retry is None
or isinstance(retry, int)
or isinstance(retry, urllib3.util.Retry)
), retry
assert pool_size is None or isinstance(pool_size, int), pool_size
assert isinstance(jwt_expiry, int), jwt_expiry
assert Consts.MIN_JWT_EXPIRY <= jwt_expiry <= Consts.MAX_JWT_EXPIRY, jwt_expiry
assert isinstance(jwt_issued_at, int)
@@ -81,14 +107,21 @@ class GithubIntegration:
self.__requester = Requester(
auth=auth,
base_url=self.base_url,
timeout=Consts.DEFAULT_TIMEOUT,
user_agent="PyGithub/Python",
per_page=Consts.DEFAULT_PER_PAGE,
verify=True,
retry=None,
pool_size=None,
timeout=timeout,
user_agent=user_agent,
per_page=per_page,
verify=verify,
retry=retry,
pool_size=pool_size,
)
def get_github_for_installation(self, installation_id):
# The installation has to authenticate as an installation, not an app
auth = self.auth.get_installation_auth(
installation_id, requester=self.__requester
)
return github.Github(**self.__requester.withAuth(auth).kwargs)
def _get_headers(self):
"""
Get headers for the requests.
+10
View File
@@ -1,5 +1,8 @@
from typing import Union, Optional, Dict
from urllib3 import Retry
import github
from github.Auth import AppAuth
from github.Installation import Installation
from github.InstallationAuthorization import InstallationAuthorization
@@ -15,11 +18,18 @@ class GithubIntegration:
integration_id: Optional[Union[int, str]] = ...,
private_key: Optional[str] = ...,
base_url: str = ...,
timeout: int = ...,
user_agent: str = ...,
per_page: int = ...,
verify: Union[bool, str] = ...,
retry: Optional[Union[int, Retry]] = ...,
pool_size: Optional[int] = ...,
jwt_expiry: int = ...,
jwt_issued_at: int = ...,
jwt_algorithm: str = ...,
auth: Optional[AppAuth] = ...,
) -> None: ...
def get_github_for_installation(self, installation_id: int) -> github.Github: ...
def _get_installed_app(self, url: str) -> Installation: ...
def _get_headers(self) -> Dict[str, str]: ...
def create_jwt(self, expiration: Optional[int] = ...) -> str: ...
+3
View File
@@ -60,6 +60,9 @@ class Installation(github.GithubObject.NonCompletableGithubObject):
def __repr__(self):
return self.get__repr__({"id": self._id.value})
def get_github_for_installation(self):
return github.Github(**self._requester.kwargs)
@property
def id(self):
"""
+2
View File
@@ -1,5 +1,6 @@
from typing import Any, Dict
import github
from github.GithubObject import NonCompletableGithubObject
from github.PaginatedList import PaginatedList
from github.Repository import Repository
@@ -7,6 +8,7 @@ from github.Repository import Repository
class Installation(NonCompletableGithubObject):
def _initAttributes(self) -> None: ...
def _useAttributes(self, attributes: Dict[str, Any]) -> None: ...
def get_github_for_installation(self) -> github.Github: ...
@property
def id(self) -> int: ...
def get_repos(self) -> PaginatedList[Repository]: ...
+4 -1
View File
@@ -83,6 +83,7 @@ class Github:
This is the main class you instantiate to access the Github API v3. Optional parameters allow different authentication methods.
"""
# keep non-deprecated arguments in-sync with Requester
# v2: remove login_or_token, password, jwt and app_auth
# v2: move auth to the front of arguments
# v2: add * before first argument so all arguments must be named,
@@ -95,7 +96,7 @@ class Github:
app_auth=None,
base_url=Consts.DEFAULT_BASE_URL,
timeout=Consts.DEFAULT_TIMEOUT,
user_agent="PyGithub/Python",
user_agent=Consts.DEFAULT_USER_AGENT,
per_page=Consts.DEFAULT_PER_PAGE,
verify=True,
retry=None,
@@ -123,6 +124,8 @@ class Github:
assert isinstance(base_url, str), base_url
assert isinstance(timeout, int), timeout
assert user_agent is None or isinstance(user_agent, str), user_agent
assert isinstance(per_page, int), per_page
assert isinstance(verify, (bool, str)), verify
assert (
retry is None
or isinstance(retry, int)
+3 -1
View File
@@ -38,7 +38,7 @@ class Github:
timeout: int = ...,
user_agent: str = ...,
per_page: int = ...,
verify: bool = ...,
verify: Union[bool, str] = ...,
retry: Optional[Union[int, Retry]] = ...,
pool_size: Optional[int] = ...,
auth: Optional[Auth] = ...,
@@ -95,6 +95,8 @@ class Github:
self, since: Union[int, _NotSetType] = ...
) -> PaginatedList[NamedUser]: ...
def load(self, f: BytesIO) -> Repository: ...
# argument slug is deprecated, not included here
def get_app(self): ...
def get_oauth_application(
self, client_id: str, client_secret: str
) -> ApplicationOAuth: ...
+24 -12
View File
@@ -344,6 +344,7 @@ class Requester:
__hostname: str
__authorizationHeader: Optional[str]
# keep arguments in-sync with github.MainClass and GithubIntegration
def __init__(
self,
auth: Optional["Auth"],
@@ -351,7 +352,7 @@ class Requester:
timeout: int,
user_agent: str,
per_page: int,
verify: bool,
verify: Union[bool, str],
retry: Optional[Union[int, Retry]],
pool_size: Optional[int],
):
@@ -394,6 +395,24 @@ class Requester:
if isinstance(self.__auth, WithRequester):
self.__auth.withRequester(self)
@property
def kwargs(self):
"""
Returns arguments required to recreate this Requester with Requester.__init__, as well as
with MainClass.__init__ and GithubIntegration.__init__.
:return:
"""
return dict(
auth=self.__auth,
base_url=self.__base_url,
timeout=self.__timeout,
user_agent=self.__userAgent,
per_page=self.per_page,
verify=self.__verify,
retry=self.__retry,
pool_size=self.__pool_size,
)
@property
def base_url(self) -> str:
return self.__base_url
@@ -406,18 +425,11 @@ class Requester:
"""
Create a new requester instance with identical configuration but the given authentication method.
:param auth: authentication method
:return: new Reqester implementation
:return: new Requester implementation
"""
return Requester(
auth=auth,
base_url=self.__base_url,
timeout=self.__timeout,
user_agent=self.__userAgent,
per_page=self.per_page,
verify=self.__verify,
retry=self.__retry,
pool_size=self.__pool_size,
)
kwargs = self.kwargs
kwargs.update(auth=auth)
return Requester(**kwargs)
def requestJsonAndCheck(
self,
+7
View File
@@ -34,6 +34,7 @@
# #
################################################################################
import contextlib
import io
import json
import os
@@ -344,6 +345,12 @@ class BasicTestCase(unittest.TestCase):
]
self.assertSequenceEqual(actual, expected)
@contextlib.contextmanager
def ignoreWarning(self, category=Warning, module=""):
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=category, module=module)
yield
def __openFile(self, mode):
for (_, _, functionName, _) in traceback.extract_stack():
if (
+3 -6
View File
@@ -106,12 +106,9 @@ class GithubApp(Framework.TestCase):
g = github.Github(auth=auth)
with self.assertWarns(DeprecationWarning) as warning:
# we ignore warnings from httpretty dependency
import warnings
warnings.filterwarnings("ignore", module="httpretty")
app = g.get_app()
# httpretty has some deprecation warnings in Python 3.12
with self.ignoreWarning(category=DeprecationWarning, module="httpretty"):
app = g.get_app()
self.assertWarning(
warning,
+43
View File
@@ -1,8 +1,11 @@
import time # NOQA
import requests # NOQA
from urllib3.exceptions import InsecureRequestWarning
import github
from github import Consts
from github.Auth import AppInstallationAuth
from . import Framework
@@ -86,6 +89,46 @@ class GithubIntegration(Framework.BasicTestCase):
self.assertEqual(installations[0].id, self.org_installation_id)
self.assertEqual(installations[1].id, self.repo_installation_id)
def testGetGithubForInstallation(self):
# with verify=False, urllib3.connectionpool rightly may issue an InsecureRequestWarning
# we ignore InsecureRequestWarning from urllib3.connectionpool
with self.ignoreWarning(
category=InsecureRequestWarning, module="urllib3.connectionpool"
):
auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY)
github_integration = github.GithubIntegration(
auth=auth,
base_url="https://api.github.com",
timeout=Consts.DEFAULT_TIMEOUT + 10,
user_agent="PyGithub/Python-Test",
per_page=Consts.DEFAULT_PER_PAGE + 10,
verify=False,
retry=3,
pool_size=10,
)
g = github_integration.get_github_for_installation(36541767)
self.assertIsInstance(g._Github__requester.auth, AppInstallationAuth)
self.assertEqual(
g._Github__requester._Requester__base_url, "https://api.github.com"
)
self.assertEqual(
g._Github__requester._Requester__timeout, Consts.DEFAULT_TIMEOUT + 10
)
self.assertEqual(
g._Github__requester._Requester__userAgent, "PyGithub/Python-Test"
)
self.assertEqual(
g._Github__requester.per_page, Consts.DEFAULT_PER_PAGE + 10
)
self.assertEqual(g._Github__requester._Requester__verify, False)
self.assertEqual(g._Github__requester._Requester__retry, 3)
self.assertEqual(g._Github__requester._Requester__pool_size, 10)
repo = g.get_repo("PyGithub/PyGithub")
self.assertEqual(repo.full_name, "PyGithub/PyGithub")
def testGetAccessToken(self):
auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY)
github_integration = github.GithubIntegration(auth=auth)
+46 -1
View File
@@ -20,8 +20,11 @@
# #
################################################################################
from urllib3.exceptions import InsecureRequestWarning
import github
from github.Auth import AppAuth
from github import Consts
from github.Auth import AppAuth, AppInstallationAuth
from . import Framework, GithubIntegration
@@ -44,3 +47,45 @@ class Installation(Framework.BasicTestCase):
self.assertListEqual(
[repo.full_name for repo in repos], ["EnricoMi/sandbox", "EnricoMi/python"]
)
def testGetGithubForInstallation(self):
# with verify=False, urllib3.connectionpool rightly may issue an InsecureRequestWarning
# we ignore InsecureRequestWarning from urllib3.connectionpool
with self.ignoreWarning(
category=InsecureRequestWarning, module="urllib3.connectionpool"
):
self.auth = AppAuth(319953, GithubIntegration.PRIVATE_KEY)
self.integration = github.GithubIntegration(
auth=self.auth,
base_url="https://api.github.com",
timeout=Consts.DEFAULT_TIMEOUT + 10,
user_agent="PyGithub/Python-Test",
per_page=Consts.DEFAULT_PER_PAGE + 10,
verify=False,
retry=3,
pool_size=10,
)
installations = list(self.integration.get_installations())
installation = installations[0]
g = installation.get_github_for_installation()
self.assertIsInstance(g._Github__requester.auth, AppInstallationAuth)
self.assertEqual(
g._Github__requester._Requester__base_url, "https://api.github.com"
)
self.assertEqual(
g._Github__requester._Requester__timeout, Consts.DEFAULT_TIMEOUT + 10
)
self.assertEqual(
g._Github__requester._Requester__userAgent, "PyGithub/Python-Test"
)
self.assertEqual(
g._Github__requester.per_page, Consts.DEFAULT_PER_PAGE + 10
)
self.assertEqual(g._Github__requester._Requester__verify, False)
self.assertEqual(g._Github__requester._Requester__retry, 3)
self.assertEqual(g._Github__requester._Requester__pool_size, 10)
repo = g.get_repo("PyGithub/PyGithub")
self.assertEqual(repo.full_name, "PyGithub/PyGithub")
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+84
View File
@@ -39,6 +39,90 @@ class Requester(Framework.TestCase):
github.Requester.Requester.resetLogger()
super().tearDown()
def testRecreation(self):
class TestAuth(github.Auth.AppAuth):
pass
# create a Requester with non-default arguments
auth = TestAuth(123, "key")
requester = github.Requester.Requester(
auth=auth,
base_url="https://base.url",
timeout=1,
user_agent="user agent",
per_page=123,
verify=False,
retry=3,
pool_size=5,
)
kwargs = requester.kwargs
# assert kwargs consists of ALL constructor arguments
self.assertEqual(
kwargs.keys(), github.Requester.Requester.__init__.__annotations__.keys()
)
self.assertEqual(
kwargs,
dict(
auth=auth,
base_url="https://base.url",
timeout=1,
user_agent="user agent",
per_page=123,
verify=False,
retry=3,
pool_size=5,
),
)
# create a copy Requester, assert identity via kwargs
copy = github.Requester.Requester(**kwargs)
self.assertEqual(copy.kwargs, kwargs)
# create Github instance, assert identity requester
gh = github.Github(**kwargs)
self.assertEqual(gh._Github__requester.kwargs, kwargs)
# create GithubIntegration instance, assert identity requester
gi = github.GithubIntegration(**kwargs)
self.assertEqual(gi._GithubIntegration__requester.kwargs, kwargs)
def testWithAuth(self):
class TestAuth(github.Auth.AppAuth):
pass
# create a Requester with non-default arguments
auth = TestAuth(123, "key")
requester = github.Requester.Requester(
auth=auth,
base_url="https://base.url",
timeout=1,
user_agent="user agent",
per_page=123,
verify=False,
retry=3,
pool_size=5,
)
# create a copy with different auth
auth2 = TestAuth(456, "key2")
copy = requester.withAuth(auth2)
# assert kwargs of copy
self.assertEqual(
copy.kwargs,
dict(
auth=auth2,
base_url="https://base.url",
timeout=1,
user_agent="user agent",
per_page=123,
verify=False,
retry=3,
pool_size=5,
),
)
def testLoggingRedirection(self):
self.assertEqual(self.g.get_repo("EnricoMi/test").name, "test-renamed")
self.logger.info.assert_called_once_with(