10000 feat: add basic GraphQL support (wip) · nickbroon/python-gitlab@b8ca5ff · GitHub
[go: up one dir, main page]

Skip to content

Commit b8ca5ff

Browse files
committed
feat: add basic GraphQL support (wip)
1 parent 04e0d24 commit b8ca5ff

File tree

11 files changed

+241
-33
lines changed

11 files changed

+241
-33
lines changed

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/graphql-api-usage.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#####################
2+
Using the GraphQL API
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
12+
13+
It is currently unstable and its implementation may change. You can expect a more
14+
mature client in one of the upcoming major versions.
15+
16+
The ``gitlab.GraphQLGitlab`` class
17+
==================================
18+
19+
As with the REST client, you connect to a GitLab instance by creating a ``gitlab.GraphQLGitlab`` object:
20+
21+
.. code-block:: python
22+
23+
import gitlab
24+
25+
# anonymous read-only access for public resources (GitLab.com)
26+
gl = gitlab.GraphQLGitlab()
27+
28+
# anonymous read-only access for public resources (self-hosted GitLab instance)
29+
gl = gitlab.GraphQLGitlab('https://gitlab.example.com')
30+
31+
# private token or personal token authentication (GitLab.com)
32+
gl = gitlab.GraphQLGitlab(private_token='JVNSESs8EwWRx5yDxM5q')
33+
34+
# private token or personal token authentication (self-hosted GitLab instance)
35+
gl = gitlab.GraphQLGitlab(url='https://gitlab.example.com', private_token='JVNSESs8EwWRx5yDxM5q')
36+
37+
# oauth token authentication
38+
gl = gitlab.GraphQLGitlab('https://gitlab.example.com', oauth_token='my_long_token_here')
39+
40+
Sending queries
41+
===============
42+
43+
Get the result of a simple query:
44+
45+
.. code-block:: python
46+
47+
query = """{
48+
query {
49+
currentUser {
50+
name
51+
}
52+
}
53+
"""
54+
55+
result = gl.execute(query)

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
cli-usage
88
api-usage
9+
graphql-api-usage
910
cli-examples
1011
api-objects
1112
api/gitlab

gitlab/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
__title__,
3030
__version__,
3131
)
32-
from gitlab.client import Gitlab, GitlabList # noqa: F401
32+
from gitlab.client import Gitlab, GitlabList, GraphQLGitlab # noqa: F401
3333
from gitlab.exceptions import * # noqa: F401,F403
3434

3535
warnings.filterwarnings("default", category=DeprecationWarning, module="^gitlab")

gitlab/client.py

Lines changed: 149 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
"""Wrapper for the GitLab API."""
1818

19+
import abc
1920
import os
21+
import sys
2022
import time
2123
from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
2224

@@ -30,6 +32,10 @@
3032
import gitlab.exceptions
3133
from gitlab import utils
3234

35+
if TYPE_CHECKING or ("sphinx" in sys.modules):
36+
import gql
37+
from graphql import DocumentNode
38+
3339
REDIRECT_MSG = (
3440
"python-gitlab detected a {status_code} ({reason!r}) redirection. You must update "
3541
"your GitLab URL to the correct URL to avoid issues. The redirection was from: "
@@ -45,7 +51,7 @@
4551
)
4652

4753

48-
class Gitlab:
54+
class _BaseGitlab:
4955
"""Represents a GitLab server connection.
5056
5157
Args:
@@ -76,21 +82,14 @@ def __init__(
7682
ssl_verify: Union[bool, str] = True,
7783
http_username: Optional[str] = None,
7884
http_password: Optional[str] = None,
79-
timeout: Optional[float] = None,
80-
api_version: str = "4",
8185
session: Optional[requests.Session] = None,
82-
per_page: Optional[int] = None,
83-
pagination: Optional[str] = None,
84-
order_by: Optional[str] = None,
86+
timeout: Optional[float] = None,
8587
user_agent: str = gitlab.const.USER_AGENT,
8688
retry_transient_errors: bool = False,
8789
) -> None:
88-
89-
self._api_version = str(api_version)
9090
self._server_version: Optional[str] = None
9191
self._server_revision: Optional[str] = None
9292
self._base_url = self._get_base_url(url)
93-
self._url = f"{self._base_url}/api/v{api_version}"
9493
#: Timeout to use for requests to gitlab server
9594
self.timeout = timeout
9695
self.retry_transient_errors = retry_transient_errors
@@ -110,6 +109,84 @@ def __init__(
110109
#: Create a session object for requests
111110
self.session = session or requests.Session()
112111

112+
def _get_base_url(self, url: Optional[str] = None) -> str:
113+
"""Return the base URL with the trailing slash stripped.
114+
If the URL is a Falsy value, return the default URL.
115+
Returns:
116+
The base URL
117+
"""
118+
if not url:
119+
return gitlab.const.DEFAULT_URL
120+
121+
return url.rstrip("/")
122+
123+
@property
124+
def url(self) -> str:
125+
"""The user-provided server URL."""
126+
return self._base_url
127+
128+
@abc.abstractmethod
129+
def _set_auth_info(self) -> None:
130+
pass
131+
132+
133+
class Gitlab(_BaseGitlab):
134+
"""Represents a GitLab server connection.
135+
136+
Args:
137+
url: The URL of the GitLab server (defaults to https://gitlab.com).
138+
private_token: The user private token
139+
oauth_token: An oauth token
140+
job_token: A CI job token
141+
ssl_verify: Whether SSL certificates should be validated. If
142+
the value is a string, it is the path to a CA file used for
143+
certificate validation.
144+
timeout: Timeout to use for requests to the GitLab server.
145+
http_username: Username for HTTP authentication
146+
http_password: Password for HTTP authentication
147+
api_version: Gitlab API version to use (support for 4 only)
148+
pagination: Can be set to 'keyset' to use keyset pagination
149+
order_by: Set order_by globally
150+
user_agent: A custom user agent to use for making HTTP requests.
151+
retry_transient_errors: Whether to retry after 500, 502, 503, 504
152+
or 52x responses. Defaults to False.
153+
"""
154+
155+
def __init__(
156+
self,
157+
url: Optional[str] = None,
158+
private_token: Optional[str] = None,
159+
oauth_token: Optional[str] = None,
160+
job_token: Optional[str] = None,
161+
ssl_verify: Union[bool, str] = True,
162+
http_username: Optional[str] = None,
163+
http_password: Optional[str] = None,
164+
timeout: Optional[float] = None,
165+
api_version: str = "4",
166+
session: Optional[requests.Session] = None,
167+
per_page: Optional[int] = None,
168+
pagination: Optional[str] = None,
169+
order_by: Optional[str] = None,
170+
user_agent: str = gitlab.const.USER_AGENT,
171+
retry_transient_errors: bool = False,
172+
) -> None:
173+
super().__init__(
174+
url,
175+
private_token,
176+
oauth_token,
177+
job_token,
178+
ssl_verify,
179+
http_username,
180+
http_password,
181+
session,
182+
timeout,
183+
user_agent,
184+
retry_transient_errors,
185+
)
186+
self._api_version = str(api_version)
187+
self._url = f"{self._base_url}/api/v{api_version}"
188+
self._set_auth_info()
189+
113190
self.per_page = per_page
114191
self.pagination = pagination
115192
self.order_by = order_by
@@ -215,11 +292,6 @@ def __setstate__(self, state: Dict[str, Any]) -> None:
215292

216293
self._objects = gitlab.v4.objects
217294

218-
@property
219-
def url(self) -> str:
220-
"""The user-provided server URL."""
221-
return self._base_url
222-
223295
@property
224296
def api_url(self) -> str:
225297
"""The computed API base URL."""
@@ -531,17 +603,6 @@ def _get_session_opts(self) -> Dict[str, Any]:
531603
"verify": self.ssl_verify,
532604
}
533605

534-
def _get_base_url(self, url: Optional[str] = None) -> str:
535-
"""Return the base URL with the trailing slash stripped.
536-
If the URL is a Falsy value, return the default URL.
537-
Returns:
538-
The base URL
539-
"""
540-
if not url:
541-
return gitlab.const.DEFAULT_URL
542-
543-
return url.rstrip("/")
544-
545606
def _build_url(self, path: str) -> str:
546607
"""Returns the full url from path.
547608
@@ -1150,3 +1211,66 @@ def next(self) -> Dict[str, Any]:
11501211
return self.next()
11511212

11521213
raise StopIteration
1214+
1215+
1216+
class GraphQLGitlab(_BaseGitlab):
1217+
def __init__(
1218+
self,
1219+
url: Optional[str] = None,
1220+
private_token: Optional[str] = None,
1221+
oauth_token: Optional[str] = None,
1222+
job_token: Optional[str] = None,
1223+
ssl_verify: Union[bool, str] = True,
1224+
http_username: Optional[str] = None,
1225+
http_password: Optional[str] = None,
1226+
session: Optional[requests.Session] = None,
1227+
timeout: Optional[float] = None,
1228+
user_agent: str = gitlab.const.USER_AGENT,
1229+
retry_transient_errors: bool = False,
1230+
fetch_schema_from_transport: bool = False,
1231+
) -> None:
1232+
super().__init__(
1233+
url,
1234+
private_token,
1235+
oauth_token,
1236+
job_token,
1237+
ssl_verify,
1238+
http_username,
1239+
http_password,
1240+
session,
1241+
timeout,
1242+
user_agent,
1243+
retry_transient_errors,
1244+
)
1245+
self._url = f"{self._base_url}/api/graphql"
1246+
self.fetch_schema_from_transport = fetch_schema_from_transport
1247+
1248+
try:
1249+
import gql
1250+
from gql.dsl import DSLSchema
1251+
1252+
from .graphql.transport import GitlabSyncTransport
1253+
except ImportError:
1254+
raise ImportError(
1255+
"The GraphQLGitlab client could not be initialized because "
1256+
"the gql dependency is not installed. "
1257+
"Install it with 'pip install python-gitlab[graphql]'"
1258+
)
1259+
1260+
self._transport = GitlabSyncTransport(self._url, session=self.session)
1261+
self._client = gql.Client(
1262+
transport=self._transport,
1263+
fetch_schema_from_transport=fetch_schema_from_transport,
1264+
)
1265+
1266+
def _set_auth_info(self) -> None:
1267+
if self.private_token and self.oauth_token:
1268+
raise ValueError(
1269+
"Only one of private_token or oauth_token should be defined"
1270+
)
1271+
1272+
token = self.private_token or self.oauth_token
1273+
self.headers["Authorization"] = f"Bearer {token}"
1274+
1275+
def execute(self, document: "DocumentNode", *args, **kwargs) -> Any:
1276+
return self._client.execute(document, *args, **kwargs)

gitlab/graphql/__init__.py

Whitespace-only changes.

gitlab/graphql/transport.py

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

requirements-lint.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
-r requirements.txt
12
argcomplete<=2.0.0
23
black==22.3.0
34
commitizen==2.24.0

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
gql==3.2.0
12
requests==2.27.1
23
requests-toolbelt==0.9.1

0 commit comments

Comments
 (0)
0