Coverage for src/pydal2sql/cli.py: 100%

73 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-31 16:22 +0200

1import os 

2import sys 

3import typing 

4from pathlib import Path 

5from typing import Annotated, Optional 

6 

7import typer 

8from configuraptor import Singleton 

9from rich import print 

10from typer import Argument 

11from typing_extensions import Never 

12 

13from .__about__ import __version__ 

14from .cli_support import ( 

15 extract_file_version_and_path, 

16 extract_file_versions_and_paths, 

17 find_git_root, 

18 get_absolute_path_info, 

19 get_file_for_version, 

20 handle_cli, 

21) 

22from .typer_support import ( 

23 DEFAULT_VERBOSITY, 

24 IS_DEBUG, 

25 ApplicationState, 

26 DB_Types, 

27 Verbosity, 

28 with_exit_code, 

29) 

30 

31## type fuckery: 

32DBType_Option = Annotated[DB_Types, typer.Option("--db-type", "--dialect", "-d")] 

33 

34T = typing.TypeVar("T") 

35 

36OptionalArgument = Annotated[Optional[T], Argument()] 

37# usage: (myparam: OptionalArgument[some_type]) 

38 

39OptionalOption = Annotated[Optional[T], typer.Option()] 

40# usage: (myparam: OptionalOption[some_type]) 

41 

42### end typing stuff, start app: 

43 

44app = typer.Typer( 

45 no_args_is_help=True, 

46) 

47state = ApplicationState() 

48 

49 

50def info(*args: str) -> None: # pragma: no cover 

51 """ 

52 'print' but with blue text. 

53 """ 

54 print(f"[blue]{' '.join(args)}[/blue]", file=sys.stderr) 

55 

56 

57def warn(*args: str) -> None: # pragma: no cover 

58 """ 

59 'print' but with yellow text. 

60 """ 

61 print(f"[yellow]{' '.join(args)}[/yellow]", file=sys.stderr) 

62 

63 

64def danger(*args: str) -> None: # pragma: no cover 

65 """ 

66 'print' but with red text. 

67 """ 

68 print(f"[red]{' '.join(args)}[/red]", file=sys.stderr) 

69 

70 

71@app.command() 

72@with_exit_code(hide_tb=not IS_DEBUG) 

73def create( 

74 filename: OptionalArgument[str] = None, 

75 tables: Annotated[ 

76 Optional[list[str]], 

77 typer.Option("--table", "--tables", "-t", help="One or more table names, default is all tables."), 

78 ] = None, 

79 db_type: DBType_Option = None, 

80 magic: Optional[bool] = None, 

81 noop: Optional[bool] = None, 

82 function: Optional[str] = None, 

83) -> bool: 

84 """ 

85 todo: docs 

86 

87 Examples: 

88 pydal2sql create models.py 

89 cat models.py | pydal2sql 

90 pydal2sql # output from stdin 

91 """ 

92 git_root = find_git_root() or Path(os.getcwd()) 

93 

94 config = state.update_config(magic=magic, noop=noop, tables=tables, db_type=db_type.value if db_type else None, function=function) 

95 

96 file_version, file_path = extract_file_version_and_path( 

97 filename, default_version="current" if filename else "stdin" 

98 ) 

99 file_exists, file_absolute_path = get_absolute_path_info(file_path, file_version, git_root) 

100 

101 if not file_exists: 

102 raise ValueError(f"Source file {filename} could not be found.") 

103 

104 text = get_file_for_version(file_absolute_path, file_version, prompt_description="table definition") 

105 

106 return handle_cli( 

107 "", 

108 text, 

109 db_type=config.db_type, 

110 tables=config.tables, 

111 verbose=state.verbosity > Verbosity.normal, 

112 noop=config.noop, 

113 magic=config.magic, 

114 function_name=config.function, 

115 ) 

116 

117 

118@app.command() 

119@with_exit_code(hide_tb=not IS_DEBUG) 

120def alter( 

121 filename_before: OptionalArgument[str] = None, 

122 filename_after: OptionalArgument[str] = None, 

123 db_type: DBType_Option = None, 

124 tables: Annotated[ 

125 Optional[list[str]], 

126 typer.Option("--table", "--tables", "-t", help="One or more table names, default is all tables."), 

127 ] = None, 

128 magic: Optional[bool] = None, 

129 noop: Optional[bool] = None, 

130) -> bool: 

131 """ 

132 Todo: docs 

133 

134 Examples: 

135 > pydal2sql alter @b3f24091a9201d6 examples/magic.py 

136 compare magic.py at commit b3f... to current (= as in workdir). 

137 

138 > pydal2sql alter examples/magic.py@@b3f24091a9201d6 examples/magic_after_rename.py@latest 

139 compare magic.py (which was renamed to magic_after_rename.py), 

140 at a specific commit to the latest version in git (ignore workdir version). 

141 

142 Todo: 

143 alter myfile.py # only one arg 

144 # = alter myfile.py@latest myfile.py@current 

145 # != alter myfile.py - # with - for cli 

146 # != alter - myfile.py 

147 """ 

148 git_root = find_git_root() or Path(os.getcwd()) 

149 

150 config = state.update_config(magic=magic, noop=noop, tables=tables, db_type=db_type.value if db_type else None) 

151 

152 before, after = extract_file_versions_and_paths(filename_before, filename_after) 

153 

154 version_before, filename_before = before 

155 version_after, filename_after = after 

156 

157 # either ./file exists or /file exists (seen from git root): 

158 

159 before_exists, before_absolute_path = get_absolute_path_info(filename_before, version_before, git_root) 

160 after_exists, after_absolute_path = get_absolute_path_info(filename_after, version_after, git_root) 

161 

162 if not (before_exists and after_exists): 

163 message = "" 

164 message += "" if before_exists else f"Path {filename_before} does not exist! " 

165 if filename_before != filename_after: 

166 message += "" if after_exists else f"Path {filename_after} does not exist!" 

167 raise ValueError(message) 

168 

169 code_before = get_file_for_version( 

170 before_absolute_path, version_before, prompt_description="current table definition" 

171 ) 

172 code_after = get_file_for_version(after_absolute_path, version_after, prompt_description="desired table definition") 

173 

174 if not (code_before and code_after): 

175 message = "" 

176 message += "" if code_before else "Before code is empty (Maybe try `pydal2sql create`)! " 

177 message += "" if code_after else "After code is empty! " 

178 raise ValueError(message) 

179 

180 if code_before == code_after: 

181 raise ValueError("Both contain the same code!") 

182 

183 return handle_cli( 

184 code_before, 

185 code_after, 

186 db_type=config.db_type, 

187 tables=config.tables, 

188 verbose=state.verbosity > Verbosity.normal, 

189 noop=config.noop, 

190 magic=config.magic, 

191 ) 

192 

193 

194""" 

195todo: 

196- db type in config 

197- models.py with db import or define_tables method. 

198- `public.` prefix 

199""" 

200 

201 

202""" 

203def pin: 

204pydal2sql pin 96de5b37b586e75b8ac053b9bef7647f544fe502 # -> default pin created 

205pydal2sql alter myfile.py # should compare between pin/@latest and @current 

206 # replaces @current for Before, not for After in case of ALTER. 

207pydal2sql pin --remove # -> default pin removed 

208 

209pydal2sql pin 96de5b37b586e75b8ac053b9bef7647f544fe502 --name my_custom_name # -> pin '@my_custom_name' created 

210pydal2sql pin 96de5b37b586e75b8ac053b9bef7647f544fe503 --name my_custom_name #-> pin '@my_custom_name' overwritten 

211pydal2sql create myfile.py@my_custom_name 

212pydal2sql pin 96de5b37b586e75b8ac053b9bef7647f544fe502 --remove -> pin '@my_custom_name' removed 

213 

214pydal2sql pins 

215# lists hash with name 

216""" 

217 

218 

219def show_config_callback() -> Never: 

220 """ 

221 --show-config requested! 

222 """ 

223 print(state) 

224 raise typer.Exit(0) 

225 

226 

227def version_callback() -> Never: 

228 """ 

229 --version requested! 

230 """ 

231 print(f"pydal2sql Version: {__version__}") 

232 

233 raise typer.Exit(0) 

234 

235 

236@app.callback(invoke_without_command=True) 

237def main( 

238 _: typer.Context, 

239 config: str = None, 

240 verbosity: Verbosity = DEFAULT_VERBOSITY, 

241 # stops the program: 

242 show_config: bool = False, 

243 version: bool = False, 

244) -> None: 

245 """ 

246 Todo: docs 

247 

248 Args: 

249 _: context to determine if a subcommand is passed, etc 

250 config: path to a different config toml file 

251 verbosity: level of detail to print out (1 - 3) 

252 

253 show_config: display current configuration? 

254 version: display current version? 

255 

256 """ 

257 if state.config: 

258 # if a config already exists, it's outdated, so we clear it. 

259 # only really applicable in Pytest scenarios where multiple commands are executed after eachother 

260 Singleton.clear(state.config) 

261 

262 state.load_config(config_file=config, verbosity=verbosity) 

263 

264 if show_config: 

265 show_config_callback() 

266 elif version: 

267 version_callback() 

268 # else: just continue 

269 

270 

271# if __name__ == "__main__": 

272# app()