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.
Files changed (100) hide show
  1. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/PKG-INFO +2 -2
  2. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/README.md +1 -1
  3. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/pyproject.toml +1 -1
  4. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/app_project.py +19 -51
  5. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/cli.py +17 -3
  6. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/new.py +3 -1
  7. simple_module_cli-0.0.19/simple_module_cli/pins.py +66 -0
  8. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/scaffolding.py +45 -4
  9. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/README.md +1 -1
  10. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-cli/SKILL.md +3 -2
  11. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-creating/SKILL.md +1 -1
  12. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-database/SKILL.md +3 -15
  13. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-doctor/SKILL.md +7 -4
  14. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-registries/SKILL.md +2 -2
  15. simple_module_cli-0.0.19/simple_module_cli/templates/host/Makefile +55 -0
  16. simple_module_cli-0.0.19/simple_module_cli/templates/host/main.py +59 -0
  17. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/migrations/env.py +1 -4
  18. simple_module_cli-0.0.19/simple_module_cli/templates/host/pyproject.toml.tpl +51 -0
  19. simple_module_cli-0.0.19/simple_module_cli/templates/workspace/Makefile +68 -0
  20. simple_module_cli-0.0.19/simple_module_cli/templates/workspace/pyproject.toml.tpl +61 -0
  21. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_new.py +0 -82
  22. simple_module_cli-0.0.19/tests/test_cli_new_scaffold_layout.py +171 -0
  23. simple_module_cli-0.0.19/tests/test_module_pages_manifest.py +70 -0
  24. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_scaffolding_host.py +86 -68
  25. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_scaffolding_module.py +57 -0
  26. simple_module_cli-0.0.17/simple_module_cli/templates/host/Makefile +0 -32
  27. simple_module_cli-0.0.17/simple_module_cli/templates/host/main.py +0 -46
  28. simple_module_cli-0.0.17/simple_module_cli/templates/host/pyproject.toml.tpl +0 -18
  29. simple_module_cli-0.0.17/simple_module_cli/templates/workspace/Makefile +0 -42
  30. simple_module_cli-0.0.17/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -17
  31. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/.gitignore +0 -0
  32. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/LICENSE +0 -0
  33. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/__init__.py +0 -0
  34. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/_env.py +0 -0
  35. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/case.py +0 -0
  36. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/catalog.py +0 -0
  37. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/package_update.py +0 -0
  38. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/plugins.py +0 -0
  39. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/recipes.py +0 -0
  40. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
  41. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
  42. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
  43. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
  44. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
  45. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/skills_cmd.py +0 -0
  46. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/.env.example +0 -0
  47. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/.gitignore +0 -0
  48. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/README.md.tpl +0 -0
  49. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
  50. {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
  51. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +0 -0
  52. {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
  53. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
  54. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/alembic.ini +0 -0
  55. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
  56. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
  57. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
  58. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
  59. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
  60. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
  61. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/styles.css +0 -0
  62. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
  63. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
  64. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
  65. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
  66. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/routes.py +0 -0
  67. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/host/templates/index.html +0 -0
  68. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
  69. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
  70. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/.gitignore +0 -0
  71. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/README.md.tpl +0 -0
  72. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  73. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  74. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
  75. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
  76. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
  77. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
  78. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
  79. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/package.json.tpl +0 -0
  80. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
  81. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
  82. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
  83. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/workspace/.env.example +0 -0
  84. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/workspace/.gitignore +0 -0
  85. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
  86. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
  87. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/simple_module_cli/wizard.py +0 -0
  88. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_build_packaging.py +0 -0
  89. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_case.py +0 -0
  90. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_catalog.py +0 -0
  91. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_new_dest_tolerance.py +0 -0
  92. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_new_regressions.py +0 -0
  93. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_package_update.py +0 -0
  94. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_recipes.py +0 -0
  95. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_cli_wizard.py +0 -0
  96. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_env_helper.py +0 -0
  97. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_no_framework_deps.py +0 -0
  98. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_plugin_discovery.py +0 -0
  99. {simple_module_cli-0.0.17 → simple_module_cli-0.0.19}/tests/test_scaffold_rollback.py +0 -0
  100. {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.17
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_cli"
3
- version = "0.0.17"
3
+ version = "0.0.19"
4
4
  description = "Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- def _resolve_framework_version() -> str:
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(target, name=name, preserve_existing=SAFE_PRESERVED_NAMES)
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
- create_module(sample_dest, name=_SAMPLE_MODULE_NAME)
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
- _create_host(target, name=name, modules=selected)
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 head")
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
- _create_module(target, name=name)
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
- subprocess.run([*_ALEMBIC, "upgrade", "head"], cwd=host_dir, check=False)
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), ``package.json``
133
- (npm workspace), ``Makefile`` (delegates to host), ``.env.example``,
134
- ``.gitignore``, and ``README.md``. Does NOT create the host or any
135
- modules — those go under ``dest/host`` and ``dest/modules/`` afterwards.
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`–`SM018`) printed at boot |
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 four kinds of operations: scaffolding new things, project-time helpers for the host, admin shortcuts for the bundled modules, and installing the bundled agent skills.
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, products, 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
+ `--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 three places: the Postgres schema name, the SQLite `__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).
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" # SQLite-safe prefix; required (see below)
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
- The provider is auto-detected from `SM_DATABASE_URL`. Pin it explicitly only in tests:
26
+ ## Table naming
27
27
 
28
- ```python
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–SM018) 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".
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` (Postgres-schema / SQLite-prefix collision) | Rename one module's `meta.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/*` | Use plain `fetch()` for JSON endpoints; Inertia's client expects an Inertia response, not JSON |
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 don't try to look them up. Output format is one line per finding, e.g. `[SM009] ERROR: <subject>`.
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 the `RequireAnyPermissionDep` / `RequireAllPermissionsDep` dependencies (see auth/users), not by reading the registry by hand.
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 `RequireAnyPermissionDep` / `RequireAllPermissionsDep`. The dependency handles wildcard expansion and 401 vs 403 distinction.
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
+ )