simple-module-cli 0.0.18__tar.gz → 0.0.20__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.18 → simple_module_cli-0.0.20}/.gitignore +1 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/PKG-INFO +2 -2
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/README.md +1 -1
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/pyproject.toml +1 -1
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/app_project.py +27 -55
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/cli.py +40 -3
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/new.py +3 -1
- simple_module_cli-0.0.20/simple_module_cli/pins.py +66 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/scaffolding.py +80 -4
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/README.md +1 -1
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-cli/SKILL.md +3 -2
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-doctor/SKILL.md +7 -4
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-registries/SKILL.md +2 -2
- simple_module_cli-0.0.20/simple_module_cli/templates/host/Makefile +55 -0
- simple_module_cli-0.0.20/simple_module_cli/templates/host/main.py +59 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/migrations/env.py +1 -4
- simple_module_cli-0.0.20/simple_module_cli/templates/host/pyproject.toml.tpl +51 -0
- simple_module_cli-0.0.20/simple_module_cli/templates/workspace/Makefile +68 -0
- simple_module_cli-0.0.20/simple_module_cli/templates/workspace/pyproject.toml.tpl +61 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_new.py +0 -82
- simple_module_cli-0.0.20/tests/test_cli_new_scaffold_layout.py +171 -0
- simple_module_cli-0.0.20/tests/test_create_module_ci_skip.py +107 -0
- simple_module_cli-0.0.20/tests/test_module_pages_manifest.py +70 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_scaffolding_host.py +86 -68
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_scaffolding_module.py +57 -0
- simple_module_cli-0.0.18/simple_module_cli/templates/host/Makefile +0 -32
- simple_module_cli-0.0.18/simple_module_cli/templates/host/main.py +0 -46
- simple_module_cli-0.0.18/simple_module_cli/templates/host/pyproject.toml.tpl +0 -18
- simple_module_cli-0.0.18/simple_module_cli/templates/workspace/Makefile +0 -42
- simple_module_cli-0.0.18/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -17
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/LICENSE +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_new_regressions.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/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.20
|
|
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
|
|
|
@@ -15,10 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import json as _json
|
|
17
17
|
import secrets as _secrets
|
|
18
|
-
import shutil as _shutil
|
|
19
18
|
from collections.abc import Sequence
|
|
20
|
-
from importlib.metadata import PackageNotFoundError
|
|
21
|
-
from importlib.metadata import version as _pkg_version
|
|
22
19
|
from pathlib import Path
|
|
23
20
|
from typing import Any
|
|
24
21
|
|
|
@@ -32,6 +29,7 @@ from simple_module_cli.scaffolding import (
|
|
|
32
29
|
create_host,
|
|
33
30
|
create_module,
|
|
34
31
|
create_workspace,
|
|
32
|
+
resolve_framework_version,
|
|
35
33
|
)
|
|
36
34
|
|
|
37
35
|
__all__ = ["create_app_project"]
|
|
@@ -40,30 +38,21 @@ _SAMPLE_MODULE_NAME = "hello"
|
|
|
40
38
|
_SAMPLE_MODULE_PKG = _module_to_pypi_name(_SAMPLE_MODULE_NAME)
|
|
41
39
|
|
|
42
40
|
|
|
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()
|
|
41
|
+
_FRAMEWORK_VERSION = resolve_framework_version()
|
|
59
42
|
|
|
60
43
|
# Pin ``simple_module_cli`` as a dev dep so ``uv run smpy`` resolves to the
|
|
61
44
|
# 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).
|
|
45
|
+
# that can't see the project's plugin entry points (issue #134). The lint /
|
|
46
|
+
# test tooling (ruff, ty, pytest-*) backs the generated `make lint`/`make
|
|
47
|
+
# test` targets so a fresh app can run its own quality gates.
|
|
63
48
|
_APP_PY_DEV_DEPS = [
|
|
64
49
|
f"simple_module_test=={_FRAMEWORK_VERSION}",
|
|
65
50
|
f"simple_module_cli=={_FRAMEWORK_VERSION}",
|
|
66
51
|
"pytest>=8.0",
|
|
52
|
+
"pytest-asyncio>=0.24",
|
|
53
|
+
"pytest-playwright>=0.7.2",
|
|
54
|
+
"ruff>=0.8",
|
|
55
|
+
"ty>=0.0.29",
|
|
67
56
|
]
|
|
68
57
|
|
|
69
58
|
_APP_NPM_DEPS = {
|
|
@@ -120,7 +109,12 @@ def create_app_project(
|
|
|
120
109
|
preserved: list[Path] = []
|
|
121
110
|
if not flat:
|
|
122
111
|
preserved.extend(
|
|
123
|
-
create_workspace(
|
|
112
|
+
create_workspace(
|
|
113
|
+
target,
|
|
114
|
+
name=name,
|
|
115
|
+
framework_version=_FRAMEWORK_VERSION,
|
|
116
|
+
preserve_existing=SAFE_PRESERVED_NAMES,
|
|
117
|
+
)
|
|
124
118
|
)
|
|
125
119
|
preserved.extend(
|
|
126
120
|
create_host(
|
|
@@ -195,11 +189,18 @@ def _scaffold_sample_module(target: Path) -> None:
|
|
|
195
189
|
sample_dest = target / "modules" / _SAMPLE_MODULE_NAME
|
|
196
190
|
if sample_dest.exists():
|
|
197
191
|
return
|
|
198
|
-
|
|
199
|
-
#
|
|
200
|
-
# .
|
|
201
|
-
|
|
202
|
-
|
|
192
|
+
# Pin the sample's framework deps to the exact framework version so the
|
|
193
|
+
# workspace resolves (the template's >=1.0,<2.0 ranges don't exist on PyPI
|
|
194
|
+
# pre-1.0). See GH #195.
|
|
195
|
+
# The sample lives inside the workspace, so it gets no per-module .github/:
|
|
196
|
+
# GitHub only reads workflows from the repo root, where the template's
|
|
197
|
+
# .github/ would be dead anyway. See GH #210.
|
|
198
|
+
create_module(
|
|
199
|
+
sample_dest,
|
|
200
|
+
name=_SAMPLE_MODULE_NAME,
|
|
201
|
+
framework_version=_FRAMEWORK_VERSION,
|
|
202
|
+
include_ci=False,
|
|
203
|
+
)
|
|
203
204
|
_seed_static_dist_placeholder(sample_dest / _SAMPLE_MODULE_NAME / "static" / "dist")
|
|
204
205
|
|
|
205
206
|
|
|
@@ -210,35 +211,6 @@ def _seed_static_dist_placeholder(static_dist: Path) -> None:
|
|
|
210
211
|
(static_dist / ".gitkeep").touch()
|
|
211
212
|
|
|
212
213
|
|
|
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
214
|
def _db_url(db: str, slug: str, *, flat: bool) -> str:
|
|
243
215
|
if db == "postgres":
|
|
244
216
|
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 is_inside_existing_repo, 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
|
|
|
@@ -79,18 +90,44 @@ def create_module(
|
|
|
79
90
|
Path | None,
|
|
80
91
|
typer.Option("--dest", help="Destination dir. Defaults to ./simple_module_<name>."),
|
|
81
92
|
] = None,
|
|
93
|
+
standalone: Annotated[
|
|
94
|
+
bool,
|
|
95
|
+
typer.Option(
|
|
96
|
+
"--standalone",
|
|
97
|
+
help="Emit the module's own .github/ CI + PyPI publish workflows. "
|
|
98
|
+
"By default they are omitted when the module lands inside an "
|
|
99
|
+
"existing repo/host (nested workflows never run there).",
|
|
100
|
+
),
|
|
101
|
+
] = False,
|
|
82
102
|
) -> None:
|
|
83
103
|
"""Scaffold a publishable SimpleModule module package."""
|
|
84
104
|
slug = to_kebab_case(name)
|
|
85
105
|
package = slug.replace("-", "_")
|
|
86
106
|
target = dest or Path.cwd() / f"simple_module_{package}"
|
|
107
|
+
# An in-repo module (the documented modules/* layout) gets no .github/: those
|
|
108
|
+
# nested workflows never run and publish.yml is a PyPI footgun. --standalone
|
|
109
|
+
# forces them for a module that lives in its own repo. See GH #210.
|
|
110
|
+
include_ci = standalone or not is_inside_existing_repo(target)
|
|
87
111
|
try:
|
|
88
|
-
|
|
112
|
+
# Pin framework deps to the installed framework version so the module
|
|
113
|
+
# resolves against the app that created it (the template's >=1.0,<2.0
|
|
114
|
+
# ranges don't exist on PyPI pre-1.0). See GH #195.
|
|
115
|
+
_create_module(
|
|
116
|
+
target,
|
|
117
|
+
name=name,
|
|
118
|
+
framework_version=resolve_framework_version(),
|
|
119
|
+
include_ci=include_ci,
|
|
120
|
+
)
|
|
89
121
|
except FileExistsError as exc:
|
|
90
122
|
typer.echo(f"ERROR: {exc}", err=True)
|
|
91
123
|
raise typer.Exit(code=1) from exc
|
|
92
124
|
|
|
93
125
|
typer.echo(f"Created module 'simple_module_{package}' at {target}")
|
|
126
|
+
if not include_ci:
|
|
127
|
+
typer.echo(
|
|
128
|
+
"Skipped .github/ workflows: this module is inside an existing repo, "
|
|
129
|
+
"where nested workflows never run. Use --standalone to emit them."
|
|
130
|
+
)
|
|
94
131
|
typer.echo("\nNext steps:")
|
|
95
132
|
typer.echo(f" cd {target}")
|
|
96
133
|
typer.echo(" uv sync --extra dev")
|
|
@@ -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,18 @@ 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
|
+
"is_inside_existing_repo",
|
|
41
|
+
"pin_framework_deps",
|
|
42
|
+
"resolve_framework_version",
|
|
36
43
|
]
|
|
37
44
|
|
|
38
45
|
logger = logging.getLogger(__name__)
|
|
@@ -55,6 +62,42 @@ def _module_to_pypi_name(name: str) -> str:
|
|
|
55
62
|
return f"simple_module_{name.lower()}"
|
|
56
63
|
|
|
57
64
|
|
|
65
|
+
def is_inside_existing_repo(dest: Path) -> bool:
|
|
66
|
+
"""Return True when ``dest`` lands inside an existing repo / host project.
|
|
67
|
+
|
|
68
|
+
A module scaffolded under an existing host application (the documented
|
|
69
|
+
monorepo ``modules/*`` layout) is an *in-repo* module: GitHub only runs
|
|
70
|
+
workflows from the repository-root ``.github/workflows/``, so a per-module
|
|
71
|
+
``.github/`` is dead weight there — and the bundled ``publish.yml`` (which
|
|
72
|
+
publishes ``simple_module_<name>`` to PyPI on any ``v*`` tag) is a footgun if
|
|
73
|
+
it ever surfaces at the repo root. We detect this by walking up from
|
|
74
|
+
``dest``'s parent for a ``.git`` directory or a ``pyproject.toml`` (an
|
|
75
|
+
existing repo / host / workspace member).
|
|
76
|
+
|
|
77
|
+
``dest`` itself is *excluded* from the walk — the module's own scaffolded
|
|
78
|
+
``pyproject.toml`` must not count as "an existing host". A truly standalone
|
|
79
|
+
target (no repo/pyproject above it) returns False. See GH #210.
|
|
80
|
+
"""
|
|
81
|
+
# ``resolve()`` allows ``dest`` to not exist yet; the walk is over its
|
|
82
|
+
# absolute parents so a relative ``--dest`` is handled the same way.
|
|
83
|
+
start = Path(dest).resolve().parent
|
|
84
|
+
for parent in (start, *start.parents):
|
|
85
|
+
if (parent / ".git").exists() or (parent / "pyproject.toml").is_file():
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _should_pin_framework_version(version: str | None) -> bool:
|
|
91
|
+
"""Whether ``version`` is a concrete pin rather than a skip sentinel.
|
|
92
|
+
|
|
93
|
+
Both scaffolders skip pinning for ``None`` (a library caller that wants the
|
|
94
|
+
template's ranges kept verbatim) and ``"*"`` (the npm-wildcard default,
|
|
95
|
+
which would otherwise render the invalid PEP 508 specifier ``==*``). One
|
|
96
|
+
rule, so the two paths can't drift. See GH #195 / #206.
|
|
97
|
+
"""
|
|
98
|
+
return bool(version) and version != "*"
|
|
99
|
+
|
|
100
|
+
|
|
58
101
|
def _iter_template_files(template_root: Path):
|
|
59
102
|
"""Yield every file under ``template_root``. Skips ``_optional/`` paths."""
|
|
60
103
|
for path in template_root.rglob("*"):
|
|
@@ -124,15 +167,20 @@ def create_workspace(
|
|
|
124
167
|
dest: Path,
|
|
125
168
|
name: str,
|
|
126
169
|
template_root: Path | None = None,
|
|
170
|
+
framework_version: str = "*",
|
|
127
171
|
*,
|
|
128
172
|
preserve_existing: frozenset[str] = frozenset(),
|
|
129
173
|
) -> list[Path]:
|
|
130
174
|
"""Materialize the workspace-root shell at ``dest``; return preserved paths.
|
|
131
175
|
|
|
132
|
-
Lays down the top-level ``pyproject.toml`` (uv workspace
|
|
133
|
-
|
|
134
|
-
``.gitignore``, and ``README.md``.
|
|
135
|
-
modules — those go under ``dest/host`` and
|
|
176
|
+
Lays down the top-level ``pyproject.toml`` (uv workspace + dev tooling for
|
|
177
|
+
``make test``/``lint``), ``package.json`` (npm workspace), ``Makefile``
|
|
178
|
+
(delegates to host), ``.env.example``, ``.gitignore``, and ``README.md``.
|
|
179
|
+
Does NOT create the host or any modules — those go under ``dest/host`` and
|
|
180
|
+
``dest/modules/`` afterwards.
|
|
181
|
+
|
|
182
|
+
``framework_version`` pins ``simple_module_test`` in the root dev group;
|
|
183
|
+
defaults to ``"*"`` for callers that don't need an exact pin.
|
|
136
184
|
|
|
137
185
|
``preserve_existing`` lists top-level entry names that may already exist
|
|
138
186
|
in ``dest``; the scaffold's copy is skipped and the preserved path is
|
|
@@ -147,6 +195,7 @@ def create_workspace(
|
|
|
147
195
|
{
|
|
148
196
|
"{{HOST_NAME}}": validate_scaffold_name(name),
|
|
149
197
|
"{{HOST_PYPI_NAME}}": to_kebab_case(name),
|
|
198
|
+
"{{FRAMEWORK_VERSION}}": framework_version,
|
|
150
199
|
},
|
|
151
200
|
preserve_existing=preserve_existing,
|
|
152
201
|
)
|
|
@@ -181,6 +230,12 @@ def create_host(
|
|
|
181
230
|
},
|
|
182
231
|
preserve_existing=preserve_existing,
|
|
183
232
|
)
|
|
233
|
+
# Pin every simple_module_* host dep (framework packages *and* selected
|
|
234
|
+
# bundled modules) to the lockstep version so the host's first `uv sync`
|
|
235
|
+
# resolves — the template's >=1.0,<2.0 / >=0.1,<1.0 ranges match nothing
|
|
236
|
+
# against pre-1.0 dists. See GH #206.
|
|
237
|
+
if _should_pin_framework_version(framework_version):
|
|
238
|
+
pin_framework_deps(dest / "pyproject.toml", framework_version)
|
|
184
239
|
logger.info(
|
|
185
240
|
"Scaffolded host '%s' at %s (modules: %s)", name, dest, ", ".join(modules) or "<none>"
|
|
186
241
|
)
|
|
@@ -191,7 +246,24 @@ def create_module(
|
|
|
191
246
|
dest: Path,
|
|
192
247
|
name: str,
|
|
193
248
|
template_root: Path | None = None,
|
|
249
|
+
*,
|
|
250
|
+
framework_version: str | None = None,
|
|
251
|
+
include_ci: bool = True,
|
|
194
252
|
) -> Path:
|
|
253
|
+
"""Scaffold a module package at ``dest``.
|
|
254
|
+
|
|
255
|
+
When ``framework_version`` is a concrete version, the template's
|
|
256
|
+
forward-looking ``simple_module_*`` ranges are rewritten to an exact pin so
|
|
257
|
+
the module resolves against that framework version (e.g. ``uv add`` into the
|
|
258
|
+
workspace that created it). Left as ``None`` (or the ``"*"`` sentinel), the
|
|
259
|
+
template's ranges are kept verbatim. See GH #195.
|
|
260
|
+
|
|
261
|
+
When ``include_ci`` is False, the scaffolded ``.github/`` (CI + PyPI publish
|
|
262
|
+
workflows) is omitted. Those nested workflows never run inside an existing
|
|
263
|
+
host repo (GitHub only reads the repo-root ``.github/``) and ``publish.yml``
|
|
264
|
+
is a footgun there, so callers creating an *in-repo* module pass
|
|
265
|
+
``include_ci=False``. See GH #210.
|
|
266
|
+
"""
|
|
195
267
|
dest = Path(dest)
|
|
196
268
|
existed_before = dest.exists()
|
|
197
269
|
_require_empty_dest(dest)
|
|
@@ -210,6 +282,10 @@ def create_module(
|
|
|
210
282
|
},
|
|
211
283
|
path_rewrites={_PACKAGE_PATH_TOKEN: package_name},
|
|
212
284
|
)
|
|
285
|
+
if not include_ci:
|
|
286
|
+
shutil.rmtree(dest / ".github", ignore_errors=True)
|
|
287
|
+
if _should_pin_framework_version(framework_version):
|
|
288
|
+
pin_framework_deps(dest / "pyproject.toml", framework_version)
|
|
213
289
|
except Exception:
|
|
214
290
|
# Rollback so a half-scaffolded directory doesn't leave the user
|
|
215
291
|
# 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
|
|
|
@@ -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)"
|