From 63f98c38641a43031cf894bee5db750802482728 Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Thu, 16 Jul 2026 03:04:34 -0300 Subject: [PATCH] feat(core): cpa.config.json loader and schema docs Closes #28 Co-authored-by: Cursor --- docs/cpa-config-schema.md | 19 ++++++ .../src/create_python_app_core/__init__.py | 4 ++ .../src/create_python_app_core/config.py | 63 +++++++++++++++++++ .../tests/test_config.py | 35 +++++++++++ 4 files changed, 121 insertions(+) create mode 100644 docs/cpa-config-schema.md create mode 100644 packages/create-python-app-core/src/create_python_app_core/config.py create mode 100644 packages/create-python-app-core/tests/test_config.py diff --git a/docs/cpa-config-schema.md b/docs/cpa-config-schema.md new file mode 100644 index 0000000..b78603b --- /dev/null +++ b/docs/cpa-config-schema.md @@ -0,0 +1,19 @@ +# `cpa.config.json` schema + +Mirrors `cna.config.json` from Create-Node-App with Python-oriented fields. + +```json +{ + "name": "fastapi-starter", + "customOptions": [ + { + "key": "projectName", + "type": "string", + "message": "Project name", + "default": "my-app" + } + ] +} +``` + +Parse errors raise `CPA_CONFIG_PARSE`. diff --git a/packages/create-python-app-core/src/create_python_app_core/__init__.py b/packages/create-python-app-core/src/create_python_app_core/__init__.py index 5e21b16..1ee4b65 100644 --- a/packages/create-python-app-core/src/create_python_app_core/__init__.py +++ b/packages/create-python-app-core/src/create_python_app_core/__init__.py @@ -50,6 +50,10 @@ "merge_layers", "load_layer", "get_template_dir_path", + "CpaConfig", + "CpaCustomOption", + "load_cpa_config", + "assert_directory_is_empty", "CPA_USER_AGENT", "check_for_latest_version", "check_python_version", diff --git a/packages/create-python-app-core/src/create_python_app_core/config.py b/packages/create-python-app-core/src/create_python_app_core/config.py new file mode 100644 index 0000000..c8ac898 --- /dev/null +++ b/packages/create-python-app-core/src/create_python_app_core/config.py @@ -0,0 +1,63 @@ +"""cpa.config.json schema and loaders.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from create_python_app_core.errors import ( + ConfigParseError, + NonEmptyTargetDirectoryError, +) + + +@dataclass +class CpaCustomOption: + key: str + type: str = "string" + message: str = "" + default: Any = None + + +@dataclass +class CpaConfig: + name: str | None = None + custom_options: list[CpaCustomOption] = field(default_factory=list) + raw: dict[str, Any] = field(default_factory=dict) + + +def load_cpa_config(path: Path) -> CpaConfig: + if not path.is_file(): + return CpaConfig() + try: + data = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ConfigParseError(f"Invalid cpa.config.json: {exc}") from exc + if not isinstance(data, dict): + raise ConfigParseError("cpa.config.json must be a JSON object") + options: list[CpaCustomOption] = [] + for item in data.get("customOptions") or data.get("custom_options") or []: + if not isinstance(item, dict) or "key" not in item: + raise ConfigParseError("custom option missing key") + options.append( + CpaCustomOption( + key=str(item["key"]), + type=str(item.get("type", "string")), + message=str(item.get("message", "")), + default=item.get("default"), + ) + ) + return CpaConfig( + name=data.get("name"), + custom_options=options, + raw=data, + ) + + +def assert_directory_is_empty(path: Path, *, force: bool = False) -> None: + if force: + return + if path.exists() and any(path.iterdir()): + raise NonEmptyTargetDirectoryError(f"Target directory is not empty: {path}") diff --git a/packages/create-python-app-core/tests/test_config.py b/packages/create-python-app-core/tests/test_config.py new file mode 100644 index 0000000..a5567e6 --- /dev/null +++ b/packages/create-python-app-core/tests/test_config.py @@ -0,0 +1,35 @@ +import json +from pathlib import Path + +import pytest +from create_python_app_core.config import assert_directory_is_empty, load_cpa_config +from create_python_app_core.errors import ConfigParseError, NonEmptyTargetDirectoryError + + +def test_load_config(tmp_path: Path) -> None: + path = tmp_path / "cpa.config.json" + path.write_text( + json.dumps( + { + "name": "demo", + "customOptions": [{"key": "projectName", "default": "x"}], + } + ) + ) + cfg = load_cpa_config(path) + assert cfg.name == "demo" + assert cfg.custom_options[0].key == "projectName" + + +def test_bad_json(tmp_path: Path) -> None: + path = tmp_path / "cpa.config.json" + path.write_text("{") + with pytest.raises(ConfigParseError): + load_cpa_config(path) + + +def test_non_empty(tmp_path: Path) -> None: + (tmp_path / "f").write_text("x") + with pytest.raises(NonEmptyTargetDirectoryError): + assert_directory_is_empty(tmp_path) + assert_directory_is_empty(tmp_path, force=True)