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
« 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
7import typer
8from configuraptor import Singleton
9from rich import print
10from typer import Argument
11from typing_extensions import Never
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)
31## type fuckery:
32DBType_Option = Annotated[DB_Types, typer.Option("--db-type", "--dialect", "-d")]
34T = typing.TypeVar("T")
36OptionalArgument = Annotated[Optional[T], Argument()]
37# usage: (myparam: OptionalArgument[some_type])
39OptionalOption = Annotated[Optional[T], typer.Option()]
40# usage: (myparam: OptionalOption[some_type])
42### end typing stuff, start app:
44app = typer.Typer(
45 no_args_is_help=True,
46)
47state = ApplicationState()
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)
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)
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)
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
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())
94 config = state.update_config(magic=magic, noop=noop, tables=tables, db_type=db_type.value if db_type else None, function=function)
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)
101 if not file_exists:
102 raise ValueError(f"Source file {filename} could not be found.")
104 text = get_file_for_version(file_absolute_path, file_version, prompt_description="table definition")
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 )
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
134 Examples:
135 > pydal2sql alter @b3f24091a9201d6 examples/magic.py
136 compare magic.py at commit b3f... to current (= as in workdir).
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).
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())
150 config = state.update_config(magic=magic, noop=noop, tables=tables, db_type=db_type.value if db_type else None)
152 before, after = extract_file_versions_and_paths(filename_before, filename_after)
154 version_before, filename_before = before
155 version_after, filename_after = after
157 # either ./file exists or /file exists (seen from git root):
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)
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)
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")
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)
180 if code_before == code_after:
181 raise ValueError("Both contain the same code!")
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 )
194"""
195todo:
196- db type in config
197- models.py with db import or define_tables method.
198- `public.` prefix
199"""
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
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
214pydal2sql pins
215# lists hash with name
216"""
219def show_config_callback() -> Never:
220 """
221 --show-config requested!
222 """
223 print(state)
224 raise typer.Exit(0)
227def version_callback() -> Never:
228 """
229 --version requested!
230 """
231 print(f"pydal2sql Version: {__version__}")
233 raise typer.Exit(0)
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
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)
253 show_config: display current configuration?
254 version: display current version?
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)
262 state.load_config(config_file=config, verbosity=verbosity)
264 if show_config:
265 show_config_callback()
266 elif version:
267 version_callback()
268 # else: just continue
271# if __name__ == "__main__":
272# app()