simple-module-cli 0.0.8__tar.gz → 0.0.9__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.8 → simple_module_cli-0.0.9}/PKG-INFO +1 -1
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/pyproject.toml +1 -1
- simple_module_cli-0.0.9/simple_module_cli/app_project.py +264 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/recipes.py +13 -6
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/scaffolding.py +35 -4
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/Makefile +5 -1
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +34 -0
- simple_module_cli-0.0.9/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +44 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/client_app/package.json.tpl +3 -2
- simple_module_cli-0.0.9/simple_module_cli/templates/host/client_app/vite.config.ts +152 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -6
- simple_module_cli-0.0.9/simple_module_cli/templates/host/templates/index.html +24 -0
- simple_module_cli-0.0.9/simple_module_cli/templates/workspace/.env.example +19 -0
- simple_module_cli-0.0.9/simple_module_cli/templates/workspace/.gitignore +20 -0
- simple_module_cli-0.0.9/simple_module_cli/templates/workspace/Makefile +42 -0
- simple_module_cli-0.0.9/simple_module_cli/templates/workspace/README.md.tpl +62 -0
- simple_module_cli-0.0.9/simple_module_cli/templates/workspace/package.json.tpl +13 -0
- simple_module_cli-0.0.9/simple_module_cli/templates/workspace/pyproject.toml.tpl +17 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_cli_new.py +57 -28
- simple_module_cli-0.0.8/simple_module_cli/app_project.py +0 -179
- simple_module_cli-0.0.8/simple_module_cli/templates/host/client_app/vite.config.ts +0 -60
- simple_module_cli-0.0.8/simple_module_cli/templates/host/templates/index.html +0 -12
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/.gitignore +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/LICENSE +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/README.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/cli.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/new.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_scaffolding_host.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_scaffolding_module.py +0 -0
- {simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/tests/test_skills_cmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9
|
|
4
4
|
Summary: Standalone scaffolder for the SimpleModule framework — `sm new`, `sm create-module`, plugin host.
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Greenfield ``simple-module new`` scaffolding.
|
|
2
|
+
|
|
3
|
+
Wraps :func:`simple_module_cli.scaffolding.create_host` (and, in
|
|
4
|
+
workspace mode, :func:`create_workspace`) with the opinionated bits —
|
|
5
|
+
module-list resolution from the CLI catalog, secret generation, DB URL
|
|
6
|
+
selection, ``pyproject.toml`` / ``package.json`` rewriting, and
|
|
7
|
+
post-scaffold recipe application.
|
|
8
|
+
|
|
9
|
+
Lives in its own module to keep ``scaffolding.py`` under the per-file
|
|
10
|
+
line cap and to make the surface area of "host scaffold" vs "app
|
|
11
|
+
scaffold" obvious to readers.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json as _json
|
|
17
|
+
import secrets as _secrets
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from importlib.metadata import PackageNotFoundError
|
|
20
|
+
from importlib.metadata import version as _pkg_version
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from simple_module_cli._env import set_env_key
|
|
25
|
+
from simple_module_cli.case import to_kebab_case, to_pascal_case
|
|
26
|
+
from simple_module_cli.catalog import CATALOG, PRESETS, expand_deps
|
|
27
|
+
from simple_module_cli.recipes import RECIPES, ScaffoldCtx
|
|
28
|
+
from simple_module_cli.scaffolding import (
|
|
29
|
+
_module_to_pypi_name,
|
|
30
|
+
create_host,
|
|
31
|
+
create_module,
|
|
32
|
+
create_workspace,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = ["create_app_project"]
|
|
36
|
+
|
|
37
|
+
_SAMPLE_MODULE_NAME = "hello"
|
|
38
|
+
_SAMPLE_MODULE_PKG = _module_to_pypi_name(_SAMPLE_MODULE_NAME)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_framework_version() -> str:
|
|
42
|
+
"""Resolve the framework version to pin scaffolded apps against.
|
|
43
|
+
|
|
44
|
+
The CLI ships in lockstep with the rest of the framework (one
|
|
45
|
+
``bump_version.py`` rewrites every ``pyproject.toml`` in the repo), so
|
|
46
|
+
its own installed version is the source of truth. Falling back to a
|
|
47
|
+
placeholder lets editable installs without dist-info still scaffold —
|
|
48
|
+
but that path should never be reached in a release wheel.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
return _pkg_version("simple_module_cli")
|
|
52
|
+
except PackageNotFoundError:
|
|
53
|
+
return "0.0.0"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_FRAMEWORK_VERSION = _resolve_framework_version()
|
|
57
|
+
|
|
58
|
+
_APP_PY_DEV_DEPS = [f"simple_module_test=={_FRAMEWORK_VERSION}", "pytest>=8.0"]
|
|
59
|
+
|
|
60
|
+
_APP_NPM_DEPS = {
|
|
61
|
+
"@simple-module-py/ui": _FRAMEWORK_VERSION,
|
|
62
|
+
"@simple-module-py/i18n": _FRAMEWORK_VERSION,
|
|
63
|
+
"react": "^19.0.0",
|
|
64
|
+
"react-dom": "^19.0.0",
|
|
65
|
+
"@inertiajs/react": "^2.0.0",
|
|
66
|
+
}
|
|
67
|
+
_APP_NPM_DEV_DEPS = {
|
|
68
|
+
"@simple-module-py/tsconfig": _FRAMEWORK_VERSION,
|
|
69
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
70
|
+
"typescript": "^5.6.0",
|
|
71
|
+
"vite": "^8.0.0",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Files the host template ships that the workspace template re-emits at
|
|
75
|
+
# the project root. Host copies are stripped in workspace mode.
|
|
76
|
+
_HOST_FILES_OWNED_BY_WORKSPACE = (
|
|
77
|
+
".env.example",
|
|
78
|
+
".gitignore",
|
|
79
|
+
"README.md",
|
|
80
|
+
"Makefile",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_app_project(
|
|
85
|
+
target: Path,
|
|
86
|
+
*,
|
|
87
|
+
name: str,
|
|
88
|
+
db: str = "sqlite",
|
|
89
|
+
tenancy: bool = False,
|
|
90
|
+
selected: Sequence[str] | None = None,
|
|
91
|
+
flat: bool = False,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Greenfield ``simple-module new`` scaffold.
|
|
94
|
+
|
|
95
|
+
In workspace mode (the default), lays down a uv + npm workspace at
|
|
96
|
+
``target/`` with the host under ``target/host/`` and a sample module
|
|
97
|
+
under ``target/modules/hello/``. In flat mode (``flat=True``), keeps
|
|
98
|
+
the legacy single-host layout: host files at ``target/`` with no
|
|
99
|
+
``modules/`` directory or workspace plumbing.
|
|
100
|
+
|
|
101
|
+
Generates a secret, picks a DB URL, rewrites the host's
|
|
102
|
+
``pyproject.toml`` / the relevant ``package.json`` to pin exact
|
|
103
|
+
framework versions, and applies any matching post-scaffold recipes
|
|
104
|
+
(e.g. the ``background_tasks`` recipe drops a Celery worker stack).
|
|
105
|
+
"""
|
|
106
|
+
if target.exists() and any(target.iterdir()):
|
|
107
|
+
raise FileExistsError(
|
|
108
|
+
f"Destination {target} already exists and is non-empty; "
|
|
109
|
+
"choose a new path or remove its contents first."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
chosen = list(selected) if selected is not None else list(PRESETS["standard"])
|
|
113
|
+
resolved, _added = expand_deps(chosen)
|
|
114
|
+
|
|
115
|
+
display_names = [to_pascal_case(CATALOG[m].display) for m in resolved]
|
|
116
|
+
host_dir = target if flat else target / "host"
|
|
117
|
+
if not flat:
|
|
118
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
create_workspace(target, name=name)
|
|
120
|
+
create_host(host_dir, name=name, modules=display_names, framework_version=_FRAMEWORK_VERSION)
|
|
121
|
+
if not flat:
|
|
122
|
+
_strip_workspace_owned_files(host_dir)
|
|
123
|
+
|
|
124
|
+
py_deps = [f"simple_module_hosting=={_FRAMEWORK_VERSION}"] + [
|
|
125
|
+
f"{CATALOG[m].package}=={_FRAMEWORK_VERSION}" for m in resolved
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
workspace_sources: list[str] = []
|
|
129
|
+
if not flat:
|
|
130
|
+
_scaffold_sample_module(target)
|
|
131
|
+
py_deps.append(_SAMPLE_MODULE_PKG)
|
|
132
|
+
workspace_sources.append(_SAMPLE_MODULE_PKG)
|
|
133
|
+
|
|
134
|
+
env_path = target / ".env.example"
|
|
135
|
+
env_text = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
136
|
+
env_text = set_env_key(env_text, "SM_SECRET_KEY", _secrets.token_urlsafe(32))
|
|
137
|
+
env_text = set_env_key(env_text, "SM_DATABASE_URL", _db_url(db, to_kebab_case(name), flat=flat))
|
|
138
|
+
env_text = set_env_key(env_text, "SM_MULTI_TENANT", "true" if tenancy else "false")
|
|
139
|
+
env_path.write_text(env_text, encoding="utf-8")
|
|
140
|
+
|
|
141
|
+
host_pyproject = host_dir / "pyproject.toml"
|
|
142
|
+
text = host_pyproject.read_text(encoding="utf-8")
|
|
143
|
+
# Workspace mode needs the host's [project].name distinct from the
|
|
144
|
+
# workspace root's, otherwise uv refuses with "two workspace members
|
|
145
|
+
# are both named ...". Flat mode keeps the user's exact name.
|
|
146
|
+
project_name = None if flat else f"{to_kebab_case(name)}-host"
|
|
147
|
+
text = _rewrite_pyproject(
|
|
148
|
+
text, py_deps, _APP_PY_DEV_DEPS, sources=workspace_sources, project_name=project_name
|
|
149
|
+
)
|
|
150
|
+
host_pyproject.write_text(text, encoding="utf-8")
|
|
151
|
+
|
|
152
|
+
if flat:
|
|
153
|
+
# The workspace template already emits a top-level package.json
|
|
154
|
+
# with workspaces; flat mode has none, so seed one with the
|
|
155
|
+
# framework npm pins so `npm install` resolves at the root.
|
|
156
|
+
pkg_path = target / "package.json"
|
|
157
|
+
pkg_data: dict[str, Any] = (
|
|
158
|
+
_json.loads(pkg_path.read_text(encoding="utf-8"))
|
|
159
|
+
if pkg_path.exists()
|
|
160
|
+
else {"name": to_kebab_case(name), "private": True, "type": "module"}
|
|
161
|
+
)
|
|
162
|
+
pkg_data.setdefault("dependencies", {}).update(_APP_NPM_DEPS)
|
|
163
|
+
pkg_data.setdefault("devDependencies", {}).update(_APP_NPM_DEV_DEPS)
|
|
164
|
+
pkg_path.write_text(_json.dumps(pkg_data, indent=2) + "\n", encoding="utf-8")
|
|
165
|
+
|
|
166
|
+
ctx = ScaffoldCtx(name=name, db=db, tenancy=tenancy, selected=tuple(resolved))
|
|
167
|
+
for mod_name in resolved:
|
|
168
|
+
recipe_key = CATALOG[mod_name].recipe
|
|
169
|
+
if recipe_key is not None and recipe_key in RECIPES:
|
|
170
|
+
RECIPES[recipe_key].apply(target, ctx)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _strip_workspace_owned_files(host_dir: Path) -> None:
|
|
174
|
+
"""Drop host copies of files the workspace root owns in workspace mode."""
|
|
175
|
+
for relpath in _HOST_FILES_OWNED_BY_WORKSPACE:
|
|
176
|
+
(host_dir / relpath).unlink(missing_ok=True)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _scaffold_sample_module(target: Path) -> None:
|
|
180
|
+
sample_dest = target / "modules" / _SAMPLE_MODULE_NAME
|
|
181
|
+
if sample_dest.exists():
|
|
182
|
+
return
|
|
183
|
+
create_module(sample_dest, name=_SAMPLE_MODULE_NAME)
|
|
184
|
+
_pin_sample_module_deps(sample_dest)
|
|
185
|
+
_seed_static_dist_placeholder(sample_dest / _SAMPLE_MODULE_NAME / "static" / "dist")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _seed_static_dist_placeholder(static_dist: Path) -> None:
|
|
189
|
+
# Hatch's force-include resolves at build time even for editable installs;
|
|
190
|
+
# an empty placeholder keeps `uv sync` from failing before vite build runs.
|
|
191
|
+
static_dist.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
(static_dist / ".gitkeep").touch()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _pin_sample_module_deps(sample_dest: Path) -> None:
|
|
196
|
+
"""Replace the module template's future-API range pins with exact pins.
|
|
197
|
+
|
|
198
|
+
The shared ``sm create-module`` template ships ``>=1.0,<2.0`` against the
|
|
199
|
+
framework's eventual stable line, but the workspace-bundled sample has to
|
|
200
|
+
resolve against whatever the framework version actually is today (``==X``
|
|
201
|
+
in pre-1.0). Without rewriting, ``uv sync`` can't satisfy the workspace.
|
|
202
|
+
"""
|
|
203
|
+
import tomlkit
|
|
204
|
+
|
|
205
|
+
pyproject = sample_dest / "pyproject.toml"
|
|
206
|
+
doc = tomlkit.parse(pyproject.read_text(encoding="utf-8"))
|
|
207
|
+
project = doc.setdefault("project", tomlkit.table())
|
|
208
|
+
project["dependencies"] = [_pin_or_keep(dep) for dep in project.get("dependencies", [])]
|
|
209
|
+
optional = project.get("optional-dependencies")
|
|
210
|
+
if optional is not None:
|
|
211
|
+
for extra, deps in list(optional.items()):
|
|
212
|
+
optional[extra] = [_pin_or_keep(dep) for dep in deps]
|
|
213
|
+
pyproject.write_text(tomlkit.dumps(doc), encoding="utf-8")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _pin_or_keep(dep: str) -> str:
|
|
217
|
+
"""Pin a ``simple_module_*`` requirement to the framework version; pass through otherwise."""
|
|
218
|
+
pkg = dep.split(">=", 1)[0].split("==", 1)[0].split("<", 1)[0].strip()
|
|
219
|
+
if pkg.startswith(("simple_module_", "simple-module-")):
|
|
220
|
+
return f"{pkg}=={_FRAMEWORK_VERSION}"
|
|
221
|
+
return dep
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _db_url(db: str, slug: str, *, flat: bool) -> str:
|
|
225
|
+
if db == "postgres":
|
|
226
|
+
return f"postgresql+asyncpg://postgres:postgres@localhost:5432/{slug}"
|
|
227
|
+
# Workspace mode keeps the SQLite file next to host/'s alembic.ini so
|
|
228
|
+
# `cd host && uvicorn` and `cd host && alembic` resolve the same path.
|
|
229
|
+
return "sqlite+aiosqlite:///./app.db" if flat else "sqlite+aiosqlite:///./host/app.db"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _rewrite_pyproject(
|
|
233
|
+
text: str,
|
|
234
|
+
deps: list[str],
|
|
235
|
+
dev_deps: list[str],
|
|
236
|
+
*,
|
|
237
|
+
sources: Sequence[str] = (),
|
|
238
|
+
project_name: str | None = None,
|
|
239
|
+
) -> str:
|
|
240
|
+
"""Replace deps in a host ``pyproject.toml`` and pin workspace sources.
|
|
241
|
+
|
|
242
|
+
``sources`` lists ``simple_module_*`` packages that should resolve from
|
|
243
|
+
the uv workspace (``modules/*``) instead of PyPI. Emits a
|
|
244
|
+
``[tool.uv.sources]`` block per entry. Empty in flat mode.
|
|
245
|
+
|
|
246
|
+
``project_name`` overrides ``[project].name`` — set in workspace mode
|
|
247
|
+
so the host's package name differs from the workspace root's.
|
|
248
|
+
"""
|
|
249
|
+
import tomlkit
|
|
250
|
+
|
|
251
|
+
doc = tomlkit.parse(text)
|
|
252
|
+
project = doc.setdefault("project", tomlkit.table())
|
|
253
|
+
if project_name is not None:
|
|
254
|
+
project["name"] = project_name
|
|
255
|
+
project["dependencies"] = list(deps)
|
|
256
|
+
groups = doc.setdefault("dependency-groups", tomlkit.table())
|
|
257
|
+
groups["dev"] = list(dev_deps)
|
|
258
|
+
if sources:
|
|
259
|
+
tool = doc.setdefault("tool", tomlkit.table())
|
|
260
|
+
uv_table = tool.setdefault("uv", tomlkit.table())
|
|
261
|
+
uv_sources = uv_table.setdefault("sources", tomlkit.table())
|
|
262
|
+
for src in sources:
|
|
263
|
+
uv_sources[src] = {"workspace": True}
|
|
264
|
+
return tomlkit.dumps(doc)
|
|
@@ -49,16 +49,22 @@ def _optional_template_root(name: str) -> Path:
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class BackgroundTasksRecipe:
|
|
52
|
-
"""Lays down run_worker.py + compose +
|
|
52
|
+
"""Lays down run_worker.py + compose + Dockerfiles + Make targets."""
|
|
53
53
|
|
|
54
54
|
def apply(self, target: Path, ctx: ScaffoldCtx) -> None:
|
|
55
55
|
templates = _optional_template_root("background_tasks")
|
|
56
56
|
|
|
57
57
|
run_worker_dest = target / "scripts" / "run_worker.py"
|
|
58
58
|
compose_dest = target / "docker-compose.yml"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
host_dockerfile_dest = target / "docker" / "host.Dockerfile"
|
|
60
|
+
worker_dockerfile_dest = target / "docker" / "worker.Dockerfile"
|
|
61
|
+
|
|
62
|
+
for path in (
|
|
63
|
+
run_worker_dest,
|
|
64
|
+
compose_dest,
|
|
65
|
+
host_dockerfile_dest,
|
|
66
|
+
worker_dockerfile_dest,
|
|
67
|
+
):
|
|
62
68
|
if path.exists():
|
|
63
69
|
raise FileExistsError(
|
|
64
70
|
f"{path} already exists — refusing to clobber. "
|
|
@@ -70,8 +76,9 @@ class BackgroundTasksRecipe:
|
|
|
70
76
|
|
|
71
77
|
shutil.copy2(templates / "docker-compose.yml", compose_dest)
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
shutil.copy2(templates / "
|
|
79
|
+
host_dockerfile_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
shutil.copy2(templates / "host.Dockerfile", host_dockerfile_dest)
|
|
81
|
+
shutil.copy2(templates / "worker.Dockerfile", worker_dockerfile_dest)
|
|
75
82
|
|
|
76
83
|
env_path = target / ".env.example"
|
|
77
84
|
env_text = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Host + module scaffolding via package-data templates.
|
|
2
2
|
|
|
3
|
+
* :func:`create_workspace` materializes the project-root workspace shell
|
|
4
|
+
(top-level ``pyproject.toml`` / ``package.json`` / ``Makefile``) from
|
|
5
|
+
``simple_module_cli/templates/workspace/``.
|
|
3
6
|
* :func:`create_host` materializes a new host project from the templates
|
|
4
|
-
under ``
|
|
7
|
+
under ``simple_module_cli/templates/host/``.
|
|
5
8
|
* :func:`create_module` materializes a new module package from
|
|
6
|
-
``
|
|
9
|
+
``simple_module_cli/templates/module/``.
|
|
7
10
|
|
|
8
11
|
The frontend pages manifest + per-module JS dep discovery live in
|
|
9
12
|
:mod:`simple_module_hosting.manifest` (those need module-discovery and
|
|
@@ -20,7 +23,7 @@ from pathlib import Path
|
|
|
20
23
|
|
|
21
24
|
from simple_module_cli.case import to_kebab_case, to_pascal_case, to_snake_case
|
|
22
25
|
|
|
23
|
-
__all__ = ["create_host", "create_module"]
|
|
26
|
+
__all__ = ["create_host", "create_module", "create_workspace"]
|
|
24
27
|
|
|
25
28
|
logger = logging.getLogger(__name__)
|
|
26
29
|
|
|
@@ -80,11 +83,35 @@ def _apply_template_files(
|
|
|
80
83
|
shutil.copy2(src, target)
|
|
81
84
|
|
|
82
85
|
|
|
86
|
+
def create_workspace(
|
|
87
|
+
dest: Path,
|
|
88
|
+
name: str,
|
|
89
|
+
template_root: Path | None = None,
|
|
90
|
+
) -> Path:
|
|
91
|
+
"""Materialize the workspace-root shell at ``dest``.
|
|
92
|
+
|
|
93
|
+
Lays down the top-level ``pyproject.toml`` (uv workspace), ``package.json``
|
|
94
|
+
(npm workspace), ``Makefile`` (delegates to host), ``.env.example``,
|
|
95
|
+
``.gitignore``, and ``README.md``. Does NOT create the host or any
|
|
96
|
+
modules — those go under ``dest/host`` and ``dest/modules/`` afterwards.
|
|
97
|
+
"""
|
|
98
|
+
dest = Path(dest)
|
|
99
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
_apply_template_files(
|
|
101
|
+
_resolve_template_root("workspace", template_root),
|
|
102
|
+
dest,
|
|
103
|
+
{"{{HOST_NAME}}": to_kebab_case(name)},
|
|
104
|
+
)
|
|
105
|
+
logger.info("Scaffolded workspace root at %s", dest)
|
|
106
|
+
return dest
|
|
107
|
+
|
|
108
|
+
|
|
83
109
|
def create_host(
|
|
84
110
|
dest: Path,
|
|
85
111
|
name: str,
|
|
86
112
|
modules: Sequence[str],
|
|
87
113
|
template_root: Path | None = None,
|
|
114
|
+
framework_version: str = "*",
|
|
88
115
|
) -> Path:
|
|
89
116
|
dest = Path(dest)
|
|
90
117
|
_require_empty_dest(dest)
|
|
@@ -92,7 +119,11 @@ def create_host(
|
|
|
92
119
|
_apply_template_files(
|
|
93
120
|
_resolve_template_root("host", template_root),
|
|
94
121
|
dest,
|
|
95
|
-
{
|
|
122
|
+
{
|
|
123
|
+
"{{HOST_NAME}}": name,
|
|
124
|
+
"{{MODULE_DEPS}}": module_dep_lines,
|
|
125
|
+
"{{FRAMEWORK_VERSION}}": framework_version,
|
|
126
|
+
},
|
|
96
127
|
)
|
|
97
128
|
logger.info(
|
|
98
129
|
"Scaffolded host '%s' at %s (modules: %s)", name, dest, ", ".join(modules) or "<none>"
|
{simple_module_cli-0.0.8 → simple_module_cli-0.0.9}/simple_module_cli/templates/host/Makefile
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.PHONY: install dev dev-api dev-ui build migrate gen-pages sync-js-deps
|
|
1
|
+
.PHONY: install dev dev-api dev-ui build migrate migration gen-pages sync-js-deps
|
|
2
2
|
|
|
3
3
|
install:
|
|
4
4
|
uv sync
|
|
@@ -21,6 +21,10 @@ build:
|
|
|
21
21
|
migrate:
|
|
22
22
|
uv run alembic upgrade head
|
|
23
23
|
|
|
24
|
+
migration:
|
|
25
|
+
@test -n "$(msg)" || (echo 'Usage: make migration msg="describe the change"' && exit 1)
|
|
26
|
+
uv run alembic revision --autogenerate -m "$(msg)"
|
|
27
|
+
|
|
24
28
|
gen-pages:
|
|
25
29
|
uv run python -m simple_module_hosting gen-pages --host-dir=client_app
|
|
26
30
|
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:16
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_DB: simple_module
|
|
6
|
+
POSTGRES_USER: sm
|
|
7
|
+
POSTGRES_PASSWORD: sm
|
|
8
|
+
ports:
|
|
9
|
+
- "5432:5432"
|
|
10
|
+
volumes:
|
|
11
|
+
- pgdata:/var/lib/postgresql/data
|
|
12
|
+
healthcheck:
|
|
13
|
+
test: ["CMD-SHELL", "pg_isready -U sm -d simple_module"]
|
|
14
|
+
interval: 5s
|
|
15
|
+
timeout: 5s
|
|
16
|
+
retries: 10
|
|
17
|
+
|
|
2
18
|
redis:
|
|
3
19
|
image: redis:7-alpine
|
|
4
20
|
ports:
|
|
@@ -11,6 +27,19 @@ services:
|
|
|
11
27
|
timeout: 3s
|
|
12
28
|
retries: 10
|
|
13
29
|
|
|
30
|
+
host:
|
|
31
|
+
build:
|
|
32
|
+
context: .
|
|
33
|
+
dockerfile: docker/host.Dockerfile
|
|
34
|
+
env_file: .env
|
|
35
|
+
environment:
|
|
36
|
+
SM_DATABASE_URL: ${SM_DATABASE_URL:-postgresql+asyncpg://sm:sm@postgres:5432/simple_module}
|
|
37
|
+
ports:
|
|
38
|
+
- "8000:8000"
|
|
39
|
+
depends_on:
|
|
40
|
+
postgres:
|
|
41
|
+
condition: service_healthy
|
|
42
|
+
|
|
14
43
|
worker:
|
|
15
44
|
build:
|
|
16
45
|
context: .
|
|
@@ -19,9 +48,12 @@ services:
|
|
|
19
48
|
environment:
|
|
20
49
|
SM_BG_TASKS_BROKER_URL: redis://redis:6379/0
|
|
21
50
|
SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1
|
|
51
|
+
SM_DATABASE_URL: ${SM_DATABASE_URL:-postgresql+asyncpg://sm:sm@postgres:5432/simple_module}
|
|
22
52
|
depends_on:
|
|
23
53
|
redis:
|
|
24
54
|
condition: service_healthy
|
|
55
|
+
postgres:
|
|
56
|
+
condition: service_healthy
|
|
25
57
|
command:
|
|
26
58
|
- "uv"
|
|
27
59
|
- "run"
|
|
@@ -41,6 +73,7 @@ services:
|
|
|
41
73
|
environment:
|
|
42
74
|
SM_BG_TASKS_BROKER_URL: redis://redis:6379/0
|
|
43
75
|
SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1
|
|
76
|
+
SM_DATABASE_URL: ${SM_DATABASE_URL:-postgresql+asyncpg://sm:sm@postgres:5432/simple_module}
|
|
44
77
|
depends_on:
|
|
45
78
|
redis:
|
|
46
79
|
condition: service_healthy
|
|
@@ -57,4 +90,5 @@ services:
|
|
|
57
90
|
- "info"
|
|
58
91
|
|
|
59
92
|
volumes:
|
|
93
|
+
pgdata:
|
|
60
94
|
redisdata:
|
simple_module_cli-0.0.9/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# FastAPI host image. Multi-stage: Node builds the Vite client bundle,
|
|
2
|
+
# Python serves uvicorn. Migrations run on container start.
|
|
3
|
+
|
|
4
|
+
FROM node:22-slim AS frontend
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
COPY package.json package-lock.json* ./
|
|
7
|
+
COPY host/client_app/package.json host/client_app/
|
|
8
|
+
COPY modules/ modules/
|
|
9
|
+
RUN npm ci --workspaces --include-workspace-root
|
|
10
|
+
COPY host/ host/
|
|
11
|
+
RUN npm --workspace host/client_app run build
|
|
12
|
+
|
|
13
|
+
FROM python:3.12-slim AS runtime
|
|
14
|
+
|
|
15
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
16
|
+
PYTHONUNBUFFERED=1 \
|
|
17
|
+
UV_LINK_MODE=copy \
|
|
18
|
+
UV_COMPILE_BYTECODE=1 \
|
|
19
|
+
UV_SYSTEM_PYTHON=1
|
|
20
|
+
|
|
21
|
+
RUN apt-get update \
|
|
22
|
+
&& apt-get install -y --no-install-recommends curl ca-certificates build-essential \
|
|
23
|
+
&& rm -rf /var/lib/apt/lists/* \
|
|
24
|
+
&& pip install --no-cache-dir uv
|
|
25
|
+
|
|
26
|
+
WORKDIR /app
|
|
27
|
+
|
|
28
|
+
COPY pyproject.toml uv.lock* ./
|
|
29
|
+
COPY host/pyproject.toml host/
|
|
30
|
+
COPY modules/ modules/
|
|
31
|
+
RUN uv sync --all-packages --no-dev
|
|
32
|
+
|
|
33
|
+
COPY host/ host/
|
|
34
|
+
COPY --from=frontend /app/host/static/dist host/static/dist
|
|
35
|
+
|
|
36
|
+
RUN useradd --system --uid 10001 --home /app --shell /usr/sbin/nologin app \
|
|
37
|
+
&& chown -R app:app /app
|
|
38
|
+
USER app
|
|
39
|
+
|
|
40
|
+
EXPOSE 8000
|
|
41
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
42
|
+
CMD curl -fsS http://localhost:8000/health || exit 1
|
|
43
|
+
|
|
44
|
+
CMD ["sh", "-c", "cd host && uv run alembic upgrade head && uv run uvicorn main:app --host 0.0.0.0 --port 8000"]
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@inertiajs/react": "^2.0.0",
|
|
12
|
-
"@simple-module-py/i18n": "
|
|
13
|
-
"@simple-module-py/ui": "
|
|
12
|
+
"@simple-module-py/i18n": "{{FRAMEWORK_VERSION}}",
|
|
13
|
+
"@simple-module-py/ui": "{{FRAMEWORK_VERSION}}",
|
|
14
14
|
"react": "^19.0.0",
|
|
15
15
|
"react-dom": "^19.0.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
+
"@simple-module-py/tsconfig": "{{FRAMEWORK_VERSION}}",
|
|
18
19
|
"@tailwindcss/vite": "^4.0.0",
|
|
19
20
|
"@types/node": "^22.0.0",
|
|
20
21
|
"@types/react": "^19.0.0",
|