Skip to content

Commit d6b1b0a

Browse files
nejchmax-wittig
authored andcommitted
feat: add a minimal GraphQL client
1 parent d44ddd2 commit d6b1b0a

File tree

14 files changed

+210
-24
lines changed

14 files changed

+210
-24
lines changed

docs/api-usage-graphql.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
############################
2+
Using the GraphQL API (beta)
3+
############################
4+
5+
python-gitlab provides basic support for executing GraphQL queries and mutations.
6+
7+
.. danger::
8+
9+
The GraphQL client is experimental and only provides basic support.
10+
It does not currently support pagination, obey rate limits,
11+
or attempt complex retries. You can use it to build simple queries and mutations.
12+
13+
It is currently unstable and its implementation may change. You can expect a more
14+
mature client in one of the upcoming versions.
15+
16+
The ``gitlab.GraphQL`` class
17+
==================================
18+
19+
As with the REST client, you connect to a GitLab instance by creating a ``gitlab.GraphQL`` object:
20+
21+
.. code-block:: python
22+
23+
import gitlab
24+
25+
# anonymous read-only access for public resources (GitLab.com)
26+
gq = gitlab.GraphQL()
27+
28+
# anonymous read-only access for public resources (self-hosted GitLab instance)
29+
gq = gitlab.GraphQL('https://gitlab.example.com')
30+
31+
# personal access token or OAuth2 token authentication (GitLab.com)
32+
gq = gitlab.GraphQL(token='glpat-JVNSESs8EwWRx5yDxM5q')
33+
34+
# personal access token or OAuth2 token authentication (self-hosted GitLab instance)
35+
gq = gitlab.GraphQL('https://gitlab.example.com', token='glpat-JVNSESs8EwWRx5yDxM5q')
36+
37+
Sending queries
38+
===============
39+
40+
Get the result of a query:
41+
42+
.. code-block:: python
43+
44+
query = """{
45+
query {
46+
currentUser {
47+
name
48+
}
49+
}
50+
"""
51+
52+
result = gq.execute(query)

docs/api-usage.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
############################
2-
Getting started with the API
3-
############################
1+
##################
2+
Using the REST API
3+
##################
44

5-
python-gitlab only supports GitLab API v4.
5+
python-gitlab currently only supports v4 of the GitLab REST API.
66

77
``gitlab.Gitlab`` class
88
=======================

docs/cli-usage.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
############################
2-
Getting started with the CLI
3-
############################
1+
#############
2+
Using the CLI
3+
#############
44

55
``python-gitlab`` provides a :command:`gitlab` command-line tool to interact
66
with GitLab servers.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
cli-usage
88
api-usage
99
api-usage-advanced
10+
api-usage-graphql
1011
cli-examples
1112
api-objects
1213
api/gitlab

gitlab/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
__title__,
2828
__version__,
2929
)
30-
from gitlab.client import Gitlab, GitlabList # noqa: F401
30+
from gitlab.client import Gitlab, GitlabList, GraphQL # noqa: F401
3131
from gitlab.exceptions import * # noqa: F401,F403
3232

3333
warnings.filterwarnings("default", category=DeprecationWarning, module="^gitlab")
@@ -42,5 +42,6 @@
4242
"__version__",
4343
"Gitlab",
4444
"GitlabList",
45+
"GraphQL",
4546
]
4647
__all__.extend(gitlab.exceptions.__all__)

gitlab/_backends/graphql.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Any
2+
3+
import httpx
4+
from gql.transport.httpx import HTTPXTransport
5+
6+
7+
class GitlabTransport(HTTPXTransport):
8+
"""A gql httpx transport that reuses an existing httpx.Client.
9+
By default, gql's transports do not have a keep-alive session
10+
and do not enable providing your own session that's kept open.
11+
This transport lets us provide and close our session on our own
12+
and provide additional auth.
13+
For details, see https://github.com/graphql-python/gql/issues/91.
14+
"""
15+
16+
def __init__(self, *args: Any, client: httpx.Client, **kwargs: Any):
17+
super().__init__(*args, **kwargs)
18+
self.client = client
19+
20+
def connect(self) -> None:
21+
pass
22+
23+
def close(self) -> None:
24+
pass

gitlab/client.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@
2525
import gitlab.exceptions
2626
from gitlab import _backends, utils
2727

28+
try:
29+
import gql
30+
import graphql
31+
import httpx
32+
33+
from ._backends.graphql import GitlabTransport
34+
35+
_GQL_INSTALLED = True
36+
except ImportError: # pragma: no cover
37+
_GQL_INSTALLED = False
38+
39+
2840
REDIRECT_MSG = (
2941
"python-gitlab detected a {status_code} ({reason!r}) redirection. You must update "
3042
"your GitLab URL to the correct URL to avoid issues. The redirection was from: "
@@ -89,7 +101,7 @@ def __init__(
89101
self._api_version = str(api_version)
90102
self._server_version: Optional[str] = None
91103
self._server_revision: Optional[str] = None
92-
self._base_url = self._get_base_url(url)
104+
self._base_url = utils.get_base_url(url)
93105
self._url = f"{self._base_url}/api/v{api_version}"
94106
#: Timeout to use for requests to gitlab server
95107
self.timeout = timeout
@@ -557,18 +569,6 @@ def _get_session_opts(self) -> Dict[str, Any]:
557569
"verify": self.ssl_verify,
558570
}
559571

560-
@staticmethod
561-
def _get_base_url(url: Optional[str] = None) -> str:
562-
"""Return the base URL with the trailing slash stripped.
563-
If the URL is a Falsy value, return the default URL.
564-
Returns:
565-
The base URL
566-
"""
567-
if not url:
568-
return gitlab.const.DEFAULT_URL
569-
570-
return url.rstrip("/")
571-
572572
def _build_url(self, path: str) -> str:
573573
"""Returns the full url from path.
574574
@@ -1296,3 +1296,62 @@ def next(self) -> Dict[str, Any]:
12961296
return self.next()
12971297

12981298
raise StopIteration
1299+
1300+
1301+
class GraphQL:
1302+
def __init__(
1303+
self,
1304+
url: Optional[str] = None,
1305+
*,
1306+
token: Optional[str] = None,
1307+
ssl_verify: Union[bool, str] = True,
1308+
client: Optional[httpx.Client] = None,
1309+
timeout: Optional[float] = None,
1310+
user_agent: str = gitlab.const.USER_AGENT,
1311+
fetch_schema_from_transport: bool = False,
1312+
) -> None:
1313+
if not _GQL_INSTALLED:
1314+
raise ImportError(
1315+
"The GraphQL client could not be initialized because "
1316+
"the gql dependencies are not installed. "
1317+
"Install them with 'pip install python-gitlab[graphql]'"
1318+
)
1319+
self._base_url = utils.get_base_url(url)
1320+
self._timeout = timeout
1321+
self._token = token
1322+
self._url = f"{self._base_url}/api/graphql"
1323+
self._user_agent = user_agent
1324+
self._ssl_verify = ssl_verify
1325+
1326+
opts = self._get_client_opts()
1327+
self._http_client = client or httpx.Client(**opts)
1328+
self._transport = GitlabTransport(self._url, client=self._http_client)
1329+
self._client = gql.Client(
1330+
transport=self._transport,
1331+
fetch_schema_from_transport=fetch_schema_from_transport,
1332+
)
1333+
self._gql = gql.gql
1334+
1335+
def __enter__(self) -> "GraphQL":
1336+
return self
1337+
1338+
def __exit__(self, *args: Any) -> None:
1339+
self._http_client.close()
1340+
1341+
def _get_client_opts(self) -> Dict[str, Any]:
1342+
headers = {"User-Agent": self._user_agent}
1343+
1344+
if self._token:
1345+
headers["Authorization"] = f"Bearer {self._token}"
1346+
1347+
return {
1348+
"headers": headers,
1349+
"timeout": self._timeout,
1350+
"verify": self._ssl_verify,
1351+
}
1352+
1353+
def execute(
1354+
self, request: Union[str, graphql.Source], *args: Any, **kwargs: Any
1355+
) -> Any:
1356+
parsed_document = self._gql(request)
1357+
return self._client.execute(parsed_document, *args, **kwargs)

gitlab/utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,26 @@
99

1010
import requests
1111

12-
from gitlab import types
12+
from gitlab import const, types
1313

1414

1515
class _StdoutStream:
1616
def __call__(self, chunk: Any) -> None:
1717
print(chunk)
1818

1919

20+
def get_base_url(url: Optional[str] = None) -> str:
21+
"""Return the base URL with the trailing slash stripped.
22+
If the URL is a Falsy value, return the default URL.
23+
Returns:
24+
The base URL
25+
"""
26+
if not url:
27+
return const.DEFAULT_URL
28+
29+
return url.rstrip("/")
30+
31+
2032
def get_content_type(content_type: Optional[str]) -> str:
2133
message = email.message.Message()
2234
if content_type is not None:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dynamic = ["version"]
4343
[project.optional-dependencies]
4444
autocompletion = ["argcomplete>=1.10.0,<3"]
4545
yaml = ["PyYaml>=6.0.1"]
46+
graphql = ["gql[httpx]>=3.5.0,<4"]
4647

4748
[project.scripts]
4849
gitlab = "gitlab.cli:main"

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
gql==3.5.0
2+
httpx==0.27.0
13
requests==2.32.3
24
requests-toolbelt==1.0.0

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy