diff --git a/doc/examples/Authentication.rst b/doc/examples/Authentication.rst index c8b72599..685c1650 100644 --- a/doc/examples/Authentication.rst +++ b/doc/examples/Authentication.rst @@ -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 ------------------------------- diff --git a/github/Consts.py b/github/Consts.py index e442ec9d..547a05ba 100644 --- a/github/Consts.py +++ b/github/Consts.py @@ -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. diff --git a/github/GithubIntegration.py b/github/GithubIntegration.py index 83640fa9..2e9ee911 100644 --- a/github/GithubIntegration.py +++ b/github/GithubIntegration.py @@ -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. diff --git a/github/GithubIntegration.pyi b/github/GithubIntegration.pyi index 513becb6..eda9521e 100644 --- a/github/GithubIntegration.pyi +++ b/github/GithubIntegration.pyi @@ -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: ... diff --git a/github/Installation.py b/github/Installation.py index 217ed867..26c1a1a7 100644 --- a/github/Installation.py +++ b/github/Installation.py @@ -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): """ diff --git a/github/Installation.pyi b/github/Installation.pyi index c0026a28..46ec8bcd 100644 --- a/github/Installation.pyi +++ b/github/Installation.pyi @@ -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]: ... diff --git a/github/MainClass.py b/github/MainClass.py index cbec4248..71b24a6a 100644 --- a/github/MainClass.py +++ b/github/MainClass.py @@ -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) diff --git a/github/MainClass.pyi b/github/MainClass.pyi index e0f9be84..5d0e4951 100644 --- a/github/MainClass.pyi +++ b/github/MainClass.pyi @@ -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: ... diff --git a/github/Requester.py b/github/Requester.py index 36e46be6..f97e5bb6 100644 --- a/github/Requester.py +++ b/github/Requester.py @@ -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, diff --git a/tests/Framework.py b/tests/Framework.py index a9b150e0..df644331 100644 --- a/tests/Framework.py +++ b/tests/Framework.py @@ -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 ( diff --git a/tests/GithubApp.py b/tests/GithubApp.py index 558720ed..b0710ff7 100644 --- a/tests/GithubApp.py +++ b/tests/GithubApp.py @@ -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, diff --git a/tests/GithubIntegration.py b/tests/GithubIntegration.py index ab378b40..f205fd95 100644 --- a/tests/GithubIntegration.py +++ b/tests/GithubIntegration.py @@ -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) diff --git a/tests/Installation.py b/tests/Installation.py index f4ea1c09..7529011b 100644 --- a/tests/Installation.py +++ b/tests/Installation.py @@ -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") diff --git a/tests/ReplayData/GithubIntegration.testGetGithubForInstallation.txt b/tests/ReplayData/GithubIntegration.testGetGithubForInstallation.txt new file mode 100644 index 00000000..7cdc4ca1 --- /dev/null +++ b/tests/ReplayData/GithubIntegration.testGetGithubForInstallation.txt @@ -0,0 +1,22 @@ +https +POST +api.github.com +None +/app/installations/36541767/access_tokens +{'Accept': 'application/vnd.github.machine-man-preview+json', 'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +201 +[('Server', 'GitHub.com'), ('Date', 'Mon, 19 Jun 2023 07:14:58 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Content-Length', '231'), ('Cache-Control', 'public, max-age=60, s-maxage=60'), ('Vary', 'Accept, Accept-Encoding, Accept, X-Requested-With'), ('ETag', '"04bc8f50376b119ec74528a85ed2ca423635d43147beced32e7285a87ee752b4"'), ('X-GitHub-Media-Type', 'github.v3; param=machine-man-preview; format=json'), ('x-github-api-version-selected', '2022-11-28'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('X-GitHub-Request-Id', 'D2F2:647B:BB00E92:BD3FE86:64900071')] +{"token":"private_token_removed","expires_at":"2023-06-19T08:14:58Z","permissions":{"metadata":"read"},"repository_selection":"selected"} + +https +GET +api.github.com +None +/repos/PyGithub/PyGithub +{'Authorization': 'token private_token_removed', 'User-Agent': 'PyGithub/Python-Test'} +None +200 +[('Server', 'GitHub.com'), ('Date', 'Mon, 19 Jun 2023 07:14:58 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding, Accept, X-Requested-With'), ('ETag', 'W/"96638f82d16c24c7ac6a39ec94c5019c8934df328cd2d704e9bca157a1fe1e75"'), ('Last-Modified', 'Mon, 19 Jun 2023 02:15:49 GMT'), ('X-GitHub-Media-Type', 'github.v3; format=json'), ('x-github-api-version-selected', '2022-11-28'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4996'), ('X-RateLimit-Reset', '1687161558'), ('X-RateLimit-Used', '4'), ('X-RateLimit-Resource', 'core'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'D2FE:1E0C:D6EFF60:D92EFC2:64900072')] +{"id":3544490,"node_id":"MDEwOlJlcG9zaXRvcnkzNTQ0NDkw","name":"PyGithub","full_name":"PyGithub/PyGithub","private":false,"owner":{"login":"PyGithub","id":11288996,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMjg4OTk2","avatar_url":"https://avatars.githubusercontent.com/u/11288996?v=4","gravatar_id":"","url":"https://api.github.com/users/PyGithub","html_url":"https://github.com/PyGithub","followers_url":"https://api.github.com/users/PyGithub/followers","following_url":"https://api.github.com/users/PyGithub/following{/other_user}","gists_url":"https://api.github.com/users/PyGithub/gists{/gist_id}","starred_url":"https://api.github.com/users/PyGithub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PyGithub/subscriptions","organizations_url":"https://api.github.com/users/PyGithub/orgs","repos_url":"https://api.github.com/users/PyGithub/repos","events_url":"https://api.github.com/users/PyGithub/events{/privacy}","received_events_url":"https://api.github.com/users/PyGithub/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/PyGithub/PyGithub","description":"Typed interactions with the GitHub API v3","fork":false,"url":"https://api.github.com/repos/PyGithub/PyGithub","forks_url":"https://api.github.com/repos/PyGithub/PyGithub/forks","keys_url":"https://api.github.com/repos/PyGithub/PyGithub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/PyGithub/PyGithub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/PyGithub/PyGithub/teams","hooks_url":"https://api.github.com/repos/PyGithub/PyGithub/hooks","issue_events_url":"https://api.github.com/repos/PyGithub/PyGithub/issues/events{/number}","events_url":"https://api.github.com/repos/PyGithub/PyGithub/events","assignees_url":"https://api.github.com/repos/PyGithub/PyGithub/assignees{/user}","branches_url":"https://api.github.com/repos/PyGithub/PyGithub/branches{/branch}","tags_url":"https://api.github.com/repos/PyGithub/PyGithub/tags","blobs_url":"https://api.github.com/repos/PyGithub/PyGithub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/PyGithub/PyGithub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/PyGithub/PyGithub/git/refs{/sha}","trees_url":"https://api.github.com/repos/PyGithub/PyGithub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/PyGithub/PyGithub/statuses/{sha}","languages_url":"https://api.github.com/repos/PyGithub/PyGithub/languages","stargazers_url":"https://api.github.com/repos/PyGithub/PyGithub/stargazers","contributors_url":"https://api.github.com/repos/PyGithub/PyGithub/contributors","subscribers_url":"https://api.github.com/repos/PyGithub/PyGithub/subscribers","subscription_url":"https://api.github.com/repos/PyGithub/PyGithub/subscription","commits_url":"https://api.github.com/repos/PyGithub/PyGithub/commits{/sha}","git_commits_url":"https://api.github.com/repos/PyGithub/PyGithub/git/commits{/sha}","comments_url":"https://api.github.com/repos/PyGithub/PyGithub/comments{/number}","issue_comment_url":"https://api.github.com/repos/PyGithub/PyGithub/issues/comments{/number}","contents_url":"https://api.github.com/repos/PyGithub/PyGithub/contents/{+path}","compare_url":"https://api.github.com/repos/PyGithub/PyGithub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/PyGithub/PyGithub/merges","archive_url":"https://api.github.com/repos/PyGithub/PyGithub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/PyGithub/PyGithub/downloads","issues_url":"https://api.github.com/repos/PyGithub/PyGithub/issues{/number}","pulls_url":"https://api.github.com/repos/PyGithub/PyGithub/pulls{/number}","milestones_url":"https://api.github.com/repos/PyGithub/PyGithub/milestones{/number}","notifications_url":"https://api.github.com/repos/PyGithub/PyGithub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/PyGithub/PyGithub/labels{/name}","releases_url":"https://api.github.com/repos/PyGithub/PyGithub/releases{/id}","deployments_url":"https://api.github.com/repos/PyGithub/PyGithub/deployments","created_at":"2012-02-25T12:53:47Z","updated_at":"2023-06-19T02:15:49Z","pushed_at":"2023-06-18T23:07:29Z","git_url":"git://github.com/PyGithub/PyGithub.git","ssh_url":"git@github.com:PyGithub/PyGithub.git","clone_url":"https://github.com/PyGithub/PyGithub.git","svn_url":"https://github.com/PyGithub/PyGithub","homepage":"https://pygithub.readthedocs.io/","size":14088,"stargazers_count":6075,"watchers_count":6075,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":true,"forks_count":1641,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":247,"license":{"key":"lgpl-3.0","name":"GNU Lesser General Public License v3.0","spdx_id":"LGPL-3.0","url":"https://api.github.com/licenses/lgpl-3.0","node_id":"MDc6TGljZW5zZTEy"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["github","github-api","pygithub","python"],"visibility":"public","forks":1641,"open_issues":247,"watchers":6075,"default_branch":"master","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":false},"temp_clone_token":"","organization":{"login":"PyGithub","id":11288996,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMjg4OTk2","avatar_url":"https://avatars.githubusercontent.com/u/11288996?v=4","gravatar_id":"","url":"https://api.github.com/users/PyGithub","html_url":"https://github.com/PyGithub","followers_url":"https://api.github.com/users/PyGithub/followers","following_url":"https://api.github.com/users/PyGithub/following{/other_user}","gists_url":"https://api.github.com/users/PyGithub/gists{/gist_id}","starred_url":"https://api.github.com/users/PyGithub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PyGithub/subscriptions","organizations_url":"https://api.github.com/users/PyGithub/orgs","repos_url":"https://api.github.com/users/PyGithub/repos","events_url":"https://api.github.com/users/PyGithub/events{/privacy}","received_events_url":"https://api.github.com/users/PyGithub/received_events","type":"Organization","site_admin":false},"network_count":1641,"subscribers_count":115} + diff --git a/tests/ReplayData/Installation.testGetGithubForInstallation.txt b/tests/ReplayData/Installation.testGetGithubForInstallation.txt new file mode 100644 index 00000000..0f9d3218 --- /dev/null +++ b/tests/ReplayData/Installation.testGetGithubForInstallation.txt @@ -0,0 +1,33 @@ +https +GET +api.github.com +None +/app/installations?per_page=40 +{'Accept': 'application/vnd.github.machine-man-preview+json', 'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python-Test'} +None +200 +[('Server', 'GitHub.com'), ('Date', 'Mon, 19 Jun 2023 06:59:50 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Cache-Control', 'public, max-age=60, s-maxage=60'), ('Vary', 'Accept, Accept-Encoding, Accept, X-Requested-With'), ('ETag', 'W/"b272db57fded7547562b4a357681c6c84054e8fb5169b32206e833bbe7f542e5"'), ('X-GitHub-Media-Type', 'github.v3; param=machine-man-preview; format=json'), ('x-github-api-version-selected', '2022-11-28'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'B368:A85B:CEF8FE3:D136340:648FFCE5')] +[{"id":36541767,"account":{"login":"EnricoMi","id":44700269,"node_id":"MDQ6VXNlcjQ0NzAwMjY5","avatar_url":"https://avatars.githubusercontent.com/u/44700269?v=4","gravatar_id":"","url":"https://api.github.com/users/EnricoMi","html_url":"https://github.com/EnricoMi","followers_url":"https://api.github.com/users/EnricoMi/followers","following_url":"https://api.github.com/users/EnricoMi/following{/other_user}","gists_url":"https://api.github.com/users/EnricoMi/gists{/gist_id}","starred_url":"https://api.github.com/users/EnricoMi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/EnricoMi/subscriptions","organizations_url":"https://api.github.com/users/EnricoMi/orgs","repos_url":"https://api.github.com/users/EnricoMi/repos","events_url":"https://api.github.com/users/EnricoMi/events{/privacy}","received_events_url":"https://api.github.com/users/EnricoMi/received_events","type":"User","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/36541767/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/settings/installations/36541767","app_id":319953,"app_slug":"publish-test-results","target_id":44700269,"target_type":"User","permissions":{"checks":"write","issues":"read","contents":"read","metadata":"read","pull_requests":"write"},"events":[],"created_at":"2023-04-17T16:18:05.000Z","updated_at":"2023-06-08T07:38:12.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null}] + +https +POST +api.github.com +None +/app/installations/36541767/access_tokens +{'Accept': 'application/vnd.github.machine-man-preview+json', 'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +201 +[('Server', 'GitHub.com'), ('Date', 'Mon, 19 Jun 2023 06:59:50 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Content-Length', '231'), ('Cache-Control', 'public, max-age=60, s-maxage=60'), ('Vary', 'Accept, Accept-Encoding, Accept, X-Requested-With'), ('ETag', '"056522667824941d4f276f2d2051ea5c4fd160bd6be7da7765aa63926fc99593"'), ('X-GitHub-Media-Type', 'github.v3; param=machine-man-preview; format=json'), ('x-github-api-version-selected', '2022-11-28'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('X-GitHub-Request-Id', 'B370:F0F6:D6213DB:D85E7E7:648FFCE6')] +{"token":"private_token_removed","expires_at":"2023-06-19T07:59:50Z","permissions":{"metadata":"read"},"repository_selection":"selected"} + +https +GET +api.github.com +None +/repos/PyGithub/PyGithub +{'Authorization': 'token private_token_removed', 'User-Agent': 'PyGithub/Python-Test'} +None +200 +[('Server', 'GitHub.com'), ('Date', 'Mon, 19 Jun 2023 06:59:50 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding, Accept, X-Requested-With'), ('ETag', 'W/"f8d860d9f470388781718161c81385fc94b9326916fb10534be0a0942e0a8ee8"'), ('Last-Modified', 'Mon, 19 Jun 2023 02:15:49 GMT'), ('X-GitHub-Media-Type', 'github.v3; format=json'), ('x-github-api-version-selected', '2022-11-28'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4998'), ('X-RateLimit-Reset', '1687161558'), ('X-RateLimit-Used', '2'), ('X-RateLimit-Resource', 'core'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'B37A:102E8:CDC2411:CFFF826:648FFCE6')] +{"id":3544490,"node_id":"MDEwOlJlcG9zaXRvcnkzNTQ0NDkw","name":"PyGithub","full_name":"PyGithub/PyGithub","private":false,"owner":{"login":"PyGithub","id":11288996,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMjg4OTk2","avatar_url":"https://avatars.githubusercontent.com/u/11288996?v=4","gravatar_id":"","url":"https://api.github.com/users/PyGithub","html_url":"https://github.com/PyGithub","followers_url":"https://api.github.com/users/PyGithub/followers","following_url":"https://api.github.com/users/PyGithub/following{/other_user}","gists_url":"https://api.github.com/users/PyGithub/gists{/gist_id}","starred_url":"https://api.github.com/users/PyGithub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PyGithub/subscriptions","organizations_url":"https://api.github.com/users/PyGithub/orgs","repos_url":"https://api.github.com/users/PyGithub/repos","events_url":"https://api.github.com/users/PyGithub/events{/privacy}","received_events_url":"https://api.github.com/users/PyGithub/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/PyGithub/PyGithub","description":"Typed interactions with the GitHub API v3","fork":false,"url":"https://api.github.com/repos/PyGithub/PyGithub","forks_url":"https://api.github.com/repos/PyGithub/PyGithub/forks","keys_url":"https://api.github.com/repos/PyGithub/PyGithub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/PyGithub/PyGithub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/PyGithub/PyGithub/teams","hooks_url":"https://api.github.com/repos/PyGithub/PyGithub/hooks","issue_events_url":"https://api.github.com/repos/PyGithub/PyGithub/issues/events{/number}","events_url":"https://api.github.com/repos/PyGithub/PyGithub/events","assignees_url":"https://api.github.com/repos/PyGithub/PyGithub/assignees{/user}","branches_url":"https://api.github.com/repos/PyGithub/PyGithub/branches{/branch}","tags_url":"https://api.github.com/repos/PyGithub/PyGithub/tags","blobs_url":"https://api.github.com/repos/PyGithub/PyGithub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/PyGithub/PyGithub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/PyGithub/PyGithub/git/refs{/sha}","trees_url":"https://api.github.com/repos/PyGithub/PyGithub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/PyGithub/PyGithub/statuses/{sha}","languages_url":"https://api.github.com/repos/PyGithub/PyGithub/languages","stargazers_url":"https://api.github.com/repos/PyGithub/PyGithub/stargazers","contributors_url":"https://api.github.com/repos/PyGithub/PyGithub/contributors","subscribers_url":"https://api.github.com/repos/PyGithub/PyGithub/subscribers","subscription_url":"https://api.github.com/repos/PyGithub/PyGithub/subscription","commits_url":"https://api.github.com/repos/PyGithub/PyGithub/commits{/sha}","git_commits_url":"https://api.github.com/repos/PyGithub/PyGithub/git/commits{/sha}","comments_url":"https://api.github.com/repos/PyGithub/PyGithub/comments{/number}","issue_comment_url":"https://api.github.com/repos/PyGithub/PyGithub/issues/comments{/number}","contents_url":"https://api.github.com/repos/PyGithub/PyGithub/contents/{+path}","compare_url":"https://api.github.com/repos/PyGithub/PyGithub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/PyGithub/PyGithub/merges","archive_url":"https://api.github.com/repos/PyGithub/PyGithub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/PyGithub/PyGithub/downloads","issues_url":"https://api.github.com/repos/PyGithub/PyGithub/issues{/number}","pulls_url":"https://api.github.com/repos/PyGithub/PyGithub/pulls{/number}","milestones_url":"https://api.github.com/repos/PyGithub/PyGithub/milestones{/number}","notifications_url":"https://api.github.com/repos/PyGithub/PyGithub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/PyGithub/PyGithub/labels{/name}","releases_url":"https://api.github.com/repos/PyGithub/PyGithub/releases{/id}","deployments_url":"https://api.github.com/repos/PyGithub/PyGithub/deployments","created_at":"2012-02-25T12:53:47Z","updated_at":"2023-06-19T02:15:49Z","pushed_at":"2023-06-18T23:07:29Z","git_url":"git://github.com/PyGithub/PyGithub.git","ssh_url":"git@github.com:PyGithub/PyGithub.git","clone_url":"https://github.com/PyGithub/PyGithub.git","svn_url":"https://github.com/PyGithub/PyGithub","homepage":"https://pygithub.readthedocs.io/","size":14088,"stargazers_count":6075,"watchers_count":6075,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":true,"forks_count":1641,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":247,"license":{"key":"lgpl-3.0","name":"GNU Lesser General Public License v3.0","spdx_id":"LGPL-3.0","url":"https://api.github.com/licenses/lgpl-3.0","node_id":"MDc6TGljZW5zZTEy"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["github","github-api","pygithub","python"],"visibility":"public","forks":1641,"open_issues":247,"watchers":6075,"default_branch":"master","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":false},"temp_clone_token":"","organization":{"login":"PyGithub","id":11288996,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMjg4OTk2","avatar_url":"https://avatars.githubusercontent.com/u/11288996?v=4","gravatar_id":"","url":"https://api.github.com/users/PyGithub","html_url":"https://github.com/PyGithub","followers_url":"https://api.github.com/users/PyGithub/followers","following_url":"https://api.github.com/users/PyGithub/following{/other_user}","gists_url":"https://api.github.com/users/PyGithub/gists{/gist_id}","starred_url":"https://api.github.com/users/PyGithub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PyGithub/subscriptions","organizations_url":"https://api.github.com/users/PyGithub/orgs","repos_url":"https://api.github.com/users/PyGithub/repos","events_url":"https://api.github.com/users/PyGithub/events{/privacy}","received_events_url":"https://api.github.com/users/PyGithub/received_events","type":"Organization","site_admin":false},"network_count":1641,"subscribers_count":115} + diff --git a/tests/Requester.py b/tests/Requester.py index a9bfb2a1..57b5184b 100644 --- a/tests/Requester.py +++ b/tests/Requester.py @@ -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(