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.
Files changed (62) hide show
  1. simple_module_cli-0.0.2/.gitignore +59 -0
  2. simple_module_cli-0.0.2/LICENSE +21 -0
  3. simple_module_cli-0.0.2/PKG-INFO +63 -0
  4. simple_module_cli-0.0.2/README.md +38 -0
  5. simple_module_cli-0.0.2/pyproject.toml +44 -0
  6. simple_module_cli-0.0.2/simple_module_cli/__init__.py +0 -0
  7. simple_module_cli-0.0.2/simple_module_cli/_env.py +12 -0
  8. simple_module_cli-0.0.2/simple_module_cli/app_project.py +123 -0
  9. simple_module_cli-0.0.2/simple_module_cli/case.py +30 -0
  10. simple_module_cli-0.0.2/simple_module_cli/catalog.py +107 -0
  11. simple_module_cli-0.0.2/simple_module_cli/cli.py +104 -0
  12. simple_module_cli-0.0.2/simple_module_cli/new.py +124 -0
  13. simple_module_cli-0.0.2/simple_module_cli/plugins.py +56 -0
  14. simple_module_cli-0.0.2/simple_module_cli/recipes.py +93 -0
  15. simple_module_cli-0.0.2/simple_module_cli/scaffolding.py +124 -0
  16. simple_module_cli-0.0.2/simple_module_cli/templates/host/.env.example +20 -0
  17. simple_module_cli-0.0.2/simple_module_cli/templates/host/.gitignore +19 -0
  18. simple_module_cli-0.0.2/simple_module_cli/templates/host/Makefile +24 -0
  19. simple_module_cli-0.0.2/simple_module_cli/templates/host/README.md.tpl +59 -0
  20. simple_module_cli-0.0.2/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +12 -0
  21. simple_module_cli-0.0.2/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +60 -0
  22. simple_module_cli-0.0.2/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +17 -0
  23. simple_module_cli-0.0.2/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +37 -0
  24. simple_module_cli-0.0.2/simple_module_cli/templates/host/alembic.ini +36 -0
  25. simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/app.tsx +16 -0
  26. simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/main.tsx +2 -0
  27. simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/package.json.tpl +23 -0
  28. simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/pages/Error.tsx +13 -0
  29. simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/pages.ts +47 -0
  30. simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/styles.css +7 -0
  31. simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/tsconfig.json +16 -0
  32. simple_module_cli-0.0.2/simple_module_cli/templates/host/client_app/vite.config.ts +39 -0
  33. simple_module_cli-0.0.2/simple_module_cli/templates/host/main.py +27 -0
  34. simple_module_cli-0.0.2/simple_module_cli/templates/host/migrations/env.py +80 -0
  35. simple_module_cli-0.0.2/simple_module_cli/templates/host/migrations/script.py.mako +26 -0
  36. simple_module_cli-0.0.2/simple_module_cli/templates/host/migrations/versions/.gitkeep +1 -0
  37. simple_module_cli-0.0.2/simple_module_cli/templates/host/pyproject.toml.tpl +17 -0
  38. simple_module_cli-0.0.2/simple_module_cli/templates/host/templates/index.html +12 -0
  39. simple_module_cli-0.0.2/simple_module_cli/templates/module/.github/workflows/ci.yml +32 -0
  40. simple_module_cli-0.0.2/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +52 -0
  41. simple_module_cli-0.0.2/simple_module_cli/templates/module/.gitignore +14 -0
  42. simple_module_cli-0.0.2/simple_module_cli/templates/module/README.md.tpl +82 -0
  43. simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  44. simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  45. simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
  46. simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +46 -0
  47. simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
  48. simple_module_cli-0.0.2/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +22 -0
  49. simple_module_cli-0.0.2/simple_module_cli/templates/module/package.json.tpl +16 -0
  50. simple_module_cli-0.0.2/simple_module_cli/templates/module/pyproject.toml.tpl +39 -0
  51. simple_module_cli-0.0.2/simple_module_cli/templates/module/tests/__init__.py +0 -0
  52. simple_module_cli-0.0.2/simple_module_cli/templates/module/tests/test_module.py.tpl +27 -0
  53. simple_module_cli-0.0.2/simple_module_cli/templates/module/tsconfig.json.tpl +11 -0
  54. simple_module_cli-0.0.2/simple_module_cli/wizard.py +48 -0
  55. simple_module_cli-0.0.2/tests/test_cli_catalog.py +86 -0
  56. simple_module_cli-0.0.2/tests/test_cli_new.py +210 -0
  57. simple_module_cli-0.0.2/tests/test_cli_recipes.py +87 -0
  58. simple_module_cli-0.0.2/tests/test_cli_wizard.py +78 -0
  59. simple_module_cli-0.0.2/tests/test_no_framework_deps.py +26 -0
  60. simple_module_cli-0.0.2/tests/test_plugin_discovery.py +97 -0
  61. simple_module_cli-0.0.2/tests/test_scaffolding_host.py +171 -0
  62. 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()