create-awesome-python-app 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl

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.
@@ -1,3 +1,3 @@
1
1
  """Create Awesome Python App CLI."""
2
2
 
3
- __version__ = "0.2.5"
3
+ __version__ = "0.2.7"
@@ -13,7 +13,6 @@ from typing import Any
13
13
 
14
14
  from create_python_app_core.paths import default_cache_dir, resolve_source
15
15
  from rich.console import Console
16
- from rich.table import Table
17
16
 
18
17
  from create_awesome_python_app import __version__
19
18
  from create_awesome_python_app.prompt_style import (
@@ -159,6 +158,17 @@ def validate_extension_compatibility(
159
158
  raise IncompatibleExtensionsError(pairs)
160
159
 
161
160
 
161
+ @dataclass(frozen=True)
162
+ class CategoryInfo:
163
+ """Catalog category with optional rich metadata (CNA parity)."""
164
+
165
+ slug: str
166
+ name: str
167
+ description: str = ""
168
+ details: str = ""
169
+ labels: tuple[str, ...] = ()
170
+
171
+
162
172
  def short_category_label(category_name: str) -> str:
163
173
  """Derive a compact badge label from a catalog category name."""
164
174
  stop_words = {"Applications", "Application", "Boilerplate"}
@@ -168,11 +178,48 @@ def short_category_label(category_name: str) -> str:
168
178
  return " ".join(words[:2]) or category_name
169
179
 
170
180
 
181
+ def category_index(data: dict[str, Any]) -> dict[str, CategoryInfo]:
182
+ """Index categories by slug, including description/details/labels."""
183
+ out: dict[str, CategoryInfo] = {}
184
+ for raw in data.get("categories", []):
185
+ if not isinstance(raw, dict):
186
+ continue
187
+ slug = str(raw.get("slug", "")).strip()
188
+ if not slug:
189
+ continue
190
+ labels_raw = raw.get("labels", [])
191
+ labels = (
192
+ tuple(str(label) for label in labels_raw)
193
+ if isinstance(labels_raw, list)
194
+ else ()
195
+ )
196
+ out[slug] = CategoryInfo(
197
+ slug=slug,
198
+ name=str(raw.get("name", slug)),
199
+ description=str(raw.get("description", "")).strip(),
200
+ details=str(raw.get("details", "")).strip(),
201
+ labels=labels,
202
+ )
203
+ return out
204
+
205
+
206
+ def get_category_data(data: dict[str, Any], slug: str) -> CategoryInfo | None:
207
+ return category_index(data).get(slug)
208
+
209
+
210
+ def format_category_choice_title(info: CategoryInfo, extension_count: int) -> str:
211
+ """Human title for the interactive extension-category picker."""
212
+ noun = "extension" if extension_count == 1 else "extensions"
213
+ title = f"{info.name} ({extension_count} {noun})"
214
+ if info.description:
215
+ title = f"{title} — {info.description}"
216
+ if info.labels:
217
+ title = f"{title} · {', '.join(info.labels[:3])}"
218
+ return title
219
+
220
+
171
221
  def _category_map(data: dict[str, Any]) -> dict[str, str]:
172
- return {
173
- str(category.get("slug", "")): str(category.get("name", ""))
174
- for category in data.get("categories", [])
175
- }
222
+ return {slug: info.name for slug, info in category_index(data).items()}
176
223
 
177
224
 
178
225
  def _search_text(template: dict[str, Any], category_name: str) -> str:
@@ -344,9 +391,9 @@ CACHE_TTL_SECONDS = 3600
344
391
  FETCH_TIMEOUT_SECONDS = 10
345
392
  USER_AGENT = f"create-awesome-python-app/{__version__} (https://github.com/Create-Python-App/create-python-app)"
346
393
 
347
- _FIXTURE = (
348
- Path(__file__).resolve().parents[4] / "fixtures" / "catalog" / "templates.json"
349
- )
394
+ _AUTO_FIXTURE_DIR = Path(__file__).resolve().parents[4]
395
+ _SENTINEL = object()
396
+ _fixture_root_override: Path | None | object = _SENTINEL
350
397
 
351
398
  _memory_cache: dict[str, Any] | None = None
352
399
  _memory_ts: float = 0.0
@@ -360,13 +407,61 @@ def catalog_cache_path() -> Path:
360
407
  return default_cache_dir() / "catalog" / "templates.json"
361
408
 
362
409
 
410
+ def resolve_fixture_root() -> Path | None:
411
+ """Resolve the repo root that contains ``fixtures/catalog/templates.json``.
412
+
413
+ Priority: ``CPA_FIXTURE_DIR`` → walk-up from package → ``cwd``.
414
+ """
415
+ if _fixture_root_override is not _SENTINEL:
416
+ return _fixture_root_override # type: ignore[return-value]
417
+
418
+ env = os.environ.get("CPA_FIXTURE_DIR", "").strip()
419
+ if env:
420
+ return Path(env).expanduser().resolve()
421
+
422
+ def _has_fixture_catalog(root: Path) -> bool:
423
+ return (root / "fixtures" / "catalog" / "templates.json").is_file()
424
+
425
+ if _has_fixture_catalog(_AUTO_FIXTURE_DIR):
426
+ return _AUTO_FIXTURE_DIR
427
+
428
+ # Editable / site-packages layouts vary; walk up from this file.
429
+ for parent in Path(__file__).resolve().parents:
430
+ if _has_fixture_catalog(parent):
431
+ return parent
432
+
433
+ cwd = Path.cwd()
434
+ if _has_fixture_catalog(cwd):
435
+ return cwd
436
+ return None
437
+
438
+
439
+ def set_fixture_root_for_tests(root: Path | None) -> None:
440
+ """Override fixture root (test helper)."""
441
+ global _fixture_root_override
442
+ _fixture_root_override = root
443
+
444
+
445
+ def reset_fixture_root_for_tests() -> None:
446
+ global _fixture_root_override
447
+ _fixture_root_override = _SENTINEL
448
+
449
+
450
+ def fixture_catalog_path() -> Path | None:
451
+ root = resolve_fixture_root()
452
+ if root is None:
453
+ return None
454
+ return root / "fixtures" / "catalog" / "templates.json"
455
+
456
+
363
457
  def _read_json_file(path: Path) -> dict[str, Any]:
364
458
  return json.loads(path.read_text(encoding="utf-8"))
365
459
 
366
460
 
367
461
  def _read_fixture() -> dict[str, Any]:
368
- if _FIXTURE.is_file():
369
- return _read_json_file(_FIXTURE)
462
+ path = fixture_catalog_path()
463
+ if path is not None and path.is_file():
464
+ return _read_json_file(path)
370
465
  return {"templates": [], "extensions": [], "categories": []}
371
466
 
372
467
 
@@ -417,6 +512,19 @@ def get_catalog_data(*, force_refresh: bool = False) -> dict[str, Any]:
417
512
  """Load templates.json from remote URL, disk cache, or local fixture."""
418
513
  global _memory_cache, _memory_ts
419
514
 
515
+ if os.environ.get("CPA_CATALOG_FIXTURE") == "1":
516
+ path = fixture_catalog_path()
517
+ if path is None or not path.is_file():
518
+ raise RuntimeError(
519
+ "Fixture mode is enabled (CPA_CATALOG_FIXTURE=1) but the fixture "
520
+ "root could not be resolved. Set CPA_FIXTURE_DIR to the repo root "
521
+ "containing fixtures/catalog/templates.json."
522
+ )
523
+ data = _read_fixture()
524
+ _memory_cache = data
525
+ _memory_ts = time.time()
526
+ return data
527
+
420
528
  if (
421
529
  not force_refresh
422
530
  and _memory_cache is not None
@@ -425,38 +533,33 @@ def get_catalog_data(*, force_refresh: bool = False) -> dict[str, Any]:
425
533
  ):
426
534
  return _memory_cache
427
535
 
428
- if os.environ.get("CPA_CATALOG_FIXTURE") == "1":
429
- data = _read_fixture()
430
- else:
431
- url = catalog_url()
432
- try:
433
- data = _fetch_remote(url)
434
- _write_disk_cache(data)
435
- except (
436
- urllib.error.URLError,
437
- TimeoutError,
438
- OSError,
439
- json.JSONDecodeError,
440
- ) as err:
441
- disk = _read_disk_cache()
442
- if disk is not None:
536
+ url = catalog_url()
537
+ try:
538
+ data = _fetch_remote(url)
539
+ _write_disk_cache(data)
540
+ except (
541
+ urllib.error.URLError,
542
+ TimeoutError,
543
+ OSError,
544
+ json.JSONDecodeError,
545
+ ) as err:
546
+ disk = _read_disk_cache()
547
+ if disk is not None:
548
+ console.print(
549
+ "[yellow][cpa] Could not refresh catalog "
550
+ f"({err}); using disk cache.[/yellow]"
551
+ )
552
+ data = disk
553
+ else:
554
+ fixture = _read_fixture()
555
+ if fixture.get("templates"):
443
556
  console.print(
444
557
  "[yellow][cpa] Could not refresh catalog "
445
- f"({err}); using disk cache.[/yellow]"
558
+ f"({err}); using fixture.[/yellow]"
446
559
  )
447
- data = disk
560
+ data = fixture
448
561
  else:
449
- fixture = _read_fixture()
450
- if fixture.get("templates"):
451
- console.print(
452
- "[yellow][cpa] Could not refresh catalog "
453
- f"({err}); using fixture.[/yellow]"
454
- )
455
- data = fixture
456
- else:
457
- raise RuntimeError(
458
- f"Failed to load template catalog: {err}"
459
- ) from err
562
+ raise RuntimeError(f"Failed to load template catalog: {err}") from err
460
563
 
461
564
  _memory_cache = data
462
565
  _memory_ts = time.time()
@@ -471,44 +574,96 @@ def reset_catalog_cache_for_tests() -> None:
471
574
 
472
575
  def list_templates() -> None:
473
576
  data = get_catalog_data()
474
- table = Table(title="Templates")
475
- table.add_column("slug")
476
- table.add_column("category")
477
- table.add_column("type")
478
- for t in data.get("templates", []):
479
- table.add_row(
480
- str(t.get("slug", "")),
481
- str(t.get("category", "")),
482
- str(t.get("type", "")),
483
- )
484
- console.print(table)
577
+ categories = category_index(data)
578
+ templates = [t for t in data.get("templates", []) if isinstance(t, dict)]
579
+
580
+ console.print("[bold blue]\nAvailable Templates:[/bold blue]")
581
+ # Preserve catalog category order, then any unknown slugs.
582
+ ordered_slugs = list(categories)
583
+ seen: set[str] = set()
584
+ for slug in ordered_slugs:
585
+ seen.add(slug)
586
+ group = [t for t in templates if str(t.get("category", "")) == slug]
587
+ if not group:
588
+ continue
589
+ info = categories[slug]
590
+ console.print(f"[bold green]\n{info.name}:[/bold green]")
591
+ if info.description:
592
+ console.print(f" {info.description}")
593
+ if info.details:
594
+ console.print(f" [dim]{info.details}[/dim]")
595
+ if info.labels:
596
+ console.print(f" [dim]Keywords: {', '.join(info.labels)}[/dim]")
597
+ for template in group:
598
+ name = str(template.get("name", template.get("slug", "")))
599
+ tslug = str(template.get("slug", ""))
600
+ console.print(f" [yellow]{name}[/yellow] ([cyan]{tslug}[/cyan])")
601
+ desc = str(template.get("description", "")).strip()
602
+ if desc:
603
+ console.print(f" {desc}")
604
+ labels = template.get("labels", [])
605
+ if isinstance(labels, list) and labels:
606
+ console.print(f" Keywords: {', '.join(str(x) for x in labels)}")
607
+
608
+ orphans = [t for t in templates if str(t.get("category", "")) not in seen]
609
+ if orphans:
610
+ console.print("[bold green]\nOther:[/bold green]")
611
+ for template in orphans:
612
+ name = str(template.get("name", template.get("slug", "")))
613
+ tslug = str(template.get("slug", ""))
614
+ console.print(f" [yellow]{name}[/yellow] ([cyan]{tslug}[/cyan])")
485
615
 
486
616
 
487
617
  def list_addons(template_slug: str | None = None) -> None:
488
618
  data = get_catalog_data()
619
+ categories = category_index(data)
489
620
  template_type: str | None = None
490
621
  if template_slug:
491
622
  for t in data.get("templates", []):
492
- if t.get("slug") == template_slug:
623
+ if isinstance(t, dict) and t.get("slug") == template_slug:
493
624
  template_type = str(t.get("type", ""))
494
625
  break
495
626
 
496
- table = Table(title="Extensions")
497
- table.add_column("slug")
498
- table.add_column("category")
499
- table.add_column("type")
500
- for ext in data.get("extensions", data.get("addons", [])):
627
+ console.print("[bold blue]\nAvailable Addons:[/bold blue]")
628
+ if template_slug:
629
+ console.print(
630
+ f"[bold green]\nCompatible with template: {template_slug}[/bold green]"
631
+ )
632
+
633
+ extensions = [
634
+ ext
635
+ for ext in data.get("extensions", data.get("addons", []))
636
+ if isinstance(ext, dict)
637
+ ]
638
+ grouped: dict[str, list[dict[str, Any]]] = {}
639
+ for ext in extensions:
501
640
  ext_types = ext.get("type", [])
502
641
  if isinstance(ext_types, str):
503
642
  ext_types = [ext_types]
504
643
  if template_type and template_type not in ext_types:
505
644
  continue
506
- type_label = (
507
- ", ".join(ext_types) if isinstance(ext_types, list) else str(ext_types)
508
- )
509
- table.add_row(
510
- str(ext.get("slug", "")),
511
- str(ext.get("category", "")),
512
- type_label,
513
- )
514
- console.print(table)
645
+ slug = str(ext.get("category", "custom"))
646
+ grouped.setdefault(slug, []).append(ext)
647
+
648
+ for slug in list(categories) + [s for s in grouped if s not in categories]:
649
+ group = grouped.get(slug)
650
+ if not group:
651
+ continue
652
+ info = categories.get(slug) or CategoryInfo(slug=slug, name=slug)
653
+ console.print(f"[bold green]\n{info.name}:[/bold green]")
654
+ if info.description:
655
+ console.print(f" {info.description}")
656
+ if info.details:
657
+ console.print(f" [dim]{info.details}[/dim]")
658
+ if info.labels:
659
+ console.print(f" [dim]Keywords: {', '.join(info.labels)}[/dim]")
660
+ for ext in group:
661
+ name = str(ext.get("name", ext.get("slug", "")))
662
+ eslug = str(ext.get("slug", ""))
663
+ console.print(f" [yellow]{name}[/yellow] ([cyan]{eslug}[/cyan])")
664
+ desc = str(ext.get("description", "")).strip()
665
+ if desc:
666
+ console.print(f" {desc}")
667
+ labels = ext.get("labels", [])
668
+ if isinstance(labels, list) and labels:
669
+ console.print(f" Keywords: {', '.join(str(x) for x in labels)}")
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import os
7
+ import sys
7
8
  from pathlib import Path
8
9
  from typing import Any
9
10
 
@@ -25,17 +26,60 @@ from rich.console import Console
25
26
 
26
27
  from create_awesome_python_app import __version__
27
28
 
29
+ # Sentinel for bare ``--fixture`` (optional DIR rewritten in argv preprocess).
30
+ _FIXTURE_AUTO = "__CPA_FIXTURE_AUTO__"
31
+
28
32
  app = typer.Typer(
29
33
  name="create-awesome-python-app",
30
34
  help="Composable scaffolding CLI for production-ready Python apps.",
31
35
  no_args_is_help=False,
32
36
  add_completion=True,
37
+ # Keep cache out of this Typer app (routed in main()). A nested command
38
+ # group turns the CLI into Click Group form `[ARGS] COMMAND`, so
39
+ # `cpa my-api --template …` fails with "No such command '--template'".
40
+ epilog="Cache: create-awesome-python-app cache [list|dir|clean|verify|…]",
33
41
  )
34
42
  cache_app = typer.Typer(help="Inspect and manage the local template cache")
35
- app.add_typer(cache_app, name="cache")
36
43
  console = Console(stderr=True)
37
44
 
38
45
 
46
+ def _preprocess_fixture_argv(argv: list[str] | None = None) -> list[str]:
47
+ """Rewrite bare ``--fixture`` to ``--fixture=__CPA_FIXTURE_AUTO__``.
48
+
49
+ Typer/Click requires an option argument; Commander allows ``--fixture [dir]``.
50
+ This keeps CNA-compatible UX: ``--fixture`` alone enables auto-detect mode.
51
+ """
52
+ raw = list(sys.argv if argv is None else argv)
53
+ if not raw:
54
+ return raw
55
+ out = [raw[0]]
56
+ i = 1
57
+ while i < len(raw):
58
+ arg = raw[i]
59
+ if arg == "--fixture":
60
+ if i + 1 < len(raw) and not raw[i + 1].startswith("-"):
61
+ out.extend(["--fixture", raw[i + 1]])
62
+ i += 2
63
+ else:
64
+ out.append(f"--fixture={_FIXTURE_AUTO}")
65
+ i += 1
66
+ continue
67
+ out.append(arg)
68
+ i += 1
69
+ if argv is None:
70
+ sys.argv = out
71
+ return out
72
+
73
+
74
+ def apply_fixture_mode(fixture: str | None) -> None:
75
+ """Translate ``--fixture`` into ``CPA_CATALOG_FIXTURE`` / ``CPA_FIXTURE_DIR``."""
76
+ if fixture is None and os.environ.get("CPA_CATALOG_FIXTURE") != "1":
77
+ return
78
+ os.environ["CPA_CATALOG_FIXTURE"] = "1"
79
+ if fixture is not None and fixture != _FIXTURE_AUTO and fixture != "":
80
+ os.environ["CPA_FIXTURE_DIR"] = fixture
81
+
82
+
39
83
  def _in_ci() -> bool:
40
84
  return os.environ.get("CI", "").lower() in {"1", "true", "yes"}
41
85
 
@@ -159,9 +203,8 @@ def main() -> None:
159
203
  `create-awesome-python-app cache dir` works (Typer would otherwise
160
204
  treat `cache` as project_directory).
161
205
  """
162
- import sys
163
-
164
206
  check_python_version(">=3.12", "create-awesome-python-app")
207
+ _preprocess_fixture_argv()
165
208
  if len(sys.argv) > 1 and sys.argv[1] == "cache":
166
209
  sys.argv = [sys.argv[0], *sys.argv[2:]]
167
210
  cache_app(prog_name="create-awesome-python-app cache")
@@ -169,9 +212,8 @@ def main() -> None:
169
212
  app()
170
213
 
171
214
 
172
- @app.callback(invoke_without_command=True)
215
+ @app.command(name="create-awesome-python-app")
173
216
  def scaffold(
174
- ctx: typer.Context,
175
217
  project_directory: str | None = typer.Argument("my-project"),
176
218
  version: bool = typer.Option(False, "--version"),
177
219
  info: bool = typer.Option(False, "--info", "-i"),
@@ -192,14 +234,25 @@ def scaffold(
192
234
  refresh: str | None = typer.Option(None, "--refresh"),
193
235
  strict_version: bool = typer.Option(False, "--strict-version"),
194
236
  keep_on_failure: bool = typer.Option(False, "--keep-on-failure"),
237
+ fixture: str | None = typer.Option(
238
+ None,
239
+ "--fixture",
240
+ help=(
241
+ "Load the template catalog from the local fixtures/ directory "
242
+ "instead of the network (optional DIR = repo root; also "
243
+ "CPA_FIXTURE_DIR / CPA_CATALOG_FIXTURE)"
244
+ ),
245
+ ),
195
246
  ) -> None:
196
247
  if version:
197
248
  console.print(__version__)
198
249
  raise typer.Exit(0)
199
250
  if info:
200
251
  print_env_info()
201
- if ctx.invoked_subcommand is not None:
202
- return
252
+
253
+ # Translate --fixture into env vars before catalog loads
254
+ # (--list-templates / interactive / scaffold).
255
+ apply_fixture_mode(fixture)
203
256
 
204
257
  if list_templates or list_addons:
205
258
  from create_awesome_python_app.catalog import list_addons as la
@@ -315,12 +368,22 @@ def scaffold(
315
368
  extension_choices = build_extension_choices(interactive_catalog, template)
316
369
  grouped_extensions = group_extension_choices(extension_choices)
317
370
  if grouped_extensions:
371
+ from create_awesome_python_app.catalog import (
372
+ CategoryInfo,
373
+ category_index,
374
+ format_category_choice_title,
375
+ )
376
+
377
+ categories = category_index(interactive_catalog)
318
378
  category_choices = [
319
379
  Choice(
320
- title=(
321
- f"{choices[0].category_name} "
322
- f"({len(choices)} extension"
323
- f"{'s' if len(choices) != 1 else ''})"
380
+ title=format_category_choice_title(
381
+ categories.get(category_slug)
382
+ or CategoryInfo(
383
+ slug=category_slug,
384
+ name=choices[0].category_name,
385
+ ),
386
+ len(choices),
324
387
  ),
325
388
  value=category_slug,
326
389
  )
@@ -444,8 +507,29 @@ def cache_dir_cmd() -> None:
444
507
  console.print(str(default_cache_dir()))
445
508
 
446
509
 
510
+ def _cache_json_print(payload: Any) -> None:
511
+ """Print machine-readable JSON to stdout (not the stderr console)."""
512
+ import json
513
+ from dataclasses import asdict, is_dataclass
514
+
515
+ def _to_jsonable(value: Any) -> Any:
516
+ if isinstance(value, Path):
517
+ return str(value)
518
+ if is_dataclass(value) and not isinstance(value, type):
519
+ return {k: _to_jsonable(v) for k, v in asdict(value).items()}
520
+ if isinstance(value, list):
521
+ return [_to_jsonable(item) for item in value]
522
+ if isinstance(value, dict):
523
+ return {k: _to_jsonable(v) for k, v in value.items()}
524
+ return value
525
+
526
+ typer.echo(json.dumps(_to_jsonable(payload), indent=2))
527
+
528
+
447
529
  @cache_app.command("list")
448
- def cache_list_cmd() -> None:
530
+ def cache_list_cmd(
531
+ json_out: bool = typer.Option(False, "--json", help="Output as JSON"),
532
+ ) -> None:
449
533
  from create_awesome_python_app.cache import (
450
534
  format_age,
451
535
  format_bytes,
@@ -454,6 +538,9 @@ def cache_list_cmd() -> None:
454
538
  )
455
539
 
456
540
  entries = list_cache_entries()
541
+ if json_out:
542
+ _cache_json_print(entries)
543
+ return
457
544
  if not entries:
458
545
  console.print("[dim]No cached templates or extensions.[/dim]")
459
546
  console.print(
@@ -488,10 +575,38 @@ def cache_list_cmd() -> None:
488
575
  def cache_clean_cmd(
489
576
  id: str | None = typer.Argument(None),
490
577
  catalog: bool = typer.Option(False, "--catalog"),
578
+ json_out: bool = typer.Option(False, "--json", help="Output as JSON"),
579
+ force: bool = typer.Option(
580
+ False, "--force", "-f", help="Skip interactive confirmation"
581
+ ),
491
582
  ) -> None:
492
583
  from create_awesome_python_app.cache import clean_cache
493
584
 
585
+ # Targeted cleans (id / --catalog) never prompt.
586
+ if not catalog and id is None and not json_out and not force:
587
+ if not sys.stdin.isatty():
588
+ console.print(
589
+ "[yellow]Non-interactive shell — use --json or --force to skip "
590
+ "the prompt, or specify an id to target a specific entry.[/yellow]"
591
+ )
592
+ return
593
+ import questionary
594
+
595
+ from create_awesome_python_app.prompt_style import CPA_PROMPT_STYLE
596
+
597
+ confirmed = questionary.confirm(
598
+ "Remove ALL cached templates and extensions?",
599
+ default=False,
600
+ style=CPA_PROMPT_STYLE,
601
+ ).ask()
602
+ if not confirmed:
603
+ console.print("[dim]Clean cancelled.[/dim]")
604
+ return
605
+
494
606
  result = clean_cache(id, catalog=catalog)
607
+ if json_out:
608
+ _cache_json_print(result)
609
+ return
495
610
  if result.not_found:
496
611
  console.print(f"[yellow]No cache entry found for id: {id}[/yellow]")
497
612
  return
@@ -503,10 +618,18 @@ def cache_clean_cmd(
503
618
 
504
619
 
505
620
  @cache_app.command("verify")
506
- def cache_verify_cmd(id: str | None = typer.Argument(None)) -> None:
621
+ def cache_verify_cmd(
622
+ id: str | None = typer.Argument(None),
623
+ json_out: bool = typer.Option(False, "--json", help="Output as JSON"),
624
+ ) -> None:
507
625
  from create_awesome_python_app.cache import verify_cache
508
626
 
509
627
  results = verify_cache(id)
628
+ if json_out:
629
+ _cache_json_print(results)
630
+ if any(not bool(entry.fsck_ok) for entry in results):
631
+ raise typer.Exit(1)
632
+ raise typer.Exit(0)
510
633
  if not results:
511
634
  console.print("[dim]No cached entries.[/dim]")
512
635
  raise typer.Exit(0)
@@ -527,10 +650,15 @@ def cache_verify_cmd(id: str | None = typer.Argument(None)) -> None:
527
650
 
528
651
 
529
652
  @cache_app.command("outdated")
530
- def cache_outdated_cmd() -> None:
653
+ def cache_outdated_cmd(
654
+ json_out: bool = typer.Option(False, "--json", help="Output as JSON"),
655
+ ) -> None:
531
656
  from create_awesome_python_app.cache import check_outdated
532
657
 
533
658
  results = check_outdated()
659
+ if json_out:
660
+ _cache_json_print(results)
661
+ return
534
662
  if not results:
535
663
  console.print("[dim]No cached entries to check.[/dim]")
536
664
  return
@@ -590,10 +718,17 @@ def cache_update_cmd(id: str | None = typer.Argument(None)) -> None:
590
718
 
591
719
 
592
720
  @cache_app.command("doctor")
593
- def cache_doctor_cmd() -> None:
721
+ def cache_doctor_cmd(
722
+ json_out: bool = typer.Option(False, "--json", help="Output as JSON"),
723
+ ) -> None:
594
724
  from create_awesome_python_app.cache import run_doctor
595
725
 
596
726
  results = run_doctor()
727
+ if json_out:
728
+ _cache_json_print(results)
729
+ if any(not row.ok for row in results):
730
+ raise typer.Exit(1)
731
+ raise typer.Exit(0)
597
732
  all_ok = True
598
733
  for row in results:
599
734
  mark = "[green]✓[/green]" if row.ok else "[red]✗[/red]"
@@ -0,0 +1,622 @@
1
+ Metadata-Version: 2.4
2
+ Name: create-awesome-python-app
3
+ Version: 0.2.7
4
+ Summary: Composable scaffolding CLI for production-ready Python apps
5
+ Project-URL: Homepage, https://github.com/Create-Python-App/create-python-app
6
+ Project-URL: Repository, https://github.com/Create-Python-App/create-python-app
7
+ Project-URL: Issues, https://github.com/Create-Python-App/create-python-app/issues
8
+ Project-URL: Changelog, https://github.com/Create-Python-App/create-python-app/blob/main/CHANGELOG.md
9
+ License-Expression: MIT
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: create-python-app-core>=0.2.6
12
+ Requires-Dist: questionary>=2.1.1
13
+ Requires-Dist: rich>=15.0.0
14
+ Requires-Dist: typer>=0.27.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ <!--lint disable double-link awesome-heading awesome-git-repo-age awesome-toc-->
18
+
19
+ <div align="center">
20
+
21
+ <img src="https://raw.githubusercontent.com/Create-Python-App/create-python-app/main/packages/create-awesome-python-app/assets/hero.svg" alt="Create Awesome Python App banner" width="100%" />
22
+
23
+ # Create Awesome Python App
24
+
25
+ **One command. Any stack.** Generate production-ready Python apps by composing templates, addons, and AI-ready conventions.
26
+
27
+ From blank folder to a working FastAPI, Django, Celery, CLI, or uv workspace project with modern tooling and team-friendly automation.
28
+
29
+ [![PyPI][pypiversion]][pypiurl]
30
+ [![Python][pythonbadge]][pythonurl]
31
+ [![Stars][starsbadge]][starsurl]
32
+ [![License: MIT][licensebadge]][licenseurl]
33
+
34
+ [![AUR][aurbadge]][aururl]
35
+ [![Homebrew][homebrewbadge]][homebrewurl]
36
+ [![Docker][dockerbadge]][dockerurl]
37
+ [![Smoke tests][smokebadge]][smokeurl]
38
+
39
+ [![Tests][testsbadge]][testsurl]
40
+ [![Lint][lintbadge]][linturl]
41
+ [![Typecheck][typecheckbadge]][typecheckurl]
42
+ [![MegaLinter][megalinterbadge]][megalinterurl]
43
+ [![Shellcheck][shellcheckbadge]][shellcheckurl]
44
+ [![Commit Activity][commitactivitybadge]][commitactivityurl]
45
+ [![Discord](https://img.shields.io/discord/1527933660764831825?label=Discord&logo=discord&logoColor=white)](https://discord.gg/dwFTsR7fK2)
46
+
47
+ **[Official Site](https://create-awesome-python-app.vercel.app)** · [Templates](https://create-awesome-python-app.vercel.app/templates) · [Extensions](https://create-awesome-python-app.vercel.app/extensions) · [Docs](https://create-awesome-python-app.vercel.app/docs) · [GitHub](https://github.com/Create-Python-App/create-python-app) · [PyPI](https://pypi.org/project/create-awesome-python-app/)
48
+
49
+ </div>
50
+
51
+ ---
52
+
53
+ ## Install
54
+
55
+ **uv (recommended):**
56
+
57
+ ```bash
58
+ uvx create-awesome-python-app@latest my-app
59
+ ```
60
+
61
+ **pipx:**
62
+
63
+ ```bash
64
+ pipx run create-awesome-python-app my-app
65
+ ```
66
+
67
+ **Homebrew (macOS / Linux):**
68
+
69
+ ```bash
70
+ brew tap Create-Python-App/tap
71
+ brew install create-awesome-python-app
72
+ ```
73
+
74
+ **AUR (Arch Linux):**
75
+
76
+ ```bash
77
+ yay -S create-awesome-python-app # or: paru -S create-awesome-python-app
78
+ ```
79
+
80
+ **Docker:**
81
+
82
+ ```bash
83
+ docker run --rm -it -v "${PWD}:/app" -w /app \
84
+ ulisesjeremias/create-awesome-python-app:latest my-app \
85
+ --template fastapi-starter
86
+ ```
87
+
88
+ Interactive by default outside CI. For automation, run headless with flags:
89
+
90
+ ```bash
91
+ uvx create-awesome-python-app my-api \
92
+ --template fastapi-starter \
93
+ --addons github-setup \
94
+ --addons fastapi-sqlalchemy \
95
+ --no-interactive
96
+ ```
97
+
98
+ | If you want... | Start here |
99
+ | ----------------------------- | ------------------------------------------------------- |
100
+ | A guided local setup | `uvx create-awesome-python-app@latest my-app` |
101
+ | A repeatable CI/platform flow | `--no-interactive` with explicit flags |
102
+ | Your company starter | `--template <github-url>` or `--template file://` |
103
+ | Private standards layered in | `--extend <url>` |
104
+
105
+ ### Shell completion
106
+
107
+ ```bash
108
+ create-awesome-python-app --install-completion # bash / zsh / fish
109
+ create-awesome-python-app --show-completion # print script only
110
+ ```
111
+
112
+ After installing, restart the shell (or `source` your profile) and tab-complete
113
+ flags such as `--template`, `--addons`, and `cache` subcommands.
114
+
115
+ ---
116
+
117
+ ## Why CPA?
118
+
119
+ | Capability | Value |
120
+ | -------------------------------- | ---------------------------------------------------------------------------------------------- |
121
+ | **Composable by design** | Start with a template, then layer only the addons your project actually needs. |
122
+ | **Production-ready defaults** | uv, Ruff, Pyright, tests, docs, and practical DX defaults out of the box. |
123
+ | **AI-ready from day one** | Supported templates generate `AGENTS.md` so coding agents understand the project context. |
124
+ | **CI and platform friendly** | Use `--no-interactive`, `--set`, `--template <url>`, and `--extend <url>` for repeatable runs. |
125
+
126
+ ---
127
+
128
+ ## Composition Model
129
+
130
+ ```text
131
+ template -> addons -> custom options -> install -> git init -> AI-ready project
132
+ ```
133
+
134
+ You can mix catalog templates and addons with your own GitHub or `file://` sources.
135
+
136
+ ---
137
+
138
+ ## What You Can Generate
139
+
140
+ ### Template Families
141
+
142
+ | Category | Example templates |
143
+ | -------- | ------------------------------------------------------ |
144
+ | Backend | `fastapi-starter`, `django-api`, `celery-worker` |
145
+ | CLI | `cli-starter` |
146
+ | Monorepo | `uv-workspace-starter` |
147
+
148
+ ### Addon Families
149
+
150
+ | Category | Examples |
151
+ | -------------- | ------------------------------------------------------------- |
152
+ | CI / tooling | `github-setup`, `development-container` |
153
+ | Containers | `fastapi-docker`, `django-docker`, `celery-docker` |
154
+ | Database | `postgres`, `fastapi-sqlalchemy`, `fastapi-redis` |
155
+ | Observability | `fastapi-sentry` |
156
+ | Security | `fastapi-auth-jwt` |
157
+
158
+ Browse the live catalog on the [official site](https://create-awesome-python-app.vercel.app/templates) or in [`cpa-templates`](https://github.com/Create-Python-App/cpa-templates).
159
+
160
+ ---
161
+
162
+ ## Popular Recipes
163
+
164
+ ### FastAPI + GitHub + SQLAlchemy
165
+
166
+ ```bash
167
+ uvx create-awesome-python-app my-api \
168
+ --template fastapi-starter \
169
+ --addons github-setup \
170
+ --addons fastapi-sqlalchemy \
171
+ --no-interactive
172
+ ```
173
+
174
+ ### Django API + Postgres + Docker
175
+
176
+ ```bash
177
+ uvx create-awesome-python-app my-django \
178
+ --template django-api \
179
+ --addons github-setup \
180
+ --addons postgres \
181
+ --addons django-docker \
182
+ --no-interactive
183
+ ```
184
+
185
+ ### Celery worker + Redis-friendly stack
186
+
187
+ ```bash
188
+ uvx create-awesome-python-app my-worker \
189
+ --template celery-worker \
190
+ --addons github-setup \
191
+ --addons celery-docker \
192
+ --no-interactive
193
+ ```
194
+
195
+ ### Typer / Click CLI starter
196
+
197
+ ```bash
198
+ uvx create-awesome-python-app my-cli \
199
+ --template cli-starter \
200
+ --addons github-setup \
201
+ --no-interactive
202
+ ```
203
+
204
+ ### uv workspace monorepo
205
+
206
+ ```bash
207
+ uvx create-awesome-python-app my-workspace \
208
+ --template uv-workspace-starter \
209
+ --addons github-setup \
210
+ --no-interactive
211
+ ```
212
+
213
+ ### Internal platform template (GitHub URL)
214
+
215
+ ```bash
216
+ uvx create-awesome-python-app my-internal-app \
217
+ --template "https://github.com/your-org/platform-starters?subdir=templates/internal-app" \
218
+ --no-interactive
219
+ ```
220
+
221
+ ### Local template development (`file://`)
222
+
223
+ ```bash
224
+ uvx create-awesome-python-app my-local-app \
225
+ --template "file:///absolute/path/to/cpa-templates?subdir=templates/fastapi-starter" \
226
+ --no-interactive
227
+ ```
228
+
229
+ > `file://` template URLs should be absolute paths (for example `file:///Users/...` or `file:///home/...`).
230
+
231
+ ### Layer a private extension
232
+
233
+ ```bash
234
+ uvx create-awesome-python-app my-app \
235
+ --template fastapi-starter \
236
+ --addons github-setup \
237
+ --extend "https://github.com/your-org/platform-starters?subdir=extensions/company-ci"
238
+ ```
239
+
240
+ ### Pass custom template values
241
+
242
+ ```bash
243
+ uvx create-awesome-python-app my-app \
244
+ --template fastapi-starter \
245
+ --set "productName=Acme Cloud" \
246
+ --set "author=Platform Team" \
247
+ --no-interactive
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Built For Modern Teams
253
+
254
+ - Python 3.12+ runtime support.
255
+ - uv-first workflows (with pipx / Homebrew / AUR / Docker install paths).
256
+ - Interactive wizard for local workflows.
257
+ - `--no-interactive` mode for CI, scripts, and platform automation.
258
+ - GitHub URL and local `file://` template inputs.
259
+ - `--extend` support for private addon layering.
260
+ - `--set key=value` overrides for deterministic custom options.
261
+
262
+ ---
263
+
264
+ ## Explore The Catalog
265
+
266
+ Browse visually at **[create-awesome-python-app.vercel.app](https://create-awesome-python-app.vercel.app)** or discover from the terminal:
267
+
268
+ ```bash
269
+ # List all available templates
270
+ uvx create-awesome-python-app --list-templates
271
+
272
+ # List addons (optionally filtered by template)
273
+ uvx create-awesome-python-app --template fastapi-starter --list-addons
274
+ ```
275
+
276
+ Full catalog:
277
+
278
+ - **Templates:** [create-awesome-python-app.vercel.app/templates](https://create-awesome-python-app.vercel.app/templates)
279
+ - **Extensions:** [create-awesome-python-app.vercel.app/extensions](https://create-awesome-python-app.vercel.app/extensions)
280
+ - **Source data:** [`cpa-templates/templates.json`](https://github.com/Create-Python-App/cpa-templates/blob/main/templates.json)
281
+
282
+ Override the catalog URL with `CPA_CATALOG_URL` (HTTPS or `file://`) for forks and local testing.
283
+
284
+ ---
285
+
286
+ ## AI-Ready With `AGENTS.md`
287
+
288
+ Supported templates generate an `AGENTS.md` file so coding assistants understand project context before editing:
289
+
290
+ | Context | Why it matters |
291
+ | ---------------------- | ----------------------------------------------------------- |
292
+ | Project purpose | Agents understand what the app is for before changing code. |
293
+ | Directory layout | Suggestions align with the real structure. |
294
+ | Scripts and validation | Agents know how to lint, test, and verify changes. |
295
+ | Team conventions | Output follows naming and workflow expectations. |
296
+
297
+ ---
298
+
299
+ ## Interactive Wizard
300
+
301
+ Run the CLI without flags and CPA guides you through:
302
+
303
+ | Step | What you choose |
304
+ | ----------------- | ----------------------------------------------------------------------- |
305
+ | Project name | Confirm or set the target directory |
306
+ | Category | Backend, CLI, Monorepo, or custom URL |
307
+ | Template | Pick from curated starters with descriptions and labels |
308
+ | Addons | Multi-select compatible extensions grouped by purpose |
309
+ | Custom options | Answer `cpa.config.json` / registry prompts when present |
310
+ | Custom extensions | Layer extra URLs for internal standards |
311
+
312
+ ---
313
+
314
+ ## Requirements
315
+
316
+ - **Python >= 3.12**
317
+ - [uv](https://docs.astral.sh/uv/) recommended (or pipx / Homebrew / AUR / Docker)
318
+ - `git` available on `PATH` (required to clone templates)
319
+
320
+ Recommended quick start:
321
+
322
+ ```bash
323
+ uv python install 3.12
324
+ uvx create-awesome-python-app@latest my-app
325
+ ```
326
+
327
+ ---
328
+
329
+ ## CLI Reference
330
+
331
+ ```text
332
+ Usage: create-awesome-python-app [OPTIONS] [project_directory]
333
+ ```
334
+
335
+ | Flag | Description |
336
+ | ---------------------------- | ----------------------------------------------------- |
337
+ | `--interactive` | Force interactive wizard (default outside CI) |
338
+ | `--no-interactive` | Disable wizard and use flags only |
339
+ | `-t, --template <slug\|url>` | Template slug from catalog or remote/local URL |
340
+ | `--addons <slug\|url>` | Addon slug or URL (repeat the flag for multiple) |
341
+ | `--extend <url>` | Extra extension URL layered on top (repeatable) |
342
+ | `--set <key=value>` | Set custom template options; quote values with spaces |
343
+ | `--no-install` | Generate files without installing dependencies |
344
+ | `-f, --force` | Allow scaffolding into a non-empty target directory |
345
+ | `--list-templates` | Print all templates |
346
+ | `--list-addons` | Print addons, optionally filtered by `--template` |
347
+ | `--offline` | Use the local cache only; do not refresh templates |
348
+ | `--no-cache` | Disable the catalog cache; force a refresh each run |
349
+ | `--cache-dir <path>` | Override the cache root (default: `~/.cache/cpa`) |
350
+ | `--refresh <mode>` | When to refresh: `always` \| `stale` \| `manual` |
351
+ | `--pin <ref>` | Pin template to a specific commit SHA, tag, or branch |
352
+ | `--strict-version` | Fail if a newer CLI version is available |
353
+ | `--keep-on-failure` | Keep partial output if scaffolding fails |
354
+ | `-v, --verbose` | Output resolved generation config as JSON |
355
+ | `-i, --info` | Print Python, uv, git, and OS diagnostics |
356
+ | `--version` | Print CLI version |
357
+ | `--help` | Show help |
358
+
359
+ ### `cache` subcommand
360
+
361
+ ```text
362
+ Usage: create-awesome-python-app cache [OPTIONS] COMMAND [ARGS]...
363
+
364
+ Commands:
365
+ dir Print the cache root directory
366
+ list List cached templates and extensions
367
+ clean Remove one or all entries
368
+ verify Run `git fsck` on one or all entries
369
+ outdated List cached entries that are behind their remote tip
370
+ update Refresh one or all cached entries from their remote
371
+ doctor Diagnose cache health (git, network, permissions)
372
+ ```
373
+
374
+ Inspect and manage the on-disk cache:
375
+
376
+ ```bash
377
+ # Where is my cache?
378
+ uvx create-awesome-python-app cache dir
379
+ # /home/<you>/.cache/cpa
380
+
381
+ # What's in it?
382
+ uvx create-awesome-python-app cache list
383
+
384
+ # Verify integrity
385
+ uvx create-awesome-python-app cache verify
386
+
387
+ # Clear everything
388
+ uvx create-awesome-python-app cache clean
389
+
390
+ # Check for outdated entries
391
+ uvx create-awesome-python-app cache outdated
392
+
393
+ # Refresh a specific entry (or all)
394
+ uvx create-awesome-python-app cache update
395
+
396
+ # Diagnose cache health
397
+ uvx create-awesome-python-app cache doctor
398
+ ```
399
+
400
+ ---
401
+
402
+ ## Cache & Updates
403
+
404
+ CPA caches both the **template catalog** (`templates.json` from
405
+ `raw.githubusercontent.com`) and the **template git repos** themselves. The
406
+ cache lives at `~/.cache/cpa` by default; override with `--cache-dir
407
+ <path>` or `CPA_CACHE_DIR`.
408
+
409
+ | Path | Contents |
410
+ | ----------------------- | ---------------------------------- |
411
+ | `~/.cache/cpa/catalog/` | Cached `templates.json` |
412
+ | `~/.cache/cpa/repos/` | Shallow clones of templates / addons |
413
+
414
+ ### Refresh modes (set with `--refresh <mode>` or `CPA_REFRESH=<mode>`)
415
+
416
+ - **`stale`** (default): pull only when the cache is older than
417
+ `CPA_REFRESH_AFTER_HOURS` (default `24`). No network on a warm cache.
418
+ - **`always`**: pull on every run.
419
+ - **`manual`**: never pull unless `--refresh` is passed.
420
+
421
+ ### Pinning templates
422
+
423
+ Pin a template to a specific commit SHA, tag, or branch:
424
+
425
+ ```bash
426
+ uvx create-awesome-python-app my-app \
427
+ --template fastapi-starter \
428
+ --pin abc123def456abc123def456abc123def456abc1 \
429
+ --no-interactive
430
+ ```
431
+
432
+ The `--pin` flag is equivalent to appending `?ref=<ref>` to the template URL.
433
+ Combine with `CPA_STRICT_REPRO=1` to enforce full 40-character SHAs.
434
+
435
+ ### CI and offline usage
436
+
437
+ ```bash
438
+ # Fully offline CI: use the local cache only, no network.
439
+ uvx create-awesome-python-app my-app \
440
+ --template fastapi-starter \
441
+ --offline \
442
+ --no-interactive
443
+
444
+ # Pin the cache to a project-local directory (useful in monorepos and CI).
445
+ CPA_CACHE_DIR="$PWD/.cpa-cache" uvx create-awesome-python-app my-app \
446
+ --template fastapi-starter \
447
+ --no-interactive
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Programmatic Usage
453
+
454
+ Need to integrate CPA into your own tooling? The core is importable:
455
+
456
+ ```python
457
+ import asyncio
458
+ from create_python_app_core import create_python_app
459
+
460
+ asyncio.run(
461
+ create_python_app(
462
+ "my-app",
463
+ {
464
+ "template": (
465
+ "https://github.com/Create-Python-App/cpa-templates"
466
+ "?subdir=templates/fastapi-starter"
467
+ ),
468
+ "addons": [],
469
+ "install": False,
470
+ },
471
+ )
472
+ )
473
+ ```
474
+
475
+ > The programmatic API is experimental and subject to change. Prefer the CLI for stable usage.
476
+
477
+ See also: [`create-python-app-core` README](https://github.com/Create-Python-App/create-python-app/blob/main/packages/create-python-app-core/README.md).
478
+
479
+ ---
480
+
481
+ ## Security
482
+
483
+ CPA downloads templates from remote sources. Prefer catalog slugs or hash-pinned
484
+ refs (`--pin` / `?ref=`), audit custom URLs before use, and report vulnerabilities
485
+ via GitHub Security Advisories on
486
+ [Create-Python-App/create-python-app](https://github.com/Create-Python-App/create-python-app).
487
+
488
+ ---
489
+
490
+ ## FAQ
491
+
492
+ <details>
493
+ <summary><strong>Why another scaffolder?</strong></summary>
494
+
495
+ Most scaffolders lock you into one stack. CPA is composable: choose a template, layer focused addons, and plug in your own GitHub/local blueprints — mirroring [Create Awesome Node App](https://github.com/Create-Node-App/create-node-app) for the Python ecosystem.
496
+
497
+ </details>
498
+
499
+ <details>
500
+ <summary><strong>Can I use my own template?</strong></summary>
501
+
502
+ Yes. Pass a GitHub URL or local `file://` URL with `--template` (supports `?subdir=` and `?ref=`).
503
+
504
+ </details>
505
+
506
+ <details>
507
+ <summary><strong>Can I use private/internal extensions?</strong></summary>
508
+
509
+ Yes. Use `--extend <url>` to layer private extensions on top of a template and addon set.
510
+
511
+ </details>
512
+
513
+ <details>
514
+ <summary><strong>Are addons order-sensitive?</strong></summary>
515
+
516
+ Yes. Addons are applied in sequence. If two addons modify the same file, later addons win.
517
+
518
+ </details>
519
+
520
+ <details>
521
+ <summary><strong>Does it support monorepos?</strong></summary>
522
+
523
+ Yes. Use `uv-workspace-starter` to bootstrap a multi-package uv workspace with shared tooling.
524
+
525
+ </details>
526
+
527
+ <details>
528
+ <summary><strong>Can I use it in CI?</strong></summary>
529
+
530
+ Yes. Pass all required flags and use `--no-interactive` for deterministic automation. Set `CI=true` to disable the wizard by default.
531
+
532
+ </details>
533
+
534
+ <details>
535
+ <summary><strong>Is Python 3.12 required?</strong></summary>
536
+
537
+ Yes. CPA targets Python 3.12+ to keep runtime behavior modern and predictable.
538
+
539
+ </details>
540
+
541
+ <details>
542
+ <summary><strong>Does CPA work with AI coding assistants?</strong></summary>
543
+
544
+ Yes. Supported templates generate `AGENTS.md`, helping assistants understand project layout, scripts, and conventions.
545
+
546
+ </details>
547
+
548
+ <details>
549
+ <summary><strong>Docker says `git executable not found`</strong></summary>
550
+
551
+ Images from `0.2.5` onward ship `git`. Pull `ulisesjeremias/create-awesome-python-app:latest` (or `0.2.5+`) and retry.
552
+
553
+ </details>
554
+
555
+ ---
556
+
557
+ ## Roadmap
558
+
559
+ - More framework templates and vertical starters.
560
+ - Additional testing and observability packs.
561
+ - Diff-based upgrade paths for pinned templates.
562
+ - Richer template analytics and usage insights.
563
+
564
+ Track progress in [Issues](https://github.com/Create-Python-App/create-python-app/issues) and [Discussions](https://github.com/Create-Python-App/create-python-app/discussions).
565
+
566
+ ---
567
+
568
+ ## Contributing
569
+
570
+ Templates, addons, bug fixes, docs, recipes, and ideas are all welcome.
571
+
572
+ - **Main repo:** [github.com/Create-Python-App/create-python-app](https://github.com/Create-Python-App/create-python-app)
573
+ - **Template and extension data:** [github.com/Create-Python-App/cpa-templates](https://github.com/Create-Python-App/cpa-templates)
574
+ - **Contributing guide:** [CONTRIBUTING.md](https://github.com/Create-Python-App/create-python-app/blob/main/CONTRIBUTING.md)
575
+ - **Troubleshooting:** [docs/TROUBLESHOOTING.md](https://github.com/Create-Python-App/create-python-app/blob/main/docs/TROUBLESHOOTING.md)
576
+
577
+ ---
578
+
579
+ ## License
580
+
581
+ MIT © [Create Python App Contributors](https://github.com/Create-Python-App/create-python-app/graphs/contributors)
582
+
583
+ ---
584
+
585
+ <div align="center">
586
+
587
+ **[create-awesome-python-app.vercel.app](https://create-awesome-python-app.vercel.app)**
588
+
589
+ _Built for developers who value speed, composability, craft, and AI-ready workflows._
590
+
591
+ </div>
592
+
593
+ <!-- Reference links -->
594
+
595
+ [testsbadge]: https://github.com/Create-Python-App/create-python-app/actions/workflows/test.yml/badge.svg
596
+ [lintbadge]: https://github.com/Create-Python-App/create-python-app/actions/workflows/lint.yml/badge.svg
597
+ [typecheckbadge]: https://github.com/Create-Python-App/create-python-app/actions/workflows/type-check.yml/badge.svg
598
+ [shellcheckbadge]: https://github.com/Create-Python-App/create-python-app/actions/workflows/shellcheck.yml/badge.svg
599
+ [megalinterbadge]: https://github.com/Create-Python-App/create-python-app/actions/workflows/mega-linter.yml/badge.svg
600
+ [pypiversion]: https://img.shields.io/pypi/v/create-awesome-python-app.svg?style=flat-square&color=3775A9
601
+ [pythonbadge]: https://img.shields.io/pypi/pyversions/create-awesome-python-app.svg?style=flat-square
602
+ [starsbadge]: https://img.shields.io/github/stars/Create-Python-App/create-python-app?style=flat-square&color=yellow
603
+ [licensebadge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square
604
+ [testsurl]: https://github.com/Create-Python-App/create-python-app/actions/workflows/test.yml
605
+ [linturl]: https://github.com/Create-Python-App/create-python-app/actions/workflows/lint.yml
606
+ [typecheckurl]: https://github.com/Create-Python-App/create-python-app/actions/workflows/type-check.yml
607
+ [shellcheckurl]: https://github.com/Create-Python-App/create-python-app/actions/workflows/shellcheck.yml
608
+ [megalinterurl]: https://github.com/Create-Python-App/create-python-app/actions/workflows/mega-linter.yml
609
+ [pypiurl]: https://pypi.org/project/create-awesome-python-app/
610
+ [pythonurl]: https://pypi.org/project/create-awesome-python-app/
611
+ [licenseurl]: https://github.com/Create-Python-App/create-python-app/blob/main/LICENSE
612
+ [starsurl]: https://github.com/Create-Python-App/create-python-app/stargazers
613
+ [commitactivitybadge]: https://img.shields.io/github/commit-activity/m/Create-Python-App/create-python-app?style=flat-square&logo=github&label=commits
614
+ [commitactivityurl]: https://github.com/Create-Python-App/create-python-app/pulse
615
+ [aururl]: https://aur.archlinux.org/packages/create-awesome-python-app
616
+ [aurbadge]: https://img.shields.io/aur/version/create-awesome-python-app?style=flat-square&label=AUR&logo=archlinux
617
+ [homebrewurl]: https://github.com/Create-Python-App/homebrew-tap
618
+ [homebrewbadge]: https://img.shields.io/badge/homebrew-Create--Python--App%2Ftap-orange?style=flat-square&logo=homebrew
619
+ [dockerurl]: https://hub.docker.com/r/ulisesjeremias/create-awesome-python-app
620
+ [dockerbadge]: https://img.shields.io/docker/v/ulisesjeremias/create-awesome-python-app?style=flat-square&label=Docker&logo=docker&color=2496ED
621
+ [smokebadge]: https://github.com/Create-Python-App/create-python-app/actions/workflows/smoke-distribution.yml/badge.svg?event=schedule
622
+ [smokeurl]: https://github.com/Create-Python-App/create-python-app/actions/workflows/smoke-distribution.yml
@@ -0,0 +1,9 @@
1
+ create_awesome_python_app/__init__.py,sha256=ePecLVnfNi_mPoF_Pogp9juH5iycJGewOy1U0mxuMvM,60
2
+ create_awesome_python_app/cache.py,sha256=eHKUlunGexZKYJo0HDNoBoOVq0DjLyRYgEe_wUh4Fk0,11812
3
+ create_awesome_python_app/catalog.py,sha256=ZB-ieyEn_TBqVBbCbFkK9h3VJoYtifvvOxs_BM2tDEw,22754
4
+ create_awesome_python_app/cli.py,sha256=JGdXNAXAXqQ-grQbZOf14Vzii3Qh3eZHt5zRJ-tMIBk,26156
5
+ create_awesome_python_app/prompt_style.py,sha256=pERX1qPQSLZXeJvPVFC7z85dvte73ZV4iuENPQduheE,3463
6
+ create_awesome_python_app-0.2.7.dist-info/METADATA,sha256=gpy3o2dcc-Qw_l1uHnipFJ-IkOUe9GT50bKeGtEqkUc,23271
7
+ create_awesome_python_app-0.2.7.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ create_awesome_python_app-0.2.7.dist-info/entry_points.txt,sha256=rszFbUjY-lvMDZ96zX1lALJwgD1mI3CkUuNyF2qqjYo,81
9
+ create_awesome_python_app-0.2.7.dist-info/RECORD,,
@@ -1,42 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: create-awesome-python-app
3
- Version: 0.2.5
4
- Summary: Composable scaffolding CLI for production-ready Python apps
5
- Project-URL: Homepage, https://github.com/Create-Python-App/create-python-app
6
- Project-URL: Repository, https://github.com/Create-Python-App/create-python-app
7
- Project-URL: Issues, https://github.com/Create-Python-App/create-python-app/issues
8
- Project-URL: Changelog, https://github.com/Create-Python-App/create-python-app/blob/main/CHANGELOG.md
9
- License-Expression: MIT
10
- Requires-Python: >=3.12
11
- Requires-Dist: create-python-app-core>=0.2.5
12
- Requires-Dist: questionary>=2.1.1
13
- Requires-Dist: rich>=15.0.0
14
- Requires-Dist: typer>=0.27.0
15
- Description-Content-Type: text/markdown
16
-
17
- # create-awesome-python-app
18
-
19
- ![banner](./assets/hero.svg)
20
-
21
- CLI package. Framework: **Typer** (chosen over Click for richer typing/help).
22
-
23
- ```bash
24
- uv run create-awesome-python-app --help
25
- uvx create-awesome-python-app@latest my-app # after PyPI publish
26
- pipx run create-awesome-python-app my-app
27
- ```
28
-
29
- ## Shell completion
30
-
31
- Typer installs completion scripts for bash, zsh, and fish:
32
-
33
- ```bash
34
- # Interactive install into your shell profile
35
- create-awesome-python-app --install-completion
36
-
37
- # Or print the script and source it manually
38
- create-awesome-python-app --show-completion
39
- ```
40
-
41
- After installing, restart the shell (or `source` your profile) and tab-complete
42
- flags such as `--template`, `--addons`, and `cache` subcommands.
@@ -1,9 +0,0 @@
1
- create_awesome_python_app/__init__.py,sha256=SiOaZbkhOxVxSBSkzI-OorI4YyurD6WMGgl3o9avmw4,60
2
- create_awesome_python_app/cache.py,sha256=eHKUlunGexZKYJo0HDNoBoOVq0DjLyRYgEe_wUh4Fk0,11812
3
- create_awesome_python_app/catalog.py,sha256=w5GS_kQ6MM37AilutXbYZ9Hk5JGa_12crzI0LYzKfRg,16821
4
- create_awesome_python_app/cli.py,sha256=6og82WI-RZxTqHETqrieA8relgppwCqFntOEmOFrIPk,21195
5
- create_awesome_python_app/prompt_style.py,sha256=pERX1qPQSLZXeJvPVFC7z85dvte73ZV4iuENPQduheE,3463
6
- create_awesome_python_app-0.2.5.dist-info/METADATA,sha256=K2hrmcatR8whohIFkG79TxS4CSEsRqupdxZeUKNYNtg,1428
7
- create_awesome_python_app-0.2.5.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
- create_awesome_python_app-0.2.5.dist-info/entry_points.txt,sha256=rszFbUjY-lvMDZ96zX1lALJwgD1mI3CkUuNyF2qqjYo,81
9
- create_awesome_python_app-0.2.5.dist-info/RECORD,,