simple-module-hosting 0.0.1__tar.gz → 0.0.3__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_hosting-0.0.1 → simple_module_hosting-0.0.3}/.gitignore +4 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/PKG-INFO +10 -14
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/README.md +7 -11
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/pyproject.toml +5 -6
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/app_builder.py +2 -2
- simple_module_hosting-0.0.3/simple_module_hosting/host_cli.py +117 -0
- simple_module_hosting-0.0.3/tests/test_host_cli.py +28 -0
- simple_module_hosting-0.0.1/simple_module_hosting/cli.py +0 -292
- simple_module_hosting-0.0.1/simple_module_hosting/scaffolding.py +0 -294
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/.env.example +0 -20
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/.gitignore +0 -19
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/Makefile +0 -24
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/README.md.tpl +0 -59
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/alembic.ini +0 -36
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/app.tsx +0 -16
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/main.tsx +0 -2
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/package.json.tpl +0 -23
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/pages/Error.tsx +0 -13
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/pages.ts +0 -47
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/styles.css +0 -7
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/tsconfig.json +0 -16
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/vite.config.ts +0 -39
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/main.py +0 -27
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/migrations/env.py +0 -80
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/migrations/script.py.mako +0 -26
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/migrations/versions/.gitkeep +0 -1
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/pyproject.toml.tpl +0 -17
- simple_module_hosting-0.0.1/simple_module_hosting/templates/host/templates/index.html +0 -12
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/.github/workflows/ci.yml +0 -32
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/.github/workflows/publish.yml.tpl +0 -52
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/.gitignore +0 -14
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/README.md.tpl +0 -82
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/__init__.py +0 -0
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -11
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/module.py.tpl +0 -46
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/pages/.gitkeep +0 -1
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/services.py.tpl +0 -22
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/package.json.tpl +0 -16
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/pyproject.toml.tpl +0 -39
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/tests/__init__.py +0 -0
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/tests/test_module.py.tpl +0 -27
- simple_module_hosting-0.0.1/simple_module_hosting/templates/module/tsconfig.json.tpl +0 -11
- simple_module_hosting-0.0.1/tests/test_cli_new.py +0 -78
- simple_module_hosting-0.0.1/tests/test_scaffolding_host.py +0 -171
- simple_module_hosting-0.0.1/tests/test_scaffolding_module.py +0 -179
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/LICENSE +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/__init__.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_error_handlers.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_hydrate_step.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_inertia_setup.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_inertia_shared.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_observability.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_phase_helpers.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/bootstrap_settings.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/health.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/host_settings.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/i18n_deps.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/i18n_middleware.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/inertia_deps.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/inertia_utils.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/logging.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/manifest.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/middleware.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/migrations.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/permissions.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/py.typed +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/redirects.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/settings.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_app.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_health.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_hosting_permissions.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_inertia_i18n_shared_props.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_locale_middleware.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_logging.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_settings_i18n.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_settings_secrets.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_tenant_middleware.py +0 -0
- {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_translator_dep.py +0 -0
|
@@ -36,6 +36,10 @@ uploads/
|
|
|
36
36
|
# Vite
|
|
37
37
|
host/static/dist/
|
|
38
38
|
|
|
39
|
+
# VitePress (docs)
|
|
40
|
+
docs/.vitepress/cache/
|
|
41
|
+
docs/.vitepress/dist/
|
|
42
|
+
|
|
39
43
|
# Auto-generated frontend module manifest (regenerated by the host at boot
|
|
40
44
|
# or via `make gen-pages`).
|
|
41
45
|
host/client_app/modules.manifest.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_hosting
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -26,8 +26,8 @@ Requires-Dist: fastapi-inertia>=1.0
|
|
|
26
26
|
Requires-Dist: fastapi>=0.115
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
28
|
Requires-Dist: jinja2>=3.1
|
|
29
|
-
Requires-Dist: simple-module-core==0.0.
|
|
30
|
-
Requires-Dist: simple-module-db==0.0.
|
|
29
|
+
Requires-Dist: simple-module-core==0.0.3
|
|
30
|
+
Requires-Dist: simple-module-db==0.0.3
|
|
31
31
|
Requires-Dist: starlette>=0.44
|
|
32
32
|
Requires-Dist: tomlkit>=0.13
|
|
33
33
|
Requires-Dist: uvicorn[standard]>=0.34
|
|
@@ -35,7 +35,7 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
|
|
36
36
|
# simple_module_hosting
|
|
37
37
|
|
|
38
|
-
FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline,
|
|
38
|
+
FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline, and contributes the `sm host` plugin to the standalone `sm` CLI.
|
|
39
39
|
|
|
40
40
|
## Install
|
|
41
41
|
|
|
@@ -43,10 +43,10 @@ FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/ant
|
|
|
43
43
|
pip install simple_module_hosting
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
For a new project, most users run the generator instead:
|
|
46
|
+
For a new project, most users run the generator instead (shipped by the standalone `simple_module_cli` distribution):
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
uvx
|
|
49
|
+
uvx --from simple_module_cli sm new my-app
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## What it provides
|
|
@@ -54,8 +54,7 @@ uvx simple-module new my-app
|
|
|
54
54
|
- `create_app(settings)` — returns a fully-wired `FastAPI` instance with all discovered modules registered.
|
|
55
55
|
- Middleware pipeline (execution order): CorrelationId → RequestLogging → SecurityHeaders → Session → `<module middleware>` → Tenant (opt-in) → Locale → InertiaLayoutData → app.
|
|
56
56
|
- Inertia wiring — shared props (`auth`, `menus`, `i18n`), `InertiaDep`, page-route lookup.
|
|
57
|
-
-
|
|
58
|
-
- Scaffolders — `sm create-host`, `sm create-module`, `sm new` (greenfield app with users + dashboard + permissions pre-wired), `sm gen-pages`.
|
|
57
|
+
- `sm host` plugin — `sm host gen-pages` regenerates the frontend pages manifest; `sm host sync-js-deps` installs JS deps declared by installed modules. The `sm` binary itself comes from `simple_module_cli`.
|
|
59
58
|
|
|
60
59
|
## Usage
|
|
61
60
|
|
|
@@ -73,16 +72,13 @@ if __name__ == "__main__":
|
|
|
73
72
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
CLI:
|
|
75
|
+
CLI (after also installing `simple_module_cli`):
|
|
77
76
|
|
|
78
77
|
```bash
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
simple-module gen-pages # regenerate client_app/modules.generated.ts
|
|
78
|
+
sm host gen-pages # regenerate client_app/modules.generated.ts
|
|
79
|
+
sm host sync-js-deps # sync module JS deps into client_app/node_modules
|
|
82
80
|
```
|
|
83
81
|
|
|
84
|
-
`sm` works identically to `simple-module`.
|
|
85
|
-
|
|
86
82
|
## Depends on
|
|
87
83
|
|
|
88
84
|
- `simple_module_core`, `simple_module_db`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# simple_module_hosting
|
|
2
2
|
|
|
3
|
-
FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline,
|
|
3
|
+
FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline, and contributes the `sm host` plugin to the standalone `sm` CLI.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,10 +8,10 @@ FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/ant
|
|
|
8
8
|
pip install simple_module_hosting
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
For a new project, most users run the generator instead:
|
|
11
|
+
For a new project, most users run the generator instead (shipped by the standalone `simple_module_cli` distribution):
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
uvx
|
|
14
|
+
uvx --from simple_module_cli sm new my-app
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## What it provides
|
|
@@ -19,8 +19,7 @@ uvx simple-module new my-app
|
|
|
19
19
|
- `create_app(settings)` — returns a fully-wired `FastAPI` instance with all discovered modules registered.
|
|
20
20
|
- Middleware pipeline (execution order): CorrelationId → RequestLogging → SecurityHeaders → Session → `<module middleware>` → Tenant (opt-in) → Locale → InertiaLayoutData → app.
|
|
21
21
|
- Inertia wiring — shared props (`auth`, `menus`, `i18n`), `InertiaDep`, page-route lookup.
|
|
22
|
-
-
|
|
23
|
-
- Scaffolders — `sm create-host`, `sm create-module`, `sm new` (greenfield app with users + dashboard + permissions pre-wired), `sm gen-pages`.
|
|
22
|
+
- `sm host` plugin — `sm host gen-pages` regenerates the frontend pages manifest; `sm host sync-js-deps` installs JS deps declared by installed modules. The `sm` binary itself comes from `simple_module_cli`.
|
|
24
23
|
|
|
25
24
|
## Usage
|
|
26
25
|
|
|
@@ -38,16 +37,13 @@ if __name__ == "__main__":
|
|
|
38
37
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
CLI:
|
|
40
|
+
CLI (after also installing `simple_module_cli`):
|
|
42
41
|
|
|
43
42
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
simple-module gen-pages # regenerate client_app/modules.generated.ts
|
|
43
|
+
sm host gen-pages # regenerate client_app/modules.generated.ts
|
|
44
|
+
sm host sync-js-deps # sync module JS deps into client_app/node_modules
|
|
47
45
|
```
|
|
48
46
|
|
|
49
|
-
`sm` works identically to `simple-module`.
|
|
50
|
-
|
|
51
47
|
## Depends on
|
|
52
48
|
|
|
53
49
|
- `simple_module_core`, `simple_module_db`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_hosting"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
4
4
|
description = "FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -26,16 +26,15 @@ dependencies = [
|
|
|
26
26
|
"fastapi-inertia>=1.0",
|
|
27
27
|
"httpx>=0.27",
|
|
28
28
|
"jinja2>=3.1",
|
|
29
|
-
"simple_module_core==0.0.
|
|
30
|
-
"simple_module_db==0.0.
|
|
29
|
+
"simple_module_core==0.0.3",
|
|
30
|
+
"simple_module_db==0.0.3",
|
|
31
31
|
"starlette>=0.44",
|
|
32
32
|
"tomlkit>=0.13",
|
|
33
33
|
"uvicorn[standard]>=0.34",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
|
-
[project.
|
|
37
|
-
|
|
38
|
-
simple-module = "simple_module_hosting.cli:main"
|
|
36
|
+
[project.entry-points."simple_module_cli.cli_plugins"]
|
|
37
|
+
host = "simple_module_hosting.host_cli:app"
|
|
39
38
|
|
|
40
39
|
[project.urls]
|
|
41
40
|
Homepage = "https://github.com/antosubash/simple_module_python"
|
{simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/app_builder.py
RENAMED
|
@@ -69,7 +69,7 @@ def wire_module_routes(app: FastAPI, module) -> None:
|
|
|
69
69
|
"""Attach a module's API + view routers to ``app`` using its Meta prefixes.
|
|
70
70
|
|
|
71
71
|
The single canonical implementation so ``create_app`` and the test harness
|
|
72
|
-
in ``
|
|
72
|
+
in ``simple_module_test`` stay in lockstep if ``ModuleBase`` ever gains
|
|
73
73
|
a new router type.
|
|
74
74
|
"""
|
|
75
75
|
api_router = APIRouter(prefix=module.meta.route_prefix, tags=[module.meta.name])
|
|
@@ -131,7 +131,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
131
131
|
# Emit frontend module-pages manifest so Vite can find pages shipped
|
|
132
132
|
# inside pip-installed module wheels. See scaffolding.py.
|
|
133
133
|
try:
|
|
134
|
-
from simple_module_hosting.
|
|
134
|
+
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
135
135
|
|
|
136
136
|
client_app = _PROJECT_ROOT / "host" / "client_app"
|
|
137
137
|
if client_app.is_dir():
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""``sm host`` plugin — project-time helpers exposed through the simple-module CLI.
|
|
2
|
+
|
|
3
|
+
Commands here need module discovery (``simple_module_core.discover_modules``)
|
|
4
|
+
and the manifest helpers; they're not part of the standalone scaffolder.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from simple_module_core import discover_modules
|
|
17
|
+
|
|
18
|
+
from simple_module_hosting.manifest import (
|
|
19
|
+
collect_module_js_deps,
|
|
20
|
+
repo_root_from_client_app,
|
|
21
|
+
write_module_pages_manifest,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(
|
|
25
|
+
help="Project-time helpers (frontend pages manifest, module JS dep sync).",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command("gen-pages")
|
|
31
|
+
def gen_pages(
|
|
32
|
+
host_dir: Annotated[
|
|
33
|
+
Path,
|
|
34
|
+
typer.Option(
|
|
35
|
+
"--host-dir",
|
|
36
|
+
help="Path to the host's client_app directory. Defaults to ./client_app.",
|
|
37
|
+
),
|
|
38
|
+
] = Path("client_app"),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Regenerate client_app/modules.{manifest.json,generated.ts,generated.css}."""
|
|
41
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
42
|
+
if not host_dir.is_dir():
|
|
43
|
+
typer.echo(f"ERROR: client_app directory not found at {host_dir}", err=True)
|
|
44
|
+
raise typer.Exit(code=1)
|
|
45
|
+
modules = discover_modules()
|
|
46
|
+
written = write_module_pages_manifest(modules, host_dir)
|
|
47
|
+
typer.echo(
|
|
48
|
+
f"Wrote {written['manifest'].name}, {written['generated'].name}, "
|
|
49
|
+
f"{written['css'].name} to {host_dir}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("sync-js-deps")
|
|
54
|
+
def sync_js_deps(
|
|
55
|
+
host_client_app: Annotated[
|
|
56
|
+
Path,
|
|
57
|
+
typer.Option(
|
|
58
|
+
"--host-client-app",
|
|
59
|
+
help="Path to host/client_app. Defaults to ./client_app.",
|
|
60
|
+
),
|
|
61
|
+
] = Path("client_app"),
|
|
62
|
+
dry_run: Annotated[
|
|
63
|
+
bool, typer.Option("--dry-run", help="Print the npm install command only.")
|
|
64
|
+
] = False,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Install JS deps declared by installed modules into host's node_modules."""
|
|
67
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
68
|
+
if not host_client_app.is_dir():
|
|
69
|
+
typer.echo(f"ERROR: client_app directory not found at {host_client_app}", err=True)
|
|
70
|
+
raise typer.Exit(code=1)
|
|
71
|
+
|
|
72
|
+
modules = discover_modules()
|
|
73
|
+
by_module = collect_module_js_deps(modules)
|
|
74
|
+
if not by_module:
|
|
75
|
+
typer.echo("No module JS dependencies declared.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
specs: list[str] = []
|
|
79
|
+
for mod_name in sorted(by_module):
|
|
80
|
+
for dep, rng in sorted(by_module[mod_name].items()):
|
|
81
|
+
specs.append(f"{dep}@{rng}")
|
|
82
|
+
deduped: list[str] = []
|
|
83
|
+
seen: set[str] = set()
|
|
84
|
+
for spec in specs:
|
|
85
|
+
if spec not in seen:
|
|
86
|
+
seen.add(spec)
|
|
87
|
+
deduped.append(spec)
|
|
88
|
+
|
|
89
|
+
npm = shutil.which("npm")
|
|
90
|
+
if npm is None:
|
|
91
|
+
typer.echo("ERROR: npm not found on PATH.", err=True)
|
|
92
|
+
raise typer.Exit(code=1)
|
|
93
|
+
|
|
94
|
+
repo_root = repo_root_from_client_app(host_client_app)
|
|
95
|
+
try:
|
|
96
|
+
workspace = str(host_client_app.resolve().relative_to(repo_root))
|
|
97
|
+
except ValueError:
|
|
98
|
+
workspace = str(host_client_app.resolve())
|
|
99
|
+
|
|
100
|
+
cmd = [
|
|
101
|
+
npm,
|
|
102
|
+
"install",
|
|
103
|
+
"--workspace",
|
|
104
|
+
workspace,
|
|
105
|
+
"--save=false",
|
|
106
|
+
"--no-audit",
|
|
107
|
+
"--no-fund",
|
|
108
|
+
*deduped,
|
|
109
|
+
]
|
|
110
|
+
typer.echo("Installing module JS deps:")
|
|
111
|
+
for spec in deduped:
|
|
112
|
+
typer.echo(f" {spec}")
|
|
113
|
+
if dry_run:
|
|
114
|
+
typer.echo("(dry-run) " + " ".join(cmd))
|
|
115
|
+
return
|
|
116
|
+
result = subprocess.run(cmd, cwd=repo_root, check=False)
|
|
117
|
+
raise typer.Exit(code=result.returncode)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Smoke tests for the simple_module_hosting host_cli Typer plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from simple_module_hosting.host_cli import app
|
|
9
|
+
from typer.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_app_is_typer_instance() -> None:
|
|
13
|
+
assert isinstance(app, typer.Typer)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_help_lists_gen_pages_and_sync_js_deps() -> None:
|
|
17
|
+
runner = CliRunner()
|
|
18
|
+
result = runner.invoke(app, ["--help"])
|
|
19
|
+
assert result.exit_code == 0
|
|
20
|
+
assert "gen-pages" in result.output
|
|
21
|
+
assert "sync-js-deps" in result.output
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_gen_pages_errors_on_missing_client_app(tmp_path: Path) -> None:
|
|
25
|
+
runner = CliRunner()
|
|
26
|
+
result = runner.invoke(app, ["gen-pages", "--host-dir", str(tmp_path / "does-not-exist")])
|
|
27
|
+
assert result.exit_code != 0
|
|
28
|
+
assert "not found" in result.output.lower() or "not found" in (result.stderr or "").lower()
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
"""SimpleModule CLI — `sm` console script.
|
|
2
|
-
|
|
3
|
-
Currently exposes:
|
|
4
|
-
|
|
5
|
-
* ``sm create-host <name>`` — scaffold a new host directory.
|
|
6
|
-
* ``sm create-module <name>`` — scaffold a new module package.
|
|
7
|
-
* ``sm gen-pages`` — regenerate the frontend pages manifest + Tailwind CSS.
|
|
8
|
-
* ``sm sync-js-deps`` — install JS deps declared by installed modules.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import logging
|
|
14
|
-
import shutil
|
|
15
|
-
import subprocess
|
|
16
|
-
import sys
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
|
|
19
|
-
import click
|
|
20
|
-
from simple_module_core import discover_modules
|
|
21
|
-
|
|
22
|
-
from simple_module_hosting.scaffolding import (
|
|
23
|
-
_to_kebab_case,
|
|
24
|
-
collect_module_js_deps,
|
|
25
|
-
create_module,
|
|
26
|
-
repo_root_from_client_app,
|
|
27
|
-
write_module_pages_manifest,
|
|
28
|
-
)
|
|
29
|
-
from simple_module_hosting.scaffolding import (
|
|
30
|
-
create_host as _create_host,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@click.group()
|
|
35
|
-
def main() -> None:
|
|
36
|
-
"""SimpleModule developer CLI."""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@main.command("new")
|
|
40
|
-
@click.argument("name")
|
|
41
|
-
@click.option(
|
|
42
|
-
"--dest",
|
|
43
|
-
type=click.Path(file_okay=False, path_type=Path),
|
|
44
|
-
default=None,
|
|
45
|
-
help="Destination directory. Defaults to ./<name>.",
|
|
46
|
-
)
|
|
47
|
-
@click.option(
|
|
48
|
-
"--db",
|
|
49
|
-
type=click.Choice(["sqlite", "postgres"]),
|
|
50
|
-
default="sqlite",
|
|
51
|
-
show_default=True,
|
|
52
|
-
help="Database backend to configure in .env.example.",
|
|
53
|
-
)
|
|
54
|
-
@click.option(
|
|
55
|
-
"--tenancy/--no-tenancy",
|
|
56
|
-
default=False,
|
|
57
|
-
show_default=True,
|
|
58
|
-
help="Enable the multi-tenant middleware by default.",
|
|
59
|
-
)
|
|
60
|
-
@click.option(
|
|
61
|
-
"--yes",
|
|
62
|
-
"-y",
|
|
63
|
-
is_flag=True,
|
|
64
|
-
default=False,
|
|
65
|
-
help="Skip interactive prompts; accept all defaults.",
|
|
66
|
-
)
|
|
67
|
-
@click.option(
|
|
68
|
-
"--no-install",
|
|
69
|
-
is_flag=True,
|
|
70
|
-
default=False,
|
|
71
|
-
help="Skip 'uv sync' / 'npm install' / 'alembic upgrade head' after scaffolding.",
|
|
72
|
-
)
|
|
73
|
-
def new_project(
|
|
74
|
-
name: str,
|
|
75
|
-
dest: Path | None,
|
|
76
|
-
db: str,
|
|
77
|
-
tenancy: bool,
|
|
78
|
-
yes: bool,
|
|
79
|
-
no_install: bool,
|
|
80
|
-
) -> None:
|
|
81
|
-
"""Scaffold a new SimpleModule app — pre-wired with users, dashboard, permissions."""
|
|
82
|
-
target = dest or Path.cwd() / name
|
|
83
|
-
if not yes:
|
|
84
|
-
db = click.prompt(
|
|
85
|
-
"Database backend",
|
|
86
|
-
default=db,
|
|
87
|
-
type=click.Choice(["sqlite", "postgres"]),
|
|
88
|
-
)
|
|
89
|
-
tenancy = click.confirm("Enable multi-tenancy?", default=tenancy)
|
|
90
|
-
|
|
91
|
-
from simple_module_hosting.scaffolding import create_app_project
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
create_app_project(target, name=name, db=db, tenancy=tenancy)
|
|
95
|
-
except FileExistsError as exc:
|
|
96
|
-
click.echo(f"ERROR: {exc}", err=True)
|
|
97
|
-
sys.exit(1)
|
|
98
|
-
|
|
99
|
-
click.echo(f"Created app '{name}' at {target}")
|
|
100
|
-
click.echo("\nPre-wired modules: users, dashboard, permissions")
|
|
101
|
-
click.echo("\nNext steps:")
|
|
102
|
-
click.echo(f" cd {target}")
|
|
103
|
-
if no_install:
|
|
104
|
-
click.echo(" uv sync")
|
|
105
|
-
click.echo(" npm install")
|
|
106
|
-
click.echo(" alembic upgrade head")
|
|
107
|
-
click.echo(" make dev")
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
click.echo("Installing dependencies...")
|
|
111
|
-
for cmd in (["uv", "sync"], ["npm", "install"]):
|
|
112
|
-
result = subprocess.run(cmd, cwd=target, check=False)
|
|
113
|
-
if result.returncode != 0:
|
|
114
|
-
click.echo(
|
|
115
|
-
f"WARNING: {' '.join(cmd)} failed (exit {result.returncode}); "
|
|
116
|
-
f"finish setup manually.",
|
|
117
|
-
err=True,
|
|
118
|
-
)
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
subprocess.run(["uv", "run", "alembic", "upgrade", "head"], cwd=target, check=False)
|
|
122
|
-
click.echo("\nSetup complete. Run `make dev` in the new directory.")
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@main.command("create-host")
|
|
126
|
-
@click.argument("name")
|
|
127
|
-
@click.option(
|
|
128
|
-
"--dest",
|
|
129
|
-
type=click.Path(file_okay=False, path_type=Path),
|
|
130
|
-
default=None,
|
|
131
|
-
help="Destination directory. Defaults to ./<name>.",
|
|
132
|
-
)
|
|
133
|
-
@click.option(
|
|
134
|
-
"--with",
|
|
135
|
-
"modules",
|
|
136
|
-
default="",
|
|
137
|
-
help="Comma-separated module names to declare as deps (e.g. --with=Auth,Products).",
|
|
138
|
-
)
|
|
139
|
-
def create_host(name: str, dest: Path | None, modules: str) -> None:
|
|
140
|
-
"""Scaffold a new SimpleModule host project at ./<NAME>."""
|
|
141
|
-
target = dest or Path.cwd() / name
|
|
142
|
-
selected = [m.strip() for m in modules.split(",") if m.strip()]
|
|
143
|
-
|
|
144
|
-
try:
|
|
145
|
-
_create_host(target, name=name, modules=selected)
|
|
146
|
-
except FileExistsError as exc:
|
|
147
|
-
click.echo(f"ERROR: {exc}", err=True)
|
|
148
|
-
sys.exit(1)
|
|
149
|
-
|
|
150
|
-
click.echo(f"Created host '{name}' at {target}")
|
|
151
|
-
if selected:
|
|
152
|
-
click.echo(f"Declared modules: {', '.join(selected)}")
|
|
153
|
-
click.echo("\nNext steps:")
|
|
154
|
-
click.echo(f" cd {target}")
|
|
155
|
-
click.echo(" uv sync")
|
|
156
|
-
click.echo(" cp .env.example .env")
|
|
157
|
-
click.echo(' alembic revision --autogenerate -m "initial schema"')
|
|
158
|
-
click.echo(" alembic upgrade head")
|
|
159
|
-
click.echo(" python main.py")
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@main.command("create-module")
|
|
163
|
-
@click.argument("name")
|
|
164
|
-
@click.option(
|
|
165
|
-
"--dest",
|
|
166
|
-
type=click.Path(file_okay=False, path_type=Path),
|
|
167
|
-
default=None,
|
|
168
|
-
help="Destination directory. Defaults to ./simple_module_<package>.",
|
|
169
|
-
)
|
|
170
|
-
def create_module_cmd(name: str, dest: Path | None) -> None:
|
|
171
|
-
"""Scaffold a publishable SimpleModule module package."""
|
|
172
|
-
slug = _to_kebab_case(name)
|
|
173
|
-
package = slug.replace("-", "_")
|
|
174
|
-
target = dest or Path.cwd() / f"simple_module_{package}"
|
|
175
|
-
|
|
176
|
-
try:
|
|
177
|
-
create_module(target, name=name)
|
|
178
|
-
except FileExistsError as exc:
|
|
179
|
-
click.echo(f"ERROR: {exc}", err=True)
|
|
180
|
-
sys.exit(1)
|
|
181
|
-
|
|
182
|
-
click.echo(f"Created module 'simple_module_{package}' at {target}")
|
|
183
|
-
click.echo("\nNext steps:")
|
|
184
|
-
click.echo(f" cd {target}")
|
|
185
|
-
click.echo(" uv sync --extra dev")
|
|
186
|
-
click.echo(" uv run pytest")
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
@main.command("gen-pages")
|
|
190
|
-
@click.option(
|
|
191
|
-
"--host-dir",
|
|
192
|
-
type=click.Path(file_okay=False, exists=True, path_type=Path),
|
|
193
|
-
default=None,
|
|
194
|
-
help="Path to the host's client_app directory. Defaults to ./client_app.",
|
|
195
|
-
)
|
|
196
|
-
def gen_pages(host_dir: Path | None) -> None:
|
|
197
|
-
"""Regenerate client_app/modules.{manifest.json,generated.ts,generated.css}."""
|
|
198
|
-
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
199
|
-
output = host_dir or Path.cwd() / "client_app"
|
|
200
|
-
if not output.is_dir():
|
|
201
|
-
click.echo(f"ERROR: client_app directory not found at {output}", err=True)
|
|
202
|
-
sys.exit(1)
|
|
203
|
-
|
|
204
|
-
modules = discover_modules()
|
|
205
|
-
written = write_module_pages_manifest(modules, output)
|
|
206
|
-
click.echo(
|
|
207
|
-
f"Wrote {written['manifest'].name}, {written['generated'].name}, "
|
|
208
|
-
f"{written['css'].name} to {output}"
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@main.command("sync-js-deps")
|
|
213
|
-
@click.option(
|
|
214
|
-
"--host-client-app",
|
|
215
|
-
type=click.Path(file_okay=False, exists=True, path_type=Path),
|
|
216
|
-
default=None,
|
|
217
|
-
help="Path to host/client_app. Defaults to ./client_app.",
|
|
218
|
-
)
|
|
219
|
-
@click.option(
|
|
220
|
-
"--dry-run",
|
|
221
|
-
is_flag=True,
|
|
222
|
-
default=False,
|
|
223
|
-
help="Print the npm install command without running it.",
|
|
224
|
-
)
|
|
225
|
-
def sync_js_deps(host_client_app: Path | None, dry_run: bool) -> None:
|
|
226
|
-
"""Install JS deps declared by installed modules into host's node_modules.
|
|
227
|
-
|
|
228
|
-
Walks every discovered module, reads its package.json, and runs a single
|
|
229
|
-
``npm install --workspace host/client_app --save=false <specs>``. Use
|
|
230
|
-
this after ``pip install``-ing a module wheel that declares JS deps;
|
|
231
|
-
in-repo modules already flow through npm workspaces and need nothing.
|
|
232
|
-
"""
|
|
233
|
-
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
234
|
-
|
|
235
|
-
output = host_client_app or Path.cwd() / "client_app"
|
|
236
|
-
if not output.is_dir():
|
|
237
|
-
click.echo(f"ERROR: client_app directory not found at {output}", err=True)
|
|
238
|
-
sys.exit(1)
|
|
239
|
-
|
|
240
|
-
modules = discover_modules()
|
|
241
|
-
by_module = collect_module_js_deps(modules)
|
|
242
|
-
if not by_module:
|
|
243
|
-
click.echo("No module JS dependencies declared.")
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
# Flatten into a single spec list. npm's own resolver handles conflicts.
|
|
247
|
-
specs: list[str] = []
|
|
248
|
-
for mod_name in sorted(by_module):
|
|
249
|
-
for dep, rng in sorted(by_module[mod_name].items()):
|
|
250
|
-
specs.append(f"{dep}@{rng}")
|
|
251
|
-
# Dedupe while preserving first-seen order.
|
|
252
|
-
deduped: list[str] = []
|
|
253
|
-
seen: set[str] = set()
|
|
254
|
-
for spec in specs:
|
|
255
|
-
if spec not in seen:
|
|
256
|
-
seen.add(spec)
|
|
257
|
-
deduped.append(spec)
|
|
258
|
-
|
|
259
|
-
npm = shutil.which("npm")
|
|
260
|
-
if npm is None:
|
|
261
|
-
click.echo("ERROR: npm not found on PATH.", err=True)
|
|
262
|
-
sys.exit(1)
|
|
263
|
-
|
|
264
|
-
# Workspace path is relative to the repo root — derive it from output.
|
|
265
|
-
repo_root = repo_root_from_client_app(output)
|
|
266
|
-
try:
|
|
267
|
-
workspace = str(output.resolve().relative_to(repo_root))
|
|
268
|
-
except ValueError:
|
|
269
|
-
workspace = str(output.resolve())
|
|
270
|
-
|
|
271
|
-
cmd = [
|
|
272
|
-
npm,
|
|
273
|
-
"install",
|
|
274
|
-
"--workspace",
|
|
275
|
-
workspace,
|
|
276
|
-
"--save=false",
|
|
277
|
-
"--no-audit",
|
|
278
|
-
"--no-fund",
|
|
279
|
-
*deduped,
|
|
280
|
-
]
|
|
281
|
-
click.echo("Installing module JS deps:")
|
|
282
|
-
for spec in deduped:
|
|
283
|
-
click.echo(f" {spec}")
|
|
284
|
-
if dry_run:
|
|
285
|
-
click.echo("(dry-run) " + " ".join(cmd))
|
|
286
|
-
return
|
|
287
|
-
result = subprocess.run(cmd, cwd=repo_root, check=False)
|
|
288
|
-
sys.exit(result.returncode)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if __name__ == "__main__":
|
|
292
|
-
main()
|