simple-module-cli 0.0.17__tar.gz → 0.0.19__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.17 → simple_module_cli-0.0.19}/PKG-INFO +2 -2
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/README.md +1 -1
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/pyproject.toml +1 -1
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/app_project.py +19 -51
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/cli.py +17 -3
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/new.py +3 -1
- simple_module_cli-0.0.19/simple_module_cli/pins.py +66 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/scaffolding.py +45 -4
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/README.md +1 -1
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-cli/SKILL.md +3 -2
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-creating/SKILL.md +1 -1
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-database/SKILL.md +3 -15
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-doctor/SKILL.md +7 -4
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-registries/SKILL.md +2 -2
- simple_module_cli-0.0.19/simple_module_cli/templates/host/Makefile +55 -0
- simple_module_cli-0.0.19/simple_module_cli/templates/host/main.py +59 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/migrations/env.py +1 -4
- simple_module_cli-0.0.19/simple_module_cli/templates/host/pyproject.toml.tpl +51 -0
- simple_module_cli-0.0.19/simple_module_cli/templates/workspace/Makefile +68 -0
- simple_module_cli-0.0.19/simple_module_cli/templates/workspace/pyproject.toml.tpl +61 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_new.py +0 -82
- simple_module_cli-0.0.19/tests/test_cli_new_scaffold_layout.py +171 -0
- simple_module_cli-0.0.19/tests/test_module_pages_manifest.py +70 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_scaffolding_host.py +86 -68
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_scaffolding_module.py +57 -0
- simple_module_cli-0.0.17/simple_module_cli/templates/host/Makefile +0 -32
- simple_module_cli-0.0.17/simple_module_cli/templates/host/main.py +0 -46
- simple_module_cli-0.0.17/simple_module_cli/templates/host/pyproject.toml.tpl +0 -18
- simple_module_cli-0.0.17/simple_module_cli/templates/workspace/Makefile +0 -42
- simple_module_cli-0.0.17/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -17
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/.gitignore +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/LICENSE +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_new_regressions.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/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.19
|
|
4
4
|
Summary: Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy 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
|
|
@@ -48,7 +48,7 @@ smpy create-module my_feature # scaffold a publishable module package
|
|
|
48
48
|
smpy create-host bare-host # scaffold a bare host (no opinionated wiring)
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`.
|
|
51
|
+
Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`, `smpy package-update`, and `smpy skills {list,add,update}`.
|
|
52
52
|
|
|
53
53
|
When other framework packages are installed, they contribute additional subcommands via the `simple_module_cli.cli_plugins` entry-point group:
|
|
54
54
|
|
|
@@ -23,7 +23,7 @@ smpy create-module my_feature # scaffold a publishable module package
|
|
|
23
23
|
smpy create-host bare-host # scaffold a bare host (no opinionated wiring)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`.
|
|
26
|
+
Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`, `smpy package-update`, and `smpy skills {list,add,update}`.
|
|
27
27
|
|
|
28
28
|
When other framework packages are installed, they contribute additional subcommands via the `simple_module_cli.cli_plugins` entry-point group:
|
|
29
29
|
|
|
@@ -17,8 +17,6 @@ import json as _json
|
|
|
17
17
|
import secrets as _secrets
|
|
18
18
|
import shutil as _shutil
|
|
19
19
|
from collections.abc import Sequence
|
|
20
|
-
from importlib.metadata import PackageNotFoundError
|
|
21
|
-
from importlib.metadata import version as _pkg_version
|
|
22
20
|
from pathlib import Path
|
|
23
21
|
from typing import Any
|
|
24
22
|
|
|
@@ -32,6 +30,7 @@ from simple_module_cli.scaffolding import (
|
|
|
32
30
|
create_host,
|
|
33
31
|
create_module,
|
|
34
32
|
create_workspace,
|
|
33
|
+
resolve_framework_version,
|
|
35
34
|
)
|
|
36
35
|
|
|
37
36
|
__all__ = ["create_app_project"]
|
|
@@ -40,30 +39,21 @@ _SAMPLE_MODULE_NAME = "hello"
|
|
|
40
39
|
_SAMPLE_MODULE_PKG = _module_to_pypi_name(_SAMPLE_MODULE_NAME)
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
"""Resolve the framework version to pin scaffolded apps against.
|
|
45
|
-
|
|
46
|
-
The CLI ships in lockstep with the rest of the framework (one
|
|
47
|
-
``bump_version.py`` rewrites every ``pyproject.toml`` in the repo), so
|
|
48
|
-
its own installed version is the source of truth. Falling back to a
|
|
49
|
-
placeholder lets editable installs without dist-info still scaffold —
|
|
50
|
-
but that path should never be reached in a release wheel.
|
|
51
|
-
"""
|
|
52
|
-
try:
|
|
53
|
-
return _pkg_version("simple_module_cli")
|
|
54
|
-
except PackageNotFoundError:
|
|
55
|
-
return "0.0.0"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
_FRAMEWORK_VERSION = _resolve_framework_version()
|
|
42
|
+
_FRAMEWORK_VERSION = resolve_framework_version()
|
|
59
43
|
|
|
60
44
|
# Pin ``simple_module_cli`` as a dev dep so ``uv run smpy`` resolves to the
|
|
61
45
|
# project venv. The global ``uv tool`` install runs in its own isolated venv
|
|
62
|
-
# that can't see the project's plugin entry points (issue #134).
|
|
46
|
+
# that can't see the project's plugin entry points (issue #134). The lint /
|
|
47
|
+
# test tooling (ruff, ty, pytest-*) backs the generated `make lint`/`make
|
|
48
|
+
# test` targets so a fresh app can run its own quality gates.
|
|
63
49
|
_APP_PY_DEV_DEPS = [
|
|
64
50
|
f"simple_module_test=={_FRAMEWORK_VERSION}",
|
|
65
51
|
f"simple_module_cli=={_FRAMEWORK_VERSION}",
|
|
66
52
|
"pytest>=8.0",
|
|
53
|
+
"pytest-asyncio>=0.24",
|
|
54
|
+
"pytest-playwright>=0.7.2",
|
|
55
|
+
"ruff>=0.8",
|
|
56
|
+
"ty>=0.0.29",
|
|
67
57
|
]
|
|
68
58
|
|
|
69
59
|
_APP_NPM_DEPS = {
|
|
@@ -120,7 +110,12 @@ def create_app_project(
|
|
|
120
110
|
preserved: list[Path] = []
|
|
121
111
|
if not flat:
|
|
122
112
|
preserved.extend(
|
|
123
|
-
create_workspace(
|
|
113
|
+
create_workspace(
|
|
114
|
+
target,
|
|
115
|
+
name=name,
|
|
116
|
+
framework_version=_FRAMEWORK_VERSION,
|
|
117
|
+
preserve_existing=SAFE_PRESERVED_NAMES,
|
|
118
|
+
)
|
|
124
119
|
)
|
|
125
120
|
preserved.extend(
|
|
126
121
|
create_host(
|
|
@@ -195,11 +190,13 @@ def _scaffold_sample_module(target: Path) -> None:
|
|
|
195
190
|
sample_dest = target / "modules" / _SAMPLE_MODULE_NAME
|
|
196
191
|
if sample_dest.exists():
|
|
197
192
|
return
|
|
198
|
-
|
|
193
|
+
# Pin the sample's framework deps to the exact framework version so the
|
|
194
|
+
# workspace resolves (the template's >=1.0,<2.0 ranges don't exist on PyPI
|
|
195
|
+
# pre-1.0). See GH #195.
|
|
196
|
+
create_module(sample_dest, name=_SAMPLE_MODULE_NAME, framework_version=_FRAMEWORK_VERSION)
|
|
199
197
|
# GitHub only reads workflows from the repo root, so the template's
|
|
200
198
|
# .github/ is dead inside a workspace.
|
|
201
199
|
_shutil.rmtree(sample_dest / ".github")
|
|
202
|
-
_pin_sample_module_deps(sample_dest)
|
|
203
200
|
_seed_static_dist_placeholder(sample_dest / _SAMPLE_MODULE_NAME / "static" / "dist")
|
|
204
201
|
|
|
205
202
|
|
|
@@ -210,35 +207,6 @@ def _seed_static_dist_placeholder(static_dist: Path) -> None:
|
|
|
210
207
|
(static_dist / ".gitkeep").touch()
|
|
211
208
|
|
|
212
209
|
|
|
213
|
-
def _pin_sample_module_deps(sample_dest: Path) -> None:
|
|
214
|
-
"""Replace the module template's future-API range pins with exact pins.
|
|
215
|
-
|
|
216
|
-
The shared ``smpy create-module`` template ships ``>=1.0,<2.0`` against the
|
|
217
|
-
framework's eventual stable line, but the workspace-bundled sample has to
|
|
218
|
-
resolve against whatever the framework version actually is today (``==X``
|
|
219
|
-
in pre-1.0). Without rewriting, ``uv sync`` can't satisfy the workspace.
|
|
220
|
-
"""
|
|
221
|
-
import tomlkit
|
|
222
|
-
|
|
223
|
-
pyproject = sample_dest / "pyproject.toml"
|
|
224
|
-
doc = tomlkit.parse(pyproject.read_text(encoding="utf-8"))
|
|
225
|
-
project = doc.setdefault("project", tomlkit.table())
|
|
226
|
-
project["dependencies"] = [_pin_or_keep(dep) for dep in project.get("dependencies", [])]
|
|
227
|
-
optional = project.get("optional-dependencies")
|
|
228
|
-
if optional is not None:
|
|
229
|
-
for extra, deps in list(optional.items()):
|
|
230
|
-
optional[extra] = [_pin_or_keep(dep) for dep in deps]
|
|
231
|
-
pyproject.write_text(tomlkit.dumps(doc), encoding="utf-8")
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _pin_or_keep(dep: str) -> str:
|
|
235
|
-
"""Pin a ``simple_module_*`` requirement to the framework version; pass through otherwise."""
|
|
236
|
-
pkg = dep.split(">=", 1)[0].split("==", 1)[0].split("<", 1)[0].strip()
|
|
237
|
-
if pkg.startswith(("simple_module_", "simple-module-")):
|
|
238
|
-
return f"{pkg}=={_FRAMEWORK_VERSION}"
|
|
239
|
-
return dep
|
|
240
|
-
|
|
241
|
-
|
|
242
210
|
def _db_url(db: str, slug: str, *, flat: bool) -> str:
|
|
243
211
|
if db == "postgres":
|
|
244
212
|
return f"postgresql+asyncpg://postgres:postgres@localhost:5432/{slug}"
|
|
@@ -23,6 +23,7 @@ from simple_module_cli.package_update import package_update
|
|
|
23
23
|
from simple_module_cli.plugins import discover_and_mount
|
|
24
24
|
from simple_module_cli.scaffolding import create_host as _create_host
|
|
25
25
|
from simple_module_cli.scaffolding import create_module as _create_module
|
|
26
|
+
from simple_module_cli.scaffolding import resolve_framework_version
|
|
26
27
|
from simple_module_cli.skills_cmd import app as skills_app
|
|
27
28
|
|
|
28
29
|
app = typer.Typer(
|
|
@@ -55,7 +56,17 @@ def create_host(
|
|
|
55
56
|
target = dest or Path.cwd() / name
|
|
56
57
|
selected = [m.strip() for m in modules.split(",") if m.strip()]
|
|
57
58
|
try:
|
|
58
|
-
|
|
59
|
+
# Pin the host's framework + module deps to the installed framework
|
|
60
|
+
# version so the generated host's first `uv sync` resolves (the
|
|
61
|
+
# template's >=1.0,<2.0 / >=0.1,<1.0 ranges don't exist pre-1.0). The
|
|
62
|
+
# workspace `smpy new` path rewrites these via _rewrite_pyproject, but
|
|
63
|
+
# standalone create-host never did — see GH #206.
|
|
64
|
+
_create_host(
|
|
65
|
+
target,
|
|
66
|
+
name=name,
|
|
67
|
+
modules=selected,
|
|
68
|
+
framework_version=resolve_framework_version(),
|
|
69
|
+
)
|
|
59
70
|
except FileExistsError as exc:
|
|
60
71
|
typer.echo(f"ERROR: {exc}", err=True)
|
|
61
72
|
raise typer.Exit(code=1) from exc
|
|
@@ -68,7 +79,7 @@ def create_host(
|
|
|
68
79
|
typer.echo(" uv sync")
|
|
69
80
|
typer.echo(" cp .env.example .env")
|
|
70
81
|
typer.echo(' alembic revision --autogenerate -m "initial schema"')
|
|
71
|
-
typer.echo(" alembic upgrade
|
|
82
|
+
typer.echo(" alembic upgrade heads")
|
|
72
83
|
typer.echo(" python main.py")
|
|
73
84
|
|
|
74
85
|
|
|
@@ -85,7 +96,10 @@ def create_module(
|
|
|
85
96
|
package = slug.replace("-", "_")
|
|
86
97
|
target = dest or Path.cwd() / f"simple_module_{package}"
|
|
87
98
|
try:
|
|
88
|
-
|
|
99
|
+
# Pin framework deps to the installed framework version so the module
|
|
100
|
+
# resolves against the app that created it (the template's >=1.0,<2.0
|
|
101
|
+
# ranges don't exist on PyPI pre-1.0). See GH #195.
|
|
102
|
+
_create_module(target, name=name, framework_version=resolve_framework_version())
|
|
89
103
|
except FileExistsError as exc:
|
|
90
104
|
typer.echo(f"ERROR: {exc}", err=True)
|
|
91
105
|
raise typer.Exit(code=1) from exc
|
|
@@ -164,7 +164,9 @@ def new_project(
|
|
|
164
164
|
return
|
|
165
165
|
|
|
166
166
|
_bootstrap_initial_migration(host_dir)
|
|
167
|
-
|
|
167
|
+
# `heads` (plural) applies every per-module branch head; `head` (singular)
|
|
168
|
+
# errors once a second module ships its own migration branch label.
|
|
169
|
+
subprocess.run([*_ALEMBIC, "upgrade", "heads"], cwd=host_dir, check=False)
|
|
168
170
|
typer.echo("\nSetup complete. Run `make dev` in the new directory.")
|
|
169
171
|
if "background_tasks" in resolved:
|
|
170
172
|
typer.echo("For background jobs, also run: docker compose up -d redis worker beat")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Resolve and pin the framework version that scaffolded apps depend on.
|
|
2
|
+
|
|
3
|
+
Kept separate from :mod:`simple_module_cli.scaffolding` (template
|
|
4
|
+
materialization) because version resolution + dependency pinning is its own
|
|
5
|
+
concern: the scaffolders, ``cli`` commands, and ``app_project`` all reach for
|
|
6
|
+
it independently. The module templates ship forward-looking ``simple_module_*``
|
|
7
|
+
ranges (``>=1.0,<2.0`` / ``>=0.1,<1.0``) against the framework's eventual stable
|
|
8
|
+
line, but the published distributions are pre-1.0 (``0.0.x``), so those ranges
|
|
9
|
+
resolve to nothing on PyPI. Rewriting them to an exact ``==`` pin lets a freshly
|
|
10
|
+
scaffolded app/module resolve against the framework version that created it.
|
|
11
|
+
See GH #195 / #206.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_framework_version() -> str:
|
|
20
|
+
"""Resolve the framework version that scaffolded apps should pin against.
|
|
21
|
+
|
|
22
|
+
The CLI ships in lockstep with the rest of the framework (one
|
|
23
|
+
``bump_version.py`` rewrites every ``pyproject.toml``), so its own
|
|
24
|
+
installed distribution version is the source of truth. Falls back to a
|
|
25
|
+
placeholder for editable installs lacking dist-info — never reached from a
|
|
26
|
+
release wheel.
|
|
27
|
+
"""
|
|
28
|
+
from importlib.metadata import PackageNotFoundError
|
|
29
|
+
from importlib.metadata import version as pkg_version
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
return pkg_version("simple_module_cli")
|
|
33
|
+
except PackageNotFoundError:
|
|
34
|
+
return "0.0.0"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _pin_one(dep: str, version: str) -> str:
|
|
38
|
+
"""Pin a single ``simple_module_*`` requirement to ``==version``; else pass through."""
|
|
39
|
+
pkg = dep.split(">=", 1)[0].split("==", 1)[0].split("<", 1)[0].strip()
|
|
40
|
+
if pkg.startswith(("simple_module_", "simple-module-")):
|
|
41
|
+
return f"{pkg}=={version}"
|
|
42
|
+
return dep
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pin_framework_deps(pyproject_path: Path, version: str) -> None:
|
|
46
|
+
"""Pin every ``simple_module_*`` requirement in a pyproject to ``==version``.
|
|
47
|
+
|
|
48
|
+
Rewrites both ``dependencies`` and every ``optional-dependencies`` extra
|
|
49
|
+
(the module template's ``dev`` extra pins ``simple_module_test``). Used by
|
|
50
|
+
``create_module`` and ``create_host`` so a freshly scaffolded package
|
|
51
|
+
resolves against the framework version that created it. See GH #195 / #206.
|
|
52
|
+
"""
|
|
53
|
+
import tomlkit
|
|
54
|
+
|
|
55
|
+
doc = tomlkit.parse(pyproject_path.read_text(encoding="utf-8"))
|
|
56
|
+
project = doc.get("project")
|
|
57
|
+
if project is None:
|
|
58
|
+
return
|
|
59
|
+
deps = project.get("dependencies")
|
|
60
|
+
if deps is not None:
|
|
61
|
+
project["dependencies"] = [_pin_one(dep, version) for dep in deps]
|
|
62
|
+
optional = project.get("optional-dependencies")
|
|
63
|
+
if optional is not None:
|
|
64
|
+
for extra, items in list(optional.items()):
|
|
65
|
+
optional[extra] = [_pin_one(dep, version) for dep in items]
|
|
66
|
+
pyproject_path.write_text(tomlkit.dumps(doc), encoding="utf-8")
|
|
@@ -28,11 +28,17 @@ from simple_module_cli.case import (
|
|
|
28
28
|
validate_scaffold_name,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
# Re-exported so the scaffolders and their long-standing callers keep importing
|
|
32
|
+
# version pinning from one place; the implementations live in pins.py.
|
|
33
|
+
from simple_module_cli.pins import pin_framework_deps, resolve_framework_version
|
|
34
|
+
|
|
31
35
|
__all__ = [
|
|
32
36
|
"SAFE_PRESERVED_NAMES",
|
|
33
37
|
"create_host",
|
|
34
38
|
"create_module",
|
|
35
39
|
"create_workspace",
|
|
40
|
+
"pin_framework_deps",
|
|
41
|
+
"resolve_framework_version",
|
|
36
42
|
]
|
|
37
43
|
|
|
38
44
|
logger = logging.getLogger(__name__)
|
|
@@ -55,6 +61,17 @@ def _module_to_pypi_name(name: str) -> str:
|
|
|
55
61
|
return f"simple_module_{name.lower()}"
|
|
56
62
|
|
|
57
63
|
|
|
64
|
+
def _should_pin_framework_version(version: str | None) -> bool:
|
|
65
|
+
"""Whether ``version`` is a concrete pin rather than a skip sentinel.
|
|
66
|
+
|
|
67
|
+
Both scaffolders skip pinning for ``None`` (a library caller that wants the
|
|
68
|
+
template's ranges kept verbatim) and ``"*"`` (the npm-wildcard default,
|
|
69
|
+
which would otherwise render the invalid PEP 508 specifier ``==*``). One
|
|
70
|
+
rule, so the two paths can't drift. See GH #195 / #206.
|
|
71
|
+
"""
|
|
72
|
+
return bool(version) and version != "*"
|
|
73
|
+
|
|
74
|
+
|
|
58
75
|
def _iter_template_files(template_root: Path):
|
|
59
76
|
"""Yield every file under ``template_root``. Skips ``_optional/`` paths."""
|
|
60
77
|
for path in template_root.rglob("*"):
|
|
@@ -124,15 +141,20 @@ def create_workspace(
|
|
|
124
141
|
dest: Path,
|
|
125
142
|
name: str,
|
|
126
143
|
template_root: Path | None = None,
|
|
144
|
+
framework_version: str = "*",
|
|
127
145
|
*,
|
|
128
146
|
preserve_existing: frozenset[str] = frozenset(),
|
|
129
147
|
) -> list[Path]:
|
|
130
148
|
"""Materialize the workspace-root shell at ``dest``; return preserved paths.
|
|
131
149
|
|
|
132
|
-
Lays down the top-level ``pyproject.toml`` (uv workspace
|
|
133
|
-
|
|
134
|
-
``.gitignore``, and ``README.md``.
|
|
135
|
-
modules — those go under ``dest/host`` and
|
|
150
|
+
Lays down the top-level ``pyproject.toml`` (uv workspace + dev tooling for
|
|
151
|
+
``make test``/``lint``), ``package.json`` (npm workspace), ``Makefile``
|
|
152
|
+
(delegates to host), ``.env.example``, ``.gitignore``, and ``README.md``.
|
|
153
|
+
Does NOT create the host or any modules — those go under ``dest/host`` and
|
|
154
|
+
``dest/modules/`` afterwards.
|
|
155
|
+
|
|
156
|
+
``framework_version`` pins ``simple_module_test`` in the root dev group;
|
|
157
|
+
defaults to ``"*"`` for callers that don't need an exact pin.
|
|
136
158
|
|
|
137
159
|
``preserve_existing`` lists top-level entry names that may already exist
|
|
138
160
|
in ``dest``; the scaffold's copy is skipped and the preserved path is
|
|
@@ -147,6 +169,7 @@ def create_workspace(
|
|
|
147
169
|
{
|
|
148
170
|
"{{HOST_NAME}}": validate_scaffold_name(name),
|
|
149
171
|
"{{HOST_PYPI_NAME}}": to_kebab_case(name),
|
|
172
|
+
"{{FRAMEWORK_VERSION}}": framework_version,
|
|
150
173
|
},
|
|
151
174
|
preserve_existing=preserve_existing,
|
|
152
175
|
)
|
|
@@ -181,6 +204,12 @@ def create_host(
|
|
|
181
204
|
},
|
|
182
205
|
preserve_existing=preserve_existing,
|
|
183
206
|
)
|
|
207
|
+
# Pin every simple_module_* host dep (framework packages *and* selected
|
|
208
|
+
# bundled modules) to the lockstep version so the host's first `uv sync`
|
|
209
|
+
# resolves — the template's >=1.0,<2.0 / >=0.1,<1.0 ranges match nothing
|
|
210
|
+
# against pre-1.0 dists. See GH #206.
|
|
211
|
+
if _should_pin_framework_version(framework_version):
|
|
212
|
+
pin_framework_deps(dest / "pyproject.toml", framework_version)
|
|
184
213
|
logger.info(
|
|
185
214
|
"Scaffolded host '%s' at %s (modules: %s)", name, dest, ", ".join(modules) or "<none>"
|
|
186
215
|
)
|
|
@@ -191,7 +220,17 @@ def create_module(
|
|
|
191
220
|
dest: Path,
|
|
192
221
|
name: str,
|
|
193
222
|
template_root: Path | None = None,
|
|
223
|
+
*,
|
|
224
|
+
framework_version: str | None = None,
|
|
194
225
|
) -> Path:
|
|
226
|
+
"""Scaffold a module package at ``dest``.
|
|
227
|
+
|
|
228
|
+
When ``framework_version`` is a concrete version, the template's
|
|
229
|
+
forward-looking ``simple_module_*`` ranges are rewritten to an exact pin so
|
|
230
|
+
the module resolves against that framework version (e.g. ``uv add`` into the
|
|
231
|
+
workspace that created it). Left as ``None`` (or the ``"*"`` sentinel), the
|
|
232
|
+
template's ranges are kept verbatim. See GH #195.
|
|
233
|
+
"""
|
|
195
234
|
dest = Path(dest)
|
|
196
235
|
existed_before = dest.exists()
|
|
197
236
|
_require_empty_dest(dest)
|
|
@@ -210,6 +249,8 @@ def create_module(
|
|
|
210
249
|
},
|
|
211
250
|
path_rewrites={_PACKAGE_PATH_TOKEN: package_name},
|
|
212
251
|
)
|
|
252
|
+
if _should_pin_framework_version(framework_version):
|
|
253
|
+
pin_framework_deps(dest / "pyproject.toml", framework_version)
|
|
213
254
|
except Exception:
|
|
214
255
|
# Rollback so a half-scaffolded directory doesn't leave the user
|
|
215
256
|
# with an unparseable Python package and the impression that a
|
|
@@ -47,7 +47,7 @@ The CLI is [vercel-labs/skills](https://github.com/vercel-labs/skills); see its
|
|
|
47
47
|
| [simple-module-locales](./simple-module-locales/SKILL.md) | Adding or debugging i18n in a module — `locale_dirs()`, namespaces, CLDR plurals, the Zod-in-hook rule |
|
|
48
48
|
| [simple-module-registries](./simple-module-registries/SKILL.md) | Contributing menu items, permissions, feature flags, or events from a module |
|
|
49
49
|
| [simple-module-testing](./simple-module-testing/SKILL.md) | Writing pytest tests — picking the right fixture (`db_session` / `app` / `authenticated_client`), single-test runs, e2e |
|
|
50
|
-
| [simple-module-doctor](./simple-module-doctor/SKILL.md) | Interpreting a diagnostic code (`SM001`–`
|
|
50
|
+
| [simple-module-doctor](./simple-module-doctor/SKILL.md) | Interpreting a diagnostic code (`SM001`–`SM021`) printed at boot |
|
|
51
51
|
|
|
52
52
|
The skills are designed to stand alone — install them into any host or module-package project and they'll work without needing access to the framework's source repo.
|
|
53
53
|
|
|
@@ -5,7 +5,7 @@ description: Use when invoking the `smpy` CLI for a simple_module_python project
|
|
|
5
5
|
|
|
6
6
|
# simple_module_python: the `smpy` CLI
|
|
7
7
|
|
|
8
|
-
The `smpy` command is provided by `simple_module_cli` (installed as a dep of `simple_module_hosting`). It groups
|
|
8
|
+
The `smpy` command is provided by `simple_module_cli` (installed as a dep of `simple_module_hosting`). It groups several kinds of operations: scaffolding new things, project-time helpers for the host, admin shortcuts for the bundled modules, installing the bundled agent skills, and bumping `simple_module_*` deps.
|
|
9
9
|
|
|
10
10
|
## Top-level commands
|
|
11
11
|
|
|
@@ -14,6 +14,7 @@ The `smpy` command is provided by `simple_module_cli` (installed as a dep of `si
|
|
|
14
14
|
| `smpy new <name>` | Greenfield: scaffold a complete app (host + selected modules) in one shot, with an interactive wizard for DB / tenancy / module preset |
|
|
15
15
|
| `smpy create-host <name>` | You want just a bare host project; you'll add modules later by `pip install`-ing them |
|
|
16
16
|
| `smpy create-module <name>` | You're authoring a publishable module package (separate repo, distributed via PyPI) |
|
|
17
|
+
| `smpy package-update` | Bump every `simple_module_*` / `simple-module-*` dependency in `pyproject.toml` to its latest non-yanked PyPI release (workspace/path/git/URL sources are left untouched) |
|
|
17
18
|
| `smpy skills …` | Install / update the bundled agent-skill packs into a project (`add`, `list`, `update`) |
|
|
18
19
|
| `smpy host …` | Project-time helpers run from inside a host directory (page manifest, JS dep sync) |
|
|
19
20
|
| `smpy settings …` | Settings-module admin — currently `import-from-env` |
|
|
@@ -47,7 +48,7 @@ smpy new MyApp --no-install
|
|
|
47
48
|
| `full` | every module in the catalog |
|
|
48
49
|
| `custom` | interactive — pick each module yes/no |
|
|
49
50
|
|
|
50
|
-
`--with` accepts a comma-separated list of catalog keys (`auth, users, permissions,
|
|
51
|
+
`--with` accepts a comma-separated list of catalog keys (`auth, users, permissions, dashboard, settings, feature_flags, file_storage, background_tasks`). Transitive `requires` are auto-added; the wizard prints `Added X (required by Y)` so you can see what got pulled in.
|
|
51
52
|
|
|
52
53
|
**Options summary:**
|
|
53
54
|
|
|
@@ -68,7 +68,7 @@ class OrdersModule(ModuleBase):
|
|
|
68
68
|
)
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
`ModuleMeta.name` is load-bearing in
|
|
71
|
+
`ModuleMeta.name` is load-bearing in two places: the `__tablename__` prefix you author and the PascalCase Inertia component namespace. So directory `blog_posts` → `name="BlogPosts"` → `inertia.render("BlogPosts/Index", ...)` → `pages/Index.tsx`. Mismatches fire diagnostic codes `SM003` (orphan page) / `SM004` (phantom render).
|
|
72
72
|
|
|
73
73
|
For modules you intend to publish, also add `version=` (your module's semver) and `requires_framework=` (a PEP 440 spec for the framework API range, e.g. `">=1.0,<2.0"`) so the host can reject incompatible installs at boot.
|
|
74
74
|
|
|
@@ -18,26 +18,14 @@ from simple_module_db.mixins import AuditMixin
|
|
|
18
18
|
Base = create_module_base("orders")
|
|
19
19
|
|
|
20
20
|
class Order(Base, AuditMixin, table=True):
|
|
21
|
-
__tablename__ = "orders_order" #
|
|
21
|
+
__tablename__ = "orders_order" # module-name prefix; required
|
|
22
22
|
id: int | None = Field(default=None, primary_key=True)
|
|
23
23
|
name: str = Field(max_length=200)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
## Table naming
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
from simple_module_db.base import create_module_base, DatabaseProvider
|
|
30
|
-
Base = create_module_base("orders", provider=DatabaseProvider.SQLITE)
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Postgres vs SQLite — the same code, different physical layout
|
|
34
|
-
|
|
35
|
-
| Provider | Physical placement | Required convention |
|
|
36
|
-
|---|---|---|
|
|
37
|
-
| **PostgreSQL** | One **schema** per module (`orders.order`, `users.user`). Created automatically. | Set `__tablename__` to a name unique within the module. |
|
|
38
|
-
| **SQLite** | Single schema. Cross-module name collisions break things. | Prefix `__tablename__` with the module name (`orders_order`). |
|
|
39
|
-
|
|
40
|
-
Always include the module-name prefix in `__tablename__`. It's redundant on Postgres (the schema already namespaces it) and **required** on SQLite. Code that runs in CI on SQLite and prod on Postgres needs both — pick the prefix.
|
|
28
|
+
All modules — on Postgres and SQLite alike — share the host's single schema. Cross-module name collisions break things, so `__tablename__` must be prefixed with the module name (e.g. `orders_order`, `users_user`). The framework does not enforce the prefix; it's a convention the migrations and diagnostics rely on.
|
|
41
29
|
|
|
42
30
|
## Mixins
|
|
43
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: simple-module-doctor
|
|
3
|
-
description: Use when interpreting a simple_module_python diagnostic code (SM001–
|
|
3
|
+
description: Use when interpreting a simple_module_python diagnostic code (SM001–SM021) printed at boot or by the diagnostics runner — what the code means, whether it's blocking, and the concrete fix. Triggers on any "SMnnn" code in logs, "InvalidModuleError", "module silently doesn't load", or "production failed boot".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# simple_module_python: diagnostics
|
|
@@ -17,7 +17,7 @@ You can also call `run_diagnostics(...)` from `simple_module_core` programmatica
|
|
|
17
17
|
| **SM003** | WARNING | A `pages/<Name>.tsx` exists but no `inertia.render("<Module>/<Name>", ...)` references it | Either delete the orphan file or wire up the render call |
|
|
18
18
|
| **SM004** | WARNING | `inertia.render("<Module>/<Name>", ...)` is called but no matching `.tsx` exists | Fix the render-key typo or create the page file |
|
|
19
19
|
| **SM007** | INFO | Module overrides no `register_*` hooks at all — likely scaffolded but empty | Either implement at least one hook or delete the module if unused |
|
|
20
|
-
| **SM008** | ERROR | Two modules declare the same `ModuleMeta.name` (
|
|
20
|
+
| **SM008** | ERROR | Two modules declare the same `ModuleMeta.name` (their lowercased names would collide as table prefixes in the shared schema) | Rename one module's `meta.name` |
|
|
21
21
|
| **SM009** | ERROR | A `framework/*` package directly imports from a plugin module (`modules/*`) | Move the symbol *up* into `simple_module_core` / `simple_module_hosting`, or invert the dependency via the event bus / a registry |
|
|
22
22
|
| **SM010** | ERROR | Live DB revision is behind the migration head | Run `alembic upgrade head` before booting; in CI, ensure migrations are part of the deploy step |
|
|
23
23
|
| **SM011** | WARNING | A module declares a SQLModel table that has no entry in migration history | Run `alembic revision --autogenerate -m "..."` and apply |
|
|
@@ -27,9 +27,12 @@ You can also call `run_diagnostics(...)` from `simple_module_core` programmatica
|
|
|
27
27
|
| **SM015** | WARNING | A non-default locale has keys *not* present in the default | Remove dead keys from the non-default file or add them to default |
|
|
28
28
|
| **SM016** | ERROR | A locale JSON file is invalid or contains non-string leaves | Fix the JSON; only string leaves are allowed (interpolation is `{name}` placeholders) |
|
|
29
29
|
| **SM017** | WARNING | A module ships `.tsx` pages but is missing `package.json` / `tsconfig.json` | Add the JS workspace files so the host's frontend toolchain can resolve module imports |
|
|
30
|
-
| **SM018** | WARNING | An Inertia page calls `router.{post,patch,put,delete}()` targeting `/api/*` |
|
|
30
|
+
| **SM018** | WARNING | An Inertia page calls `router.{post,patch,put,delete}()` targeting `/api/*` | Point the call at a view endpoint that returns `RedirectResponse(..., status_code=303)`, or use plain `fetch()` for the JSON endpoint; Inertia's client expects an Inertia response, not JSON |
|
|
31
|
+
| **SM019** | WARNING | A module registers view routes (non-empty `view_prefix` + overrides `register_routes`) but overrides neither `register_menu_items` nor `register_permissions` — its pages exist with no sidebar entry and no role-editor visibility | Add `register_menu_items()` (sidebar) or `register_permissions()` (role editor), or clear `view_prefix` if the module is API-only |
|
|
32
|
+
| **SM020** | ERROR | More than one auth provider module is installed | Install exactly one auth provider (e.g. `users` OR `keycloak`, not both) |
|
|
33
|
+
| **SM021** | WARNING | No auth provider module is installed | Install an auth provider module (e.g. `simple-module-users` or `simple-module-keycloak`) |
|
|
31
34
|
|
|
32
|
-
Codes `SM002`, `SM005`, `SM006` are reserved/retired
|
|
35
|
+
Codes `SM002`, `SM005`, `SM006`, `SM012` are not part of this table — `SM002`/`SM005`/`SM006` are reserved/retired, and `SM012` (`register_settings` overridden but nothing on `app.state.<module_lower>`, WARNING, dev boot only) is raised from the hosting layer, not the core diagnostics runner. Output format is one line per finding, e.g. `[SM009] ERROR: <subject>`.
|
|
33
36
|
|
|
34
37
|
Warnings are load-bearing: the framework only emits one when something concrete *will* break under a specific condition (locale switch, schema downgrade, deploy ordering). Ignored long enough they become the next on-call page. If you're suppressing warnings in CI to make it green, you're trading the CI signal for a production-boot failure later.
|
|
35
38
|
|
|
@@ -47,7 +47,7 @@ class OrdersModule(ModuleBase):
|
|
|
47
47
|
|
|
48
48
|
The runtime expansion (role → permissions) is cached. `register_permissions` is called once at boot; mutating the registry afterwards bypasses the cache and invalidates user sessions until the cache TTL elapses. Don't mutate at request time.
|
|
49
49
|
|
|
50
|
-
To check inside an endpoint, depend on
|
|
50
|
+
To check inside an endpoint, depend on `RequiresPermission("<module>.<action>")` (from `simple_module_hosting.permissions`), not by reading the registry by hand. The dependency handles wildcard expansion and the 401-vs-403 distinction.
|
|
51
51
|
|
|
52
52
|
## Feature flags — `register_feature_flags(registry: FeatureFlagRegistry)`
|
|
53
53
|
|
|
@@ -132,7 +132,7 @@ Module A consuming Module B's events should import only from `b.contracts.events
|
|
|
132
132
|
## Pitfalls
|
|
133
133
|
|
|
134
134
|
- **Mutated a registry after boot.** Boot-phase only. Cached views (menus, role→permission map) aren't invalidated for live requests; mutations look fine in dev with auto-reload and silently rot in prod.
|
|
135
|
-
- **Raw permission strings in endpoints (`request.state.user.permissions`).** Use `
|
|
135
|
+
- **Raw permission strings in endpoints (`request.state.user.permissions`).** Use `RequiresPermission(...)`. The dependency handles wildcard expansion and the 401-vs-403 distinction.
|
|
136
136
|
- **Forgot a feature flag's `default_enabled=False`.** A flag added with `default_enabled=True` is on for every tenant on first deploy — defeats the point of gating a rollout. Default to `False`; flip via override after the rollout window.
|
|
137
137
|
- **Subscribed to an event in `register_settings` instead of `register_event_handlers`.** `register_settings` runs **before** the event bus is constructed; the subscription silently no-ops.
|
|
138
138
|
- **Used `publish_nowait` inside a request handler that needs the listener to commit a DB row in the same transaction.** It returns immediately — the handler runs after the request has already committed/rolled back. For "in this request, do X then Y", just call Y directly.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
.PHONY: install dev dev-api dev-ui build test test-py test-js lint doctor migrate migration gen-pages sync-js-deps
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync
|
|
5
|
+
cd client_app && npm install
|
|
6
|
+
$(MAKE) sync-js-deps
|
|
7
|
+
|
|
8
|
+
dev: gen-pages
|
|
9
|
+
@echo "Starting API and UI dev servers..."
|
|
10
|
+
$(MAKE) -j2 dev-api dev-ui
|
|
11
|
+
|
|
12
|
+
dev-api:
|
|
13
|
+
uv run uvicorn main:app --reload --port 8000
|
|
14
|
+
|
|
15
|
+
dev-ui:
|
|
16
|
+
cd client_app && npm run dev
|
|
17
|
+
|
|
18
|
+
build:
|
|
19
|
+
cd client_app && npm run build
|
|
20
|
+
|
|
21
|
+
# Testing
|
|
22
|
+
test: test-py test-js
|
|
23
|
+
|
|
24
|
+
test-py:
|
|
25
|
+
uv run pytest
|
|
26
|
+
|
|
27
|
+
# --if-present is a no-op until you add a "test" script (e.g. vitest) to package.json.
|
|
28
|
+
test-js:
|
|
29
|
+
cd client_app && npm run test --if-present
|
|
30
|
+
|
|
31
|
+
# Lint + typecheck (Python). Mirrors the framework's own quality gate.
|
|
32
|
+
lint:
|
|
33
|
+
uv run ruff format --check .
|
|
34
|
+
uv run ruff check .
|
|
35
|
+
uv run ty check
|
|
36
|
+
|
|
37
|
+
# Module diagnostics — the same checks that run at prod boot (orphan pages,
|
|
38
|
+
# coupling violations, migration drift, locale issues).
|
|
39
|
+
doctor:
|
|
40
|
+
uv run python -m simple_module_core
|
|
41
|
+
|
|
42
|
+
gen-pages:
|
|
43
|
+
uv run python -m simple_module_hosting gen-pages --host-dir=client_app
|
|
44
|
+
|
|
45
|
+
sync-js-deps:
|
|
46
|
+
uv run python -m simple_module_hosting sync-js-deps --host-client-app=client_app
|
|
47
|
+
|
|
48
|
+
# `upgrade heads` (plural) applies every per-module branch head; `upgrade head`
|
|
49
|
+
# (singular) errors once a second module adds its own migration branch label.
|
|
50
|
+
migrate:
|
|
51
|
+
uv run alembic upgrade heads
|
|
52
|
+
|
|
53
|
+
migration:
|
|
54
|
+
@test -n "$(msg)" || (echo 'Usage: make migration msg="describe the change"' && exit 1)
|
|
55
|
+
uv run alembic revision --autogenerate -m "$(msg)"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Host application entry point.
|
|
2
|
+
|
|
3
|
+
This file was generated by `smpy create-host`. Modules are discovered at boot
|
|
4
|
+
via entry_points; add them to this host's pyproject.toml to install them.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from simple_module_core.dotenv import load_dotenv_into_environ
|
|
12
|
+
|
|
13
|
+
# Pin this host directory on ``sys.path`` as an ABSOLUTE path *before* the
|
|
14
|
+
# chdir below, so ``from routes import ...`` resolves no matter what the cwd
|
|
15
|
+
# is. The scaffolded Makefile launches the app as ``cd host && uvicorn
|
|
16
|
+
# main:app``; uvicorn puts the launch cwd on ``sys.path`` as the empty string
|
|
17
|
+
# ``''`` (resolved lazily against the *current* cwd). Once we chdir to the
|
|
18
|
+
# repo root, that ``''`` entry points at the wrong directory and the sibling
|
|
19
|
+
# ``routes`` module is no longer importable — and the ``--reload`` subprocess
|
|
20
|
+
# re-imports via the same path, so it breaks there too. See GH #194.
|
|
21
|
+
_HOST_DIR = Path(__file__).resolve().parent
|
|
22
|
+
_REPO_ROOT = _HOST_DIR.parent
|
|
23
|
+
if str(_HOST_DIR) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(_HOST_DIR))
|
|
25
|
+
|
|
26
|
+
# Resolve the workspace root from this file's location so the web process
|
|
27
|
+
# behaves the same regardless of where uvicorn was launched (``uv run
|
|
28
|
+
# --project host`` or a wheel deployment may run from elsewhere). chdir up
|
|
29
|
+
# front so cwd-relative paths in ``.env`` (e.g.
|
|
30
|
+
# ``sqlite+aiosqlite:///./host/app.db``) resolve consistently; load ``.env``
|
|
31
|
+
# into ``os.environ`` so framework code reading ``os.environ.get("SM_…")``
|
|
32
|
+
# directly sees the same values pydantic does.
|
|
33
|
+
os.chdir(_REPO_ROOT)
|
|
34
|
+
load_dotenv_into_environ(_REPO_ROOT / ".env")
|
|
35
|
+
|
|
36
|
+
from simple_module_hosting import Settings, create_app # noqa: E402
|
|
37
|
+
from simple_module_hosting.logging import setup_logging # noqa: E402
|
|
38
|
+
|
|
39
|
+
from routes import router as host_router # noqa: E402
|
|
40
|
+
|
|
41
|
+
settings = Settings()
|
|
42
|
+
|
|
43
|
+
setup_logging(
|
|
44
|
+
level=settings.log_level,
|
|
45
|
+
json_format=settings.log_format == "json",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
app = create_app(settings)
|
|
49
|
+
app.include_router(host_router)
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
import uvicorn
|
|
53
|
+
|
|
54
|
+
uvicorn.run(
|
|
55
|
+
"main:app",
|
|
56
|
+
host="0.0.0.0",
|
|
57
|
+
port=8000,
|
|
58
|
+
reload=settings.is_development,
|
|
59
|
+
)
|