simple-module-cli 0.0.2__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.
- simple_module_cli-0.0.2/.gitignore +59 -0
- simple_module_cli-0.0.2/LICENSE +21 -0
- simple_module_cli-0.0.2/PKG-INFO +63 -0
- simple_module_cli-0.0.2/README.md +38 -0
- simple_module_cli-0.0.2/pyproject.toml +44 -0
- simple_module_cli-0.0.2/simple_module_cli/__init__.py +0 -0
- simple_module_cli-0.0.2/simple_module_cli/_env.py +12 -0
- simple_module_cli-0.0.2/simple_module_cli/app_project.py +123 -0
- simple_module_cli-0.0.2/simple_module_cli/case.py +30 -0
- simple_module_cli-0.0.2/simple_module_cli/catalog.py +107 -0
- simple_module_cli-0.0.2/simple_module_cli/cli.py +104 -0
- simple_module_cli-0.0.2/simple_module_cli/new.py +124 -0
- simple_module_cli-0.0.2/simple_module_cli/plugins.py +56 -0
- simple_module_cli-0.0.2/simple_module_cli/recipes.py +93 -0
- simple_module_cli-0.0.2/simple_module_cli/scaffolding.py +124 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/.env.example +20 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/.gitignore +19 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/Makefile +24 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/README.md.tpl +59 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +12 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +60 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +17 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +37 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/alembic.ini +36 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/app.tsx +16 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/main.tsx +2 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/package.json.tpl +23 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/pages/Error.tsx +13 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/pages.ts +47 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/styles.css +7 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/tsconfig.json +16 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/vite.config.ts +39 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/main.py +27 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/migrations/env.py +80 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/migrations/script.py.mako +26 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/migrations/versions/.gitkeep +1 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/pyproject.toml.tpl +17 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/host/templates/index.html +12 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/.github/workflows/ci.yml +32 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +52 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/.gitignore +14 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/README.md.tpl +82 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +46 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +22 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/package.json.tpl +16 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/pyproject.toml.tpl +39 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/tests/__init__.py +0 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/tests/test_module.py.tpl +27 -0
- simple_module_cli-0.0.2/simple_module_cli/templates/module/tsconfig.json.tpl +11 -0
- simple_module_cli-0.0.2/simple_module_cli/wizard.py +48 -0
- simple_module_cli-0.0.2/tests/test_cli_catalog.py +86 -0
- simple_module_cli-0.0.2/tests/test_cli_new.py +210 -0
- simple_module_cli-0.0.2/tests/test_cli_recipes.py +87 -0
- simple_module_cli-0.0.2/tests/test_cli_wizard.py +78 -0
- simple_module_cli-0.0.2/tests/test_no_framework_deps.py +26 -0
- simple_module_cli-0.0.2/tests/test_plugin_discovery.py +97 -0
- simple_module_cli-0.0.2/tests/test_scaffolding_host.py +171 -0
- simple_module_cli-0.0.2/tests/test_scaffolding_module.py +179 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
*.egg
|
|
9
|
+
|
|
10
|
+
# UV
|
|
11
|
+
uv.lock
|
|
12
|
+
|
|
13
|
+
# Node
|
|
14
|
+
node_modules/
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.idea/
|
|
18
|
+
.vscode/
|
|
19
|
+
*.swp
|
|
20
|
+
*.swo
|
|
21
|
+
|
|
22
|
+
# Environment
|
|
23
|
+
.env
|
|
24
|
+
|
|
25
|
+
# Database
|
|
26
|
+
*.db
|
|
27
|
+
*.sqlite3
|
|
28
|
+
|
|
29
|
+
# Module-managed runtime state (e.g. uploaded dataset files,
|
|
30
|
+
# default storage_dir for SM_DATASETS_STORAGE_DIR).
|
|
31
|
+
var/
|
|
32
|
+
|
|
33
|
+
# file_storage filesystem backend default root (override via SM_FILE_STORAGE_FS_ROOT_PATH).
|
|
34
|
+
uploads/
|
|
35
|
+
|
|
36
|
+
# Vite
|
|
37
|
+
host/static/dist/
|
|
38
|
+
|
|
39
|
+
# Auto-generated frontend module manifest (regenerated by the host at boot
|
|
40
|
+
# or via `make gen-pages`).
|
|
41
|
+
host/client_app/modules.manifest.json
|
|
42
|
+
host/client_app/modules.generated.ts
|
|
43
|
+
host/client_app/modules.generated.css
|
|
44
|
+
|
|
45
|
+
# Worktrees
|
|
46
|
+
.worktrees/
|
|
47
|
+
|
|
48
|
+
# Performance profiles
|
|
49
|
+
.memray/
|
|
50
|
+
.benchmarks/
|
|
51
|
+
|
|
52
|
+
# OS
|
|
53
|
+
.DS_Store
|
|
54
|
+
Thumbs.db
|
|
55
|
+
|
|
56
|
+
.playwright-cli/*
|
|
57
|
+
.playwright-mcp/*
|
|
58
|
+
host/client_app/.playwright-cli/*
|
|
59
|
+
.superpowers/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anto Subash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_module_cli
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Standalone scaffolder for the SimpleModule framework — `sm new`, `sm create-module`, plugin host.
|
|
5
|
+
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
|
+
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
7
|
+
Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
|
|
8
|
+
Author-email: Anto Subash <antosubash@live.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,fastapi,modular-monolith,scaffolding,simple-module
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: tomlkit>=0.13
|
|
23
|
+
Requires-Dist: typer>=0.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# simple_module_cli
|
|
27
|
+
|
|
28
|
+
Standalone scaffolder for the [SimpleModule framework](https://github.com/antosubash/simple_module_python).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install simple_module_cli
|
|
34
|
+
# or, to keep the CLI in its own venv:
|
|
35
|
+
pipx install simple_module_cli
|
|
36
|
+
# or, to run it without installing:
|
|
37
|
+
uvx --from simple_module_cli sm new my-app
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The package depends only on `typer` and `tomlkit` — installing it does **not** pull in FastAPI, SQLModel, or any other framework runtime.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
sm new my-app # interactive wizard
|
|
46
|
+
sm new my-app --yes --preset full # all built-in modules + background jobs
|
|
47
|
+
sm create-module my_feature # scaffold a publishable module package
|
|
48
|
+
sm create-host bare-host # scaffold a bare host (no opinionated wiring)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Built-in commands: `sm new`, `sm create-host`, `sm create-module`.
|
|
52
|
+
|
|
53
|
+
When other framework packages are installed, they contribute additional subcommands via the `simple_module_cli.cli_plugins` entry-point group:
|
|
54
|
+
|
|
55
|
+
| Package | Commands |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `simple_module_hosting` | `sm host gen-pages`, `sm host sync-js-deps` |
|
|
58
|
+
| `simple_module_users` | `sm users create-admin` |
|
|
59
|
+
| `simple_module_settings` | `sm settings import-from-env` |
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# simple_module_cli
|
|
2
|
+
|
|
3
|
+
Standalone scaffolder for the [SimpleModule framework](https://github.com/antosubash/simple_module_python).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install simple_module_cli
|
|
9
|
+
# or, to keep the CLI in its own venv:
|
|
10
|
+
pipx install simple_module_cli
|
|
11
|
+
# or, to run it without installing:
|
|
12
|
+
uvx --from simple_module_cli sm new my-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package depends only on `typer` and `tomlkit` — installing it does **not** pull in FastAPI, SQLModel, or any other framework runtime.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
sm new my-app # interactive wizard
|
|
21
|
+
sm new my-app --yes --preset full # all built-in modules + background jobs
|
|
22
|
+
sm create-module my_feature # scaffold a publishable module package
|
|
23
|
+
sm create-host bare-host # scaffold a bare host (no opinionated wiring)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Built-in commands: `sm new`, `sm create-host`, `sm create-module`.
|
|
27
|
+
|
|
28
|
+
When other framework packages are installed, they contribute additional subcommands via the `simple_module_cli.cli_plugins` entry-point group:
|
|
29
|
+
|
|
30
|
+
| Package | Commands |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `simple_module_hosting` | `sm host gen-pages`, `sm host sync-js-deps` |
|
|
33
|
+
| `simple_module_users` | `sm users create-admin` |
|
|
34
|
+
| `simple_module_settings` | `sm settings import-from-env` |
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "simple_module_cli"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Standalone scaffolder for the SimpleModule framework — `sm new`, `sm create-module`, plugin host."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
authors = [{ name = "Anto Subash", email = "antosubash@live.com" }]
|
|
10
|
+
keywords = ["simple-module", "scaffolding", "cli", "fastapi", "modular-monolith"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Software Development :: Code Generators",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"typer>=0.12",
|
|
24
|
+
"tomlkit>=0.13",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
sm = "simple_module_cli.cli:main"
|
|
29
|
+
simple-module = "simple_module_cli.cli:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/antosubash/simple_module_python"
|
|
33
|
+
Repository = "https://github.com/antosubash/simple_module_python"
|
|
34
|
+
Issues = "https://github.com/antosubash/simple_module_python/issues"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["simple_module_cli"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel.shared-data]
|
|
44
|
+
"simple_module_cli/templates" = "simple_module_cli/templates"
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Shared helpers for editing dotenv-style files at scaffold time."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["set_env_key"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def set_env_key(text: str, key: str, value: str) -> str:
|
|
9
|
+
"""Replace or append ``KEY=VALUE`` in an env-style file body."""
|
|
10
|
+
lines = [ln for ln in text.splitlines() if not ln.startswith(f"{key}=")]
|
|
11
|
+
lines.append(f"{key}={value}")
|
|
12
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Greenfield ``simple-module new`` scaffolding.
|
|
2
|
+
|
|
3
|
+
Wraps :func:`simple_module_hosting.scaffolding.create_host` with the
|
|
4
|
+
opinionated bits — module-list resolution from the CLI catalog, secret
|
|
5
|
+
generation, DB URL selection, ``pyproject.toml`` / ``package.json``
|
|
6
|
+
rewriting, and post-scaffold recipe application.
|
|
7
|
+
|
|
8
|
+
Lives in its own module to keep ``scaffolding.py`` under the per-file
|
|
9
|
+
line cap and to make the surface area of "host scaffold" vs "app
|
|
10
|
+
scaffold" obvious to readers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json as _json
|
|
16
|
+
import secrets as _secrets
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from simple_module_cli._env import set_env_key
|
|
21
|
+
from simple_module_cli.case import to_kebab_case, to_pascal_case
|
|
22
|
+
from simple_module_cli.catalog import CATALOG, PRESETS, expand_deps
|
|
23
|
+
from simple_module_cli.recipes import RECIPES, ScaffoldCtx
|
|
24
|
+
from simple_module_cli.scaffolding import create_host
|
|
25
|
+
|
|
26
|
+
__all__ = ["create_app_project"]
|
|
27
|
+
|
|
28
|
+
_FRAMEWORK_VERSION = "0.0.1"
|
|
29
|
+
|
|
30
|
+
_APP_PY_DEV_DEPS = [f"simple_module_test=={_FRAMEWORK_VERSION}", "pytest>=8.0"]
|
|
31
|
+
|
|
32
|
+
_APP_NPM_DEPS = {
|
|
33
|
+
"@simple-module-py/ui": _FRAMEWORK_VERSION,
|
|
34
|
+
"@simple-module-py/i18n": _FRAMEWORK_VERSION,
|
|
35
|
+
"react": "^19.0.0",
|
|
36
|
+
"react-dom": "^19.0.0",
|
|
37
|
+
"@inertiajs/react": "^1.0.0",
|
|
38
|
+
}
|
|
39
|
+
_APP_NPM_DEV_DEPS = {
|
|
40
|
+
"@simple-module-py/tsconfig": _FRAMEWORK_VERSION,
|
|
41
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
42
|
+
"typescript": "^5.6.0",
|
|
43
|
+
"vite": "^8.0.0",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_app_project(
|
|
48
|
+
target: Path,
|
|
49
|
+
*,
|
|
50
|
+
name: str,
|
|
51
|
+
db: str = "sqlite",
|
|
52
|
+
tenancy: bool = False,
|
|
53
|
+
selected: Sequence[str] | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Greenfield ``simple-module new`` scaffold.
|
|
56
|
+
|
|
57
|
+
Wraps :func:`create_host` with a chosen module list (defaults to the
|
|
58
|
+
``standard`` preset), generates a secret, picks a DB URL, rewrites
|
|
59
|
+
the generated ``package.json`` / ``pyproject.toml`` to pin exact
|
|
60
|
+
framework versions, and applies any matching post-scaffold recipes
|
|
61
|
+
(e.g. the ``background_tasks`` recipe drops a Celery worker stack).
|
|
62
|
+
"""
|
|
63
|
+
if target.exists() and any(target.iterdir()):
|
|
64
|
+
raise FileExistsError(
|
|
65
|
+
f"Destination {target} already exists and is non-empty; "
|
|
66
|
+
"choose a new path or remove its contents first."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
chosen = list(selected) if selected is not None else list(PRESETS["standard"])
|
|
70
|
+
resolved, _added = expand_deps(chosen)
|
|
71
|
+
|
|
72
|
+
display_names = [to_pascal_case(CATALOG[m].display) for m in resolved]
|
|
73
|
+
create_host(target, name=name, modules=display_names)
|
|
74
|
+
|
|
75
|
+
py_deps = [f"simple_module_hosting=={_FRAMEWORK_VERSION}"] + [
|
|
76
|
+
f"{CATALOG[m].package}=={_FRAMEWORK_VERSION}" for m in resolved
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
env_path = target / ".env.example"
|
|
80
|
+
env_text = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
81
|
+
env_text = set_env_key(env_text, "SM_SECRET_KEY", _secrets.token_urlsafe(32))
|
|
82
|
+
env_text = set_env_key(env_text, "SM_DATABASE_URL", _db_url(db, to_kebab_case(name)))
|
|
83
|
+
env_text = set_env_key(env_text, "SM_MULTI_TENANT", "true" if tenancy else "false")
|
|
84
|
+
env_path.write_text(env_text, encoding="utf-8")
|
|
85
|
+
|
|
86
|
+
pyproject = target / "pyproject.toml"
|
|
87
|
+
if pyproject.exists():
|
|
88
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
89
|
+
text = _inject_py_deps(text, py_deps, _APP_PY_DEV_DEPS)
|
|
90
|
+
pyproject.write_text(text, encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
pkg_path = target / "package.json"
|
|
93
|
+
if pkg_path.exists():
|
|
94
|
+
data = _json.loads(pkg_path.read_text(encoding="utf-8"))
|
|
95
|
+
else:
|
|
96
|
+
data = {"name": to_kebab_case(name), "private": True, "type": "module"}
|
|
97
|
+
data.setdefault("dependencies", {}).update(_APP_NPM_DEPS)
|
|
98
|
+
data.setdefault("devDependencies", {}).update(_APP_NPM_DEV_DEPS)
|
|
99
|
+
pkg_path.write_text(_json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
100
|
+
|
|
101
|
+
ctx = ScaffoldCtx(name=name, db=db, tenancy=tenancy, selected=tuple(resolved))
|
|
102
|
+
for mod_name in resolved:
|
|
103
|
+
recipe_key = CATALOG[mod_name].recipe
|
|
104
|
+
if recipe_key is not None and recipe_key in RECIPES:
|
|
105
|
+
RECIPES[recipe_key].apply(target, ctx)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _db_url(db: str, slug: str) -> str:
|
|
109
|
+
if db == "postgres":
|
|
110
|
+
return f"postgresql+asyncpg://postgres:postgres@localhost:5432/{slug}"
|
|
111
|
+
return "sqlite+aiosqlite:///./app.db"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _inject_py_deps(text: str, deps: list[str], dev_deps: list[str]) -> str:
|
|
115
|
+
"""Replace project.dependencies + dependency-groups.dev in a pyproject.toml."""
|
|
116
|
+
import tomlkit
|
|
117
|
+
|
|
118
|
+
doc = tomlkit.parse(text)
|
|
119
|
+
project = doc.setdefault("project", tomlkit.table())
|
|
120
|
+
project["dependencies"] = list(deps)
|
|
121
|
+
groups = doc.setdefault("dependency-groups", tomlkit.table())
|
|
122
|
+
groups["dev"] = list(dev_deps)
|
|
123
|
+
return tomlkit.dumps(doc)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Identifier case-conversion helpers used by every scaffolder.
|
|
2
|
+
|
|
3
|
+
Module/host names are accepted in any case style and normalized to the
|
|
4
|
+
three forms the templates need: snake_case (Python package + entry-point
|
|
5
|
+
key), kebab-case (PyPI slug), and PascalCase (display name in Meta).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
__all__ = ["to_kebab_case", "to_pascal_case", "to_snake_case"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def to_snake_case(name: str) -> str:
|
|
16
|
+
"""'MyFeature' / 'my-feature' / 'My Feature' -> 'my_feature'."""
|
|
17
|
+
s = re.sub(r"(?<!^)(?=[A-Z])", "_", name)
|
|
18
|
+
s = re.sub(r"[\s\-]+", "_", s)
|
|
19
|
+
return s.lower()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def to_kebab_case(name: str) -> str:
|
|
23
|
+
"""'MyFeature' / 'my_feature' -> 'my-feature' (used as the PyPI slug)."""
|
|
24
|
+
return to_snake_case(name).replace("_", "-")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_pascal_case(name: str) -> str:
|
|
28
|
+
"""'my-feature' / 'my_feature' -> 'MyFeature' (the display name in Meta)."""
|
|
29
|
+
snake = to_snake_case(name)
|
|
30
|
+
return "".join(part.capitalize() for part in snake.split("_") if part)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Hardcoded catalog of installable SimpleModule modules.
|
|
2
|
+
|
|
3
|
+
Each :class:`ModuleEntry` declares the PyPI package name, a human display
|
|
4
|
+
name, transitive ``requires`` (other catalog keys), and an optional
|
|
5
|
+
``recipe`` key for post-scaffold actions handled by :mod:`.recipes`.
|
|
6
|
+
|
|
7
|
+
:func:`expand_deps` takes a user-selected subset and returns a
|
|
8
|
+
topologically ordered superset including every transitive requirement,
|
|
9
|
+
plus the list of ``(added, required_by)`` pairs for printing back to the
|
|
10
|
+
user.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
|
|
18
|
+
__all__ = ["CATALOG", "PRESETS", "ModuleEntry", "expand_deps"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ModuleEntry:
|
|
23
|
+
name: str
|
|
24
|
+
package: str
|
|
25
|
+
display: str
|
|
26
|
+
requires: tuple[str, ...] = field(default_factory=tuple)
|
|
27
|
+
recipe: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
CATALOG: dict[str, ModuleEntry] = {
|
|
31
|
+
"auth": ModuleEntry("auth", "simple_module_auth", "Auth"),
|
|
32
|
+
"users": ModuleEntry("users", "simple_module_users", "Users", requires=("auth",)),
|
|
33
|
+
"permissions": ModuleEntry(
|
|
34
|
+
"permissions",
|
|
35
|
+
"simple_module_permissions",
|
|
36
|
+
"Permissions",
|
|
37
|
+
requires=("auth", "users"),
|
|
38
|
+
),
|
|
39
|
+
"products": ModuleEntry("products", "simple_module_products", "Products"),
|
|
40
|
+
"dashboard": ModuleEntry(
|
|
41
|
+
"dashboard",
|
|
42
|
+
"simple_module_dashboard",
|
|
43
|
+
"Dashboard",
|
|
44
|
+
requires=("users", "products"),
|
|
45
|
+
),
|
|
46
|
+
"settings": ModuleEntry("settings", "simple_module_settings", "Settings"),
|
|
47
|
+
"feature_flags": ModuleEntry("feature_flags", "simple_module_feature_flags", "Feature Flags"),
|
|
48
|
+
"file_storage": ModuleEntry(
|
|
49
|
+
"file_storage",
|
|
50
|
+
"simple_module_file_storage",
|
|
51
|
+
"File Storage",
|
|
52
|
+
requires=("settings",),
|
|
53
|
+
),
|
|
54
|
+
"background_tasks": ModuleEntry(
|
|
55
|
+
"background_tasks",
|
|
56
|
+
"simple_module_background_tasks",
|
|
57
|
+
"Background Tasks",
|
|
58
|
+
requires=("users",),
|
|
59
|
+
recipe="background_tasks",
|
|
60
|
+
),
|
|
61
|
+
"datasets": ModuleEntry(
|
|
62
|
+
"datasets",
|
|
63
|
+
"simple_module_datasets",
|
|
64
|
+
"Datasets",
|
|
65
|
+
requires=("file_storage", "background_tasks"),
|
|
66
|
+
),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
PRESETS: dict[str, tuple[str, ...]] = {
|
|
71
|
+
"minimal": ("users",),
|
|
72
|
+
"standard": ("users", "dashboard", "permissions"),
|
|
73
|
+
"full": tuple(CATALOG),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def expand_deps(selected: Iterable[str]) -> tuple[list[str], list[tuple[str, str]]]:
|
|
78
|
+
"""Return ``(topo-ordered resolved list, [(added, required_by), ...])``.
|
|
79
|
+
|
|
80
|
+
Raises :class:`KeyError` if any input name is missing from the
|
|
81
|
+
catalog. The error message lists the available catalog keys so a
|
|
82
|
+
user typo (`--with=does_not_exist`) is self-correcting.
|
|
83
|
+
"""
|
|
84
|
+
selected_list = list(selected)
|
|
85
|
+
for name in selected_list:
|
|
86
|
+
if name not in CATALOG:
|
|
87
|
+
available = ", ".join(sorted(CATALOG))
|
|
88
|
+
raise KeyError(f"unknown module: {name!r}; available: {available}")
|
|
89
|
+
|
|
90
|
+
explicit = set(selected_list)
|
|
91
|
+
resolved: list[str] = []
|
|
92
|
+
in_resolved: set[str] = set()
|
|
93
|
+
added: list[tuple[str, str]] = []
|
|
94
|
+
|
|
95
|
+
def _visit(name: str, required_by: str | None) -> None:
|
|
96
|
+
if name in in_resolved:
|
|
97
|
+
return
|
|
98
|
+
for dep in CATALOG[name].requires:
|
|
99
|
+
_visit(dep, required_by=name)
|
|
100
|
+
resolved.append(name)
|
|
101
|
+
in_resolved.add(name)
|
|
102
|
+
if required_by is not None and name not in explicit:
|
|
103
|
+
added.append((name, required_by))
|
|
104
|
+
|
|
105
|
+
for name in selected_list:
|
|
106
|
+
_visit(name, required_by=None)
|
|
107
|
+
return resolved, added
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Root `sm` Typer app — scaffolders + plugin mount.
|
|
2
|
+
|
|
3
|
+
Built-in commands:
|
|
4
|
+
sm new
|
|
5
|
+
sm create-host
|
|
6
|
+
sm create-module
|
|
7
|
+
|
|
8
|
+
Plugins discovered via the ``simple_module_cli.cli_plugins`` entry-point
|
|
9
|
+
group are mounted as named subgroups (e.g. ``sm host gen-pages``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from simple_module_cli.case import to_kebab_case
|
|
20
|
+
from simple_module_cli.new import new_project
|
|
21
|
+
from simple_module_cli.plugins import discover_and_mount
|
|
22
|
+
from simple_module_cli.scaffolding import create_host as _create_host
|
|
23
|
+
from simple_module_cli.scaffolding import create_module as _create_module
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
help="SimpleModule developer CLI.",
|
|
27
|
+
no_args_is_help=True,
|
|
28
|
+
add_completion=False,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
app.command("new")(new_project)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("create-host")
|
|
35
|
+
def create_host(
|
|
36
|
+
name: Annotated[str, typer.Argument(help="Host project name.")],
|
|
37
|
+
dest: Annotated[
|
|
38
|
+
Path | None,
|
|
39
|
+
typer.Option("--dest", help="Destination directory. Defaults to ./<name>."),
|
|
40
|
+
] = None,
|
|
41
|
+
modules: Annotated[
|
|
42
|
+
str,
|
|
43
|
+
typer.Option(
|
|
44
|
+
"--with",
|
|
45
|
+
help="Comma-separated module names to declare as deps (e.g. Auth,Products).",
|
|
46
|
+
),
|
|
47
|
+
] = "",
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Scaffold a new SimpleModule host project at ./<NAME>."""
|
|
50
|
+
target = dest or Path.cwd() / name
|
|
51
|
+
selected = [m.strip() for m in modules.split(",") if m.strip()]
|
|
52
|
+
try:
|
|
53
|
+
_create_host(target, name=name, modules=selected)
|
|
54
|
+
except FileExistsError as exc:
|
|
55
|
+
typer.echo(f"ERROR: {exc}", err=True)
|
|
56
|
+
raise typer.Exit(code=1) from exc
|
|
57
|
+
|
|
58
|
+
typer.echo(f"Created host '{name}' at {target}")
|
|
59
|
+
if selected:
|
|
60
|
+
typer.echo(f"Declared modules: {', '.join(selected)}")
|
|
61
|
+
typer.echo("\nNext steps:")
|
|
62
|
+
typer.echo(f" cd {target}")
|
|
63
|
+
typer.echo(" uv sync")
|
|
64
|
+
typer.echo(" cp .env.example .env")
|
|
65
|
+
typer.echo(' alembic revision --autogenerate -m "initial schema"')
|
|
66
|
+
typer.echo(" alembic upgrade head")
|
|
67
|
+
typer.echo(" python main.py")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("create-module")
|
|
71
|
+
def create_module(
|
|
72
|
+
name: Annotated[str, typer.Argument(help="Module name (any case).")],
|
|
73
|
+
dest: Annotated[
|
|
74
|
+
Path | None,
|
|
75
|
+
typer.Option("--dest", help="Destination dir. Defaults to ./simple_module_<name>."),
|
|
76
|
+
] = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Scaffold a publishable SimpleModule module package."""
|
|
79
|
+
slug = to_kebab_case(name)
|
|
80
|
+
package = slug.replace("-", "_")
|
|
81
|
+
target = dest or Path.cwd() / f"simple_module_{package}"
|
|
82
|
+
try:
|
|
83
|
+
_create_module(target, name=name)
|
|
84
|
+
except FileExistsError as exc:
|
|
85
|
+
typer.echo(f"ERROR: {exc}", err=True)
|
|
86
|
+
raise typer.Exit(code=1) from exc
|
|
87
|
+
|
|
88
|
+
typer.echo(f"Created module 'simple_module_{package}' at {target}")
|
|
89
|
+
typer.echo("\nNext steps:")
|
|
90
|
+
typer.echo(f" cd {target}")
|
|
91
|
+
typer.echo(" uv sync --extra dev")
|
|
92
|
+
typer.echo(" uv run pytest")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
discover_and_mount(app)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main() -> None:
|
|
99
|
+
"""Entry point for the `sm` console script."""
|
|
100
|
+
app()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|