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