Coverage for src/gitlabracadabra/cli.py: 72%

93 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-10 17:02 +0100

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/>. 

18 

19from __future__ import annotations 

20 

21import logging 

22import sys 

23from argparse import ArgumentParser 

24from typing import TYPE_CHECKING 

25 

26import gitlabracadabra 

27import gitlabracadabra.parser 

28from gitlabracadabra.gitlab.connections import GitlabConnections 

29 

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 

32 

33logger = logging.getLogger(__name__) 

34 

35 

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 ) 

65 

66 return parser 

67 

68 

69class ExitCodeHandler(logging.Handler): 

70 def __init__(self) -> None: 

71 logging.Handler.__init__(self) 

72 self._max_levelno: int = logging.NOTSET 

73 

74 def emit(self, record: logging.LogRecord) -> None: 

75 if record.levelno > self._max_levelno: 

76 self._max_levelno = record.levelno 

77 

78 @property 

79 def max_levelno(self) -> int: 

80 return self._max_levelno 

81 

82 

83def main(args: Sequence[str] | None = None) -> None: 

84 argument_parser = _get_argument_parser() 

85 

86 namespace = argument_parser.parse_args(args) 

87 

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) 

91 

92 config_files = namespace.config_file 

93 gitlab_id = namespace.gitlab 

94 

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) 

110 

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) 

115 

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) 

121 

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 

139 

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) 

143 

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) 

148 

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) 

156 

157 

158if __name__ == "__main__": 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true

159 main()