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
« 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"""
5import argparse
6import pathlib
7import select
8import string
9import sys
10import textwrap
11import typing
12from typing import IO, Optional
14import rich
15from configuraptor import TypedConfig
16from rich.prompt import Prompt
17from rich.style import Style
19from .helpers import flatten
20from .magic import find_missing_variables, generate_magic_code
21from .types import DATABASE_ALIASES
24class PrettyParser(argparse.ArgumentParser): # pragma: no cover
25 """
26 Add 'rich' to the argparse output.
27 """
29 def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None:
30 rich.print(message, file=file)
33def has_stdin_data() -> bool: # pragma: no cover
34 """
35 Check if the program starts with cli data (pipe | or redirect ><).
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 )
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 *
70 from pydal2sql import generate_sql
72 db = database = DAL(None, migrate=False)
74 tables = $tables
75 db_type = '$db_type'
77 $extra
79 $code
81 if not tables:
82 tables = db._tables
84 for table in tables:
85 print(generate_sql(db[table], db_type=db_type))
86 """
87 )
88 )
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)
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)
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 )
118 if verbose:
119 print(generated_code, file=sys.stderr)
121 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)