Loading README.rst +4 −2 Original line number Diff line number Diff line Loading @@ -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: Loading @@ -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. Loading docs/api-usage-graphql.rst +26 −4 Original line number Diff line number Diff line Loading @@ -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:: Loading @@ -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 Loading @@ -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 =============== Loading @@ -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) gitlab/__init__.py +2 −1 Original line number Diff line number Diff line Loading @@ -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") Loading @@ -42,6 +42,7 @@ __all__ = [ "__version__", "Gitlab", "GitlabList", "AsyncGraphQL", "GraphQL", ] __all__.extend(gitlab.exceptions.__all__) gitlab/_backends/graphql.py +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): Loading @@ -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 gitlab/client.py +120 −16 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading @@ -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, Loading @@ -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) Loading @@ -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 Loading
README.rst +4 −2 Original line number Diff line number Diff line Loading @@ -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: Loading @@ -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. Loading
docs/api-usage-graphql.rst +26 −4 Original line number Diff line number Diff line Loading @@ -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:: Loading @@ -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 Loading @@ -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 =============== Loading @@ -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)
gitlab/__init__.py +2 −1 Original line number Diff line number Diff line Loading @@ -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") Loading @@ -42,6 +42,7 @@ __all__ = [ "__version__", "Gitlab", "GitlabList", "AsyncGraphQL", "GraphQL", ] __all__.extend(gitlab.exceptions.__all__)
gitlab/_backends/graphql.py +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): Loading @@ -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
gitlab/client.py +120 −16 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading @@ -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, Loading @@ -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) Loading @@ -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