Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c60ae1f

Browse filesBrowse files
committed
feat(cli): allow options from args and environment variables
1 parent 170a4d9 commit c60ae1f
Copy full SHA for c60ae1f

File tree

Expand file treeCollapse file tree

5 files changed

+288
-7
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+288
-7
lines changed
Open diff view settings
Collapse file

‎gitlab/cli.py‎

Copy file name to clipboardExpand all lines: gitlab/cli.py
+92-4Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import argparse
2121
import functools
22+
import os
2223
import re
2324
import sys
2425
from types import ModuleType
@@ -112,17 +113,25 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser:
112113
"-v",
113114
"--verbose",
114115
"--fancy",
115-
help="Verbose mode (legacy format only)",
116+
help="Verbose mode (legacy format only) [env var: GITLAB_VERBOSE]",
116117
action="store_true",
118+
default=os.getenv("GITLAB_VERBOSE"),
117119
)
118120
parser.add_argument(
119-
"-d", "--debug", help="Debug mode (display HTTP requests)", action="store_true"
121+
"-d",
122+
"--debug",
123+
help="Debug mode (display HTTP requests) [env var: GITLAB_DEBUG]",
124+
action="store_true",
125+
default=os.getenv("GITLAB_DEBUG"),
120126
)
121127
parser.add_argument(
122128
"-c",
123129
"--config-file",
124130
action="append",
125-
help="Configuration file to use. Can be used multiple times.",
131+
help=(
132+
"Configuration file to use. Can be used multiple times. "
133+
"[env var: PYTHON_GITLAB_CFG]"
134+
),
126135
)
127136
parser.add_argument(
128137
"-g",
@@ -151,7 +160,86 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser:
151160
),
152161
required=False,
153162
)
163+
parser.add_argument(
164+
"--url",
165+
help=("GitLab server URL [env var: GITLAB_URL]"),
166+
required=False,
167+
default=os.getenv("GITLAB_URL"),
168+
)
169+
parser.add_argument(
170+
"--ssl-verify",
171+
help=(
172+
"Whether SSL certificates should be validated. [env var: GITLAB_SSL_VERIFY]"
173+
),
174+
required=False,
175+
default=os.getenv("GITLAB_SSL_VERIFY"),
176+
)
177+
parser.add_argument(
178+
"--timeout",
179+
help=(
180+
"Timeout to use for requests to the GitLab server. "
181+
"[env var: GITLAB_TIMEOUT]"
182+
),
183+
required=False,
184+
default=os.getenv("GITLAB_TIMEOUT"),
185+
)
186+
parser.add_argument(
187+
"--api-version",
188+
help=("GitLab API version [env var: GITLAB_API_VERSION]"),
189+
required=False,
190+
default=os.getenv("GITLAB_API_VERSION"),
191+
)
192+
parser.add_argument(
193+
"--per-page",
194+
help=(
195+
"Number of entries to return per page in the response. "
196+
"[env var: GITLAB_PER_PAGE]"
197+
),
198+
required=False,
199+
default=os.getenv("GITLAB_PER_PAGE"),
200+
)
201+
parser.add_argument(
202+
"--pagination",
203+
help=(
204+
"Whether to use keyset or offset pagination [env var: GITLAB_PAGINATION]"
205+
),
206+
required=False,
207+
default=os.getenv("GITLAB_PAGINATION"),
208+
)
209+
parser.add_argument(
210+
"--order-by",
211+
help=("Set order_by globally [env var: GITLAB_ORDER_BY]"),
212+
required=False,
213+
default=os.getenv("GITLAB_ORDER_BY"),
214+
)
215+
parser.add_argument(
216+
"--user-agent",
217+
help=(
218+
"The user agent to send to GitLab with the HTTP request. "
219+
"[env var: GITLAB_USER_AGENT]"
220+
),
221+
required=False,
222+
default=os.getenv("GITLAB_USER_AGENT"),
223+
)
154224

225+
tokens = parser.add_mutually_exclusive_group()
226+
tokens.add_argument(
227+
"--private-token",
228+
help=("GitLab private access token [env var: GITLAB_PRIVATE_TOKEN]"),
229+
required=False,
230+
default=os.getenv("GITLAB_PRIVATE_TOKEN"),
231+
)
232+
tokens.add_argument(
233+
"--oauth-token",
234+
help=("GitLab OAuth token [env var: GITLAB_OAUTH_TOKEN]"),
235+
required=False,
236+
default=os.getenv("GITLAB_OAUTH_TOKEN"),
237+
)
238+
tokens.add_argument(
239+
"--job-token",
240+
help=("GitLab CI job token [env var: CI_JOB_TOKEN]"),
241+
required=False,
242+
)
155243
return parser
156244

157245

@@ -248,7 +336,7 @@ def main() -> None:
248336
args_dict = {k: _parse_value(v) for k, v in args_dict.items() if v is not None}
249337

250338
try:
251-
gl = gitlab.Gitlab.from_config(gitlab_id, config_files)
339+
gl = gitlab.Gitlab.merge_config(options, gitlab_id, config_files)
252340
if gl.private_token or gl.oauth_token or gl.job_token:
253341
gl.auth()
254342
except Exception as e:
Collapse file

‎gitlab/client.py‎

Copy file name to clipboardExpand all lines: gitlab/client.py
+83Lines changed: 83 additions & 0 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 os
1920
import time
21+
from argparse import Namespace
2022
from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
2123

2224
import requests
@@ -256,6 +258,87 @@ def from_config(
256258
retry_transient_errors=config.retry_transient_errors,
257259
)
258260

261+
@classmethod
262+
def merge_config(
263+
cls,
264+
options: Namespace,
265+
gitlab_id: Optional[str] = None,
266+
config_files: Optional[List[str]] = None,
267+
) -> "Gitlab":
268+
"""Create a Gitlab connection by merging configuration with
269+
the following precedence:
270+
271+
1. Explicitly provided CLI arguments,
272+
2. Environment variables,
273+
3. Configuration files:
274+
a. explicitly defined config files:
275+
i. via the `--config-file` CLI argument,
276+
ii. via the `PYTHON_GITLAB_CFG` environment variable,
277+
b. user-specific config file,
278+
c. system-level config file,
279+
4. Environment variables always present in CI (CI_SERVER_URL, CI_JOB_TOKEN).
280+
281+
Args:
282+
options list[str]: List of options provided via the CLI.
283+
gitlab_id (str): ID of the configuration section.
284+
config_files list[str]: List of paths to configuration files.
285+
Returns:
286+
(gitlab.Gitlab): A Gitlab connection.
287+
288+
Raises:
289+
gitlab.config.GitlabDataError: If the configuration is not correct.
290+
"""
291+
config = gitlab.config.GitlabConfigParser(
292+
gitlab_id=gitlab_id, config_files=config_files
293+
)
294+
url = (
295+
options.url
296+
or config.url
297+
or os.getenv("CI_SERVER_URL")
298+
or gitlab.const.DEFAULT_URL
299+
)
300+
private_token, oauth_token, job_token = cls._get_auth_from_env(options, config)
301+
302+
return cls(
303+
url=url,
304+
private_token=private_token,
305+
oauth_token=oauth_token,
306+
job_token=job_token,
307+
ssl_verify=options.ssl_verify or config.ssl_verify,
308+
timeout=options.timeout or config.timeout,
309+
api_version=options.api_version or config.api_version,
310+
per_page=options.per_page or config.per_page,
311+
pagination=options.pagination or config.pagination,
312+
order_by=options.order_by or config.order_by,
313+
user_agent=options.user_agent or config.user_agent,
314+
)
315+
316+
@staticmethod
317+
def _get_auth_from_env(
318+
options: Namespace, config: gitlab.config.GitlabConfigParser
319+
) -> Tuple:
320+
"""
321+
Return a tuple where at most one of 3 token types ever has a value.
322+
Since multiple types of tokens may be present in the environment,
323+
options, or config files, this precedence ensures we don't
324+
inadvertently cause errors when initializing the client.
325+
326+
This is especially relevant when executed in CI where user and
327+
CI-provided values are both available.
328+
"""
329+
private_token = options.private_token or config.private_token
330+
oauth_token = options.oauth_token or config.oauth_token
331+
job_token = options.job_token or config.job_token or os.getenv("CI_JOB_TOKEN")
332+
333+
if private_token:
334+
return (private_token, None, None)
335+
if oauth_token:
336+
return (None, oauth_token, None)
337+
if job_token:
338+
return (None, None, job_token)
339+
340+
return (None, None, None)
341+
259342
def auth(self) -> None:
260343
"""Performs an authentication using private token.
261344
Collapse file

‎gitlab/config.py‎

Copy file name to clipboardExpand all lines: gitlab/config.py
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pathlib import Path
2424
from typing import List, Optional, Union
2525

26-
from gitlab.const import DEFAULT_URL, USER_AGENT
26+
from gitlab.const import USER_AGENT
2727

2828
_DEFAULT_FILES: List[str] = [
2929
"/etc/python-gitlab.cfg",
@@ -119,7 +119,7 @@ def __init__(
119119
self.retry_transient_errors: bool = False
120120
self.ssl_verify: Union[bool, str] = True
121121
self.timeout: int = 60
122-
self.url: str = DEFAULT_URL
122+
self.url: Optional[str] = None
123123
self.user_agent: str = USER_AGENT
124124

125125
self._files = _get_config_files(config_files)
Collapse file

‎tests/unit/test_config.py‎

Copy file name to clipboardExpand all lines: tests/unit/test_config.py
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def test_default_config(mock_clean_env, monkeypatch):
148148
assert cp.retry_transient_errors is False
149149
assert cp.ssl_verify is True
150150
assert cp.timeout == 60
151-
assert cp.url == const.DEFAULT_URL
151+
assert cp.url is None
152152
assert cp.user_agent == const.USER_AGENT
153153

154154

Collapse file

‎tests/unit/test_gitlab_auth.py‎

Copy file name to clipboardExpand all lines: tests/unit/test_gitlab_auth.py
+110Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from argparse import Namespace
2+
13
import pytest
24
import requests
35

46
from gitlab import Gitlab
7+
from gitlab.config import GitlabConfigParser
58

69

710
def test_invalid_auth_args():
@@ -83,3 +86,110 @@ def test_http_auth():
8386
assert isinstance(gl._http_auth, requests.auth.HTTPBasicAuth)
8487
assert gl.headers["PRIVATE-TOKEN"] == "private_token"
8588
assert "Authorization" not in gl.headers
89+
90+
91+
@pytest.mark.parametrize(
92+
"options,expected_private_token,expected_oauth_token,expected_job_token",
93+
[
94+
(
95+
Namespace(
96+
private_token="options-private-token",
97+
oauth_token="options-oauth-token",
98+
job_token="options-job-token",
99+
),
100+
"options-private-token",
101+
None,
102+
None,
103+
),
104+
(
105+
Namespace(
106+
private_token=None,
107+
oauth_token="options-oauth-token",
108+
job_token="option-job-token",
109+
),
110+
None,
111+
"options-oauth-token",
112+
None,
113+
),
114+
(
115+
Namespace(
116+
private_token=None, oauth_token=None, job_token="options-job-token"
117+
),
118+
None,
119+
None,
120+
"options-job-token",
121+
),
122+
],
123+
)
124+
def test_get_auth_from_env_with_options(
125+
options,
126+
expected_private_token,
127+
expected_oauth_token,
128+
expected_job_token,
129+
):
130+
cp = GitlabConfigParser()
131+
cp.private_token = None
132+
cp.oauth_token = None
133+
cp.job_token = None
134+
135+
private_token, oauth_token, job_token = Gitlab._get_auth_from_env(options, cp)
136+
assert private_token == expected_private_token
137+
assert oauth_token == expected_oauth_token
138+
assert job_token == expected_job_token
139+
140+
141+
@pytest.mark.parametrize(
142+
"config,expected_private_token,expected_oauth_token,expected_job_token",
143+
[
144+
(
145+
{
146+
"private_token": "config-private-token",
147+
"oauth_token": "config-oauth-token",
148+
"job_token": "config-job-token",
149+
},
150+
"config-private-token",
151+
None,
152+
None,
153+
),
154+
(
155+
{
156+
"private_token": None,
157+
"oauth_token": "config-oauth-token",
158+
"job_token": "config-job-token",
159+
},
160+
None,
161+
"config-oauth-token",
162+
None,
163+
),
164+
(
165+
{
166+
"private_token": None,
167+
"oauth_token": None,
168+
"job_token": "config-job-token",
169+
},
170+
None,
171+
None,
172+
"config-job-token",
173+
),
174+
],
175+
)
176+
def test_get_auth_from_env_with_config(
177+
config,
178+
expected_private_token,
179+
expected_oauth_token,
180+
expected_job_token,
181+
):
182+
options = Namespace(
183+
private_token=None,
184+
oauth_token=None,
185+
job_token=None,
186+
)
187+
cp = GitlabConfigParser()
188+
cp.private_token = config["private_token"]
189+
cp.oauth_token = config["oauth_token"]
190+
cp.job_token = config["job_token"]
191+
192+
private_token, oauth_token, job_token = Gitlab._get_auth_from_env(options, cp)
193+
assert private_token == expected_private_token
194+
assert oauth_token == expected_oauth_token
195+
assert job_token == expected_job_token

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.