|
1 | 1 | # create-python-app-core |
2 | 2 |
|
3 | 3 | Programmatic scaffolding engine behind Create Awesome Python App. |
| 4 | +Import the scaffolding pipeline -- composable, headless, and CI-ready. |
| 5 | + |
| 6 | +Requires **Python >= 3.12**. |
| 7 | + |
| 8 | +> This is the _engine_ package. For the interactive CLI, use |
| 9 | +> [`create-awesome-python-app`](https://pypi.org/project/create-awesome-python-app/) |
| 10 | +> instead. |
| 11 | +
|
| 12 | +--- |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +```bash |
| 17 | +pip install create-python-app-core |
| 18 | +``` |
| 19 | + |
| 20 | +Or with uv: |
| 21 | + |
| 22 | +```bash |
| 23 | +uv add create-python-app-core |
| 24 | +``` |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## Usage |
| 29 | + |
| 30 | +### Scaffold a project programmatically |
| 31 | + |
| 32 | +```python |
| 33 | +import asyncio |
| 34 | +from create_python_app_core import create_python_app |
| 35 | + |
| 36 | + |
| 37 | +async def main() -> None: |
| 38 | + await create_python_app( |
| 39 | + "my-app", |
| 40 | + { |
| 41 | + "projectName": "my-app", |
| 42 | + "template": "file:///path/to/template", |
| 43 | + "install": True, |
| 44 | + }, |
| 45 | + transform_options=lambda opts: asyncio.sleep(0, result=opts), |
| 46 | + ) |
| 47 | + |
| 48 | + |
| 49 | +asyncio.run(main()) |
| 50 | +``` |
| 51 | + |
| 52 | +### Scaffold with the installer API |
4 | 53 |
|
5 | 54 | ```python |
6 | | -from create_python_app_core import ( |
7 | | - create_python_app, |
8 | | - check_python_version, |
9 | | - check_for_latest_version, |
10 | | - print_env_info, |
11 | | - CPA_USER_AGENT, |
| 55 | +from create_python_app_core import scaffold_project |
| 56 | + |
| 57 | +scaffold_project( |
| 58 | + "my-app", |
| 59 | + template="file:///path/to/template", |
| 60 | + addons=[], |
| 61 | + extend=[], |
| 62 | + install=True, |
| 63 | + force=False, |
| 64 | + offline=False, |
12 | 65 | ) |
13 | 66 | ``` |
14 | 67 |
|
15 | | -Requires **Python >= 3.12**. |
| 68 | +### Resolve a template source |
| 69 | + |
| 70 | +```python |
| 71 | +from create_python_app_core import resolve_source, get_template_dir_path |
| 72 | + |
| 73 | +source = resolve_source( |
| 74 | + "https://github.com/Create-Python-App/cpa-templates?ref=main&subdir=fastapi" |
| 75 | +) |
| 76 | +print(source.kind) # github |
| 77 | +print(source.ref) # main |
| 78 | +print(source.subdir) # fastapi |
| 79 | +``` |
| 80 | + |
| 81 | +### Download a repository into the cache |
| 82 | + |
| 83 | +```python |
| 84 | +from create_python_app_core import resolve_source, download_repository |
| 85 | + |
| 86 | +source = resolve_source("https://github.com/org/my-template") |
| 87 | +root = download_repository(source, refresh="stale", offline=False) |
| 88 | +template_dir = get_template_dir_path(source, root) |
| 89 | +``` |
| 90 | + |
| 91 | +### Load template configuration |
| 92 | + |
| 93 | +```python |
| 94 | +from pathlib import Path |
| 95 | + |
| 96 | +from create_python_app_core import load_cpa_config |
| 97 | + |
| 98 | +cfg = load_cpa_config(Path("/path/to/template/cpa.config.json")) |
| 99 | +for opt in cfg.custom_options: |
| 100 | + print(opt.key, opt.default) |
| 101 | +``` |
| 102 | + |
| 103 | +### Check environment info |
| 104 | + |
| 105 | +```python |
| 106 | +from create_python_app_core import print_env_info |
| 107 | + |
| 108 | +print_env_info() |
| 109 | +# Prints Python, platform, uv, and git info. Then exits. |
| 110 | +``` |
| 111 | + |
| 112 | +### Validate the Python version |
| 113 | + |
| 114 | +```python |
| 115 | +from create_python_app_core import check_python_version |
| 116 | + |
| 117 | +check_python_version(">=3.12", "my-tool") |
| 118 | +# Exits with code 1 if the interpreter does not match. |
| 119 | +``` |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +## API Reference |
| 124 | + |
| 125 | +All public exports from `create_python_app_core`: |
| 126 | + |
| 127 | +### Functions |
| 128 | + |
| 129 | +| Signature | Description | |
| 130 | +| --------- | ----------- | |
| 131 | +| `create_python_app(project_directory, options, transform_options=None)` | Async orchestrator. Applies `transform_options`, then delegates to `scaffold_project`. | |
| 132 | +| `scaffold_project(project_directory, *, template, addons=None, extend=None, force=False, install=True, offline=False, refresh=None, keep_on_failure=False, cache_dir=None, options=None)` | Main scaffolding pipeline. Resolves sources, downloads layers, merges files, runs `uv sync`, and initializes git. | |
| 133 | +| `resolve_source(spec, *, cache_dir=None)` | Parses a template/extension specifier (GitHub URL, `file://`, slug) into a `ResolvedSource`. | |
| 134 | +| `get_template_dir_path(source, root)` | Returns the `template/` subdirectory when present, otherwise the resolved root. | |
| 135 | +| `default_cache_dir()` | Returns `CPA_CACHE_DIR` or `~/.cache/cpa`. | |
| 136 | +| `download_repository(source, *, offline=False, refresh=None, cache_root=None)` | Clones or refreshes a Git repo into the cache. Returns the entry directory. | |
| 137 | +| `read_cache_meta(entry)` | Reads `.cpa-cache.json` metadata from a cache entry. | |
| 138 | +| `write_cache_meta(entry, meta)` | Writes `.cpa-cache.json` metadata for a cache entry. | |
| 139 | +| `load_cpa_config(path)` | Loads optional `cpa.config.json` (custom CLI prompts). Returns empty `CpaConfig` when missing. | |
| 140 | +| `assert_directory_is_empty(path, *, force=False)` | Raises `NonEmptyTargetDirectoryError` when the target exists and is non-empty. | |
| 141 | +| `load_layer(source, root, dest, *, overwrite=True, context=None)` | Copies one template/extension layer into `dest`. | |
| 142 | +| `merge_layers(layers, dest, *, context=None)` | Applies layers in order (template, addons, extend). Later layers win. | |
| 143 | +| `merge_pyproject_text(base_text, overlay_text)` | Deep-merges two `pyproject.toml` documents as TOML. | |
| 144 | +| `check_python_version(required, package_name)` | Compares `sys.version_info` against a PEP 440 specifier. Exits with code 1 if too old. | |
| 145 | +| `check_for_latest_version(package_name)` | Async. Fetches the latest version from PyPI. Returns `None` on failure. | |
| 146 | +| `print_env_info()` | Prints OS, Python, uv, and git info to stdout, then exits. | |
| 147 | + |
| 148 | +### Constants |
| 149 | + |
| 150 | +| Name | Description | |
| 151 | +| ---- | ----------- | |
| 152 | +| `__version__` | Installed package version string. | |
| 153 | +| `CPA_USER_AGENT` | HTTP User-Agent sent to PyPI (`create-python-app-core/<version>`). | |
| 154 | +| `NON_EMPTY_DIR_ERROR_CODE` | Stable code for `NonEmptyTargetDirectoryError` (`CPA_NON_EMPTY_TARGET_DIR`). | |
| 155 | + |
| 156 | +### Types |
| 157 | + |
| 158 | +| Type | Shape | |
| 159 | +| ---- | ----- | |
| 160 | +| `ResolvedSource` | `kind` (github \| file \| slug \| git), `url`, `ref`, `subdir`, `local_path` | |
| 161 | +| `CacheMeta` | `url`, `ref`, `fetched_at`, `commit` | |
| 162 | +| `CpaConfig` | `name`, `custom_options`, `raw` | |
| 163 | +| `CpaCustomOption` | `key`, `type`, `message`, `default` | |
| 164 | +| `CpaError` | Base exception with `.code` attribute | |
| 165 | +| `ConfigParseError` | Invalid `cpa.config.json` (code: `CPA_CONFIG_PARSE`) | |
| 166 | +| `ManifestLoadError` | Missing template directory (code: `CPA_MANIFEST_LOAD`) | |
| 167 | +| `PackageManagerFallbackError` | Package manager fallback failure (code: `CPA_PM_FALLBACK`) | |
| 168 | +| `ScaffoldAbortedError` | Scaffold failed mid-run (code: `CPA_ABORTED`) | |
| 169 | +| `NonEmptyTargetDirectoryError` | Target directory not empty (code: `CPA_NON_EMPTY_TARGET_DIR`) | |
| 170 | + |
| 171 | +### `create_python_app` options dict |
| 172 | + |
| 173 | +| Key | Type | Default | Description | |
| 174 | +| --- | ---- | ------- | ----------- | |
| 175 | +| `template` | `str` | `""` | Primary template specifier (URL, `file://`, or slug). | |
| 176 | +| `addons` | `list[str]` | `[]` | Additional template layers applied after the base template. | |
| 177 | +| `extend` | `list[str]` | `[]` | Extension layers applied last (later wins on conflicts). | |
| 178 | +| `force` | `bool` | `False` | Allow scaffolding into a non-empty directory. | |
| 179 | +| `install` | `bool` | `True` | Run `uv sync` when `pyproject.toml` is present. | |
| 180 | +| `offline` | `bool` | `False` | Use cached repos only; raise on cache miss. | |
| 181 | +| `refresh` | `str` | env / `"stale"` | Cache refresh mode: `always`, `stale`, or `manual`. | |
| 182 | +| `keep_on_failure` | `bool` | `False` | Keep the partial project directory when scaffolding fails. | |
| 183 | +| `cache_dir` | `str \| Path` | `None` | Override the default cache root. | |
| 184 | +| `set` | `dict` | `{}` | Jinja context overrides (merged into `projectName` and custom option defaults). | |
| 185 | + |
| 186 | +--- |
| 187 | + |
| 188 | +## Environment Variables |
| 189 | + |
| 190 | +All `CPA_*` variables read by the core engine: |
| 191 | + |
| 192 | +| Variable | Default | Description | |
| 193 | +| -------- | ------- | ----------- | |
| 194 | +| `CPA_CACHE_DIR` | `~/.cache/cpa` | Root directory for cloned repository cache entries. | |
| 195 | +| `CPA_REFRESH` | `stale` | Default cache refresh mode: `always`, `stale`, or `manual`. | |
| 196 | +| `CPA_REFRESH_AFTER_HOURS` | `24` | Hours before a `stale` cache entry is refreshed. | |
| 197 | +| `CPA_SKIP_GIT` | unset | Set to `1` to skip `git init` and block all git subprocess calls. | |
| 198 | +| `CPA_STRICT_REPRO` | unset | Set to `1` to require a full 40-character commit SHA in `?ref=` query params. | |
| 199 | + |
| 200 | +--- |
| 201 | + |
| 202 | +## Error Codes |
| 203 | + |
| 204 | +Stable machine-readable codes on `CpaError.code`: |
| 205 | + |
| 206 | +| Code | Exception class | When raised | |
| 207 | +| ---- | --------------- | ----------- | |
| 208 | +| `CPA_ERROR` | `CpaError` | Generic base error (default). | |
| 209 | +| `CPA_CONFIG_PARSE` | `ConfigParseError` | Malformed or invalid `cpa.config.json`. | |
| 210 | +| `CPA_MANIFEST_LOAD` | `ManifestLoadError` | Template directory not found on disk. | |
| 211 | +| `CPA_PM_FALLBACK` | `PackageManagerFallbackError` | Package manager fallback failure. | |
| 212 | +| `CPA_ABORTED` | `ScaffoldAbortedError` | Scaffold failed (template render, unexpected error, etc.). | |
| 213 | +| `CPA_NON_EMPTY_TARGET_DIR` | `NonEmptyTargetDirectoryError` | Target directory exists and is not empty. | |
| 214 | +| `CPA_GIT` | `CpaError` | Git subprocess failed or `git` not found. | |
| 215 | +| `CPA_SKIP_GIT` | `CpaError` | Git operation attempted while `CPA_SKIP_GIT=1`. | |
| 216 | +| `CPA_FILE` | `CpaError` | `file://` source path does not exist. | |
| 217 | +| `CPA_OFFLINE` | `CpaError` | Offline mode with no cached copy of the repository. | |
| 218 | +| `CPA_STRICT_REPRO` | `CpaError` | `?ref=` is not a full SHA while `CPA_STRICT_REPRO=1`. | |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## How It Works |
| 223 | + |
| 224 | +```text |
| 225 | +create_python_app() |
| 226 | + |-- transform_options() (optional) |
| 227 | + |-- scaffold_project() |
| 228 | + |-- assert_directory_is_empty() |
| 229 | + |-- resolve_source() for each template / addon / extend |
| 230 | + |-- download_repository() (git clone or file://) |
| 231 | + |-- load_cpa_config() from cpa.config.json |
| 232 | + |-- build_scaffold_context() (projectName + custom options + --set) |
| 233 | + |-- merge_layers() (Jinja .template, .append, pyproject merge) |
| 234 | + |-- uv sync (when install=True and pyproject.toml exists) |
| 235 | + |-- git init (unless CPA_SKIP_GIT=1) |
| 236 | + +-- cleanup partial directory on failure (unless keep_on_failure) |
| 237 | +``` |
| 238 | + |
| 239 | +--- |
| 240 | + |
| 241 | +## Architecture |
| 242 | + |
| 243 | +The package is organized into these modules: |
| 244 | + |
| 245 | +| Module | Responsibility | |
| 246 | +| ------ | -------------- | |
| 247 | +| `__init__.py` | Barrel export and public API surface | |
| 248 | +| `api.py` | `create_python_app`, version checks, env info, PyPI lookup | |
| 249 | +| `installer.py` | `scaffold_project` orchestration, `uv sync`, git init | |
| 250 | +| `loaders.py` | File discovery, `.template` / `.append` processing, layer merge | |
| 251 | +| `pyproject_merge.py` | Deep-merge `pyproject.toml` across template layers | |
| 252 | +| `paths.py` | URL resolution (GitHub, `file://`, slugs, `?ref=`, `?subdir=`) | |
| 253 | +| `git_cache.py` | Clone/pull with cache, refresh modes, offline support | |
| 254 | +| `config.py` | Reads optional `cpa.config.json` for custom CLI prompts | |
| 255 | +| `errors.py` | Typed `CpaError` hierarchy with stable codes | |
| 256 | + |
| 257 | +--- |
| 258 | + |
| 259 | +## Related |
| 260 | + |
| 261 | +- [`create-awesome-python-app`](https://pypi.org/project/create-awesome-python-app/) -- Interactive CLI built on this core |
| 262 | +- [Create Python App](https://github.com/Create-Python-App/create-python-app) -- Monorepo |
| 263 | +- [Templates catalog](https://github.com/Create-Python-App/cpa-templates) |
| 264 | + |
| 265 | +--- |
| 266 | + |
| 267 | +## License |
| 268 | + |
| 269 | +MIT (c) [Create Python App Contributors](https://github.com/Create-Python-App/create-python-app/graphs/contributors) |
0 commit comments