Skip to content

Commit a6390bf

Browse files
committed
feat(cli): allow options from args and environment variables
1 parent 170a4d9 commit a6390bf

File tree

4 files changed

+182
-7
lines changed

4 files changed

+182
-7
lines changed

gitlab/cli.py

Lines changed: 96 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,90 @@ 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 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=(
241+
"GitLab CI job token. Explicitly providing this is usually not needed.\n"
242+
"[env var, only if explicitly overriding CI_JOB_TOKEN: GITLAB_JOB_TOKEN]"
243+
),
244+
required=False,
245+
default=os.getenv("GITLAB_JOB_TOKEN"),
246+
)
155247
return parser
156248

157249

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

250342
try:
251-
gl = gitlab.Gitlab.from_config(gitlab_id, config_files)
343+
gl = gitlab.Gitlab.merge_config(options, gitlab_id, config_files)
252344
if gl.private_token or gl.oauth_token or gl.job_token:
253345
gl.auth()
254346
except Exception as e:

gitlab/client.py

Lines 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

gitlab/config.py

Lines 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)

tests/unit/test_config.py

Lines 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

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