From 7bbcba2656fdb248473ed032e5656bd20cba5c2c Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Fri, 18 Apr 2025 12:29:21 +0800 Subject: [PATCH 01/20] feat(graphql): Extend GraphQL class to support multiple authentication methods and more API options This commit extends the constructor (`__init__` method) of the `GraphQL` class to handle a wider range of interactions with the GitLab API. The main changes include: - **Added support for multiple authentication tokens:** In addition to the original `token`, it now supports `private_token`, `oauth_token`, and `job_token`, allowing users to configure based on different authentication requirements. - **Added HTTP Basic Authentication:** Introduced `http_username` and `http_password` parameters, allowing the use of HTTP Basic Authentication to interact with the API. - **Added API version control:** Introduced the `api_version` parameter, with a default value of `"4"`, allowing users to specify the GitLab API version to use. - **Added pagination control parameters:** Added `per_page`, `pagination`, and `order_by` parameters to provide finer control over the API's pagination behavior and data sorting. - **Added an option to keep the Base URL:** Added the `keep_base_url` parameter, allowing the configured base URL to be retained across multiple API calls. - **Added `**kwargs: Any`:** Allows passing additional keyword arguments, providing greater flexibility. These changes make the `GraphQL` class more powerful and flexible, capable of handling a broader range of GitLab API use cases. --- gitlab/client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gitlab/client.py b/gitlab/client.py index 37dd4c2e6..a42a29ad0 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1315,14 +1315,25 @@ def __init__( url: str | None = None, *, token: str | None = None, + private_token: str | None = None, + oauth_token: str | None = None, + job_token: str | None = None, ssl_verify: bool | str = True, client: httpx.Client | None = None, + http_username: str | None = None, + http_password: str | None = None, timeout: float | None = None, + api_version: str = "4", + per_page: int | None = None, + pagination: str | None = None, + order_by: str | None = 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, + keep_base_url: bool = False, + **kwargs: Any, ) -> None: super().__init__( url=url, From 824cda6821ad1fd3ce849b29a2c67a32069657f0 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Fri, 18 Apr 2025 12:32:54 +0800 Subject: [PATCH 02/20] feat(feat): Add class method to create Gitlab connection from configuration files This commit introduces a new class method from_config to the GraphQL class. This method allows users to create a Gitlab connection object by reading configuration files. Users can specify the ID and paths of the configuration files, and the method will parse the connection information (e.g., URL, tokens, SSL verification, etc.) from the files to create a pre-configured Gitlab object. This feature provides a more convenient and configurable way to manage GitLab connections, especially useful for scenarios requiring connections to multiple GitLab instances or aiming to separate connection configurations from the codebase. Example usage (assuming the configuration file is gitlab.ini): ```python gl = GraphQL.from_config(config_files=['gitlab.ini']) ``` Internally, this method utilizes gitlab.config.GitlabConfigParser to handle the parsing of the configuration files. --- gitlab/client.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/gitlab/client.py b/gitlab/client.py index a42a29ad0..112fcf290 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1335,6 +1335,49 @@ def __init__( keep_base_url: bool = False, **kwargs: Any, ) -> None: + self._api_version = str(api_version) + self._server_version: str | None = None + self._server_revision: str | None = None + self._base_url = utils.get_base_url(url) + self._url = f"{self._base_url}/api/v{api_version}" + #: Timeout to use for requests to gitlab server + self.timeout = timeout + self.retry_transient_errors = retry_transient_errors + self.keep_base_url = keep_base_url + #: Headers that will be used in request to GitLab + self.headers = {"User-Agent": user_agent} + + #: Whether SSL certificates should be validated + self.ssl_verify = ssl_verify + + self.private_token = private_token + self.http_username = http_username + self.http_password = http_password + self.oauth_token = oauth_token + self.job_token = job_token + self._set_auth_info() + + #: Create a session object for requests + _backend: type[_backends.DefaultBackend] = kwargs.pop( + "backend", _backends.DefaultBackend + ) + self._backend = _backend(**kwargs) + self.session = self._backend.client + + self.per_page = per_page + self.pagination = pagination + self.order_by = order_by + + # We only support v4 API at this time + if self._api_version not in ("4",): + raise ModuleNotFoundError(f"gitlab.v{self._api_version}.objects") + # NOTE: We must delay import of gitlab.v4.objects until now or + # otherwise it will cause circular import errors + from gitlab.v4 import objects + + self._objects = objects + self.user: objects.CurrentUser | None = None + super().__init__( url=url, token=token, From 97c4619422692c74fec34d37a0c565906e7bb827 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Fri, 18 Apr 2025 12:35:20 +0800 Subject: [PATCH 03/20] feat(graphql): Add class method to create Gitlab connection from configuration files This commit introduces a new class method from_config to the GraphQL class. This method allows users to create a Gitlab connection object by reading configuration files. Users can specify the ID and paths of the configuration files, and the method will parse the connection information (e.g., URL, tokens, SSL verification, etc.) from the files to create a pre-configured Gitlab object. This feature provides a more convenient and configurable way to manage GitLab connections, especially useful for scenarios requiring connections to multiple GitLab instances or aiming to separate connection configurations from the codebase. Example usage (assuming the configuration file is gitlab.ini): Python gl = GraphQL.from_config(config_files=['gitlab.ini']) Internally, this method utilizes gitlab.config.GitlabConfigParser to handle the parsing of the configuration files. --- gitlab/client.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/gitlab/client.py b/gitlab/client.py index 112fcf290..8fc7cc435 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1432,6 +1432,86 @@ def execute(self, request: str | graphql.Source, *args: Any, **kwargs: Any) -> A return result + @classmethod + def from_config( + cls, + gitlab_id: str | None = None, + config_files: list[str] | None = None, + **kwargs: Any, + ) -> Gitlab: + """Create a Gitlab connection from configuration files. + + Args: + gitlab_id: ID of the configuration section. + config_files list[str]: List of paths to configuration files. + + kwargs: + session requests.Session: Custom requests Session + + Returns: + A Gitlab connection. + + Raises: + gitlab.config.GitlabDataError: If the configuration is not correct. + """ + config = gitlab.config.GitlabConfigParser( + gitlab_id=gitlab_id, config_files=config_files + ) + return cls( + config.url, + private_token=config.private_token, + oauth_token=config.oauth_token, + job_token=config.job_token, + ssl_verify=config.ssl_verify, + timeout=config.timeout, + http_username=config.http_username, + http_password=config.http_password, + api_version=config.api_version, + per_page=config.per_page, + pagination=config.pagination, + order_by=config.order_by, + user_agent=config.user_agent, + retry_transient_errors=config.retry_transient_errors, + keep_base_url=config.keep_base_url, + **kwargs, + ) + + def _set_auth_info(self) -> None: + tokens = [ + token + for token in [self.private_token, self.oauth_token, self.job_token] + if token + ] + if len(tokens) > 1: + raise ValueError( + "Only one of private_token, oauth_token or job_token should " + "be defined" + ) + if (self.http_username and not self.http_password) or ( + not self.http_username and self.http_password + ): + raise ValueError("Both http_username and http_password should be defined") + if tokens and self.http_username: + raise ValueError( + "Only one of token authentications or http " + "authentication should be defined" + ) + + self._auth: requests.auth.AuthBase | None = None + if self.private_token: + self._auth = _backends.PrivateTokenAuth(self.private_token) + + if self.oauth_token: + self._auth = _backends.OAuthTokenAuth(self.oauth_token) + + if self.job_token: + self._auth = _backends.JobTokenAuth(self.job_token) + + if self.http_username and self.http_password: + self._auth = requests.auth.HTTPBasicAuth( + self.http_username, self.http_password + ) + class AsyncGraphQL(_BaseGraphQL): def __init__( From d64f91e87422f5455ed934da645f147a1f510952 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Fri, 18 Apr 2025 12:46:28 +0800 Subject: [PATCH 04/20] chore(deps): Upgrade version Upgraded version to allow successful installation of packages into the library, as the previous version was not working. --- CHANGELOG.md | 2 +- gitlab/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4cf99cd4..0ffbed64c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # CHANGELOG -## v5.6.0 (2025-01-28) +## v5.6.1 (2025-01-28) ### Features diff --git a/gitlab/_version.py b/gitlab/_version.py index 24f57a764..f0af14f8e 100644 --- a/gitlab/_version.py +++ b/gitlab/_version.py @@ -3,4 +3,4 @@ __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" __title__ = "python-gitlab" -__version__ = "6.0.0" +__version__ = "5.6.1" From 1d70177053d75616186c94794114e844dd057c00 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Fri, 18 Apr 2025 13:02:51 +0800 Subject: [PATCH 05/20] feat(api): Class _BaseGraphQL add parameter and class variable Added parameters to be compatible with parameters required by from_config. --- gitlab/client.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/gitlab/client.py b/gitlab/client.py index 8fc7cc435..12e362b56 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1269,14 +1269,25 @@ def __init__( self, url: str | None = None, *, - token: str | None = None, + private_token: str | None = None, + oauth_token: str | None = None, + job_token: str | None = None, ssl_verify: bool | str = True, + client: httpx.Client | None = None, + http_username: str | None = None, + http_password: str | None = None, timeout: float | None = None, + api_version: str = "4", + per_page: int | None = None, + pagination: str | None = None, + order_by: str | None = 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, + keep_base_url: bool = False, + **kwargs: Any, ) -> None: if not _GQL_INSTALLED: raise ImportError( @@ -1286,7 +1297,17 @@ def __init__( ) self._base_url = utils.get_base_url(url) self._timeout = timeout - self._token = token + self._private_token = private_token + self._oauth_token = oauth_token + self._job_token = job_token + self._client = client + self._http_username = http_username + self._http_password = http_password + self._api_version = api_version + self._per_page = per_page + self._pagination = pagination + self._order_by = order_by + self._keep_base_url = keep_base_url self._url = f"{self._base_url}/api/graphql" self._user_agent = user_agent self._ssl_verify = ssl_verify @@ -1299,8 +1320,8 @@ def __init__( def _get_client_opts(self) -> dict[str, Any]: headers = {"User-Agent": self._user_agent} - if self._token: - headers["Authorization"] = f"Bearer {self._token}" + if self._private_token: + headers["Authorization"] = f"Bearer {self._private_token}" return { "headers": headers, From 3858e74b8829256b6ce4a7dc145f2d05bbca2e9c Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Fri, 18 Apr 2025 13:11:16 +0800 Subject: [PATCH 06/20] refactor(api): Test retries number --- gitlab/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/client.py b/gitlab/client.py index 12e362b56..c06244ace 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1545,7 +1545,7 @@ def __init__( timeout: float | None = None, user_agent: str = gitlab.const.USER_AGENT, fetch_schema_from_transport: bool = False, - max_retries: int = 10, + max_retries: int = 100, obey_rate_limit: bool = True, retry_transient_errors: bool = False, ) -> None: From 786074ce0774e1c8ae3b95b6ba72797836d105c6 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Fri, 18 Apr 2025 13:12:51 +0800 Subject: [PATCH 07/20] refactor(api): Test retries number --- gitlab/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/client.py b/gitlab/client.py index c06244ace..19d607c62 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1350,7 +1350,7 @@ def __init__( order_by: str | None = None, user_agent: str = gitlab.const.USER_AGENT, fetch_schema_from_transport: bool = False, - max_retries: int = 10, + max_retries: int = 100, obey_rate_limit: bool = True, retry_transient_errors: bool = False, keep_base_url: bool = False, From 4d8d7eb0a784b08b193a8ce56222fd419e77dc8c Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Tue, 22 Apr 2025 20:22:25 +0800 Subject: [PATCH 08/20] feat(api): Change valum name Added parameters to be compatible with parameters required by from_config. --- gitlab/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gitlab/client.py b/gitlab/client.py index 19d607c62..701750ba9 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1267,8 +1267,8 @@ def next(self) -> dict[str, Any]: class _BaseGraphQL: def __init__( self, - url: str | None = None, *, + url: str | None = None, private_token: str | None = None, oauth_token: str | None = None, job_token: str | None = None, @@ -1335,7 +1335,6 @@ def __init__( self, url: str | None = None, *, - token: str | None = None, private_token: str | None = None, oauth_token: str | None = None, job_token: str | None = None, @@ -1401,7 +1400,7 @@ def __init__( super().__init__( url=url, - token=token, + private_token=private_token, ssl_verify=ssl_verify, timeout=timeout, user_agent=user_agent, From 68f0932a96fb4d45e0d9da9c00f20679af6d06a3 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Tue, 22 Apr 2025 20:23:47 +0800 Subject: [PATCH 09/20] feat(api): Class _BaseGraphQL add parameter and class variable Added parameters to be compatible with parameters required by from_config. --- gitlab/client.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 2 deletions(-) diff --git a/gitlab/client.py b/gitlab/client.py index 701750ba9..e746aef50 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1538,19 +1538,73 @@ def __init__( self, url: str | None = None, *, - token: str | None = None, + private_token: str | None = None, + private_token: str | None = None, + oauth_token: str | None = None, + job_token: str | None = None, ssl_verify: bool | str = True, client: httpx.AsyncClient | None = None, + http_username: str | None = None, + http_password: str | None = None, timeout: float | None = None, + api_version: str = "4", + per_page: int | None = None, + pagination: str | None = None, + order_by: str | None = None, user_agent: str = gitlab.const.USER_AGENT, fetch_schema_from_transport: bool = False, max_retries: int = 100, obey_rate_limit: bool = True, retry_transient_errors: bool = False, + keep_base_url: bool = False, + **kwargs: Any, ) -> None: + self._api_version = str(api_version) + self._server_version: str | None = None + self._server_revision: str | None = None + self._base_url = utils.get_base_url(url) + self._url = f"{self._base_url}/api/v{api_version}" + #: Timeout to use for requests to gitlab server + self.timeout = timeout + self.retry_transient_errors = retry_transient_errors + self.keep_base_url = keep_base_url + #: Headers that will be used in request to GitLab + self.headers = {"User-Agent": user_agent} + + #: Whether SSL certificates should be validated + self.ssl_verify = ssl_verify + + self.private_token = private_token + self.http_username = http_username + self.http_password = http_password + self.oauth_token = oauth_token + self.job_token = job_token + self._set_auth_info() + + #: Create a session object for requests + _backend: type[_backends.DefaultBackend] = kwargs.pop( + "backend", _backends.DefaultBackend + ) + self._backend = _backend(**kwargs) + self.session = self._backend.client + + self.per_page = per_page + self.pagination = pagination + self.order_by = order_by + + # We only support v4 API at this time + if self._api_version not in ("4",): + raise ModuleNotFoundError(f"gitlab.v{self._api_version}.objects") + # NOTE: We must delay import of gitlab.v4.objects until now or + # otherwise it will cause circular import errors + from gitlab.v4 import objects + + self._objects = objects + self.user: objects.CurrentUser | None = None + super().__init__( url=url, - token=token, + private_token=private_token, ssl_verify=ssl_verify, timeout=timeout, user_agent=user_agent, @@ -1605,3 +1659,83 @@ async def execute( ) return result + + @classmethod + def from_config( + cls, + gitlab_id: str | None = None, + config_files: list[str] | None = None, + **kwargs: Any, + ) -> Gitlab: + """Create a Gitlab connection from configuration files. + + Args: + gitlab_id: ID of the configuration section. + config_files list[str]: List of paths to configuration files. + + kwargs: + session requests.Session: Custom requests Session + + Returns: + A Gitlab connection. + + Raises: + gitlab.config.GitlabDataError: If the configuration is not correct. + """ + config = gitlab.config.GitlabConfigParser( + gitlab_id=gitlab_id, config_files=config_files + ) + return cls( + config.url, + private_token=config.private_token, + oauth_token=config.oauth_token, + job_token=config.job_token, + ssl_verify=config.ssl_verify, + timeout=config.timeout, + http_username=config.http_username, + http_password=config.http_password, + api_version=config.api_version, + per_page=config.per_page, + pagination=config.pagination, + order_by=config.order_by, + user_agent=config.user_agent, + retry_transient_errors=config.retry_transient_errors, + keep_base_url=config.keep_base_url, + **kwargs, + ) + + def _set_auth_info(self) -> None: + tokens = [ + token + for token in [self.private_token, self.oauth_token, self.job_token] + if token + ] + if len(tokens) > 1: + raise ValueError( + "Only one of private_token, oauth_token or job_token should " + "be defined" + ) + if (self.http_username and not self.http_password) or ( + not self.http_username and self.http_password + ): + raise ValueError("Both http_username and http_password should be defined") + if tokens and self.http_username: + raise ValueError( + "Only one of token authentications or http " + "authentication should be defined" + ) + + self._auth: requests.auth.AuthBase | None = None + if self.private_token: + self._auth = _backends.PrivateTokenAuth(self.private_token) + + if self.oauth_token: + self._auth = _backends.OAuthTokenAuth(self.oauth_token) + + if self.job_token: + self._auth = _backends.JobTokenAuth(self.job_token) + + if self.http_username and self.http_password: + self._auth = requests.auth.HTTPBasicAuth( + self.http_username, self.http_password + ) From 3bbacc0126f727fd03cbf3d474fd24be4fa9c6fe Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Tue, 22 Apr 2025 20:30:56 +0800 Subject: [PATCH 10/20] docs(version): Update documentation --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ffbed64c..993d1c286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,26 @@ # CHANGELOG +## v5.7.0 (2025-04-22) -## v5.6.1 (2025-01-28) +### Features + +- **group**: Add can use config file. + ([`c70cd7a`](https://github.com/python-gitlab/python-gitlab/pull/3177/commits/c70cd7a8a3c1a81dd4e4495843461c066e11d310)) + +## v5.6.1 (2025-04-18) + +### Features + +- **group**: Add can use config file. + ([`bf7eba5`](https://github.com/python-gitlab/python-gitlab/pull/3177/commits/bf7eba5909669e19757ef1c5b590f613be00d5f4)) + +## v5.6.0 (2025-01-28) ### Features - **group**: Add support for group level MR approval rules ([`304bdd0`](https://github.com/python-gitlab/python-gitlab/commit/304bdd09cd5e6526576c5ec58cb3acd7e1a783cb)) - ## v5.5.0 (2025-01-28) ### Chores From ca6c9e71996aabb09f431a21eb02b87a266a0a40 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Tue, 22 Apr 2025 20:31:12 +0800 Subject: [PATCH 11/20] docs(version): Update documentation --- gitlab/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/_version.py b/gitlab/_version.py index f0af14f8e..039dc062b 100644 --- a/gitlab/_version.py +++ b/gitlab/_version.py @@ -3,4 +3,4 @@ __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" __title__ = "python-gitlab" -__version__ = "5.6.1" +__version__ = "5.7.1" From 41e57b988f836735b287246519a0fdea62b88259 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Tue, 22 Apr 2025 20:36:39 +0800 Subject: [PATCH 12/20] fix(api): Fix duplicate argument 'private_token' in function definition --- gitlab/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gitlab/client.py b/gitlab/client.py index e746aef50..7813112bf 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1539,7 +1539,6 @@ def __init__( url: str | None = None, *, private_token: str | None = None, - private_token: str | None = None, oauth_token: str | None = None, job_token: str | None = None, ssl_verify: bool | str = True, From f86fb8e31c9010d50de16922628417ad5b5eae82 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Tue, 22 Apr 2025 21:11:55 +0800 Subject: [PATCH 13/20] feat(api): Use client.execute variables Adding this feature would directly allow specific conditions to be variables, increasing the flexibility of application. Using variables: https://gql.readthedocs.io/en/stable/usage/variables.html --- gitlab/client.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/gitlab/client.py b/gitlab/client.py index 7813112bf..959cfddff 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1424,7 +1424,12 @@ def __enter__(self) -> GraphQL: def __exit__(self, *args: Any) -> None: self._http_client.close() - def execute(self, request: str | graphql.Source, *args: Any, **kwargs: Any) -> Any: + def execute( + self, request: str | graphql.Source, + variable_values: dict, + *args: Any, + **kwargs: Any + ) -> Any: parsed_document = self._gql(request) retry = utils.Retry( max_retries=self._max_retries, @@ -1434,7 +1439,10 @@ def execute(self, request: str | graphql.Source, *args: Any, **kwargs: Any) -> A while True: try: - result = self._client.execute(parsed_document, *args, **kwargs) + result = self._client.execute( + parsed_document, + variable_values=variable_values, + *args, **kwargs) except gql.transport.exceptions.TransportServerError as e: if retry.handle_retry_on_status( status_code=e.code, headers=self._transport.response_headers From 0d4de011173534dd732f716041b45bddfab336c8 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Wed, 23 Apr 2025 16:53:53 +0800 Subject: [PATCH 14/20] feat(graphql): Add parameter and class variable to GraphQL Add GraphQL.enable_debug --- gitlab/client.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/gitlab/client.py b/gitlab/client.py index 959cfddff..e5ca2e3a4 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1540,6 +1540,39 @@ def _set_auth_info(self) -> None: self.http_username, self.http_password ) + def enable_debug(self, mask_credentials: bool = True) -> None: + import logging + from http import client + + client.HTTPConnection.debuglevel = 1 + logging.basicConfig() + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + httpclient_log = logging.getLogger("http.client") + httpclient_log.propagate = True + httpclient_log.setLevel(logging.DEBUG) + + requests_log = logging.getLogger("requests.packages.urllib3") + requests_log.setLevel(logging.DEBUG) + requests_log.propagate = True + + # shadow http.client prints to log() + # https://stackoverflow.com/a/16337639 + def print_as_log(*args: Any) -> None: + httpclient_log.log(logging.DEBUG, " ".join(args)) + + setattr(client, "print", print_as_log) + + if not mask_credentials: + return + + token = self.private_token or self.oauth_token or self.job_token + handler = logging.StreamHandler() + handler.setFormatter(utils.MaskingFormatter(masked=token)) + logger.handlers.clear() + logger.addHandler(handler) + class AsyncGraphQL(_BaseGraphQL): def __init__( From 859c550dfa43e0524f985606bb5662a749446aa2 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Wed, 23 Apr 2025 16:54:55 +0800 Subject: [PATCH 15/20] feat(graphql): Add parameter and class variable to AsyncGraphQL Add AsyncGraphQL.enable_debug --- gitlab/client.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/gitlab/client.py b/gitlab/client.py index e5ca2e3a4..5129d5c3d 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1779,3 +1779,36 @@ def _set_auth_info(self) -> None: self._auth = requests.auth.HTTPBasicAuth( self.http_username, self.http_password ) + + def enable_debug(self, mask_credentials: bool = True) -> None: + import logging + from http import client + + client.HTTPConnection.debuglevel = 1 + logging.basicConfig() + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + httpclient_log = logging.getLogger("http.client") + httpclient_log.propagate = True + httpclient_log.setLevel(logging.DEBUG) + + requests_log = logging.getLogger("requests.packages.urllib3") + requests_log.setLevel(logging.DEBUG) + requests_log.propagate = True + + # shadow http.client prints to log() + # https://stackoverflow.com/a/16337639 + def print_as_log(*args: Any) -> None: + httpclient_log.log(logging.DEBUG, " ".join(args)) + + setattr(client, "print", print_as_log) + + if not mask_credentials: + return + + token = self.private_token or self.oauth_token or self.job_token + handler = logging.StreamHandler() + handler.setFormatter(utils.MaskingFormatter(masked=token)) + logger.handlers.clear() + logger.addHandler(handler) From 4198b3bef274f4fc32ae136715e0fabd7948adf7 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Thu, 24 Apr 2025 03:37:09 +0800 Subject: [PATCH 16/20] style: Update code formatting --- gitlab/client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gitlab/client.py b/gitlab/client.py index 5129d5c3d..6967789d8 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1425,11 +1425,12 @@ def __exit__(self, *args: Any) -> None: self._http_client.close() def execute( - self, request: str | graphql.Source, - variable_values: dict, - *args: Any, - **kwargs: Any - ) -> Any: + self, + request: str | graphql.Source, + variable_values: dict, + *args: Any, + **kwargs: Any, + ) -> Any: parsed_document = self._gql(request) retry = utils.Retry( max_retries=self._max_retries, @@ -1440,9 +1441,8 @@ def execute( while True: try: result = self._client.execute( - parsed_document, - variable_values=variable_values, - *args, **kwargs) + parsed_document, variable_values=variable_values, *args, **kwargs + ) except gql.transport.exceptions.TransportServerError as e: if retry.handle_retry_on_status( status_code=e.code, headers=self._transport.response_headers From a6fd190da39e85886693933834e582277218b774 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Thu, 24 Apr 2025 03:45:06 +0800 Subject: [PATCH 17/20] feat: Add exclude path --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 05a15c6c4..9297936ae 100644 --- a/tox.ini +++ b/tox.ini @@ -89,7 +89,7 @@ commands = commands = {posargs} [flake8] -exclude = .git,.venv,.tox,dist,doc,*egg,build, +exclude = .git, venv, .tox, dist, doc, docs, docs/conf.py, *eggs, build, build-pypy, __pycache__, old, max-line-length = 88 # We ignore the following because we use black to handle code-formatting # E203: Whitespace before ':' From aa3dd24e0a7a1a93e53a098979fcdb711b3e2cf3 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Thu, 24 Apr 2025 06:45:41 +0800 Subject: [PATCH 18/20] feat(test): Using variable_values --- tests/unit/test_graphql.py | 139 +++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_graphql.py b/tests/unit/test_graphql.py index 9348dbf98..326b1ce44 100644 --- a/tests/unit/test_graphql.py +++ b/tests/unit/test_graphql.py @@ -47,15 +47,74 @@ async def test_async_graphql_as_context_manager_aexits(): def test_graphql_retries_on_429_response( gl_gql: gitlab.GraphQL, respx_mock: respx.MockRouter ): + """Test graphql_retries_on_429_response. + + query: + + query get_projects($first: Int = 2) { + group(fullPath: "treeocean") { + projects(first: $first, includeSubgroups: true) { + nodes { + id + } + } + } + } + + reply: + + { + "data": { + "group": { + "projects": { + "nodes": [ + { + "id": "gid://gitlab/Project/491" + }, + { + "id": "gid://gitlab/Project/485" + } + ] + } + } + }, + "correlationId": "01JSJ7ERZ7N5PKA5RCBJQNG9CS" + } + """ url = "https://gitlab.example.com/api/graphql" responses = [ httpx.Response(429, headers={"retry-after": "1"}), httpx.Response( - 200, json={"data": {"currentUser": {"id": "gid://gitlab/User/1"}}} + 200, + json={ + "data": { + "group": { + "projects": { + "nodes": [ + {"id": "gid://gitlab/Project/491"}, + {"id": "gid://gitlab/Project/485"}, + ] + } + } + }, + "correlationId": "01JSJ7ERZ7N5PKA5RCBJQNG9CS", + }, ), ] + variable_values = {"first": 2} + query = """ + query get_projects($first: Int) { + group(fullPath: "python-gitlab") { + projects(first: $first, includeSubgroups: true) { + nodes { + id + } + } + } + } + """ respx_mock.post(url).mock(side_effect=responses) - gl_gql.execute("query {currentUser {id}}") + gl_gql.execute(query, variable_values=variable_values) @pytest.mark.anyio @@ -68,8 +127,21 @@ async def test_async_graphql_retries_on_429_response( 200, json={"data": {"currentUser": {"id": "gid://gitlab/User/1"}}} ), ] + variable_values = {"first": 2} + query = """ + query get_projects($first: Int) { + group(fullPath: "python-gitlab") { + projects(first: $first, includeSubgroups: true) { + nodes { + id + } + } + } + } + """ + respx_mock.post(api_url).mock(side_effect=responses) - await gl_async_gql.execute("query {currentUser {id}}") + await gl_async_gql.execute(query, variable_values=variable_values) def test_graphql_raises_when_max_retries_exceeded( @@ -81,8 +153,21 @@ def test_graphql_raises_when_max_retries_exceeded( gl_gql = gitlab.GraphQL( "https://gitlab.example.com", max_retries=1, retry_transient_errors=True ) + variable_values = {"first": 2} + query = """ + query get_projects($first: Int) { + group(fullPath: "python-gitlab") { + projects(first: $first, includeSubgroups: true) { + nodes { + id + } + } + } + } + """ + with pytest.raises(gitlab.GitlabHttpError): - gl_gql.execute("query {currentUser {id}}") + gl_gql.execute(query, variable_values=variable_values) @pytest.mark.anyio @@ -95,16 +180,42 @@ async def test_async_graphql_raises_when_max_retries_exceeded( gl_async_gql = gitlab.AsyncGraphQL( "https://gitlab.example.com", max_retries=1, retry_transient_errors=True ) + variable_values = {"first": 2} + query = """ + query get_projects($first: Int) { + group(fullPath: "python-gitlab") { + projects(first: $first, includeSubgroups: true) { + nodes { + id + } + } + } + } + """ + with pytest.raises(gitlab.GitlabHttpError): - await gl_async_gql.execute("query {currentUser {id}}") + await gl_async_gql.execute(query, variable_values=variable_values) def test_graphql_raises_on_401_response( api_url: str, gl_gql: gitlab.GraphQL, respx_mock: respx.MockRouter ): respx_mock.post(api_url).mock(return_value=httpx.Response(401)) + variable_values = {"first": 2} + query = """ + query get_projects($first: Int) { + group(fullPath: "python-gitlab") { + projects(first: $first, includeSubgroups: true) { + nodes { + id + } + } + } + } + """ + with pytest.raises(gitlab.GitlabAuthenticationError): - gl_gql.execute("query {currentUser {id}}") + gl_gql.execute(query, variable_values=variable_values) @pytest.mark.anyio @@ -112,5 +223,19 @@ async def test_async_graphql_raises_on_401_response( api_url: str, gl_async_gql: gitlab.AsyncGraphQL, respx_mock: respx.MockRouter ): respx_mock.post(api_url).mock(return_value=httpx.Response(401)) + + variable_values = {"first": 2} + query = """ + query get_projects($first: Int) { + group(fullPath: "python-gitlab") { + projects(first: $first, includeSubgroups: true) { + nodes { + id + } + } + } + } + """ + with pytest.raises(gitlab.GitlabAuthenticationError): - await gl_async_gql.execute("query {currentUser {id}}") + await gl_async_gql.execute(query, variable_values=variable_values) From cf4feef8d74b09ed579a2b8c5d1d4802c27036ba Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Thu, 24 Apr 2025 06:49:03 +0800 Subject: [PATCH 19/20] fix(graphql): Fix kwargs unused --- gitlab/client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gitlab/client.py b/gitlab/client.py index 6967789d8..c7c2d4abf 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1317,6 +1317,17 @@ def __init__( self._client_opts = self._get_client_opts() self._fetch_schema_from_transport = fetch_schema_from_transport + #: Create a session object for requests + _backend: type[_backends.DefaultBackend] = kwargs.pop( + "backend", _backends.DefaultBackend + ) + self._backend = _backend(**kwargs) + self.session = self._backend.client + + self.per_page = per_page + self.pagination = pagination + self.order_by = order_by + def _get_client_opts(self) -> dict[str, Any]: headers = {"User-Agent": self._user_agent} From 58c291696e651bc43377a36b152ee6e08e605f17 Mon Sep 17 00:00:00 2001 From: timmy61109 Date: Thu, 24 Apr 2025 06:49:41 +0800 Subject: [PATCH 20/20] fix(graphql): Fix variable_values unused --- gitlab/client.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gitlab/client.py b/gitlab/client.py index c7c2d4abf..94c879e2d 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -1438,7 +1438,7 @@ def __exit__(self, *args: Any) -> None: def execute( self, request: str | graphql.Source, - variable_values: dict, + variable_values: dict[str, Any], *args: Any, **kwargs: Any, ) -> Any: @@ -1680,7 +1680,11 @@ async def __aexit__(self, *args: Any) -> None: await self._http_client.aclose() async def execute( - self, request: str | graphql.Source, *args: Any, **kwargs: Any + self, + request: str | graphql.Source, + variable_values: dict[str, Any], + *args: Any, + **kwargs: Any, ) -> Any: parsed_document = self._gql(request) retry = utils.Retry( @@ -1692,7 +1696,7 @@ async def execute( while True: try: result = await self._client.execute_async( - parsed_document, *args, **kwargs + parsed_document, variable_values=variable_values, *args, **kwargs ) except gql.transport.exceptions.TransportServerError as e: if retry.handle_retry_on_status(