1# -*- coding: utf-8 -*- 2 3############################ Copyrights and license ############################ 4# # 5# Copyright 2013 AKFish <akfish@gmail.com> # 6# Copyright 2013 Ed Jackson <ed.jackson@gmail.com> # 7# Copyright 2013 Jonathan J Hunt <hunt@braincorporation.com> # 8# Copyright 2013 Peter Golm <golm.peter@gmail.com> # 9# Copyright 2013 Steve Brown <steve@evolvedlight.co.uk> # 10# Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # 11# Copyright 2014 C. R. Oldham <cro@ncbt.org> # 12# Copyright 2014 Thialfihar <thi@thialfihar.org> # 13# Copyright 2014 Tyler Treat <ttreat31@gmail.com> # 14# Copyright 2014 Vincent Jacques <vincent@vincent-jacques.net> # 15# Copyright 2015 Daniel Pocock <daniel@pocock.pro> # 16# Copyright 2015 Joseph Rawson <joseph.rawson.works@littledebian.org> # 17# Copyright 2015 Uriel Corfa <uriel@corfa.fr> # 18# Copyright 2015 edhollandAL <eholland@alertlogic.com> # 19# Copyright 2016 Jannis Gebauer <ja.geb@me.com> # 20# Copyright 2016 Peter Buckley <dx-pbuckley@users.noreply.github.com> # 21# Copyright 2017 Colin Hoglund <colinhoglund@users.noreply.github.com> # 22# Copyright 2017 Jannis Gebauer <ja.geb@me.com> # 23# Copyright 2018 Agor Maxime <maxime.agor23@gmail.com> # 24# Copyright 2018 Joshua Hoblitt <josh@hoblitt.com> # 25# Copyright 2018 Maarten Fonville <mfonville@users.noreply.github.com> # 26# Copyright 2018 Mike Miller <github@mikeage.net> # 27# Copyright 2018 Svend Sorensen <svend@svends.net> # 28# Copyright 2018 Wan Liuyang <tsfdye@gmail.com> # 29# Copyright 2018 sfdye <tsfdye@gmail.com> # 30# Copyright 2018 itsbruce <it.is.bruce@gmail.com> # 31# Copyright 2019 Tomas Tomecek <tomas@tomecek.net> # 32# Copyright 2019 Rigas Papathanasopoulos <rigaspapas@gmail.com> # 33# # 34# This file is part of PyGithub. # 35# http://pygithub.readthedocs.io/ # 36# # 37# PyGithub is free software: you can redistribute it and/or modify it under # 38# the terms of the GNU Lesser General Public License as published by the Free # 39# Software Foundation, either version 3 of the License, or (at your option) # 40# any later version. # 41# # 42# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY # 43# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # 44# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # 45# details. # 46# # 47# You should have received a copy of the GNU Lesser General Public License # 48# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # 49# # 50################################################################################ 51 52import datetime 53import pickle 54import time 55import warnings 56 57import jwt 58import requests 59import urllib3 60 61import github.ApplicationOAuth 62import github.Event 63import github.Gist 64import github.GithubObject 65import github.License 66import github.NamedUser 67import github.PaginatedList 68import github.Topic 69 70from . import ( 71 AuthenticatedUser, 72 Consts, 73 GithubApp, 74 GithubException, 75 GitignoreTemplate, 76 HookDescription, 77 Installation, 78 InstallationAuthorization, 79 RateLimit, 80 Repository, 81) 82from .Requester import Requester 83 84DEFAULT_BASE_URL = "https://api.github.com" 85DEFAULT_STATUS_URL = "https://status.github.com" 86# As of 2018-05-17, Github imposes a 10s limit for completion of API requests. 87# Thus, the timeout should be slightly > 10s to account for network/front-end 88# latency. 89DEFAULT_TIMEOUT = 15 90DEFAULT_PER_PAGE = 30 91 92 93class Github(object): 94 """ 95 This is the main class you instantiate to access the Github API v3. Optional parameters allow different authentication methods. 96 """ 97 98 def __init__( 99 self, 100 login_or_token=None, 101 password=None, 102 jwt=None, 103 base_url=DEFAULT_BASE_URL, 104 timeout=DEFAULT_TIMEOUT, 105 client_id=None, 106 client_secret=None, 107 user_agent="PyGithub/Python", 108 per_page=DEFAULT_PER_PAGE, 109 verify=True, 110 retry=None, 111 ): 112 """ 113 :param login_or_token: string 114 :param password: string 115 :param base_url: string 116 :param timeout: integer 117 :param client_id: string 118 :param client_secret: string 119 :param user_agent: string 120 :param per_page: int 121 :param verify: boolean or string 122 :param retry: int or urllib3.util.retry.Retry object 123 """ 124 125 assert login_or_token is None or isinstance(login_or_token, str), login_or_token 126 assert password is None or isinstance(password, str), password 127 assert jwt is None or isinstance(jwt, str), jwt 128 assert isinstance(base_url, str), base_url 129 assert isinstance(timeout, int), timeout 130 assert client_id is None or isinstance(client_id, str), client_id 131 assert client_secret is None or isinstance(client_secret, str), client_secret 132 assert user_agent is None or isinstance(user_agent, str), user_agent 133 assert ( 134 retry is None 135 or isinstance(retry, (int)) 136 or isinstance(retry, (urllib3.util.Retry)) 137 ) 138 if client_id is not None or client_secret is not None: 139 warnings.warn( 140 "client_id and client_secret are deprecated and will be removed in a future release, switch to token authentication", 141 FutureWarning, 142 stacklevel=2, 143 ) 144 self.__requester = Requester( 145 login_or_token, 146 password, 147 jwt, 148 base_url, 149 timeout, 150 client_id, 151 client_secret, 152 user_agent, 153 per_page, 154 verify, 155 retry, 156 ) 157 158 def __get_FIX_REPO_GET_GIT_REF(self): 159 """ 160 :type: bool 161 """ 162 return self.__requester.FIX_REPO_GET_GIT_REF 163 164 def __set_FIX_REPO_GET_GIT_REF(self, value): 165 self.__requester.FIX_REPO_GET_GIT_REF = value 166 167 FIX_REPO_GET_GIT_REF = property( 168 __get_FIX_REPO_GET_GIT_REF, __set_FIX_REPO_GET_GIT_REF 169 ) 170 171 def __get_per_page(self): 172 """ 173 :type: int 174 """ 175 return self.__requester.per_page 176 177 def __set_per_page(self, value): 178 self.__requester.per_page = value 179 180 # v2: Remove this property? Why should it be necessary to read/modify it after construction 181 per_page = property(__get_per_page, __set_per_page) 182 183 # v2: Provide a unified way to access values of headers of last response 184 # v2: (and add/keep ad hoc properties for specific useful headers like rate limiting, oauth scopes, etc.) 185 # v2: Return an instance of a class: using a tuple did not allow to add a field "resettime" 186 @property 187 def rate_limiting(self): 188 """ 189 First value is requests remaining, second value is request limit. 190 191 :type: (int, int) 192 """ 193 remaining, limit = self.__requester.rate_limiting 194 if limit < 0: 195 self.get_rate_limit() 196 return self.__requester.rate_limiting 197 198 @property 199 def rate_limiting_resettime(self): 200 """ 201 Unix timestamp indicating when rate limiting will reset. 202 203 :type: int 204 """ 205 if self.__requester.rate_limiting_resettime == 0: 206 self.get_rate_limit() 207 return self.__requester.rate_limiting_resettime 208 209 def get_rate_limit(self): 210 """ 211 Rate limit status for different resources (core/search/graphql). 212 213 :calls: `GET /rate_limit <http://developer.github.com/v3/rate_limit>`_ 214 :rtype: :class:`github.RateLimit.RateLimit` 215 """ 216 headers, data = self.__requester.requestJsonAndCheck("GET", "/rate_limit") 217 return RateLimit.RateLimit(self.__requester, headers, data["resources"], True) 218 219 @property 220 def oauth_scopes(self): 221 """ 222 :type: list of string 223 """ 224 return self.__requester.oauth_scopes 225 226 def get_license(self, key=github.GithubObject.NotSet): 227 """ 228 :calls: `GET /license/:license <https://developer.github.com/v3/licenses/#get-an-individual-license>`_ 229 :param key: string 230 :rtype: :class:`github.License.License` 231 """ 232 233 assert isinstance(key, str), key 234 headers, data = self.__requester.requestJsonAndCheck("GET", "/licenses/" + key) 235 return github.License.License(self.__requester, headers, data, completed=True) 236 237 def get_licenses(self): 238 """ 239 :calls: `GET /licenses <https://developer.github.com/v3/licenses/#list-all-licenses>`_ 240 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.License.License` 241 """ 242 243 url_parameters = dict() 244 245 return github.PaginatedList.PaginatedList( 246 github.License.License, self.__requester, "/licenses", url_parameters 247 ) 248 249 def get_events(self): 250 """ 251 :calls: `GET /events <https://developer.github.com/v3/activity/events/#list-public-events>`_ 252 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Event.Event` 253 """ 254 255 return github.PaginatedList.PaginatedList( 256 github.Event.Event, self.__requester, "/events", None 257 ) 258 259 def get_user(self, login=github.GithubObject.NotSet): 260 """ 261 :calls: `GET /users/:user <http://developer.github.com/v3/users>`_ or `GET /user <http://developer.github.com/v3/users>`_ 262 :param login: string 263 :rtype: :class:`github.NamedUser.NamedUser` or :class:`github.AuthenticatedUser.AuthenticatedUser` 264 """ 265 assert login is github.GithubObject.NotSet or isinstance(login, str), login 266 if login is github.GithubObject.NotSet: 267 return AuthenticatedUser.AuthenticatedUser( 268 self.__requester, {}, {"url": "/user"}, completed=False 269 ) 270 else: 271 headers, data = self.__requester.requestJsonAndCheck( 272 "GET", "/users/" + login 273 ) 274 return github.NamedUser.NamedUser( 275 self.__requester, headers, data, completed=True 276 ) 277 278 def get_user_by_id(self, user_id): 279 """ 280 :calls: `GET /user/:id <http://developer.github.com/v3/users>`_ 281 :param user_id: int 282 :rtype: :class:`github.NamedUser.NamedUser` 283 """ 284 assert isinstance(user_id, int), user_id 285 headers, data = self.__requester.requestJsonAndCheck( 286 "GET", "/user/" + str(user_id) 287 ) 288 return github.NamedUser.NamedUser( 289 self.__requester, headers, data, completed=True 290 ) 291 292 def get_users(self, since=github.GithubObject.NotSet): 293 """ 294 :calls: `GET /users <http://developer.github.com/v3/users>`_ 295 :param since: integer 296 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.NamedUser.NamedUser` 297 """ 298 assert since is github.GithubObject.NotSet or isinstance(since, int), since 299 url_parameters = dict() 300 if since is not github.GithubObject.NotSet: 301 url_parameters["since"] = since 302 return github.PaginatedList.PaginatedList( 303 github.NamedUser.NamedUser, self.__requester, "/users", url_parameters 304 ) 305 306 def get_organization(self, login): 307 """ 308 :calls: `GET /orgs/:org <http://developer.github.com/v3/orgs>`_ 309 :param login: string 310 :rtype: :class:`github.Organization.Organization` 311 """ 312 assert isinstance(login, str), login 313 headers, data = self.__requester.requestJsonAndCheck("GET", "/orgs/" + login) 314 return github.Organization.Organization( 315 self.__requester, headers, data, completed=True 316 ) 317 318 def get_organizations(self, since=github.GithubObject.NotSet): 319 """ 320 :calls: `GET /organizations <http://developer.github.com/v3/orgs#list-all-organizations>`_ 321 :param since: integer 322 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Organization.Organization` 323 """ 324 assert since is github.GithubObject.NotSet or isinstance(since, int), since 325 url_parameters = dict() 326 if since is not github.GithubObject.NotSet: 327 url_parameters["since"] = since 328 return github.PaginatedList.PaginatedList( 329 github.Organization.Organization, 330 self.__requester, 331 "/organizations", 332 url_parameters, 333 ) 334 335 def get_repo(self, full_name_or_id, lazy=False): 336 """ 337 :calls: `GET /repos/:owner/:repo <http://developer.github.com/v3/repos>`_ or `GET /repositories/:id <http://developer.github.com/v3/repos>`_ 338 :rtype: :class:`github.Repository.Repository` 339 """ 340 assert isinstance(full_name_or_id, (str, int)), full_name_or_id 341 url_base = "/repositories/" if isinstance(full_name_or_id, int) else "/repos/" 342 url = "%s%s" % (url_base, full_name_or_id) 343 if lazy: 344 return Repository.Repository( 345 self.__requester, {}, {"url": url}, completed=False 346 ) 347 headers, data = self.__requester.requestJsonAndCheck( 348 "GET", "%s%s" % (url_base, full_name_or_id) 349 ) 350 return Repository.Repository(self.__requester, headers, data, completed=True) 351 352 def get_repos( 353 self, since=github.GithubObject.NotSet, visibility=github.GithubObject.NotSet 354 ): 355 """ 356 :calls: `GET /repositories <http://developer.github.com/v3/repos/#list-all-public-repositories>`_ 357 :param since: integer 358 :param visibility: string ('all','public') 359 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Repository.Repository` 360 """ 361 assert since is github.GithubObject.NotSet or isinstance(since, int), since 362 url_parameters = dict() 363 if since is not github.GithubObject.NotSet: 364 url_parameters["since"] = since 365 if visibility is not github.GithubObject.NotSet: 366 assert visibility in ("public", "all"), visibility 367 url_parameters["visibility"] = visibility 368 return github.PaginatedList.PaginatedList( 369 github.Repository.Repository, 370 self.__requester, 371 "/repositories", 372 url_parameters, 373 ) 374 375 def get_project(self, id): 376 """ 377 :calls: `GET /projects/:project_id <https://developer.github.com/v3/projects/#get-a-project>`_ 378 :rtype: :class:`github.Project.Project` 379 :param id: integer 380 """ 381 headers, data = self.__requester.requestJsonAndCheck( 382 "GET", 383 "/projects/%d" % (id), 384 headers={"Accept": Consts.mediaTypeProjectsPreview}, 385 ) 386 return github.Project.Project(self.__requester, headers, data, completed=True) 387 388 def get_project_column(self, id): 389 """ 390 :calls: `GET /projects/columns/:column_id <https://developer.github.com/v3/projects/columns/#get-a-project-column>`_ 391 :rtype: :class:`github.ProjectColumn.ProjectColumn` 392 :param id: integer 393 """ 394 headers, data = self.__requester.requestJsonAndCheck( 395 "GET", 396 "/projects/columns/%d" % id, 397 headers={"Accept": Consts.mediaTypeProjectsPreview}, 398 ) 399 return github.ProjectColumn.ProjectColumn( 400 self.__requester, headers, data, completed=True 401 ) 402 403 def get_gist(self, id): 404 """ 405 :calls: `GET /gists/:id <http://developer.github.com/v3/gists>`_ 406 :param id: string 407 :rtype: :class:`github.Gist.Gist` 408 """ 409 assert isinstance(id, str), id 410 headers, data = self.__requester.requestJsonAndCheck("GET", "/gists/" + id) 411 return github.Gist.Gist(self.__requester, headers, data, completed=True) 412 413 def get_gists(self, since=github.GithubObject.NotSet): 414 """ 415 :calls: `GET /gists/public <http://developer.github.com/v3/gists>`_ 416 :param since: datetime.datetime format YYYY-MM-DDTHH:MM:SSZ 417 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Gist.Gist` 418 """ 419 assert since is github.GithubObject.NotSet or isinstance( 420 since, datetime.datetime 421 ), since 422 url_parameters = dict() 423 if since is not github.GithubObject.NotSet: 424 url_parameters["since"] = since.strftime("%Y-%m-%dT%H:%M:%SZ") 425 return github.PaginatedList.PaginatedList( 426 github.Gist.Gist, self.__requester, "/gists/public", url_parameters 427 ) 428 429 def search_repositories( 430 self, 431 query, 432 sort=github.GithubObject.NotSet, 433 order=github.GithubObject.NotSet, 434 **qualifiers 435 ): 436 """ 437 :calls: `GET /search/repositories <http://developer.github.com/v3/search>`_ 438 :param query: string 439 :param sort: string ('stars', 'forks', 'updated') 440 :param order: string ('asc', 'desc') 441 :param qualifiers: keyword dict query qualifiers 442 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Repository.Repository` 443 """ 444 assert isinstance(query, str), query 445 url_parameters = dict() 446 if ( 447 sort is not github.GithubObject.NotSet 448 ): # pragma no branch (Should be covered) 449 assert sort in ("stars", "forks", "updated"), sort 450 url_parameters["sort"] = sort 451 if ( 452 order is not github.GithubObject.NotSet 453 ): # pragma no branch (Should be covered) 454 assert order in ("asc", "desc"), order 455 url_parameters["order"] = order 456 457 query_chunks = [] 458 if query: # pragma no branch (Should be covered) 459 query_chunks.append(query) 460 461 for qualifier, value in qualifiers.items(): 462 query_chunks.append("%s:%s" % (qualifier, value)) 463 464 url_parameters["q"] = " ".join(query_chunks) 465 assert url_parameters["q"], "need at least one qualifier" 466 467 return github.PaginatedList.PaginatedList( 468 github.Repository.Repository, 469 self.__requester, 470 "/search/repositories", 471 url_parameters, 472 ) 473 474 def search_users( 475 self, 476 query, 477 sort=github.GithubObject.NotSet, 478 order=github.GithubObject.NotSet, 479 **qualifiers 480 ): 481 """ 482 :calls: `GET /search/users <http://developer.github.com/v3/search>`_ 483 :param query: string 484 :param sort: string ('followers', 'repositories', 'joined') 485 :param order: string ('asc', 'desc') 486 :param qualifiers: keyword dict query qualifiers 487 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.NamedUser.NamedUser` 488 """ 489 assert isinstance(query, str), query 490 url_parameters = dict() 491 if sort is not github.GithubObject.NotSet: 492 assert sort in ("followers", "repositories", "joined"), sort 493 url_parameters["sort"] = sort 494 if order is not github.GithubObject.NotSet: 495 assert order in ("asc", "desc"), order 496 url_parameters["order"] = order 497 498 query_chunks = [] 499 if query: 500 query_chunks.append(query) 501 502 for qualifier, value in qualifiers.items(): 503 query_chunks.append("%s:%s" % (qualifier, value)) 504 505 url_parameters["q"] = " ".join(query_chunks) 506 assert url_parameters["q"], "need at least one qualifier" 507 508 return github.PaginatedList.PaginatedList( 509 github.NamedUser.NamedUser, 510 self.__requester, 511 "/search/users", 512 url_parameters, 513 ) 514 515 def search_issues( 516 self, 517 query, 518 sort=github.GithubObject.NotSet, 519 order=github.GithubObject.NotSet, 520 **qualifiers 521 ): 522 """ 523 :calls: `GET /search/issues <http://developer.github.com/v3/search>`_ 524 :param query: string 525 :param sort: string ('comments', 'created', 'updated') 526 :param order: string ('asc', 'desc') 527 :param qualifiers: keyword dict query qualifiers 528 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Issue.Issue` 529 """ 530 assert isinstance(query, str), query 531 url_parameters = dict() 532 if sort is not github.GithubObject.NotSet: 533 assert sort in ("comments", "created", "updated"), sort 534 url_parameters["sort"] = sort 535 if order is not github.GithubObject.NotSet: 536 assert order in ("asc", "desc"), order 537 url_parameters["order"] = order 538 539 query_chunks = [] 540 if query: # pragma no branch (Should be covered) 541 query_chunks.append(query) 542 543 for qualifier, value in qualifiers.items(): 544 query_chunks.append("%s:%s" % (qualifier, value)) 545 546 url_parameters["q"] = " ".join(query_chunks) 547 assert url_parameters["q"], "need at least one qualifier" 548 549 return github.PaginatedList.PaginatedList( 550 github.Issue.Issue, self.__requester, "/search/issues", url_parameters 551 ) 552 553 def search_code( 554 self, 555 query, 556 sort=github.GithubObject.NotSet, 557 order=github.GithubObject.NotSet, 558 highlight=False, 559 **qualifiers 560 ): 561 """ 562 :calls: `GET /search/code <http://developer.github.com/v3/search>`_ 563 :param query: string 564 :param sort: string ('indexed') 565 :param order: string ('asc', 'desc') 566 :param highlight: boolean (True, False) 567 :param qualifiers: keyword dict query qualifiers 568 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.ContentFile.ContentFile` 569 """ 570 assert isinstance(query, str), query 571 url_parameters = dict() 572 if ( 573 sort is not github.GithubObject.NotSet 574 ): # pragma no branch (Should be covered) 575 assert sort in ("indexed",), sort 576 url_parameters["sort"] = sort 577 if ( 578 order is not github.GithubObject.NotSet 579 ): # pragma no branch (Should be covered) 580 assert order in ("asc", "desc"), order 581 url_parameters["order"] = order 582 583 query_chunks = [] 584 if query: # pragma no branch (Should be covered) 585 query_chunks.append(query) 586 587 for qualifier, value in qualifiers.items(): 588 query_chunks.append("%s:%s" % (qualifier, value)) 589 590 url_parameters["q"] = " ".join(query_chunks) 591 assert url_parameters["q"], "need at least one qualifier" 592 593 headers = {"Accept": Consts.highLightSearchPreview} if highlight else None 594 595 return github.PaginatedList.PaginatedList( 596 github.ContentFile.ContentFile, 597 self.__requester, 598 "/search/code", 599 url_parameters, 600 headers=headers, 601 ) 602 603 def search_commits( 604 self, 605 query, 606 sort=github.GithubObject.NotSet, 607 order=github.GithubObject.NotSet, 608 **qualifiers 609 ): 610 """ 611 :calls: `GET /search/commits <http://developer.github.com/v3/search>`_ 612 :param query: string 613 :param sort: string ('author-date', 'committer-date') 614 :param order: string ('asc', 'desc') 615 :param qualifiers: keyword dict query qualifiers 616 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Commit.Commit` 617 """ 618 assert isinstance(query, str), query 619 url_parameters = dict() 620 if ( 621 sort is not github.GithubObject.NotSet 622 ): # pragma no branch (Should be covered) 623 assert sort in ("author-date", "committer-date"), sort 624 url_parameters["sort"] = sort 625 if ( 626 order is not github.GithubObject.NotSet 627 ): # pragma no branch (Should be covered) 628 assert order in ("asc", "desc"), order 629 url_parameters["order"] = order 630 631 query_chunks = [] 632 if query: # pragma no branch (Should be covered) 633 query_chunks.append(query) 634 635 for qualifier, value in qualifiers.items(): 636 query_chunks.append("%s:%s" % (qualifier, value)) 637 638 url_parameters["q"] = " ".join(query_chunks) 639 assert url_parameters["q"], "need at least one qualifier" 640 641 return github.PaginatedList.PaginatedList( 642 github.Commit.Commit, 643 self.__requester, 644 "/search/commits", 645 url_parameters, 646 headers={"Accept": Consts.mediaTypeCommitSearchPreview}, 647 ) 648 649 def search_topics(self, query, **qualifiers): 650 """ 651 :calls: `GET /search/topics <http://developer.github.com/v3/search>`_ 652 :param query: string 653 :param qualifiers: keyword dict query qualifiers 654 :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Topic.Topic` 655 """ 656 assert isinstance(query, str), query 657 url_parameters = dict() 658 659 query_chunks = [] 660 if query: # pragma no branch (Should be covered) 661 query_chunks.append(query) 662 663 for qualifier, value in qualifiers.items(): 664 query_chunks.append("%s:%s" % (qualifier, value)) 665 666 url_parameters["q"] = " ".join(query_chunks) 667 assert url_parameters["q"], "need at least one qualifier" 668 669 return github.PaginatedList.PaginatedList( 670 github.Topic.Topic, 671 self.__requester, 672 "/search/topics", 673 url_parameters, 674 headers={"Accept": Consts.mediaTypeTopicsPreview}, 675 ) 676 677 def render_markdown(self, text, context=github.GithubObject.NotSet): 678 """ 679 :calls: `POST /markdown <http://developer.github.com/v3/markdown>`_ 680 :param text: string 681 :param context: :class:`github.Repository.Repository` 682 :rtype: string 683 """ 684 assert isinstance(text, str), text 685 assert context is github.GithubObject.NotSet or isinstance( 686 context, github.Repository.Repository 687 ), context 688 post_parameters = {"text": text} 689 if context is not github.GithubObject.NotSet: 690 post_parameters["mode"] = "gfm" 691 post_parameters["context"] = context._identity 692 status, headers, data = self.__requester.requestJson( 693 "POST", "/markdown", input=post_parameters 694 ) 695 return data 696 697 def get_hook(self, name): 698 """ 699 :calls: `GET /hooks/:name <http://developer.github.com/v3/repos/hooks/>`_ 700 :param name: string 701 :rtype: :class:`github.HookDescription.HookDescription` 702 """ 703 assert isinstance(name, str), name 704 headers, attributes = self.__requester.requestJsonAndCheck( 705 "GET", "/hooks/" + name 706 ) 707 return HookDescription.HookDescription( 708 self.__requester, headers, attributes, completed=True 709 ) 710 711 def get_hooks(self): 712 """ 713 :calls: `GET /hooks <http://developer.github.com/v3/repos/hooks/>`_ 714 :rtype: list of :class:`github.HookDescription.HookDescription` 715 """ 716 headers, data = self.__requester.requestJsonAndCheck("GET", "/hooks") 717 return [ 718 HookDescription.HookDescription( 719 self.__requester, headers, attributes, completed=True 720 ) 721 for attributes in data 722 ] 723 724 def get_gitignore_templates(self): 725 """ 726 :calls: `GET /gitignore/templates <http://developer.github.com/v3/gitignore>`_ 727 :rtype: list of string 728 """ 729 headers, data = self.__requester.requestJsonAndCheck( 730 "GET", "/gitignore/templates" 731 ) 732 return data 733 734 def get_gitignore_template(self, name): 735 """ 736 :calls: `GET /gitignore/templates/:name <http://developer.github.com/v3/gitignore>`_ 737 :rtype: :class:`github.GitignoreTemplate.GitignoreTemplate` 738 """ 739 assert isinstance(name, str), name 740 headers, attributes = self.__requester.requestJsonAndCheck( 741 "GET", "/gitignore/templates/" + name 742 ) 743 return GitignoreTemplate.GitignoreTemplate( 744 self.__requester, headers, attributes, completed=True 745 ) 746 747 def get_emojis(self): 748 """ 749 :calls: `GET /emojis <http://developer.github.com/v3/emojis/>`_ 750 :rtype: dictionary of type => url for emoji` 751 """ 752 headers, attributes = self.__requester.requestJsonAndCheck("GET", "/emojis") 753 return attributes 754 755 def create_from_raw_data(self, klass, raw_data, headers={}): 756 """ 757 Creates an object from raw_data previously obtained by :attr:`github.GithubObject.GithubObject.raw_data`, 758 and optionally headers previously obtained by :attr:`github.GithubObject.GithubObject.raw_headers`. 759 760 :param klass: the class of the object to create 761 :param raw_data: dict 762 :param headers: dict 763 :rtype: instance of class ``klass`` 764 """ 765 return klass(self.__requester, headers, raw_data, completed=True) 766 767 def dump(self, obj, file, protocol=0): 768 """ 769 Dumps (pickles) a PyGithub object to a file-like object. 770 Some effort is made to not pickle sensitive information like the Github credentials used in the :class:`Github` instance. 771 But NO EFFORT is made to remove sensitive information from the object's attributes. 772 773 :param obj: the object to pickle 774 :param file: the file-like object to pickle to 775 :param protocol: the `pickling protocol <http://docs.python.org/2.7/library/pickle.html#data-stream-format>`_ 776 """ 777 pickle.dump((obj.__class__, obj.raw_data, obj.raw_headers), file, protocol) 778 779 def load(self, f): 780 """ 781 Loads (unpickles) a PyGithub object from a file-like object. 782 783 :param f: the file-like object to unpickle from 784 :return: the unpickled object 785 """ 786 return self.create_from_raw_data(*pickle.load(f)) 787 788 def get_oauth_application(self, client_id, client_secret): 789 return github.ApplicationOAuth.ApplicationOAuth( 790 self.__requester, 791 headers={}, 792 attributes={"client_id": client_id, "client_secret": client_secret}, 793 completed=False, 794 ) 795 796 def get_app(self, slug=github.GithubObject.NotSet): 797 """ 798 :calls: `GET /apps/:slug <https://docs.github.com/en/rest/reference/apps>`_ or `GET /app <https://docs.github.com/en/rest/reference/apps>`_ 799 :param slug: string 800 :rtype: :class:`github.GithubApp.GithubApp` 801 """ 802 assert slug is github.GithubObject.NotSet or isinstance(slug, str), slug 803 if slug is github.GithubObject.NotSet: 804 return GithubApp.GithubApp( 805 self.__requester, {}, {"url": "/app"}, completed=False 806 ) 807 else: 808 headers, data = self.__requester.requestJsonAndCheck("GET", "/apps/" + slug) 809 return GithubApp.GithubApp(self.__requester, headers, data, completed=True) 810 811 812class GithubIntegration(object): 813 """ 814 Main class to obtain tokens for a GitHub integration. 815 """ 816 817 def __init__(self, integration_id, private_key, base_url=DEFAULT_BASE_URL): 818 """ 819 :param base_url: string 820 :param integration_id: int 821 :param private_key: string 822 """ 823 self.base_url = base_url 824 self.integration_id = integration_id 825 self.private_key = private_key 826 assert isinstance(base_url, str), base_url 827 828 def create_jwt(self, expiration=60): 829 """ 830 Creates a signed JWT, valid for 60 seconds by default. 831 The expiration can be extended beyond this, to a maximum of 600 seconds. 832 833 :param expiration: int 834 :return string: 835 """ 836 now = int(time.time()) 837 payload = {"iat": now, "exp": now + expiration, "iss": self.integration_id} 838 encrypted = jwt.encode(payload, key=self.private_key, algorithm="RS256") 839 840 if isinstance(encrypted, bytes): 841 encrypted = encrypted.decode("utf-8") 842 843 return encrypted 844 845 def get_access_token(self, installation_id, user_id=None): 846 """ 847 Get an access token for the given installation id. 848 POSTs https://api.github.com/app/installations/<installation_id>/access_tokens 849 :param user_id: int 850 :param installation_id: int 851 :return: :class:`github.InstallationAuthorization.InstallationAuthorization` 852 """ 853 body = {} 854 if user_id: 855 body = {"user_id": user_id} 856 response = requests.post( 857 "{}/app/installations/{}/access_tokens".format( 858 self.base_url, installation_id 859 ), 860 headers={ 861 "Authorization": "Bearer {}".format(self.create_jwt()), 862 "Accept": Consts.mediaTypeIntegrationPreview, 863 "User-Agent": "PyGithub/Python", 864 }, 865 json=body, 866 ) 867 868 if response.status_code == 201: 869 return InstallationAuthorization.InstallationAuthorization( 870 requester=None, # not required, this is a NonCompletableGithubObject 871 headers={}, # not required, this is a NonCompletableGithubObject 872 attributes=response.json(), 873 completed=True, 874 ) 875 elif response.status_code == 403: 876 raise GithubException.BadCredentialsException( 877 status=response.status_code, data=response.text 878 ) 879 elif response.status_code == 404: 880 raise GithubException.UnknownObjectException( 881 status=response.status_code, data=response.text 882 ) 883 raise GithubException.GithubException( 884 status=response.status_code, data=response.text 885 ) 886 887 def get_installation(self, owner, repo): 888 """ 889 :calls: `GET /repos/:owner/:repo/installation <https://developer.github.com/v3/apps/#get-a-repository-installation>`_ 890 :param owner: str 891 :param repo: str 892 :rtype: :class:`github.Installation.Installation` 893 """ 894 headers = { 895 "Authorization": "Bearer {}".format(self.create_jwt()), 896 "Accept": Consts.mediaTypeIntegrationPreview, 897 "User-Agent": "PyGithub/Python", 898 } 899 900 response = requests.get( 901 "{}/repos/{}/{}/installation".format(self.base_url, owner, repo), 902 headers=headers, 903 ) 904 response_dict = response.json() 905 return Installation.Installation(None, headers, response_dict, True) 906