create-awesome-python-app 0.2.3__tar.gz → 0.2.4__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.3 → create_awesome_python_app-0.2.4}/PKG-INFO +2 -2
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/pyproject.toml +2 -2
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/src/create_awesome_python_app/__init__.py +1 -1
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/src/create_awesome_python_app/catalog.py +26 -18
- create_awesome_python_app-0.2.4/src/create_awesome_python_app/prompt_style.py +113 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_catalog_resolve.py +15 -9
- create_awesome_python_app-0.2.3/src/create_awesome_python_app/prompt_style.py +0 -61
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/.gitignore +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/README.md +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/assets/hero.svg +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/src/create_awesome_python_app/cache.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/src/create_awesome_python_app/cli.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cache_cmds.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cache_env.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_catalog.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_catalog_fetch.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cli.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cpa_templates_integration.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_flags.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_interactive.py +0 -0
- {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/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.4
|
|
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.4
|
|
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.4"
|
|
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.4",
|
|
10
10
|
"questionary>=2.1.1",
|
|
11
11
|
"rich>=15.0.0",
|
|
12
12
|
"typer>=0.27.0",
|
|
@@ -16,7 +16,10 @@ 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
|
|
19
|
+
from create_awesome_python_app.prompt_style import (
|
|
20
|
+
custom_template_title,
|
|
21
|
+
template_title_tokens,
|
|
22
|
+
)
|
|
20
23
|
|
|
21
24
|
console = Console(stderr=True)
|
|
22
25
|
|
|
@@ -27,7 +30,7 @@ CUSTOM_TEMPLATE_SENTINEL = "__custom_template__"
|
|
|
27
30
|
class TemplateChoice:
|
|
28
31
|
"""Searchable interactive template choice."""
|
|
29
32
|
|
|
30
|
-
title:
|
|
33
|
+
title: Any
|
|
31
34
|
value: str
|
|
32
35
|
search: str
|
|
33
36
|
|
|
@@ -236,35 +239,40 @@ def build_template_choices(data: dict[str, Any]) -> list[TemplateChoice]:
|
|
|
236
239
|
category_name = categories.get(category_slug, category_slug)
|
|
237
240
|
badge = short_category_label(category_name).ljust(10)[:10]
|
|
238
241
|
slug = str(template.get("slug", ""))
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
labels_raw = template.get("labels", [])
|
|
243
|
+
labels = (
|
|
244
|
+
[str(label) for label in labels_raw[:3]]
|
|
245
|
+
if isinstance(labels_raw, list)
|
|
246
|
+
else []
|
|
247
|
+
)
|
|
243
248
|
description = str(template.get("description", "")).strip()
|
|
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
249
|
name = str(template.get("name", slug))
|
|
248
|
-
|
|
249
|
-
#
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
search = _search_text(template, category_name)
|
|
251
|
+
# FormattedText tokens (not raw ANSI): select() prints str titles
|
|
252
|
+
# literally, which showed ^[[1;94m… in terminals.
|
|
253
|
+
title = template_title_tokens(
|
|
254
|
+
category_slug=category_slug,
|
|
255
|
+
badge=badge,
|
|
256
|
+
name=name,
|
|
257
|
+
slug=slug,
|
|
258
|
+
labels=labels,
|
|
259
|
+
description=description,
|
|
260
|
+
search=search,
|
|
254
261
|
)
|
|
255
262
|
choices.append(
|
|
256
263
|
TemplateChoice(
|
|
257
264
|
title=title,
|
|
258
265
|
value=template_url,
|
|
259
|
-
search=
|
|
266
|
+
search=search,
|
|
260
267
|
)
|
|
261
268
|
)
|
|
262
269
|
|
|
270
|
+
custom_search = "custom own template url github file"
|
|
263
271
|
choices.append(
|
|
264
272
|
TemplateChoice(
|
|
265
|
-
title=
|
|
273
|
+
title=custom_template_title(custom_search),
|
|
266
274
|
value=CUSTOM_TEMPLATE_SENTINEL,
|
|
267
|
-
search=
|
|
275
|
+
search=custom_search,
|
|
268
276
|
)
|
|
269
277
|
)
|
|
270
278
|
return choices
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""High-contrast questionary styles for CPA interactive prompts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from questionary import Style
|
|
9
|
+
|
|
10
|
+
# Lighter brand blues/greens than docs hex so selected rows stay readable on
|
|
11
|
+
# dark terminals (slate backgrounds common in Arch/Ghostty/Alacritty).
|
|
12
|
+
CPA_PROMPT_STYLE = Style.from_dict(
|
|
13
|
+
{
|
|
14
|
+
"qmark": "fg:#60a5fa bold",
|
|
15
|
+
"question": "bold fg:#f8fafc",
|
|
16
|
+
"answer": "fg:#4ade80 bold",
|
|
17
|
+
"pointer": "fg:#60a5fa bold",
|
|
18
|
+
"highlighted": "fg:#0f172a bg:#60a5fa bold",
|
|
19
|
+
"selected": "fg:#4ade80 bold",
|
|
20
|
+
"separator": "fg:#94a3b8",
|
|
21
|
+
"instruction": "fg:#94a3b8",
|
|
22
|
+
"text": "fg:#e2e8f0",
|
|
23
|
+
"disabled": "fg:#64748b italic",
|
|
24
|
+
"checkbox": "fg:#60a5fa",
|
|
25
|
+
"checkbox-selected": "fg:#4ade80 bold",
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Bright prompt_toolkit style strings (not raw ANSI — select() prints str titles
|
|
30
|
+
# literally, so escapes show as ^[[...m unless titles are FormattedText tokens).
|
|
31
|
+
_CATEGORY_STYLES = (
|
|
32
|
+
"fg:#facc15 bold", # yellow
|
|
33
|
+
"fg:#4ade80 bold", # green
|
|
34
|
+
"fg:#22d3ee bold", # cyan
|
|
35
|
+
"fg:#e879f9 bold", # magenta
|
|
36
|
+
"fg:#60a5fa bold", # blue
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SearchableFormattedText(list):
|
|
41
|
+
"""FormattedText tokens with ``.lower()`` for questionary search filter.
|
|
42
|
+
|
|
43
|
+
``select(use_search_filter=True)`` does ``needle in choice.title.lower()``.
|
|
44
|
+
A plain token list has no ``.lower()``; this keeps filter working while
|
|
45
|
+
titles render as styled FormattedText.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, tokens: list[tuple[str, str]], search: str) -> None:
|
|
49
|
+
super().__init__(tokens)
|
|
50
|
+
self._search = search
|
|
51
|
+
|
|
52
|
+
def lower(self) -> str:
|
|
53
|
+
return self._search.lower()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def colors_enabled() -> bool:
|
|
57
|
+
return not os.environ.get("NO_COLOR")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def category_style(slug: str) -> str:
|
|
61
|
+
idx = sum(ord(char) for char in slug) % len(_CATEGORY_STYLES)
|
|
62
|
+
return _CATEGORY_STYLES[idx]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def plain_title_text(title: Any) -> str:
|
|
66
|
+
"""Join FormattedText token text (or return a plain string title)."""
|
|
67
|
+
if isinstance(title, list):
|
|
68
|
+
return "".join(str(token[1]) for token in title)
|
|
69
|
+
return str(title)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def template_title_tokens(
|
|
73
|
+
*,
|
|
74
|
+
category_slug: str,
|
|
75
|
+
badge: str,
|
|
76
|
+
name: str,
|
|
77
|
+
slug: str,
|
|
78
|
+
labels: list[str],
|
|
79
|
+
description: str,
|
|
80
|
+
search: str,
|
|
81
|
+
) -> SearchableFormattedText | str:
|
|
82
|
+
"""Build a select-safe title: FormattedText when colors on, else plain str."""
|
|
83
|
+
label_suffix = ""
|
|
84
|
+
if labels:
|
|
85
|
+
label_suffix = " · " + ", ".join(labels[:3])
|
|
86
|
+
description_suffix = f" — {description}" if description else ""
|
|
87
|
+
plain = f"{badge} {name} ({slug}){label_suffix}{description_suffix}"
|
|
88
|
+
|
|
89
|
+
if not colors_enabled():
|
|
90
|
+
return plain
|
|
91
|
+
|
|
92
|
+
tokens: list[tuple[str, str]] = [
|
|
93
|
+
(category_style(category_slug), badge),
|
|
94
|
+
("", " "),
|
|
95
|
+
("bold", name),
|
|
96
|
+
("class:instruction", f" ({slug})"),
|
|
97
|
+
]
|
|
98
|
+
if label_suffix:
|
|
99
|
+
tokens.append(("class:instruction", label_suffix))
|
|
100
|
+
if description_suffix:
|
|
101
|
+
tokens.append(("fg:#94a3b8", description_suffix))
|
|
102
|
+
return SearchableFormattedText(tokens, search=search or plain)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def custom_template_title(search: str) -> SearchableFormattedText | str:
|
|
106
|
+
label = "Use my own template URL"
|
|
107
|
+
plain = " " * 12 + label
|
|
108
|
+
if not colors_enabled():
|
|
109
|
+
return plain
|
|
110
|
+
return SearchableFormattedText(
|
|
111
|
+
[("", " " * 12), ("italic fg:#94a3b8", label)],
|
|
112
|
+
search=search or plain,
|
|
113
|
+
)
|
{create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_catalog_resolve.py
RENAMED
|
@@ -17,6 +17,10 @@ from create_awesome_python_app.catalog import (
|
|
|
17
17
|
short_category_label,
|
|
18
18
|
validate_extension_compatibility,
|
|
19
19
|
)
|
|
20
|
+
from create_awesome_python_app.prompt_style import (
|
|
21
|
+
SearchableFormattedText,
|
|
22
|
+
plain_title_text,
|
|
23
|
+
)
|
|
20
24
|
|
|
21
25
|
SAMPLE_CATALOG = {
|
|
22
26
|
"templates": [
|
|
@@ -99,19 +103,18 @@ def test_build_template_choices_are_searchable() -> None:
|
|
|
99
103
|
choices = build_template_choices(catalog)
|
|
100
104
|
first = choices[0]
|
|
101
105
|
assert first.value == "file:///templates/fastapi"
|
|
102
|
-
|
|
103
|
-
assert "
|
|
104
|
-
assert "
|
|
106
|
+
title_text = plain_title_text(first.title)
|
|
107
|
+
assert "FastAPI Starter" in title_text
|
|
108
|
+
assert "OpenAPI" in title_text
|
|
109
|
+
assert "uv" in title_text
|
|
105
110
|
assert "openapi" in first.search
|
|
106
111
|
assert "backend" in first.search
|
|
107
112
|
assert "uv" in first.search
|
|
108
113
|
assert choices[-1].value == CUSTOM_TEMPLATE_SENTINEL
|
|
109
114
|
|
|
110
115
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
) -> None:
|
|
114
|
-
"""select() can render ANSI; badges use bright bold codes for contrast."""
|
|
116
|
+
def test_template_choice_titles_use_formatted_text(monkeypatch) -> None:
|
|
117
|
+
"""select() needs FormattedText tokens; raw ANSI shows as ^[[…m."""
|
|
115
118
|
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
116
119
|
catalog = {
|
|
117
120
|
"categories": [
|
|
@@ -127,8 +130,10 @@ def test_template_choice_titles_include_bright_category_ansi(
|
|
|
127
130
|
],
|
|
128
131
|
}
|
|
129
132
|
title = build_template_choices(catalog)[0].title
|
|
130
|
-
assert
|
|
131
|
-
assert "
|
|
133
|
+
assert isinstance(title, SearchableFormattedText)
|
|
134
|
+
assert "\033" not in plain_title_text(title)
|
|
135
|
+
assert "FastAPI Starter" in plain_title_text(title)
|
|
136
|
+
assert "fastapi" in title.lower()
|
|
132
137
|
|
|
133
138
|
|
|
134
139
|
def test_template_choice_titles_respect_no_color(monkeypatch) -> None:
|
|
@@ -147,6 +152,7 @@ def test_template_choice_titles_respect_no_color(monkeypatch) -> None:
|
|
|
147
152
|
],
|
|
148
153
|
}
|
|
149
154
|
title = build_template_choices(catalog)[0].title
|
|
155
|
+
assert isinstance(title, str)
|
|
150
156
|
assert "\033" not in title
|
|
151
157
|
assert "FastAPI Starter" in title
|
|
152
158
|
|
|
@@ -1,61 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cache_cmds.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_catalog_fetch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_interactive.py
RENAMED
|
File without changes
|
|
File without changes
|