Coverage for tembo/cli/cli.py: 100%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

95 statements  

1"""Submodule which contains the CLI implementation using Click.""" 

2 

3from __future__ import annotations 

4 

5import pathlib 

6from typing import Collection 

7 

8import click 

9 

10import tembo.cli 

11from tembo import exceptions 

12from tembo._version import __version__ 

13from tembo.journal import pages 

14from tembo.utils import Success 

15 

16CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) 

17 

18 

19@click.group(context_settings=CONTEXT_SETTINGS, options_metavar="<options>") 

20@click.version_option( 

21 __version__, 

22 "-v", 

23 "--version", 

24 prog_name="Tembo", 

25 message=f"Tembo v{__version__} 🐘", 

26) 

27def main(): 

28 """Tembo - an organiser for work notes.""" 

29 

30 

31@click.command(options_metavar="<options>", name="list") 

32def list_all(): 

33 """List all scopes defined in the config.yml.""" 

34 _all_scopes = [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] 

35 _all_scopes_joined = "', '".join(_all_scopes) 

36 cli_message(f"{len(_all_scopes)} names found in config.yml: '{_all_scopes_joined}'") 

37 raise SystemExit(0) 

38 

39 

40@click.command(options_metavar="<options>") 

41@click.argument("scope", metavar="<scope>") 

42@click.argument( 

43 "inputs", 

44 nargs=-1, 

45 metavar="<inputs>", 

46) 

47@click.option( 

48 "--dry-run", 

49 is_flag=True, 

50 default=False, 

51 help="Show the full path of the page to be created without actually saving the page to disk " 

52 "and exit.", 

53) 

54@click.option( 

55 "--example", 

56 is_flag=True, 

57 default=False, 

58 help="Show the example command in the config.yml if it exists and exit.", 

59) 

60def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): # noqa 

61 """ 

62 Create a new page. 

63 

64 \b 

65 `<scope>` The name of the scope in the config.yml. 

66 \b 

67 `<inputs>` Any input token values that are defined in the config.yml for this scope. 

68 Accepts multiple inputs separated by a space. 

69 

70 \b 

71 Example: 

72 `tembo new meeting my_presentation` 

73 """ 

74 # check that the name exists in the config.yml 

75 try: 

76 _new_verify_name_exists(scope) 

77 except ( 

78 exceptions.ScopeNotFound, 

79 exceptions.EmptyConfigYML, 

80 exceptions.MissingConfigYML, 

81 ) as tembo_exception: 

82 cli_message(tembo_exception.args[0]) 

83 raise SystemExit(1) from tembo_exception 

84 

85 # get the scope configuration from the config.yml 

86 try: 

87 config_scope = _new_get_config_scope(scope) 

88 except exceptions.MandatoryKeyNotFound as mandatory_key_not_found: 

89 cli_message(mandatory_key_not_found.args[0]) 

90 raise SystemExit(1) from mandatory_key_not_found 

91 

92 # if --example flag, return the example to the user 

93 _new_show_example(example, config_scope) 

94 

95 # if the name is in the config.yml, create the scoped page 

96 scoped_page = _new_create_scoped_page(config_scope, inputs) 

97 

98 if dry_run: 

99 cli_message(f"{scoped_page.path} will be created") 

100 raise SystemExit(0) 

101 

102 try: 

103 result = scoped_page.save_to_disk() 

104 if isinstance(result, Success): 

105 cli_message(f"Saved {result.message} to disk") 

106 raise SystemExit(0) 

107 except exceptions.ScopedPageAlreadyExists as scoped_page_already_exists: 

108 cli_message(f"File {scoped_page_already_exists}") 

109 raise SystemExit(0) from scoped_page_already_exists 

110 

111 

112def _new_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> pages.Page: 

113 page_creator_options = pages.PageCreatorOptions( 

114 base_path=tembo.cli.CONFIG.base_path, 

115 template_path=tembo.cli.CONFIG.template_path, 

116 page_path=config_scope["path"], 

117 filename=config_scope["filename"], 

118 extension=config_scope["extension"], 

119 name=config_scope["name"], 

120 example=config_scope["example"], 

121 user_input=inputs, 

122 template_filename=config_scope["template_filename"], 

123 ) 

124 try: 

125 return pages.ScopedPageCreator(page_creator_options).create_page() 

126 except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: 

127 cli_message(base_path_does_not_exist_error.args[0]) 

128 raise SystemExit(1) from base_path_does_not_exist_error 

129 except exceptions.TemplateFileNotFoundError as template_file_not_found_error: 

130 cli_message(template_file_not_found_error.args[0]) 

131 raise SystemExit(1) from template_file_not_found_error 

132 except exceptions.MismatchedTokenError as mismatched_token_error: 

133 if config_scope["example"] is not None: 

134 cli_message( 

135 f"Your tembo config.yml/template specifies {mismatched_token_error.expected}" 

136 + f" input tokens, you gave {mismatched_token_error.given}. " 

137 + f'Example: {config_scope["example"]}' 

138 ) 

139 raise SystemExit(1) from mismatched_token_error 

140 cli_message( 

141 f"Your tembo config.yml/template specifies {mismatched_token_error.expected}" 

142 + f" input tokens, you gave {mismatched_token_error.given}" 

143 ) 

144 

145 raise SystemExit(1) from mismatched_token_error 

146 

147 

148def _new_verify_name_exists(scope: str) -> None: 

149 _name_found = scope in [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] 

150 if _name_found: 

151 return 

152 if len(tembo.cli.CONFIG.scopes) > 0: 

153 # if the name is missing in the config.yml, raise error 

154 raise exceptions.ScopeNotFound(f"Scope {scope} not found in config.yml") 

155 # raise error if no config.yml found 

156 if pathlib.Path(tembo.cli.CONFIG.config_path).exists(): 

157 raise exceptions.EmptyConfigYML( 

158 f"Config.yml found in {tembo.cli.CONFIG.config_path} is empty" 

159 ) 

160 raise exceptions.MissingConfigYML(f"No config.yml found in {tembo.cli.CONFIG.config_path}") 

161 

162 

163def _new_get_config_scope(scope: str) -> dict: 

164 config_scope = {} 

165 optional_keys = ["example", "template_filename"] 

166 for option in [ 

167 "name", 

168 "path", 

169 "filename", 

170 "extension", 

171 "example", 

172 "template_filename", 

173 ]: 

174 try: 

175 config_scope.update( 

176 { 

177 option: str(user_scope[option]) 

178 for user_scope in tembo.cli.CONFIG.scopes 

179 if user_scope["name"] == scope 

180 } 

181 ) 

182 except KeyError as key_error: 

183 if key_error.args[0] in optional_keys: 

184 config_scope.update({key_error.args[0]: None}) 

185 continue 

186 raise exceptions.MandatoryKeyNotFound(f"Key {key_error} not found in config.yml") 

187 return config_scope 

188 

189 

190def _new_show_example(example: bool, config_scope: dict) -> None: 

191 if example: 

192 if isinstance(config_scope["example"], str): 

193 cli_message(f'Example for {config_scope["name"]}: {config_scope["example"]}') 

194 else: 

195 cli_message("No example in config.yml") 

196 raise SystemExit(0) 

197 

198 

199def cli_message(message: str) -> None: 

200 """ 

201 Relay a message to the user using the CLI. 

202 

203 Args: 

204 message (str): THe message to be displayed. 

205 """ 

206 click.echo(f"[TEMBO] {message} 🐘") 

207 

208 

209main.add_command(new) 

210main.add_command(list_all)