Coverage for navigaattori/cli.py: 95.29%

63 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-04 20:46:10 +00:00

1"""Command line interface for navigator (Finnish: navigaattori) guided by conventions.""" 

2 

3import datetime as dti 

4import logging 

5import pathlib 

6import sys 

7 

8import typer 

9 

10import navigaattori.api as api 

11import navigaattori.eject as eje 

12from navigaattori import ( 

13 APP_NAME, 

14 DEFAULT_STRUCTURE_NAME, 

15 QUIET, 

16 TS_FORMAT_PAYLOADS, 

17 __version__ as APP_VERSION, 

18 log, 

19) 

20 

21app = typer.Typer( 

22 add_completion=False, 

23 context_settings={'help_option_names': ['-h', '--help']}, 

24 no_args_is_help=True, 

25) 

26 

27DocumentRoot = typer.Option( 

28 '', 

29 '-d', 

30 '--document-root', 

31 help='Root of the document tree to visit. Optional\n(default: positional tree root value)', 

32) 

33StructureName = typer.Option( 

34 DEFAULT_STRUCTURE_NAME, 

35 '-s', 

36 '--structure', 

37 help='structure mapping file (default: {gat.DEFAULT_STRUCTURE_NAME})', 

38) 

39TargetName = typer.Option( 

40 '', 

41 '-t', 

42 '--target', 

43 help='target document key', 

44) 

45FacetName = typer.Option( 

46 '', 

47 '-f', 

48 '--facet', 

49 help='facet key of target document', 

50) 

51Verbosity = typer.Option( 

52 False, 

53 '-v', 

54 '--verbose', 

55 help='Verbose output (default is False)', 

56) 

57Strictness = typer.Option( 

58 False, 

59 '-s', 

60 '--strict', 

61 help='Output noisy warnings on console (default is False)', 

62) 

63Guess = typer.Option( 

64 False, 

65 '-g', 

66 '--guess', 

67 help='Guess and derive structures from folder tree structure.yml files if possible (default is False)', 

68) 

69OutputPath = typer.Option( 

70 '', 

71 '-o', 

72 '--output-path', 

73 help='Path to output unambiguous content to - like when ejecting a template', 

74) 

75Excludes = typer.Option( 

76 '.git/,render/pdf/', 

77 '-x', 

78 '--excludes', 

79 help='comma separated list of values to exclude paths\ncontaining the substring (default: .git/,render/pdf/)', 

80) 

81 

82 

83@app.callback(invoke_without_command=True) 

84def callback( 

85 version: bool = typer.Option( 

86 False, 

87 '-V', 

88 '--version', 

89 help='Display the application version and exit', 

90 is_eager=True, 

91 ) 

92) -> None: 

93 """ 

94 Navigator (Finnish: navigaattori) guided by conventions. 

95 """ 

96 if version: 

97 typer.echo(f'{APP_NAME} version {APP_VERSION}') 

98 raise typer.Exit() 

99 

100 

101def _verify_call_vector( 

102 doc_root: str, 

103 doc_root_pos: str, 

104 verbose: bool, 

105 strict: bool, 

106 guess: bool, 

107 excludes: str, 

108) -> tuple[int, str, str, dict[str, object]]: 

109 """DRY""" 

110 doc = doc_root.strip() 

111 if not doc and doc_root_pos: 

112 doc = doc_root_pos 

113 if not doc: 

114 print('Document tree root required', file=sys.stderr) 

115 return 2, 'Document tree root required', '', {} 

116 

117 doc_root_path = pathlib.Path(doc) 

118 if doc_root_path.exists(): 

119 if not doc_root_path.is_dir(): 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true

120 print(f'requested tree root at ({doc}) is not a folder', file=sys.stderr) 

121 return 2, f'requested tree root at ({doc}) is not a folder', '', {} 

122 else: 

123 print(f'requested tree root at ({doc}) does not exist', file=sys.stderr) 

124 return 2, f'requested tree root at ({doc}) does not exist', '', {} 

125 

126 options = { 

127 'quiet': QUIET and not verbose and not strict, 

128 'strict': strict, 

129 'verbose': verbose, 

130 'guess': guess, 

131 'excludes': excludes, 

132 } 

133 if verbose: 

134 logging.getLogger().setLevel(logging.DEBUG) 

135 return 0, '', doc, options 

136 

137 

138@app.command('explore') 

139def explore( # noqa 

140 doc_root_pos: str = typer.Argument(''), 

141 doc_root: str = DocumentRoot, 

142 verbose: bool = Verbosity, 

143 strict: bool = Strictness, 

144 guess: bool = Guess, 

145 excludes: str = Excludes, 

146) -> int: 

147 """ 

148 Explore the structures definition tree in the file system. 

149 """ 

150 code, message, doc, options = _verify_call_vector(doc_root, doc_root_pos, verbose, strict, guess, excludes) 

151 if code: 

152 log.error(message) 

153 return sys.exit(code) 

154 

155 start_time = dti.datetime.now(tz=dti.timezone.utc) 

156 start_ts = start_time.strftime(TS_FORMAT_PAYLOADS) 

157 log.info(f'start timestamp ({start_ts})') 

158 

159 code, _ = api.explore(doc_root=doc, options=options) 

160 

161 end_time = dti.datetime.now(tz=dti.timezone.utc) 

162 end_ts = end_time.strftime(TS_FORMAT_PAYLOADS) 

163 duration_secs = (end_time - start_time).total_seconds() 

164 log.info(f'end timestamp ({end_ts})') 

165 log.info(f'explored structures at {doc} in {duration_secs} secs') 

166 return sys.exit(code) 

167 

168 

169@app.command('eject') 

170def eject( # noqa 

171 that: str = typer.Argument(''), 

172 out: str = OutputPath, 

173) -> int: 

174 """ 

175 Eject a template. Enter unique part to retrieve, any unknown word to obtain the list of known templates. 

176 """ 

177 return sys.exit(eje.this(thing=that, out=out)) 

178 

179 

180@app.command('version') 

181def app_version() -> None: 

182 """ 

183 Display the application version and exit. 

184 """ 

185 callback(True)