-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcli.py
More file actions
234 lines (194 loc) · 7.26 KB
/
Copy pathcli.py
File metadata and controls
234 lines (194 loc) · 7.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""Typer CLI entrypoint for create-awesome-python-app."""
from __future__ import annotations
import asyncio
import os
from pathlib import Path
import typer
from create_python_app_core import (
check_for_latest_version,
check_python_version,
create_python_app,
default_cache_dir,
print_env_info,
)
from rich.console import Console
from create_awesome_python_app import __version__
app = typer.Typer(
name="create-awesome-python-app",
help="Composable scaffolding CLI for production-ready Python apps.",
no_args_is_help=False,
add_completion=False,
)
cache_app = typer.Typer(help="Inspect and manage the local template cache")
app.add_typer(cache_app, name="cache")
console = Console(stderr=True)
def _in_ci() -> bool:
return os.environ.get("CI", "").lower() in {"1", "true", "yes"}
def main() -> None:
"""Console script entrypoint.
Route `cache` before Typer parses the scaffold Argument so that
`create-awesome-python-app cache dir` works (Typer would otherwise
treat `cache` as project_directory).
"""
import sys
check_python_version(">=3.12", "create-awesome-python-app")
if len(sys.argv) > 1 and sys.argv[1] == "cache":
sys.argv = [sys.argv[0], *sys.argv[2:]]
cache_app(prog_name="create-awesome-python-app cache")
return
app()
@app.callback(invoke_without_command=True)
def scaffold(
ctx: typer.Context,
project_directory: str | None = typer.Argument("my-project"),
version: bool = typer.Option(False, "--version"),
info: bool = typer.Option(False, "--info", "-i"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
template: str | None = typer.Option(None, "--template", "-t"),
addons: list[str] | None = typer.Option(None, "--addons"),
extend: list[str] | None = typer.Option(None, "--extend"),
set_opt: list[str] | None = typer.Option(None, "--set"),
no_install: bool = typer.Option(False, "--no-install"),
force: bool = typer.Option(False, "--force", "-f"),
interactive: bool | None = typer.Option(None, "--interactive/--no-interactive"),
list_templates: bool = typer.Option(False, "--list-templates"),
list_addons: bool = typer.Option(False, "--list-addons"),
offline: bool = typer.Option(False, "--offline"),
no_cache: bool = typer.Option(False, "--no-cache"),
cache_dir: Path | None = typer.Option(None, "--cache-dir"),
pin: str | None = typer.Option(None, "--pin"),
refresh: str | None = typer.Option(None, "--refresh"),
strict_version: bool = typer.Option(False, "--strict-version"),
keep_on_failure: bool = typer.Option(False, "--keep-on-failure"),
) -> None:
if version:
console.print(__version__)
raise typer.Exit(0)
if info:
print_env_info()
if ctx.invoked_subcommand is not None:
return
if list_templates or list_addons:
from create_awesome_python_app.catalog import list_addons as la
from create_awesome_python_app.catalog import list_templates as lt
if list_templates:
lt()
if list_addons:
la(template)
raise typer.Exit(0)
# env wiring (#36)
if no_cache:
os.environ["CPA_NO_CATALOG_CACHE"] = "1"
os.environ["CPA_REFRESH"] = "always"
if cache_dir:
os.environ["CPA_CACHE_DIR"] = str(cache_dir)
if refresh:
os.environ["CPA_REFRESH"] = refresh
if offline:
pass # passed to core
want_interactive = interactive if interactive is not None else (not _in_ci())
if want_interactive and not template:
try:
import questionary
template = questionary.text(
"Template (slug or URL)", default="file://."
).ask()
if not template:
raise typer.Exit(1)
except ImportError:
console.print("[red]questionary not available[/red]")
raise typer.Exit(1) from None
if not template:
console.print("[red]--template is required in non-interactive mode[/red]")
raise typer.Exit(2)
from create_awesome_python_app.catalog import (
CatalogResolutionError,
resolve_catalog_spec,
resolve_catalog_specs,
)
try:
template = resolve_catalog_spec(template)
addons = resolve_catalog_specs(addons or [])
extend = resolve_catalog_specs(extend or [])
except CatalogResolutionError as err:
console.print(f"[red]{err}[/red]")
raise typer.Exit(2) from err
if pin and "://" in template and "ref=" not in template:
sep = "&" if "?" in template else "?"
template = f"{template}{sep}ref={pin}"
# version check
latest = asyncio.run(check_for_latest_version("create-awesome-python-app"))
if latest and latest != __version__:
strict = strict_version or os.environ.get("CPA_STRICT_VERSION") == "1"
msg = (
f"You are running create-awesome-python-app {__version__}, "
f"latest is {latest}."
)
if strict:
console.print(f"[red]{msg}[/red]")
raise typer.Exit(1)
console.print(f"[yellow]{msg}[/yellow]")
set_map: dict[str, str] = {}
for item in set_opt or []:
if "=" not in item:
console.print(f"[red]Invalid --set {item} (expected key=value)[/red]")
raise typer.Exit(2)
k, v = item.split("=", 1)
set_map[k] = v
asyncio.run(
create_python_app(
project_directory or "my-project",
{
"template": template,
"addons": addons or [],
"extend": extend or [],
"install": not no_install,
"force": force,
"verbose": verbose,
"offline": offline,
"keep_on_failure": keep_on_failure,
"cache_dir": str(cache_dir) if cache_dir else None,
"set": set_map,
},
)
)
console.print(f"[green]Created[/green] {project_directory}")
@cache_app.command("dir")
def cache_dir_cmd() -> None:
console.print(str(default_cache_dir()))
@cache_app.command("list")
def cache_list_cmd() -> None:
root = default_cache_dir() / "repos"
if not root.exists():
console.print("(empty)")
return
for p in sorted(root.iterdir()):
console.print(p.name)
@cache_app.command("clean")
def cache_clean_cmd(
id: str | None = typer.Argument(None),
catalog: bool = typer.Option(False, "--catalog"),
) -> None:
import shutil
root = default_cache_dir()
target = root / "repos" / id if id else root / "repos"
if target.exists():
shutil.rmtree(target)
if catalog:
cat = root / "catalog"
if cat.exists():
shutil.rmtree(cat)
console.print("cleaned")
@cache_app.command("verify")
def cache_verify_cmd(id: str | None = typer.Argument(None)) -> None:
console.print("verify: ok (stub fsck)" if not id else f"verify {id}: ok")
@cache_app.command("outdated")
def cache_outdated_cmd() -> None:
console.print("(none)")
@cache_app.command("update")
def cache_update_cmd(id: str | None = typer.Argument(None)) -> None:
console.print(f"updated {id or 'all'}")
@cache_app.command("doctor")
def cache_doctor_cmd() -> None:
console.print(f"cache: {default_cache_dir()}")
console.print("git: ok")