Coverage for src/pydal2sql/cli_old.py: 0%

47 statements  

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

1""" 

2CLI tool to generate SQL from PyDAL code. 

3""" 

4 

5import argparse 

6import pathlib 

7import select 

8import string 

9import sys 

10import textwrap 

11import typing 

12from typing import IO, Optional 

13 

14import rich 

15from configuraptor import TypedConfig 

16from rich.prompt import Prompt 

17from rich.style import Style 

18 

19from .helpers import flatten 

20from .magic import find_missing_variables, generate_magic_code 

21from .types import DATABASE_ALIASES 

22 

23 

24class PrettyParser(argparse.ArgumentParser): # pragma: no cover 

25 """ 

26 Add 'rich' to the argparse output. 

27 """ 

28 

29 def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: 

30 rich.print(message, file=file) 

31 

32 

33def has_stdin_data() -> bool: # pragma: no cover 

34 """ 

35 Check if the program starts with cli data (pipe | or redirect ><). 

36 

37 See Also: 

38 https://stackoverflow.com/questions/3762881/how-do-i-check-if-stdin-has-some-data 

39 """ 

40 return any( 

41 select.select( 

42 [ 

43 sys.stdin, 

44 ], 

45 [], 

46 [], 

47 0.0, 

48 )[0] 

49 ) 

50 

51 

52def handle_cli( 

53 code: str, 

54 db_type: typing.Optional[str] = None, 

55 tables: typing.Optional[list[str] | list[list[str]]] = None, 

56 verbose: typing.Optional[bool] = False, 

57 noop: typing.Optional[bool] = False, 

58 magic: typing.Optional[bool] = False, 

59) -> None: 

60 """ 

61 Handle user input. 

62 """ 

63 to_execute = string.Template( 

64 textwrap.dedent( 

65 """ 

66 from pydal import * 

67 from pydal.objects import * 

68 from pydal.validators import * 

69 

70 from pydal2sql import generate_sql 

71 

72 db = database = DAL(None, migrate=False) 

73 

74 tables = $tables 

75 db_type = '$db_type' 

76 

77 $extra 

78 

79 $code 

80 

81 if not tables: 

82 tables = db._tables 

83 

84 for table in tables: 

85 print(generate_sql(db[table], db_type=db_type)) 

86 """ 

87 ) 

88 ) 

89 

90 generated_code = to_execute.substitute( 

91 {"tables": flatten(tables or []), "db_type": db_type or "", "code": textwrap.dedent(code), "extra": ""} 

92 ) 

93 if verbose or noop: 

94 rich.print(generated_code, file=sys.stderr) 

95 

96 if not noop: 

97 try: 

98 exec(generated_code) # nosec: B102 

99 except NameError: 

100 # something is missing! 

101 missing_vars = find_missing_variables(generated_code) 

102 if not magic: 

103 rich.print( 

104 f"Your code is missing some variables: {missing_vars}. Add these or try --magic", file=sys.stderr 

105 ) 

106 else: 

107 extra_code = generate_magic_code(missing_vars) 

108 

109 generated_code = to_execute.substitute( 

110 { 

111 "tables": flatten(tables or []), 

112 "db_type": db_type or "", 

113 "extra": extra_code, 

114 "code": textwrap.dedent(code), 

115 } 

116 ) 

117 

118 if verbose: 

119 print(generated_code, file=sys.stderr) 

120 

121 exec(generated_code) # nosec: B102 

122 

123 

124class CliConfig(TypedConfig): 

125 """ 

126 Configuration from pyproject.toml or cli. 

127 """ 

128 

129 db_type: DATABASE_ALIASES | None 

130 verbose: bool | None 

131 noop: bool | None 

132 magic: bool | None 

133 filename: str | None = None 

134 tables: typing.Optional[list[str] | list[list[str]]] = None 

135 

136 def __str__(self) -> str: 

137 """ 

138 Return as semi-fancy string for Debug. 

139 """ 

140 attrs = [f"\t{key}={value},\n" for key, value in self.__dict__.items()] 

141 classname = self.__class__.__name__ 

142 

143 return f"{classname}(\n{''.join(attrs)})" 

144 

145 def __repr__(self) -> str: 

146 """ 

147 Return as fancy string for Debug. 

148 """ 

149 attrs = [] 

150 for key, value in self.__dict__.items(): # pragma: no cover 

151 if key.startswith("_"): 

152 continue 

153 style = Style() 

154 if isinstance(value, str): 

155 style = Style(color="green", italic=True, bold=True) 

156 value = f"'{value}'" 

157 elif isinstance(value, bool) or value is None: 

158 style = Style(color="orange1") 

159 elif isinstance(value, int | float): 

160 style = Style(color="blue") 

161 attrs.append(f"\t{key}={style.render(value)},\n") 

162 

163 classname = Style(color="medium_purple4").render(self.__class__.__name__) 

164 

165 return f"{classname}(\n{''.join(attrs)})" 

166 

167 

168def app() -> None: # pragma: no cover 

169 """ 

170 Entrypoint for the pydal2sql cli command. 

171 """ 

172 parser = PrettyParser( 

173 prog="pydal2sql", 

174 formatter_class=argparse.RawDescriptionHelpFormatter, 

175 description="""[green]CLI tool to generate SQL from PyDAL code.[/green]\n 

176 Aside from using cli arguments, you can also configure the tool in your code. 

177 You can set the following variables: 

178 

179 db_type: str = 'sqlite' # your desired database type; 

180 tables: list[str] = [] # your desired tables to generate SQL for;""", 

181 epilog="Example: [i]cat models.py | pydal2sql sqlite[/i]", 

182 ) 

183 

184 parser.add_argument("filename", nargs="?", help="Which file to load? Can also be done with stdin.") 

185 

186 parser.add_argument( 

187 "db_type", nargs="?", help="Which database dialect to generate ([blue]postgres, sqlite, mysql[/blue])" 

188 ) 

189 

190 parser.add_argument("--verbose", "-v", help="Show more info", action=argparse.BooleanOptionalAction, default=False) 

191 

192 parser.add_argument( 

193 "--noop", "-n", help="Only show code, don't run it.", action=argparse.BooleanOptionalAction, default=False 

194 ) 

195 

196 parser.add_argument( 

197 "--magic", "-m", help="Perform magic to fix missing vars.", action=argparse.BooleanOptionalAction, default=False 

198 ) 

199 

200 parser.add_argument( 

201 "-t", 

202 "--tables", 

203 "--table", 

204 action="append", 

205 nargs="+", 

206 help="One or more tables to generate. By default, all tables in the file will be used.", 

207 ) 

208 

209 args = parser.parse_args() 

210 

211 config = CliConfig.load(key="tool.pydal2sql") 

212 

213 config.fill(**args.__dict__) 

214 config.tables = args.tables or config.tables 

215 

216 db_type = args.db_type or args.filename or config.db_type 

217 

218 load_file_mode = (filename := (args.filename or config.filename)) and filename.endswith(".py") 

219 

220 if not (has_stdin_data() or load_file_mode): 

221 if not db_type: 

222 db_type = Prompt.ask("Which database type do you want to use?", choices=["sqlite", "postgres", "mysql"]) 

223 

224 rich.print("Please paste your define tables code below and press ctrl-D when finished.", file=sys.stderr) 

225 

226 # else: data from stdin 

227 # py code or cli args should define settings. 

228 if load_file_mode and filename: 

229 db_type = args.db_type 

230 text = pathlib.Path(filename).read_text() 

231 else: 

232 text = sys.stdin.read() 

233 rich.print("---", file=sys.stderr) 

234 

235 return handle_cli(text, db_type, config.tables, verbose=config.verbose, noop=config.noop, magic=config.magic)