Commit 288f39c8 authored by Nejc Habjan's avatar Nejc Habjan Committed by Max Wittig
Browse files

feat(graphql): add async client

parent 80463877
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -25,9 +25,10 @@ python-gitlab
.. image:: https://img.shields.io/github/license/python-gitlab/python-gitlab
   :target: https://github.com/python-gitlab/python-gitlab/blob/main/COPYING

``python-gitlab`` is a Python package providing access to the GitLab server API.
``python-gitlab`` is a Python package providing access to the GitLab APIs.

It supports the v4 API of GitLab, and provides a CLI tool (``gitlab``).
It includes a client for GitLab's v4 REST API, synchronous and asynchronous GraphQL API
clients, as well as a CLI tool (``gitlab``) wrapping REST API endpoints.

.. _features:

@@ -39,6 +40,7 @@ Features
* write Pythonic code to manage your GitLab resources.
* pass arbitrary parameters to the GitLab API. Simply follow GitLab's docs
  on what parameters are available.
* use a synchronous or asynchronous client when using the GraphQL API.
* access arbitrary endpoints as soon as they are available on GitLab, by using
  lower-level API methods.
* use persistent requests sessions for authentication, proxy and certificate handling.
+26 −4
Original line number Diff line number Diff line
@@ -2,7 +2,8 @@
Using the GraphQL API (beta)
############################

python-gitlab provides basic support for executing GraphQL queries and mutations.
python-gitlab provides basic support for executing GraphQL queries and mutations,
providing both a synchronous and asynchronous client.

.. danger::

@@ -13,10 +14,11 @@ python-gitlab provides basic support for executing GraphQL queries and mutations
   It is currently unstable and its implementation may change. You can expect a more
   mature client in one of the upcoming versions.

The ``gitlab.GraphQL`` class
==================================
The ``gitlab.GraphQL`` and ``gitlab.AsyncGraphQL`` classes
==========================================================

As with the REST client, you connect to a GitLab instance by creating a ``gitlab.GraphQL`` object:
As with the REST client, you connect to a GitLab instance by creating a ``gitlab.GraphQL``
(for synchronous code) or ``gitlab.AsyncGraphQL`` instance (for asynchronous code):

.. code-block:: python

@@ -34,6 +36,12 @@ As with the REST client, you connect to a GitLab instance by creating a ``gitlab
   # personal access token or OAuth2 token authentication (self-hosted GitLab instance)
   gq = gitlab.GraphQL('https://gitlab.example.com', token='glpat-JVNSESs8EwWRx5yDxM5q')

   # or the async equivalents
   async_gq = gitlab.AsyncGraphQL()
   async_gq = gitlab.AsyncGraphQL('https://gitlab.example.com')
   async_gq = gitlab.AsyncGraphQL(token='glpat-JVNSESs8EwWRx5yDxM5q')
   async_gq = gitlab.AsyncGraphQL('https://gitlab.example.com', token='glpat-JVNSESs8EwWRx5yDxM5q')
  
Sending queries
===============

@@ -50,3 +58,17 @@ Get the result of a query:
    """

    result = gq.execute(query)

Get the result of a query using the async client:

.. code-block:: python

    query = """{
        query {
          currentUser {
            name
          }
        }
    """

    result = await async_gq.execute(query)
+2 −1
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ from gitlab._version import ( # noqa: F401
    __title__,
    __version__,
)
from gitlab.client import Gitlab, GitlabList, GraphQL  # noqa: F401
from gitlab.client import AsyncGraphQL, Gitlab, GitlabList, GraphQL  # noqa: F401
from gitlab.exceptions import *  # noqa: F401,F403

warnings.filterwarnings("default", category=DeprecationWarning, module="^gitlab")
@@ -42,6 +42,7 @@ __all__ = [
    "__version__",
    "Gitlab",
    "GitlabList",
    "AsyncGraphQL",
    "GraphQL",
]
__all__.extend(gitlab.exceptions.__all__)
+21 −1
Original line number Diff line number Diff line
from typing import Any

import httpx
from gql.transport.httpx import HTTPXTransport
from gql.transport.httpx import HTTPXAsyncTransport, HTTPXTransport


class GitlabTransport(HTTPXTransport):
@@ -22,3 +22,23 @@ class GitlabTransport(HTTPXTransport):

    def close(self) -> None:
        pass


class GitlabAsyncTransport(HTTPXAsyncTransport):
    """An async gql httpx transport that reuses an existing httpx.AsyncClient.
    By default, gql's transports do not have a keep-alive session
    and do not enable providing your own session that's kept open.
    This transport lets us provide and close our session on our own
    and provide additional auth.
    For details, see https://github.com/graphql-python/gql/issues/91.
    """

    def __init__(self, *args: Any, client: httpx.AsyncClient, **kwargs: Any):
        super().__init__(*args, **kwargs)
        self.client = client

    async def connect(self) -> None:
        pass

    async def close(self) -> None:
        pass
+120 −16
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ try:
    import graphql
    import httpx

    from ._backends.graphql import GitlabTransport
    from ._backends.graphql import GitlabAsyncTransport, GitlabTransport

    _GQL_INSTALLED = True
except ImportError:  # pragma: no cover
@@ -1278,14 +1278,13 @@ class GitlabList:
        raise StopIteration


class GraphQL:
class _BaseGraphQL:
    def __init__(
        self,
        url: Optional[str] = None,
        *,
        token: Optional[str] = None,
        ssl_verify: Union[bool, str] = True,
        client: Optional[httpx.Client] = None,
        timeout: Optional[float] = None,
        user_agent: str = gitlab.const.USER_AGENT,
        fetch_schema_from_transport: bool = False,
@@ -1308,9 +1307,50 @@ class GraphQL:
        self._max_retries = max_retries
        self._obey_rate_limit = obey_rate_limit
        self._retry_transient_errors = retry_transient_errors
        self._client_opts = self._get_client_opts()
        self._fetch_schema_from_transport = fetch_schema_from_transport

    def _get_client_opts(self) -> Dict[str, Any]:
        headers = {"User-Agent": self._user_agent}

        if self._token:
            headers["Authorization"] = f"Bearer {self._token}"

        return {
            "headers": headers,
            "timeout": self._timeout,
            "verify": self._ssl_verify,
        }


class GraphQL(_BaseGraphQL):
    def __init__(
        self,
        url: Optional[str] = None,
        *,
        token: Optional[str] = None,
        ssl_verify: Union[bool, str] = True,
        client: Optional[httpx.Client] = None,
        timeout: Optional[float] = None,
        user_agent: str = gitlab.const.USER_AGENT,
        fetch_schema_from_transport: bool = False,
        max_retries: int = 10,
        obey_rate_limit: bool = True,
        retry_transient_errors: bool = False,
    ) -> None:
        super().__init__(
            url=url,
            token=token,
            ssl_verify=ssl_verify,
            timeout=timeout,
            user_agent=user_agent,
            fetch_schema_from_transport=fetch_schema_from_transport,
            max_retries=max_retries,
            obey_rate_limit=obey_rate_limit,
            retry_transient_errors=retry_transient_errors,
        )

        opts = self._get_client_opts()
        self._http_client = client or httpx.Client(**opts)
        self._http_client = client or httpx.Client(**self._client_opts)
        self._transport = GitlabTransport(self._url, client=self._http_client)
        self._client = gql.Client(
            transport=self._transport,
@@ -1324,19 +1364,81 @@ class GraphQL:
    def __exit__(self, *args: Any) -> None:
        self._http_client.close()

    def _get_client_opts(self) -> Dict[str, Any]:
        headers = {"User-Agent": self._user_agent}
    def execute(
        self, request: Union[str, graphql.Source], *args: Any, **kwargs: Any
    ) -> Any:
        parsed_document = self._gql(request)
        retry = utils.Retry(
            max_retries=self._max_retries,
            obey_rate_limit=self._obey_rate_limit,
            retry_transient_errors=self._retry_transient_errors,
        )

        if self._token:
            headers["Authorization"] = f"Bearer {self._token}"
        while True:
            try:
                result = self._client.execute(parsed_document, *args, **kwargs)
            except gql.transport.exceptions.TransportServerError as e:
                if retry.handle_retry_on_status(
                    status_code=e.code, headers=self._transport.response_headers
                ):
                    continue

        return {
            "headers": headers,
            "timeout": self._timeout,
            "verify": self._ssl_verify,
        }
                if e.code == 401:
                    raise gitlab.exceptions.GitlabAuthenticationError(
                        response_code=e.code,
                        error_message=str(e),
                    )

    def execute(
                raise gitlab.exceptions.GitlabHttpError(
                    response_code=e.code,
                    error_message=str(e),
                )

            return result


class AsyncGraphQL(_BaseGraphQL):
    def __init__(
        self,
        url: Optional[str] = None,
        *,
        token: Optional[str] = None,
        ssl_verify: Union[bool, str] = True,
        client: Optional[httpx.AsyncClient] = None,
        timeout: Optional[float] = None,
        user_agent: str = gitlab.const.USER_AGENT,
        fetch_schema_from_transport: bool = False,
        max_retries: int = 10,
        obey_rate_limit: bool = True,
        retry_transient_errors: bool = False,
    ) -> None:
        super().__init__(
            url=url,
            token=token,
            ssl_verify=ssl_verify,
            timeout=timeout,
            user_agent=user_agent,
            fetch_schema_from_transport=fetch_schema_from_transport,
            max_retries=max_retries,
            obey_rate_limit=obey_rate_limit,
            retry_transient_errors=retry_transient_errors,
        )

        self._http_client = client or httpx.AsyncClient(**self._client_opts)
        self._transport = GitlabAsyncTransport(self._url, client=self._http_client)
        self._client = gql.Client(
            transport=self._transport,
            fetch_schema_from_transport=fetch_schema_from_transport,
        )
        self._gql = gql.gql

    async def __aenter__(self) -> "AsyncGraphQL":
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self._http_client.aclose()

    async def execute(
        self, request: Union[str, graphql.Source], *args: Any, **kwargs: Any
    ) -> Any:
        parsed_document = self._gql(request)
@@ -1348,7 +1450,9 @@ class GraphQL:

        while True:
            try:
                result = self._client.execute(parsed_document, *args, **kwargs)
                result = await self._client.execute_async(
                    parsed_document, *args, **kwargs
                )
            except gql.transport.exceptions.TransportServerError as e:
                if retry.handle_retry_on_status(
                    status_code=e.code, headers=self._transport.response_headers
Loading