Coverage for src/gitlabracadabra/cli.py: 72%
93 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-14 23:10 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-14 23:10 +0200
1#!/usr/bin/env python
2#
3# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>
4# Copyright (C) 2019-2025 Mathieu Parent <math.parent@gmail.com>
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Lesser General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
19from __future__ import annotations
21import logging
22import sys
23from argparse import ArgumentParser
24from typing import TYPE_CHECKING
26import gitlabracadabra
27import gitlabracadabra.parser
28from gitlabracadabra.gitlab.connections import GitlabConnections
30if TYPE_CHECKING: 30 ↛ 31line 30 didn't jump to line 31 because the condition on line 30 was never true
31 from collections.abc import Sequence
33logger = logging.getLogger(__name__)
36def _get_argument_parser() -> ArgumentParser:
37 parser = ArgumentParser(description="GitLabracadabra")
38 parser.add_argument("--version", help="Display the version.", action="store_true")
39 parser.add_argument("-v", "--verbose", "--fancy", help="Verbose mode", action="store_true")
40 parser.add_argument("-d", "--debug", help="Debug mode (display HTTP requests)", action="store_true")
41 parser.add_argument("--logging-format", help="Logging format", choices=["short", "long"], default="short")
42 parser.add_argument(
43 "-c", "--config-file", action="append", help=("Configuration file to use. Can be used " "multiple times.")
44 )
45 parser.add_argument(
46 "-g",
47 "--gitlab",
48 help=("Which configuration section should " "be used. If not defined, the default selection " "will be used."),
49 required=False,
50 )
51 parser.add_argument("--dry-run", help="Dry run", action="store_true")
52 parser.add_argument("--fail-on-errors", help="Fail on errors", action="store_true")
53 parser.add_argument("--fail-on-warnings", help="Fail on warnings", action="store_true")
54 parser.add_argument(
55 "--doc-markdown",
56 help=("Output the help for the given type (project, " "group, user, application_settings) as " "Markdown."),
57 )
58 parser.add_argument(
59 "action_files",
60 help="Action file. Can be used multiple times.",
61 metavar="ACTIONFILE.yml",
62 nargs="*",
63 default=["gitlabracadabra.yml"],
64 )
66 return parser
69class ExitCodeHandler(logging.Handler):
70 def __init__(self) -> None:
71 logging.Handler.__init__(self)
72 self._max_levelno: int = logging.NOTSET
74 def emit(self, record: logging.LogRecord) -> None:
75 if record.levelno > self._max_levelno:
76 self._max_levelno = record.levelno
78 @property
79 def max_levelno(self) -> int:
80 return self._max_levelno
83def main(args: Sequence[str] | None = None) -> None:
84 argument_parser = _get_argument_parser()
86 namespace = argument_parser.parse_args(args)
88 if namespace.version: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 print(gitlabracadabra.__version__) # noqa: T201
90 sys.exit(0)
92 config_files = namespace.config_file
93 gitlab_id = namespace.gitlab
95 if namespace.logging_format == "long": 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 logging_format = "%(asctime)s [%(process)d] %(levelname)-8.8s %(name)s: %(message)s"
97 else:
98 logging_format = "%(levelname)-8.8s %(message)s"
99 log_level = logging.WARNING
100 if namespace.verbose: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 log_level = logging.INFO
102 if namespace.debug: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 log_level = logging.DEBUG
104 exit_code_handler = ExitCodeHandler()
105 logging.basicConfig(
106 format=logging_format,
107 level=log_level,
108 )
109 logging.root.addHandler(exit_code_handler)
111 if namespace.doc_markdown: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 cls = gitlabracadabra.parser.GitlabracadabraParser.get_class_for(namespace.doc_markdown)
113 print(cls.doc_markdown()) # noqa: T201
114 sys.exit(0)
116 try:
117 GitlabConnections().load(gitlab_id, config_files, debug=namespace.debug)
118 except Exception as e: # noqa: BLE001
119 logger.error(str(e))
120 sys.exit(1)
122 # First pass: Load data and preflight checks
123 objects = {}
124 has_errors = False
125 for action_file in namespace.action_files:
126 if action_file.endswith((".yml", ".yaml")): 126 ↛ 129line 126 didn't jump to line 129 because the condition on line 126 was always true
127 parser = gitlabracadabra.parser.GitlabracadabraParser.from_yaml_file(action_file)
128 else:
129 logger.error("Unhandled file: %s", action_file)
130 has_errors = True
131 continue
132 logger.debug("Parsing file: %s", action_file)
133 objects[action_file] = parser.objects()
134 for k, v in sorted(objects[action_file].items()):
135 if len(v.errors()) > 0: 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true
136 for error in v.errors():
137 logger.error("Error in %s (%s %s): %s", action_file, v.type_name(), k, str(error))
138 has_errors = True
140 if has_errors: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 logger.error("Preflight checks errors. Exiting")
142 sys.exit(1)
144 # Second pass:
145 for action_file in namespace.action_files:
146 for _name, obj in sorted(objects[action_file].items()):
147 obj.process(dry_run=namespace.dry_run)
149 fails_on = logging.CRITICAL
150 if namespace.fail_on_errors: 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true
151 fails_on = logging.ERROR
152 if namespace.fail_on_warnings: 152 ↛ 154line 152 didn't jump to line 154 because the condition on line 152 was always true
153 fails_on = logging.WARNING
154 if exit_code_handler.max_levelno >= fails_on: 154 ↛ exitline 154 didn't return from function 'main' because the condition on line 154 was always true
155 sys.exit(1)
158if __name__ == "__main__": 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true
159 main()