create-awesome-python-app 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/PKG-INFO +2 -2
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/pyproject.toml +2 -2
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/src/create_awesome_python_app/__init__.py +1 -1
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/src/create_awesome_python_app/catalog.py +11 -20
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/src/create_awesome_python_app/cli.py +27 -11
- create_awesome_python_app-0.2.3/src/create_awesome_python_app/prompt_style.py +61 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_catalog_resolve.py +43 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_interactive.py +8 -8
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/.gitignore +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/README.md +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/assets/hero.svg +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/src/create_awesome_python_app/cache.py +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_cache_cmds.py +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_cache_env.py +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_catalog.py +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_catalog_fetch.py +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_cli.py +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_cpa_templates_integration.py +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_flags.py +0 -0
- {create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: create-awesome-python-app
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Composable scaffolding CLI for production-ready Python apps
|
|
5
5
|
Project-URL: Homepage, https://github.com/Create-Python-App/create-python-app
|
|
6
6
|
Project-URL: Repository, https://github.com/Create-Python-App/create-python-app
|
|
@@ -8,7 +8,7 @@ Project-URL: Issues, https://github.com/Create-Python-App/create-python-app/issu
|
|
|
8
8
|
Project-URL: Changelog, https://github.com/Create-Python-App/create-python-app/blob/main/CHANGELOG.md
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
|
-
Requires-Dist: create-python-app-core>=0.2.
|
|
11
|
+
Requires-Dist: create-python-app-core>=0.2.3
|
|
12
12
|
Requires-Dist: questionary>=2.1.1
|
|
13
13
|
Requires-Dist: rich>=15.0.0
|
|
14
14
|
Requires-Dist: typer>=0.27.0
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "create-awesome-python-app"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "Composable scaffolding CLI for production-ready Python apps"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
dependencies = [
|
|
9
|
-
"create-python-app-core>=0.2.
|
|
9
|
+
"create-python-app-core>=0.2.3",
|
|
10
10
|
"questionary>=2.1.1",
|
|
11
11
|
"rich>=15.0.0",
|
|
12
12
|
"typer>=0.27.0",
|
|
@@ -16,18 +16,11 @@ from rich.console import Console
|
|
|
16
16
|
from rich.table import Table
|
|
17
17
|
|
|
18
18
|
from create_awesome_python_app import __version__
|
|
19
|
+
from create_awesome_python_app.prompt_style import bold, color_category, dim
|
|
19
20
|
|
|
20
21
|
console = Console(stderr=True)
|
|
21
22
|
|
|
22
23
|
CUSTOM_TEMPLATE_SENTINEL = "__custom_template__"
|
|
23
|
-
_ANSI_RESET = "\033[0m"
|
|
24
|
-
_CATEGORY_PALETTE = (
|
|
25
|
-
"\033[33m", # yellow
|
|
26
|
-
"\033[32m", # green
|
|
27
|
-
"\033[36m", # cyan
|
|
28
|
-
"\033[35m", # magenta
|
|
29
|
-
"\033[34m", # blue
|
|
30
|
-
)
|
|
31
24
|
|
|
32
25
|
|
|
33
26
|
@dataclass(frozen=True)
|
|
@@ -172,13 +165,6 @@ def short_category_label(category_name: str) -> str:
|
|
|
172
165
|
return " ".join(words[:2]) or category_name
|
|
173
166
|
|
|
174
167
|
|
|
175
|
-
def _color_category(slug: str, label: str) -> str:
|
|
176
|
-
if os.environ.get("NO_COLOR"):
|
|
177
|
-
return label
|
|
178
|
-
idx = sum(ord(char) for char in slug) % len(_CATEGORY_PALETTE)
|
|
179
|
-
return f"{_CATEGORY_PALETTE[idx]}{label}{_ANSI_RESET}"
|
|
180
|
-
|
|
181
|
-
|
|
182
168
|
def _category_map(data: dict[str, Any]) -> dict[str, str]:
|
|
183
169
|
return {
|
|
184
170
|
str(category.get("slug", "")): str(category.get("name", ""))
|
|
@@ -253,12 +239,17 @@ def build_template_choices(data: dict[str, Any]) -> list[TemplateChoice]:
|
|
|
253
239
|
labels = template.get("labels", [])
|
|
254
240
|
label_suffix = ""
|
|
255
241
|
if isinstance(labels, list) and labels:
|
|
256
|
-
label_suffix = " · " + ", ".join(str(label) for label in labels[:3])
|
|
242
|
+
label_suffix = dim(" · " + ", ".join(str(label) for label in labels[:3]))
|
|
257
243
|
description = str(template.get("description", "")).strip()
|
|
258
|
-
|
|
244
|
+
# Keep slug + short description in the title so select(use_search_filter)
|
|
245
|
+
# can match them (filter scans Choice.title only).
|
|
246
|
+
description_suffix = dim(f" — {description}") if description else ""
|
|
247
|
+
name = str(template.get("name", slug))
|
|
248
|
+
# ANSI is OK here: questionary.select renders titles as terminal text.
|
|
249
|
+
# Do not pass these titles to autocomplete (HTML match highlighting).
|
|
259
250
|
title = (
|
|
260
|
-
f"{
|
|
261
|
-
f"{
|
|
251
|
+
f"{color_category(category_slug, badge)} "
|
|
252
|
+
f"{bold(name)} ({slug})"
|
|
262
253
|
f"{label_suffix}{description_suffix}"
|
|
263
254
|
)
|
|
264
255
|
choices.append(
|
|
@@ -271,7 +262,7 @@ def build_template_choices(data: dict[str, Any]) -> list[TemplateChoice]:
|
|
|
271
262
|
|
|
272
263
|
choices.append(
|
|
273
264
|
TemplateChoice(
|
|
274
|
-
title=" " * 12 + "Use my own template URL",
|
|
265
|
+
title=" " * 12 + dim("Use my own template URL"),
|
|
275
266
|
value=CUSTOM_TEMPLATE_SENTINEL,
|
|
276
267
|
search="custom own template url github file",
|
|
277
268
|
)
|
|
@@ -104,6 +104,8 @@ def _prompt_custom_options(
|
|
|
104
104
|
) -> dict[str, str]:
|
|
105
105
|
import questionary
|
|
106
106
|
|
|
107
|
+
from create_awesome_python_app.prompt_style import CPA_PROMPT_STYLE
|
|
108
|
+
|
|
107
109
|
source = resolve_source(template, cache_dir=cache_dir)
|
|
108
110
|
root = download_repository(
|
|
109
111
|
source,
|
|
@@ -138,9 +140,12 @@ def _prompt_custom_options(
|
|
|
138
140
|
answer = questionary.confirm(
|
|
139
141
|
message,
|
|
140
142
|
default=initial.lower() in {"1", "true", "yes", "on"},
|
|
143
|
+
style=CPA_PROMPT_STYLE,
|
|
141
144
|
).ask()
|
|
142
145
|
else:
|
|
143
|
-
answer = questionary.text(
|
|
146
|
+
answer = questionary.text(
|
|
147
|
+
message, default=initial, style=CPA_PROMPT_STYLE
|
|
148
|
+
).ask()
|
|
144
149
|
if answer is None:
|
|
145
150
|
raise typer.Exit(1)
|
|
146
151
|
answers[option.key] = _stringify_option_value(answer)
|
|
@@ -230,30 +235,38 @@ def scaffold(
|
|
|
230
235
|
if want_interactive and not template:
|
|
231
236
|
try:
|
|
232
237
|
import questionary
|
|
238
|
+
from questionary import Choice
|
|
233
239
|
|
|
234
240
|
from create_awesome_python_app.catalog import (
|
|
235
241
|
CUSTOM_TEMPLATE_SENTINEL,
|
|
236
242
|
build_template_choices,
|
|
237
243
|
get_catalog_data,
|
|
238
244
|
)
|
|
245
|
+
from create_awesome_python_app.prompt_style import CPA_PROMPT_STYLE
|
|
239
246
|
|
|
240
247
|
interactive_catalog = get_catalog_data()
|
|
241
248
|
template_choices = build_template_choices(interactive_catalog)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
# select + type-to-filter: browseable list (CNA-style discovery)
|
|
250
|
+
# instead of autocomplete-only. use_jk_keys must be False with search.
|
|
251
|
+
selected_template = questionary.select(
|
|
252
|
+
"Pick a template",
|
|
253
|
+
choices=[
|
|
254
|
+
Choice(title=choice.title, value=choice.value)
|
|
255
|
+
for choice in template_choices
|
|
256
|
+
],
|
|
249
257
|
qmark="?",
|
|
258
|
+
pointer="❯",
|
|
259
|
+
style=CPA_PROMPT_STYLE,
|
|
260
|
+
use_search_filter=True,
|
|
261
|
+
use_jk_keys=False,
|
|
262
|
+
instruction="(↑↓ browse · type to filter · Enter)",
|
|
250
263
|
).ask()
|
|
251
|
-
selected_template = choice_by_title.get(str(selected_title), selected_title)
|
|
252
264
|
if selected_template == CUSTOM_TEMPLATE_SENTINEL:
|
|
253
265
|
selected_template = questionary.text(
|
|
254
266
|
"Template URL",
|
|
255
267
|
default="file://.",
|
|
256
268
|
validate=lambda value: bool(value) or "Template URL is required",
|
|
269
|
+
style=CPA_PROMPT_STYLE,
|
|
257
270
|
).ask()
|
|
258
271
|
template = selected_template
|
|
259
272
|
if not template:
|
|
@@ -296,6 +309,7 @@ def scaffold(
|
|
|
296
309
|
get_catalog_data,
|
|
297
310
|
group_extension_choices,
|
|
298
311
|
)
|
|
312
|
+
from create_awesome_python_app.prompt_style import CPA_PROMPT_STYLE
|
|
299
313
|
|
|
300
314
|
interactive_catalog = interactive_catalog or get_catalog_data()
|
|
301
315
|
extension_choices = build_extension_choices(interactive_catalog, template)
|
|
@@ -316,7 +330,8 @@ def scaffold(
|
|
|
316
330
|
"Which kinds of extensions do you need?",
|
|
317
331
|
choices=category_choices,
|
|
318
332
|
qmark="?",
|
|
319
|
-
pointer="
|
|
333
|
+
pointer="❯",
|
|
334
|
+
style=CPA_PROMPT_STYLE,
|
|
320
335
|
).ask()
|
|
321
336
|
selected_addons: list[str] = []
|
|
322
337
|
for category_slug in selected_categories or []:
|
|
@@ -330,7 +345,8 @@ def scaffold(
|
|
|
330
345
|
for choice in choices
|
|
331
346
|
],
|
|
332
347
|
qmark="?",
|
|
333
|
-
pointer="
|
|
348
|
+
pointer="❯",
|
|
349
|
+
style=CPA_PROMPT_STYLE,
|
|
334
350
|
).ask()
|
|
335
351
|
selected_addons.extend(str(item) for item in picked or [])
|
|
336
352
|
addons = selected_addons
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""High-contrast questionary styles for CPA interactive prompts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from questionary import Style
|
|
8
|
+
|
|
9
|
+
# Lighter brand blues/greens than docs hex so selected rows stay readable on
|
|
10
|
+
# dark terminals (slate backgrounds common in Arch/Ghostty/Alacritty).
|
|
11
|
+
CPA_PROMPT_STYLE = Style.from_dict(
|
|
12
|
+
{
|
|
13
|
+
"qmark": "fg:#60a5fa bold",
|
|
14
|
+
"question": "bold fg:#f8fafc",
|
|
15
|
+
"answer": "fg:#4ade80 bold",
|
|
16
|
+
"pointer": "fg:#60a5fa bold",
|
|
17
|
+
"highlighted": "fg:#0f172a bg:#60a5fa bold",
|
|
18
|
+
"selected": "fg:#4ade80 bold",
|
|
19
|
+
"separator": "fg:#94a3b8",
|
|
20
|
+
"instruction": "fg:#94a3b8",
|
|
21
|
+
"text": "fg:#e2e8f0",
|
|
22
|
+
"disabled": "fg:#64748b italic",
|
|
23
|
+
"checkbox": "fg:#60a5fa",
|
|
24
|
+
"checkbox-selected": "fg:#4ade80 bold",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def colors_enabled() -> bool:
|
|
30
|
+
return not os.environ.get("NO_COLOR")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ansi(code: str, text: str) -> str:
|
|
34
|
+
"""Wrap *text* in an ANSI SGR sequence when colors are enabled."""
|
|
35
|
+
if not colors_enabled():
|
|
36
|
+
return text
|
|
37
|
+
return f"\033[{code}m{text}\033[0m"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Bold bright ANSI — readable on dark terminals; select() renders these safely
|
|
41
|
+
# (unlike autocomplete, which HTML-parses choice text).
|
|
42
|
+
_CATEGORY_PALETTE = (
|
|
43
|
+
"1;93", # bright yellow
|
|
44
|
+
"1;92", # bright green
|
|
45
|
+
"1;96", # bright cyan
|
|
46
|
+
"1;95", # bright magenta
|
|
47
|
+
"1;94", # bright blue
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def color_category(slug: str, label: str) -> str:
|
|
52
|
+
idx = sum(ord(char) for char in slug) % len(_CATEGORY_PALETTE)
|
|
53
|
+
return ansi(_CATEGORY_PALETTE[idx], label)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def bold(text: str) -> str:
|
|
57
|
+
return ansi("1", text)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def dim(text: str) -> str:
|
|
61
|
+
return ansi("2", text)
|
{create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_catalog_resolve.py
RENAMED
|
@@ -108,6 +108,49 @@ def test_build_template_choices_are_searchable() -> None:
|
|
|
108
108
|
assert choices[-1].value == CUSTOM_TEMPLATE_SENTINEL
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def test_template_choice_titles_include_bright_category_ansi(
|
|
112
|
+
monkeypatch,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""select() can render ANSI; badges use bright bold codes for contrast."""
|
|
115
|
+
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
116
|
+
catalog = {
|
|
117
|
+
"categories": [
|
|
118
|
+
{"slug": "backend-applications", "name": "Backend Applications"}
|
|
119
|
+
],
|
|
120
|
+
"templates": [
|
|
121
|
+
{
|
|
122
|
+
"slug": "fastapi-starter",
|
|
123
|
+
"name": "FastAPI Starter",
|
|
124
|
+
"url": "file:///templates/fastapi",
|
|
125
|
+
"category": "backend-applications",
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
}
|
|
129
|
+
title = build_template_choices(catalog)[0].title
|
|
130
|
+
assert "\033[" in title
|
|
131
|
+
assert "FastAPI Starter" in title
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_template_choice_titles_respect_no_color(monkeypatch) -> None:
|
|
135
|
+
monkeypatch.setenv("NO_COLOR", "1")
|
|
136
|
+
catalog = {
|
|
137
|
+
"categories": [
|
|
138
|
+
{"slug": "backend-applications", "name": "Backend Applications"}
|
|
139
|
+
],
|
|
140
|
+
"templates": [
|
|
141
|
+
{
|
|
142
|
+
"slug": "fastapi-starter",
|
|
143
|
+
"name": "FastAPI Starter",
|
|
144
|
+
"url": "file:///templates/fastapi",
|
|
145
|
+
"category": "backend-applications",
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
}
|
|
149
|
+
title = build_template_choices(catalog)[0].title
|
|
150
|
+
assert "\033" not in title
|
|
151
|
+
assert "FastAPI Starter" in title
|
|
152
|
+
|
|
153
|
+
|
|
111
154
|
def test_build_extension_choices_filters_by_template_type() -> None:
|
|
112
155
|
catalog = {
|
|
113
156
|
"categories": [
|
{create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_interactive.py
RENAMED
|
@@ -28,10 +28,10 @@ def test_in_ci_env(monkeypatch) -> None:
|
|
|
28
28
|
os.environ.pop("CI", None)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def
|
|
31
|
+
def test_interactive_template_select_uses_search_filter(
|
|
32
32
|
tmp_path: Path, monkeypatch
|
|
33
33
|
) -> None:
|
|
34
|
-
"""
|
|
34
|
+
"""Template pick is a browsable select with type-to-filter (not autocomplete)."""
|
|
35
35
|
template_dir = tmp_path / "fastapi"
|
|
36
36
|
template_dir.mkdir()
|
|
37
37
|
(template_dir / "cpa.config.json").write_text(
|
|
@@ -60,11 +60,9 @@ def test_interactive_template_autocomplete_omits_pointer(
|
|
|
60
60
|
|
|
61
61
|
captured_kwargs: dict[str, object] = {}
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def fake_select(*_args, **kwargs):
|
|
64
64
|
captured_kwargs.update(kwargs)
|
|
65
|
-
|
|
66
|
-
# Return a mapped title so choice_by_title resolves to the template URL.
|
|
67
|
-
return FakePrompt(choices[0] if choices else template_url)
|
|
65
|
+
return FakePrompt(template_url)
|
|
68
66
|
|
|
69
67
|
def fake_checkbox(*_args, **_kwargs):
|
|
70
68
|
return FakePrompt([])
|
|
@@ -75,7 +73,7 @@ def test_interactive_template_autocomplete_omits_pointer(
|
|
|
75
73
|
captured["project_directory"] = project_directory
|
|
76
74
|
captured["options"] = options
|
|
77
75
|
|
|
78
|
-
monkeypatch.setattr("questionary.
|
|
76
|
+
monkeypatch.setattr("questionary.select", fake_select)
|
|
79
77
|
monkeypatch.setattr("questionary.checkbox", fake_checkbox)
|
|
80
78
|
monkeypatch.setattr(
|
|
81
79
|
"create_awesome_python_app.cli.create_python_app",
|
|
@@ -89,7 +87,9 @@ def test_interactive_template_autocomplete_omits_pointer(
|
|
|
89
87
|
result = runner.invoke(app, ["--interactive", "--no-install", "api"])
|
|
90
88
|
|
|
91
89
|
assert result.exit_code == 0, result.stdout + result.stderr
|
|
92
|
-
assert "
|
|
90
|
+
assert captured_kwargs.get("use_search_filter") is True
|
|
91
|
+
assert captured_kwargs.get("use_jk_keys") is False
|
|
92
|
+
assert captured_kwargs.get("style") is not None
|
|
93
93
|
assert captured["project_directory"] == "api"
|
|
94
94
|
|
|
95
95
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_cache_cmds.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{create_awesome_python_app-0.2.1 → create_awesome_python_app-0.2.3}/tests/test_catalog_fetch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|