{"id":18084860,"url":"https://github.com/jwodder/ghreq","last_synced_at":"2025-11-16T12:29:15.227Z","repository":{"id":202771373,"uuid":"708103626","full_name":"jwodder/ghreq","owner":"jwodder","description":"Minimal and opinionated GitHub API client","archived":false,"fork":false,"pushed_at":"2025-11-14T19:24:27.000Z","size":120,"stargazers_count":4,"open_issues_count":3,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-11-14T19:37:16.619Z","etag":null,"topics":["available-on-pypi","github","github-api","github-rest-api","python","rest-api-client"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jwodder.png","metadata":{"files":{"readme":"README.rst","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-10-21T14:36:59.000Z","updated_at":"2025-11-14T19:24:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"fb7350a2-a981-4d78-b93e-c26e267ce1a1","html_url":"https://github.com/jwodder/ghreq","commit_stats":null,"previous_names":["jwodder/ghreq"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/jwodder/ghreq","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwodder%2Fghreq","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwodder%2Fghreq/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwodder%2Fghreq/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwodder%2Fghreq/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jwodder","download_url":"https://codeload.github.com/jwodder/ghreq/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwodder%2Fghreq/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":284711509,"owners_count":27050939,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-16T02:00:05.974Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["available-on-pypi","github","github-api","github-rest-api","python","rest-api-client"],"created_at":"2024-10-31T15:08:26.948Z","updated_at":"2025-11-16T12:29:15.221Z","avatar_url":"https://github.com/jwodder.png","language":"Python","readme":"|repostatus| |ci-status| |coverage| |pyversions| |license|\n\n.. |repostatus| image:: https://www.repostatus.org/badges/latest/active.svg\n    :target: https://www.repostatus.org/#active\n    :alt: Project Status: Active — The project has reached a stable, usable\n          state and is being actively developed.\n\n.. |ci-status| image:: https://github.com/jwodder/ghreq/actions/workflows/test.yml/badge.svg\n    :target: https://github.com/jwodder/ghreq/actions/workflows/test.yml\n    :alt: CI Status\n\n.. |coverage| image:: https://codecov.io/gh/jwodder/ghreq/branch/master/graph/badge.svg\n    :target: https://codecov.io/gh/jwodder/ghreq\n\n.. |pyversions| image:: https://img.shields.io/pypi/pyversions/ghreq.svg\n    :target: https://pypi.org/project/ghreq/\n\n.. |license| image:: https://img.shields.io/github/license/jwodder/ghreq.svg\n    :target: https://opensource.org/licenses/MIT\n    :alt: MIT License\n\n`GitHub \u003chttps://github.com/jwodder/ghreq\u003e`_\n| `PyPI \u003chttps://pypi.org/project/ghreq/\u003e`_\n| `Issues \u003chttps://github.com/jwodder/ghreq/issues\u003e`_\n| `Changelog \u003chttps://github.com/jwodder/ghreq/blob/master/CHANGELOG.md\u003e`_\n\n``ghreq`` is a simple wrapper around requests_ with various customizations\naimed at working with the GitHub REST API.  Notable features include:\n\n- When making a request, you only need to specify the part of the URL after the\n  API base URL.  You can even construct objects for making requests with a URL\n  baked in.\n\n- All request methods return decoded JSON by default.\n\n- 4xx and 5xx responses are automatically raised as errors without needing to\n  call ``raise_for_status()``.\n\n- Errors raised for 4xx and 5xx responses include the body of the response in\n  the error message.\n\n- Support for iterating over paginated results\n\n- The ``Accept`` and ``X-GitHub-Api-Version`` headers are automatically set to\n  their recommended values.\n\n- Follows `GitHub's recommendations for dealing with rate limits`__, including\n  waiting between mutating requests and waiting \u0026 retrying in response to\n  rate-limit errors\n\n- Automatic retrying on 5xx errors with exponential backoff\n\n.. _requests: https://requests.readthedocs.io\n\n__ https://docs.github.com/en/rest/guides/best-practices-for-using-the-rest-api\n   ?apiVersion=2022-11-28#dealing-with-rate-limits\n\nInstallation\n============\n``ghreq`` requires Python 3.10 or higher.  Just use `pip \u003chttps://pip.pypa.io\u003e`_\nfor Python 3 (You have pip, right?) to install it::\n\n    python3 -m pip install ghreq\n\n\nExample\n=======\n\n.. code:: python\n\n    from ghreq import Client\n\n    with Client(token=\"your-api-token-here\") as client:\n        user = client.get(\"/user\")\n        print(\"I am user\", user[\"login\"])\n        print()\n\n        print(\"Here are my repositories:\")\n        for repo in client.paginate(\"/user/repos\"):\n            print(repo[\"full_name\"])\n        print()\n\n        hello_world = client / \"repos\" / \"octocat\" / \"Hello-World\"\n        details = hello_world.get()\n        print(f\"{details['full_name']} has been starred {details['stargazers_count']} times.\")\n        print(\"Here is the first page of its open issues \u0026 PRs:\")\n        for issue in (hello_world / \"issues\").get():\n            print(f\"#{issue['number']}: {issue['title']}\")\n\n\nAPI\n===\n\n``Client`` Class\n----------------\n\n.. code:: python\n\n    class Client:\n        def __init__(\n            *,\n            token: str | None = None,\n            api_url: str = DEFAULT_API_URL,\n            session: requests.Session | None = None,\n            set_headers: bool | None = None,\n            user_agent: str | None = None,\n            accept: str | None = DEFAULT_ACCEPT,\n            api_version: str | None = DEFAULT_API_VERSION,\n            mutation_delay: float = 1.0,\n            retry_config: RetryConfig | None = None,\n        )\n\nAn HTTP client class for interacting with the GitHub REST API (or sufficiently\nsimilar APIs).\n\nConstructor arguments:\n\n``token``\n    The GitHub access token, if any, to use to authenticate to the API.\n\n    This argument is ignored if ``set_headers`` is ``False`` or defaulted to\n    ``False``.\n\n``api_url``\n    The base URL to which to append paths passed to the request methods\n\n``session``\n    A pre-configured ``requests.Session`` instance to use for making requests.\n    If no session is supplied, a new session is instantiated.\n\n``set_headers``\n    Whether to set various headers for requests made via the session.  If\n    ``set_headers`` is ``None``, it is defaulted to ``True`` if ``session`` is\n    ``None`` and to ``False`` otherwise.\n\n    If ``set_headers`` is ``True`` or defaulted to ``True``, the following\n    request headers are set on the session:\n\n    - ``Accept`` (if ``accept`` is non-``None``)\n    - ``Authorization`` (set to ``\"Bearer {token}\"`` if ``token`` is\n      non-``None``)\n    - ``User-Agent`` (if ``user_agent`` is non-``None``)\n    - ``X-GitHub-Api-Version`` (if ``api_version`` is non-``None``)\n    - any additional headers included in ``headers``\n\n    If ``set_headers`` is ``False`` or defaulted to ``False``, then ``Client``\n    does not set any headers on the session, and the other header-related\n    parameters are ignored.\n\n``user_agent``\n    A user agent string to include in the headers of requests.  If not set, the\n    ``requests`` library's default user agent is used.\n\n    This argument is ignored if ``set_headers`` is ``False`` or defaulted to\n    ``False``.\n\n``accept``\n    Value to set the ``Accept`` header to.  Can be set to ``None`` to not set\n    the header at all.\n\n    This argument is ignored if ``set_headers`` is ``False`` or defaulted to\n    ``False``.\n\n``api_version``\n    Value to set the ``X-GitHub-Api-Version`` header to.  Can be set to\n    ``None`` to not set the header at all.\n\n    This argument is ignored if ``set_headers`` is ``False`` or defaulted to\n    ``False``.\n\n``headers``\n    Optional mapping of additional headers to set on the session after setting\n    all other headers.\n\n    This argument is ignored if ``set_headers`` is ``False`` or defaulted to\n    ``False``.\n\n``mutation_delay``\n    When making a ``POST``, ``PATCH``, ``PUT``, or ``DELETE`` request, if the\n    time since the last such request is fewer than ``mutation_delay`` seconds,\n    then the client will sleep long enough to make up the difference before\n    performing the request.\n\n``retry_config``\n    Configuration for the request retrying mechanism.  If not set, a\n    ``RetryConfig`` instance with all default attributes will be used; see\n    below.\n\n``Client`` instances can be used as context managers, in which case they close\ntheir internal ``requests.Session`` instances on exit (regardless of whether\nthe session was user-provided or not).\n\nA ``Client`` instance can be \"divided\" by a string (e.g., ``client / \"user\"``)\nto obtain an ``Endpoint`` instance that makes requests to the URL formed from\n``api_url`` and the \"divisor\"; see below.\n\n.. code:: python\n\n    Client.request(\n        method: str,\n        path: str,\n        json: Any = None,\n        *,\n        params: ParamsType = None,\n        headers: HeadersType = None,\n        data: DataType = None,\n        timeout: TimeoutType = None,\n        allow_redirects: bool = True,\n        stream: bool = False,\n        raw: bool = False,\n    ) -\u003e Any\n\nPerform an HTTP request with the given method/verb.  If ``path`` begins with\n``http://`` or ``https://``, it is used as-is for the URL of the request.\nOtherwise, ``path`` is appended to the ``api_url`` value supplied to the\nconstructor, with a forward slash inserted in between if there isn't one\npresent already.  Thus, given a ``client`` constructed with the default\n``api_url``, the following are equivalent:\n\n.. code:: python\n\n    client.request(\"GET\", \"user\")\n\n    client.request(\"GET\", \"/user\")\n\n    client.request(\"GET\", \"https://api.github.com/user\")\n\nIf the request is successful, the body is decoded as JSON and returned; if the\nbody is empty (except possibly for whitespace), ``None`` is returned.  To make\nthe method return the actual ``requests.Response`` object instead, pass\n``raw=True`` (or ``stream=True``, which implies it).\n\nThe remaining arguments have the same meaning as in ``requests``.\n\nIf the request fails, it may be retried with exponentially increasing wait\ntimes between attempts; see the documentation of ``RetryConfig`` below.  If all\nretries are exhausted without success, the exception from the final request is\nraised.\n\nIf the request fails with a 4xx or 5xx response, a ``PrettyHTTPError`` is\nraised.\n\n.. code:: python\n\n    Client.get(\n        path: str,\n        *,\n        params: ParamsType = None,\n        headers: HeadersType = None,\n        timeout: TimeoutType = None,\n        stream: bool = False,\n        raw: bool = False,\n    ) -\u003e Any\n\nPerform a ``GET`` request.  See the documentation of ``request()`` for more\ninformation.\n\n.. code:: python\n\n    Client.post(\n        path: str,\n        json: Any = None,\n        *,\n        params: ParamsType = None,\n        headers: HeadersType = None,\n        data: DataType = None,\n        timeout: TimeoutType = None,\n        stream: bool = False,\n        raw: bool = False,\n    ) -\u003e Any\n\nPerform a ``POST`` request.  See the documentation of ``request()`` for more\ninformation.\n\n.. code:: python\n\n    Client.put(\n        path: str,\n        json: Any = None,\n        *,\n        params: ParamsType = None,\n        headers: HeadersType = None,\n        data: DataType = None,\n        timeout: TimeoutType = None,\n        stream: bool = False,\n        raw: bool = False,\n    ) -\u003e Any\n\nPerform a ``PUT`` request.  See the documentation of ``request()`` for more\ninformation.\n\n.. code:: python\n\n    Client.patch(\n        path: str,\n        json: Any = None,\n        *,\n        params: ParamsType = None,\n        headers: HeadersType = None,\n        data: DataType = None,\n        timeout: TimeoutType = None,\n        stream: bool = False,\n        raw: bool = False,\n    ) -\u003e Any\n\nPerform a ``PATCH`` request.  See the documentation of ``request()`` for more\ninformation.\n\n.. code:: python\n\n    Client.delete(\n        path: str,\n        json: Any = None,\n        *,\n        params: ParamsType = None,\n        headers: HeadersType = None,\n        data: DataType = None,\n        timeout: TimeoutType = None,\n        stream: bool = False,\n        raw: bool = False,\n    ) -\u003e Any\n\nPerform a ``DELETE`` request.  See the documentation of ``request()`` for more\ninformation.\n\n.. code:: python\n\n    Client.paginate(\n        path: str,\n        *,\n        params: ParamsType = None,\n        headers: HeadersType = None,\n        timeout: TimeoutType = None,\n        raw: Literal[True, False] = False,\n    ) -\u003e Iterator\n\nPerform a series of paginated ``GET`` requests and yield the items from each\npage.  The ``path`` and ``params`` arguments are only used for the initial\nrequest; further requests follow the \"next\" entry in the ``Link`` header of\neach response.\n\nThe bodies of the responses must be either JSON lists (in which case the list\nelements are yielded) or JSON objects in which exactly one field is a list (in\nwhich case the elements of that list are yielded); otherwise, an error occurs.\n\nIf ``raw`` is ``True``, then instead of yielding each page's items, the\nreturned iterator will yield each page as a ``requests.Response`` object.\n\n.. code:: python\n\n    Client.graphql(\n        query: str,\n        variables: dict[str, Any] | None = None,\n        *,\n        headers: HeadersType = None,\n        timeout: TimeoutType = None,\n        stream: bool = False,\n        raw: bool = False,\n    ) -\u003e Any\n\nPerform a GraphQL API request.  Calling this method is similar to calling:\n\n.. code:: python\n\n    client.post(\n        \"graphql\",\n        json={\"query\": query, \"variables\": variables},\n        headers=headers,\n        timeout=timeout,\n        stream=stream,\n        raw=raw,\n    )\n\nexcept that there is no sleep for the mutation delay, and the following headers\nare changed unless overridden by the ``headers`` argument:\n\n- ``Accept``: set to ``\"*/*\"``\n- ``X-GitHub-Api-Version``: unset\n- ``X-Github-Next-Global-ID``: set to ``\"1\"``\n\n.. code:: python\n\n    Client.close() -\u003e None\n\nClose the client's internal ``requests.Session``.  No more request methods may\nbe called afterwards.\n\nThis method is called automatically on exit when using ``Client`` as a context\nmanager.\n\n\n``Endpoint`` Class\n------------------\n\n.. code:: python\n\n    class Endpoint:\n        client: Client\n        url: str\n\nA combination of a ``Client`` instance and a URL.  ``Endpoint`` has\n``request()``, ``get()``, ``post()``, ``put()``, ``patch()``, ``delete()``, and\n``paginate()`` methods that work the same way as for ``Client``, except that\n``Endpoint``'s methods do not take ``path`` arguments; instead, they make\nrequests to the stored URL.  This is useful if you find yourself making\nrequests to the same URL and/or paths under the same URL over \u0026 over.\n\nAn ``Endpoint`` instance is constructed by applying the ``/`` (division)\noperator to a ``Client`` or ``Endpoint`` instance on the left and a string on\nthe right.  If the string begins with ``http://`` or ``https://``, it is used\nas-is for the URL of the resulting ``Endpoint``.  Otherwise, the string is\nappended to the ``api_url`` or ``url`` attribute of the object on the left,\nwith a forward slash inserted in between if there isn't one present already.\nThus, given a ``client`` constructed with the default ``api_url``, the\nfollowing are equivalent:\n\n.. code:: python\n\n    client.get(\"repos/octocat/hello-world\")\n\n    (client / \"repos/octocat/hello-world\").get()\n\n    (client / \"repos\" / \"octocat\" / \"hello-world\").get()\n\n\n``RetryConfig`` Class\n---------------------\n\n.. code:: python\n\n    class RetryConfig:\n        def __init__(\n            retries: int = 10,\n            backoff_factor: float = 1.0,\n            backoff_base: float = 1.25,\n            backoff_jitter: float = 0.0\n            backoff_max: float = 120.0,\n            total_wait: float | None = 300.0,\n            retry_statuses: Container[int] = range(500, 600),\n        )\n\nA container for storing configuration for ``ghreq``'s retrying mechanism.  A\nrequest is retried if (a) a ``response.RequestException`` is raised that is not\na ``ValueError`` (e.g., a connection or timeout error), (b) the server responds\nwith a 403 status code and either the ``Retry-After`` header is present or the\nbody contains the string ``\"rate limit\"``, or (c) the server responds with a\nstatus code listed in ``retry_statuses``.\n\nWhen a request is retried, the client sleeps for increasing amounts of time\nbetween repeated requests until either a non-retriable response is obtained,\n``retries`` retry attempts have been performed, or the total amount of time\nelapsed since the start of the first request exceeds ``total_wait``, if set.\n\nThe first retry happens after sleeping for ``backoff_factor * 0.1`` seconds,\nand subsequent retries happen after sleeping for ``backoff_factor *\nbackoff_base ** (retry_number - 1) + random.random() * backoff_jitter``\nseconds, up to a maximum of ``backoff_max`` per retry.  If a ``Retry-After`` or\n``x-ratelimit-reset`` header indicates a larger duration to sleep for, that\nvalue is used instead.  If the duration indicated by such a header would result\nin the next retry attempt being after ``total_wait`` is exceeded, retrying\nstops early.\n\n\n``PrettyHTTPError`` Class\n-------------------------\n\n.. code:: python\n\n    class PrettyHTTPError(requests.HTTPError)\n\nA subclass of ``requests.HTTPError`` raised automatically by the request\nmethods if a response with a 4xx or 5xx status code is received.  Unlike its\nparent class, stringifying a ``PrettyHTTPError`` will produce a string that\ncontains the body of the response; if the body was JSON, that JSON will be\npretty-printed.\n\n\nConstants\n---------\n\n.. code:: python\n\n    DEFAULT_ACCEPT = \"application/vnd.github+json\"\n\nThe default value of the ``accept`` argument to the ``Client`` constructor\n\n.. code:: python\n\n    DEFAULT_API_URL = \"https://api.github.com\"\n\nThe default value of the ``api_url`` argument to the ``Client`` constructor\n\n.. code:: python\n\n    DEFAULT_API_VERSION = \"2022-11-28\"\n\nThe default value of the ``api_version`` argument to the ``Client`` constructor\n\n\nUtility Functions\n-----------------\n\n.. code:: python\n\n    make_user_agent(name: str, version: str | None = None, url: str | None = None) -\u003e str\n\nCreate a user agent string with the given client name, optional version, and\noptional URL.  The string will also include the version of the ``requests``\nlibrary used and the implemention \u0026 version of Python.\n\n.. code:: python\n\n    get_github_api_url() -\u003e str\n\nIf the ``GITHUB_API_URL`` environment variable is set to a nonempty string,\nthat string is returned; otherwise, ``DEFAULT_API_URL`` is returned.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjwodder%2Fghreq","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjwodder%2Fghreq","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjwodder%2Fghreq/lists"}