Coverage for src/pydal2sql/cli.py: 100%
48 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-21 11:14 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-21 11:14 +0200
1"""
2CLI tool to generate SQL from PyDAL code.
3"""
5import argparse
6import pathlib
7import select
8import string
9import sys
10import textwrap
11import typing
12from typing import IO, Optional
14import configuraptor
15import rich
16from configuraptor import TypedConfig
17from rich.prompt import Prompt
18from rich.style import Style
20from .helpers import flatten
21from .magic import find_missing_variables, generate_magic_code
22from .types import DATABASE_ALIASES
25class PrettyParser(argparse.ArgumentParser): # pragma: no cover
26 """
27 Add 'rich' to the argparse output.
28 """
30 def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None:
31 rich.print(message, file=file)
34def has_stdin_data() -> bool: # pragma: no cover
35 """
36 Check if the program starts with cli data (pipe | or redirect ><).
38 See Also:
39 https://stackoverflow.com/questions/3762881/how-do-i-check-if-stdin-has-some-data
40 """
41 return any(
42 select.select(
43 [
44 sys.stdin,
45 ],
46 [],
47 [],
48 0.0,
49 )[0]
50 )
53def handle_cli(
54 code: str,
55 db_type: typing.Optional[str] = None,
56 tables: typing.Optional[list[str] | list[list[str]]] = None,
57 verbose: typing.Optional[bool] = False,
58 noop: typing.Optional[bool] = False,
59 magic: typing.Optional[bool] = False,
60) -> None:
61 """
62 Handle user input.
63 """
64 to_execute = string.Template(
65 textwrap.dedent(
66 """
67 from pydal import *
68 from pydal.objects import *
69 from pydal.validators import *
71 from pydal2sql import generate_sql
73 db = database = DAL(None, migrate=False)
75 tables = $tables
76 db_type = '$db_type'
78 $extra
80 $code
82 if not tables:
83 tables = db._tables
85 for table in tables:
86 print(generate_sql(db[table], db_type))
87 """
88 )
89 )
91 generated_code = to_execute.substitute(
92 {"tables": flatten(tables or []), "db_type": db_type or "", "code": textwrap.dedent(code), "extra": ""}
93 )
94 if verbose or noop:
95 rich.print(generated_code, file=sys.stderr)
97 if not noop:
98 try:
99 exec(generated_code) # nosec: B102
100 except NameError:
101 # something is missing!
102 missing_vars = find_missing_variables(generated_code)
103 if not magic:
104 rich.print(
105 f"Your code is missing some variables: {missing_vars}. Add these or try --magic", file=sys.stderr
106 )
107 else:
108 extra_code = generate_magic_code(missing_vars)
110 generated_code = to_execute.substitute(
111 {
112 "tables": flatten(tables or []),
113 "db_type": db_type or "",
114 "extra": extra_code,
115 "code": textwrap.dedent(code),
116 }
117 )
119 if verbose:
120 print(generated_code, file=sys.stderr)
122 exec(generated_code) # nosec: B102
124class CliConfig(TypedConfig):
125 """
126 Configuration from pyproject.toml or cli.
127 """
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
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__
143 return f"{classname}(\n{''.join(attrs)})"
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")
163 classname = Style(color="medium_purple4").render(self.__class__.__name__)
165 return f"{classname}(\n{''.join(attrs)})"
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:
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 )
184 parser.add_argument("filename", nargs="?", help="Which file to load? Can also be done with stdin.")
186 parser.add_argument(
187 "db_type", nargs="?", help="Which database dialect to generate ([blue]postgres, sqlite, mysql[/blue])"
188 )
190 parser.add_argument("--verbose", "-v", help="Show more info", action=argparse.BooleanOptionalAction, default=False)
192 parser.add_argument(
193 "--noop", "-n", help="Only show code, don't run it.", action=argparse.BooleanOptionalAction, default=False
194 )
196 parser.add_argument(
197 "--magic", "-m", help="Perform magic to fix missing vars.", action=argparse.BooleanOptionalAction, default=False
198 )
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 )
209 args = parser.parse_args()
211 config = CliConfig.load(key="tool.pydal2sql")
213 config.fill(**args.__dict__)
214 config.tables = args.tables or config.tables
216 db_type = args.db_type or args.filename or config.db_type
218 load_file_mode = (filename := (args.filename or config.filename)) and filename.endswith(".py")
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"])
224 rich.print("Please paste your define tables code below and press ctrl-D when finished.", file=sys.stderr)
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)
235 return handle_cli(text, db_type, config.tables, verbose=config.verbose, noop=config.noop, magic=config.magic)