simple-module-cli 0.0.27__tar.gz → 0.0.29__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.27 → simple_module_cli-0.0.29}/PKG-INFO +1 -1
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/pyproject.toml +1 -1
- simple_module_cli-0.0.29/simple_module_cli/_module_build.py +92 -0
- simple_module_cli-0.0.29/simple_module_cli/_module_host.py +85 -0
- simple_module_cli-0.0.29/simple_module_cli/_templating.py +82 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/app_project.py +13 -1
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/cli.py +12 -6
- simple_module_cli-0.0.29/simple_module_cli/docker_assets.py +100 -0
- simple_module_cli-0.0.29/simple_module_cli/module_cmd.py +82 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/new.py +2 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/recipes.py +11 -22
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/scaffolding.py +43 -81
- simple_module_cli-0.0.29/simple_module_cli/templates/docker/Makefile.snippet +13 -0
- simple_module_cli-0.0.29/simple_module_cli/templates/docker/compose-base-postgres.yml.tpl +34 -0
- simple_module_cli-0.0.29/simple_module_cli/templates/docker/compose-base-sqlite.yml.tpl +22 -0
- simple_module_cli-0.0.27/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml → simple_module_cli-0.0.29/simple_module_cli/templates/docker/compose-tasks-postgres.yml.tpl +15 -43
- simple_module_cli-0.0.29/simple_module_cli/templates/docker/compose-tasks-sqlite.yml.tpl +70 -0
- simple_module_cli-0.0.29/simple_module_cli/templates/docker/dockerignore +16 -0
- simple_module_cli-0.0.29/simple_module_cli/templates/docker/host-flat.Dockerfile +68 -0
- simple_module_cli-0.0.29/simple_module_cli/templates/docker/host.Dockerfile +76 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/README.md.tpl +7 -2
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/vite.config.ts +34 -11
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/templates/index.html +6 -1
- simple_module_cli-0.0.29/simple_module_cli/templates/module/.github/workflows/ci.yml +65 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/.gitignore +5 -2
- simple_module_cli-0.0.29/simple_module_cli/templates/module/__PACKAGE__/endpoints/views.py.tpl +16 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +14 -0
- simple_module_cli-0.0.29/simple_module_cli/templates/module/__PACKAGE__/pages/Index.tsx.tpl +13 -0
- simple_module_cli-0.0.29/simple_module_cli/templates/module/_optional/standalone/package.json.tpl +27 -0
- {simple_module_cli-0.0.27/simple_module_cli/templates/module → simple_module_cli-0.0.29/simple_module_cli/templates/module/_optional/standalone}/tsconfig.json.tpl +1 -1
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/pyproject.toml.tpl +11 -6
- simple_module_cli-0.0.29/simple_module_cli/templates/module/tsconfig.json.tpl +10 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/README.md.tpl +17 -0
- simple_module_cli-0.0.29/tests/conftest.py +45 -0
- simple_module_cli-0.0.29/tests/test_cli_docker_assets.py +156 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_cli_new.py +17 -1
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_cli_new_regressions.py +7 -5
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_cli_recipes.py +10 -10
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_create_module_ci_skip.py +5 -5
- simple_module_cli-0.0.29/tests/test_module_cmd_build.py +71 -0
- simple_module_cli-0.0.29/tests/test_module_cmd_e2e.py +63 -0
- simple_module_cli-0.0.29/tests/test_module_cmd_verify.py +149 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_module_css_assets.py +50 -51
- simple_module_cli-0.0.29/tests/test_module_css_build.py +144 -0
- simple_module_cli-0.0.29/tests/test_module_npm_aliases.py +128 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_scaffolding_module.py +11 -7
- simple_module_cli-0.0.29/tests/test_scaffolding_module_js.py +105 -0
- simple_module_cli-0.0.27/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +0 -44
- simple_module_cli-0.0.27/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -37
- simple_module_cli-0.0.27/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -32
- simple_module_cli-0.0.27/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -1
- simple_module_cli-0.0.27/tests/test_module_css_build.py +0 -72
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/.gitignore +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/LICENSE +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/README.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/pins.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/Makefile +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_cli_new_scaffold_layout.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_module_pages_manifest.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/tests/test_scaffolding_host.py +0 -0
- {simple_module_cli-0.0.27 → simple_module_cli-0.0.29}/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.29
|
|
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
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""``smpy module build`` — bundle ``<pkg>/assets_src/`` into ``<pkg>/static/dist/``.
|
|
2
|
+
|
|
3
|
+
Only for assets served via :meth:`ModuleBase.static_mounts` (vendor JS,
|
|
4
|
+
widgets, images). Inertia pages do NOT need this — the consuming host's Vite
|
|
5
|
+
build compiles ``pages/*.tsx`` straight from the wheel.
|
|
6
|
+
|
|
7
|
+
The module repo carries no bundler of its own: we borrow the verify host's
|
|
8
|
+
node toolchain (``.smpy/verify-host/client_app``) and point Vite at a
|
|
9
|
+
generated lib-mode config.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from simple_module_cli._module_host import ModuleInfo, ensure_verify_host, require_binary
|
|
20
|
+
from simple_module_cli.case import to_pascal_case
|
|
21
|
+
|
|
22
|
+
_ENTRY_CANDIDATES = ("index.ts", "index.tsx", "index.js")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_build(info: ModuleInfo, *, fresh: bool = False, runner=subprocess.run) -> None:
|
|
26
|
+
assets_src = info.root / info.package_name / "assets_src"
|
|
27
|
+
entry = next((assets_src / n for n in _ENTRY_CANDIDATES if (assets_src / n).is_file()), None)
|
|
28
|
+
if entry is None:
|
|
29
|
+
typer.echo(
|
|
30
|
+
f"error: no {info.package_name}/assets_src/index.(ts|tsx|js) found. "
|
|
31
|
+
"`smpy module build` bundles static_mounts() assets only — Inertia pages "
|
|
32
|
+
"are built by the consuming host and need no bundling.",
|
|
33
|
+
err=True,
|
|
34
|
+
)
|
|
35
|
+
raise typer.Exit(code=1)
|
|
36
|
+
|
|
37
|
+
host = ensure_verify_host(info, fresh=fresh)
|
|
38
|
+
client_app = host / "client_app"
|
|
39
|
+
npm = require_binary("npm")
|
|
40
|
+
if not (client_app / "node_modules").is_dir():
|
|
41
|
+
typer.echo("[build] npm install (first run)")
|
|
42
|
+
if runner([npm, "install"], cwd=client_app).returncode != 0:
|
|
43
|
+
typer.echo("[build] FAILED at: npm install", err=True)
|
|
44
|
+
raise typer.Exit(code=1)
|
|
45
|
+
|
|
46
|
+
out_dir = info.root / info.package_name / "static" / "dist"
|
|
47
|
+
config_path = info.root / ".smpy" / "module-build.config.mjs"
|
|
48
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
config_path.write_text(
|
|
50
|
+
_vite_lib_config(entry, out_dir, global_name=to_pascal_case(info.package_name)),
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
)
|
|
53
|
+
typer.echo(f"[build] vite build -> {info.package_name}/static/dist")
|
|
54
|
+
npx = require_binary("npx")
|
|
55
|
+
if runner([npx, "vite", "build", "--config", str(config_path)], cwd=client_app).returncode != 0:
|
|
56
|
+
typer.echo("[build] FAILED at: vite build", err=True)
|
|
57
|
+
raise typer.Exit(code=1)
|
|
58
|
+
|
|
59
|
+
_warn_missing_force_include(info)
|
|
60
|
+
typer.echo("[build] OK")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _vite_lib_config(entry: Path, out_dir: Path, *, global_name: str) -> str:
|
|
64
|
+
return f"""\
|
|
65
|
+
import {{ defineConfig }} from 'vite'
|
|
66
|
+
|
|
67
|
+
export default defineConfig({{
|
|
68
|
+
build: {{
|
|
69
|
+
lib: {{
|
|
70
|
+
entry: {entry.as_posix()!r},
|
|
71
|
+
formats: ['iife'],
|
|
72
|
+
name: {global_name!r},
|
|
73
|
+
fileName: () => 'index.js',
|
|
74
|
+
}},
|
|
75
|
+
outDir: {out_dir.as_posix()!r},
|
|
76
|
+
emptyOutDir: true,
|
|
77
|
+
}},
|
|
78
|
+
}})
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _warn_missing_force_include(info: ModuleInfo) -> None:
|
|
83
|
+
"""static/dist is gitignored — without an artifacts entry the wheel silently omits it."""
|
|
84
|
+
pyproject = (info.root / "pyproject.toml").read_text(encoding="utf-8")
|
|
85
|
+
if f"{info.package_name}/static/dist" not in pyproject:
|
|
86
|
+
typer.echo(
|
|
87
|
+
"warning: pyproject.toml does not ship "
|
|
88
|
+
f'"{info.package_name}/static/dist" — the built bundle will NOT be in the wheel. '
|
|
89
|
+
"Add under [tool.hatch.build.targets.wheel]:\n"
|
|
90
|
+
f' artifacts = ["{info.package_name}/static/dist/"]',
|
|
91
|
+
err=True,
|
|
92
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Shared plumbing for ``smpy module`` commands.
|
|
2
|
+
|
|
3
|
+
Out-of-tree module repos have no frontend toolchain of their own — the
|
|
4
|
+
commands borrow one by scaffolding a throwaway host (via the same templates
|
|
5
|
+
as ``smpy create-host``) into ``.smpy/verify-host/`` and caching it there.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import tomllib
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from simple_module_cli.scaffolding import create_host, resolve_framework_version
|
|
18
|
+
|
|
19
|
+
VERIFY_HOST_RELPATH = Path(".smpy") / "verify-host"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ModuleInfo:
|
|
24
|
+
"""Identity of the module under the cwd, read from its pyproject."""
|
|
25
|
+
|
|
26
|
+
root: Path
|
|
27
|
+
pypi_name: str # [project].name, e.g. simple_module_my_feature
|
|
28
|
+
package_name: str # the simple_module entry-point key, e.g. my_feature
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_module_info(root: Path) -> ModuleInfo:
|
|
32
|
+
pyproject = root / "pyproject.toml"
|
|
33
|
+
if not pyproject.is_file():
|
|
34
|
+
typer.echo(f"error: no pyproject.toml in {root} — run from the module repo root.", err=True)
|
|
35
|
+
raise typer.Exit(code=1)
|
|
36
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
37
|
+
entry_points = data.get("project", {}).get("entry-points", {}).get("simple_module", {})
|
|
38
|
+
if not entry_points:
|
|
39
|
+
typer.echo(
|
|
40
|
+
"error: pyproject.toml declares no [project.entry-points.simple_module] — "
|
|
41
|
+
"this does not look like a SimpleModule module.",
|
|
42
|
+
err=True,
|
|
43
|
+
)
|
|
44
|
+
raise typer.Exit(code=1)
|
|
45
|
+
return ModuleInfo(
|
|
46
|
+
root=root,
|
|
47
|
+
pypi_name=data["project"]["name"],
|
|
48
|
+
package_name=next(iter(entry_points)),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def require_binary(name: str) -> str:
|
|
53
|
+
"""Resolve ``name`` on PATH or exit with a clear message (npm on Windows is npm.cmd)."""
|
|
54
|
+
path = shutil.which(name)
|
|
55
|
+
if path is None:
|
|
56
|
+
typer.echo(f"error: '{name}' not found on PATH — install it and retry.", err=True)
|
|
57
|
+
raise typer.Exit(code=1)
|
|
58
|
+
return path
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ensure_verify_host(info: ModuleInfo, *, fresh: bool = False) -> Path:
|
|
62
|
+
"""Scaffold (or reuse) the cached verify host; return its directory."""
|
|
63
|
+
host_dir = info.root / VERIFY_HOST_RELPATH
|
|
64
|
+
if fresh and host_dir.exists():
|
|
65
|
+
shutil.rmtree(host_dir)
|
|
66
|
+
if not (host_dir / "pyproject.toml").is_file():
|
|
67
|
+
create_host(
|
|
68
|
+
host_dir,
|
|
69
|
+
name="verify_host",
|
|
70
|
+
modules=[],
|
|
71
|
+
framework_version=resolve_framework_version(),
|
|
72
|
+
)
|
|
73
|
+
_wire_module_dep(host_dir / "pyproject.toml", info.pypi_name)
|
|
74
|
+
return host_dir
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _wire_module_dep(pyproject_path: Path, pypi_name: str) -> None:
|
|
78
|
+
"""Add the module as an editable path dependency of the verify host."""
|
|
79
|
+
text = pyproject_path.read_text(encoding="utf-8")
|
|
80
|
+
# Unpinned on purpose: [tool.uv.sources] overrides it with the local path.
|
|
81
|
+
# Note: pin_framework_deps may have rewritten the array onto one line, so
|
|
82
|
+
# match the opening bracket only — TOML tolerates the injected newline.
|
|
83
|
+
text = text.replace("dependencies = [", f'dependencies = [\n "{pypi_name}",', 1)
|
|
84
|
+
text += f'\n[tool.uv.sources]\n{pypi_name} = {{ path = "../..", editable = true }}\n'
|
|
85
|
+
pyproject_path.write_text(text, encoding="utf-8")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Package-data template engine shared by the scaffolders.
|
|
2
|
+
|
|
3
|
+
Extracted from :mod:`simple_module_cli.scaffolding` (which keeps the public
|
|
4
|
+
``create_workspace`` / ``create_host`` / ``create_module`` entry points) so
|
|
5
|
+
each file has one job: this one renders template trees, that one decides what
|
|
6
|
+
to render.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.resources
|
|
12
|
+
import shutil
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
_TEMPLATES_PACKAGE = "simple_module_cli.templates"
|
|
17
|
+
_PACKAGE_PATH_TOKEN = "__PACKAGE__"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _iter_template_files(template_root: Path):
|
|
21
|
+
"""Yield every file under ``template_root``. Skips ``_optional/`` paths."""
|
|
22
|
+
for path in template_root.rglob("*"):
|
|
23
|
+
if not path.is_file():
|
|
24
|
+
continue
|
|
25
|
+
if "_optional" in path.relative_to(template_root).parts:
|
|
26
|
+
continue
|
|
27
|
+
yield path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _require_empty_dest(dest: Path, *, preserve_existing: frozenset[str] = frozenset()) -> None:
|
|
31
|
+
"""Refuse a non-empty destination unless every top-level entry is allowed.
|
|
32
|
+
|
|
33
|
+
``preserve_existing`` is matched against the *name* of each top-level entry,
|
|
34
|
+
so callers can permit common pre-existing files (``.git``, ``README.md``,
|
|
35
|
+
...) without silently overwriting unrelated user content.
|
|
36
|
+
"""
|
|
37
|
+
if dest.exists():
|
|
38
|
+
unexpected = sorted(p.name for p in dest.iterdir() if p.name not in preserve_existing)
|
|
39
|
+
if unexpected:
|
|
40
|
+
raise FileExistsError(
|
|
41
|
+
f"Destination {dest} exists and contains files that would collide "
|
|
42
|
+
f"with the scaffold: {', '.join(unexpected)}. "
|
|
43
|
+
"Move them aside or choose another path."
|
|
44
|
+
)
|
|
45
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_template_root(subdir: str, override: Path | None) -> Path:
|
|
49
|
+
if override is not None:
|
|
50
|
+
return Path(override)
|
|
51
|
+
return Path(str(importlib.resources.files(_TEMPLATES_PACKAGE) / subdir))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _apply_template_files(
|
|
55
|
+
src_root: Path,
|
|
56
|
+
dest: Path,
|
|
57
|
+
substitutions: Mapping[str, str],
|
|
58
|
+
*,
|
|
59
|
+
path_rewrites: Mapping[str, str] | None = None,
|
|
60
|
+
preserve_existing: frozenset[str] = frozenset(),
|
|
61
|
+
) -> list[Path]:
|
|
62
|
+
"""Write template files into ``dest``; return paths skipped to preserve the user's copy."""
|
|
63
|
+
preserved: list[Path] = []
|
|
64
|
+
for src in _iter_template_files(src_root):
|
|
65
|
+
rel_str = str(src.relative_to(src_root))
|
|
66
|
+
for old, new in (path_rewrites or {}).items():
|
|
67
|
+
rel_str = rel_str.replace(old, new)
|
|
68
|
+
rel_str = rel_str.removesuffix(".tpl")
|
|
69
|
+
target = dest / rel_str
|
|
70
|
+
top = Path(rel_str).parts[0] if rel_str else ""
|
|
71
|
+
if top in preserve_existing and target.exists():
|
|
72
|
+
preserved.append(target)
|
|
73
|
+
continue
|
|
74
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
if src.suffix == ".tpl":
|
|
76
|
+
text = src.read_text(encoding="utf-8")
|
|
77
|
+
for placeholder, value in substitutions.items():
|
|
78
|
+
text = text.replace(placeholder, value)
|
|
79
|
+
target.write_text(text, encoding="utf-8")
|
|
80
|
+
else:
|
|
81
|
+
shutil.copy2(src, target)
|
|
82
|
+
return preserved
|
|
@@ -22,6 +22,7 @@ from typing import Any
|
|
|
22
22
|
from simple_module_cli._env import set_env_key
|
|
23
23
|
from simple_module_cli.case import to_kebab_case, to_pascal_case
|
|
24
24
|
from simple_module_cli.catalog import CATALOG, PRESETS, expand_deps
|
|
25
|
+
from simple_module_cli.docker_assets import scaffold_docker_assets
|
|
25
26
|
from simple_module_cli.recipes import RECIPES, ScaffoldCtx
|
|
26
27
|
from simple_module_cli.scaffolding import (
|
|
27
28
|
SAFE_PRESERVED_NAMES,
|
|
@@ -141,6 +142,15 @@ def create_app_project(
|
|
|
141
142
|
env_path = target / ".env.example"
|
|
142
143
|
env_text = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
143
144
|
env_text = set_env_key(env_text, "SM_SECRET_KEY", _secrets.token_urlsafe(32))
|
|
145
|
+
if "users" in resolved:
|
|
146
|
+
# UsersSettings refuses its placeholder token secrets when
|
|
147
|
+
# SM_ENVIRONMENT=production — the mode the compose containers run
|
|
148
|
+
# in — so generate real ones, same as SM_SECRET_KEY above.
|
|
149
|
+
for key in (
|
|
150
|
+
"SM_USERS_RESET_PASSWORD_TOKEN_SECRET",
|
|
151
|
+
"SM_USERS_VERIFICATION_TOKEN_SECRET",
|
|
152
|
+
):
|
|
153
|
+
env_text = set_env_key(env_text, key, _secrets.token_urlsafe(32))
|
|
144
154
|
env_text = set_env_key(env_text, "SM_DATABASE_URL", _db_url(db, to_kebab_case(name), flat=flat))
|
|
145
155
|
env_text = set_env_key(env_text, "SM_MULTI_TENANT", "true" if tenancy else "false")
|
|
146
156
|
env_path.write_text(env_text, encoding="utf-8")
|
|
@@ -176,6 +186,8 @@ def create_app_project(
|
|
|
176
186
|
if recipe_key is not None and recipe_key in RECIPES:
|
|
177
187
|
RECIPES[recipe_key].apply(target, ctx)
|
|
178
188
|
|
|
189
|
+
scaffold_docker_assets(target, ctx, flat=flat)
|
|
190
|
+
|
|
179
191
|
return host_dir, preserved
|
|
180
192
|
|
|
181
193
|
|
|
@@ -199,7 +211,7 @@ def _scaffold_sample_module(target: Path) -> None:
|
|
|
199
211
|
sample_dest,
|
|
200
212
|
name=_SAMPLE_MODULE_NAME,
|
|
201
213
|
framework_version=_FRAMEWORK_VERSION,
|
|
202
|
-
|
|
214
|
+
standalone=False,
|
|
203
215
|
)
|
|
204
216
|
_seed_static_dist_placeholder(sample_dest / _SAMPLE_MODULE_NAME / "static" / "dist")
|
|
205
217
|
|
|
@@ -18,6 +18,7 @@ from typing import Annotated
|
|
|
18
18
|
import typer
|
|
19
19
|
|
|
20
20
|
from simple_module_cli.case import to_kebab_case
|
|
21
|
+
from simple_module_cli.module_cmd import module_app
|
|
21
22
|
from simple_module_cli.new import new_project
|
|
22
23
|
from simple_module_cli.package_update import package_update
|
|
23
24
|
from simple_module_cli.plugins import discover_and_mount
|
|
@@ -35,6 +36,7 @@ app = typer.Typer(
|
|
|
35
36
|
app.command("new")(new_project)
|
|
36
37
|
app.command("package-update")(package_update)
|
|
37
38
|
app.add_typer(skills_app, name="skills")
|
|
39
|
+
app.add_typer(module_app, name="module")
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
@app.command("create-host")
|
|
@@ -104,10 +106,11 @@ def create_module(
|
|
|
104
106
|
slug = to_kebab_case(name)
|
|
105
107
|
package = slug.replace("-", "_")
|
|
106
108
|
target = dest or Path.cwd() / f"simple_module_{package}"
|
|
107
|
-
# An in-repo module (the documented modules/* layout) gets no .github
|
|
108
|
-
# nested workflows never run and publish.yml
|
|
109
|
-
#
|
|
110
|
-
|
|
109
|
+
# An in-repo module (the documented modules/* layout) gets no .github/ and
|
|
110
|
+
# keeps the workspace JS configs: nested workflows never run and publish.yml
|
|
111
|
+
# is a PyPI footgun. --standalone forces the standalone layout for a module
|
|
112
|
+
# that lives in its own repo. See GH #210.
|
|
113
|
+
standalone_mode = standalone or not is_inside_existing_repo(target)
|
|
111
114
|
try:
|
|
112
115
|
# Pin framework deps to the installed framework version so the module
|
|
113
116
|
# resolves against the app that created it (the template's >=1.0,<2.0
|
|
@@ -116,14 +119,14 @@ def create_module(
|
|
|
116
119
|
target,
|
|
117
120
|
name=name,
|
|
118
121
|
framework_version=resolve_framework_version(),
|
|
119
|
-
|
|
122
|
+
standalone=standalone_mode,
|
|
120
123
|
)
|
|
121
124
|
except FileExistsError as exc:
|
|
122
125
|
typer.echo(f"ERROR: {exc}", err=True)
|
|
123
126
|
raise typer.Exit(code=1) from exc
|
|
124
127
|
|
|
125
128
|
typer.echo(f"Created module 'simple_module_{package}' at {target}")
|
|
126
|
-
if not
|
|
129
|
+
if not standalone_mode:
|
|
127
130
|
typer.echo(
|
|
128
131
|
"Skipped .github/ workflows: this module is inside an existing repo, "
|
|
129
132
|
"where nested workflows never run. Use --standalone to emit them."
|
|
@@ -132,6 +135,9 @@ def create_module(
|
|
|
132
135
|
typer.echo(f" cd {target}")
|
|
133
136
|
typer.echo(" uv sync --extra dev")
|
|
134
137
|
typer.echo(" uv run pytest")
|
|
138
|
+
typer.echo(" npm install")
|
|
139
|
+
typer.echo(" npm run typecheck")
|
|
140
|
+
typer.echo(" uv run smpy module verify")
|
|
135
141
|
|
|
136
142
|
|
|
137
143
|
discover_and_mount(app)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Default Docker assets for ``smpy new`` scaffolds.
|
|
2
|
+
|
|
3
|
+
Every new app ships a container story out of the box: a multi-stage
|
|
4
|
+
``docker/host.Dockerfile``, a ``docker-compose.yml`` matched to the
|
|
5
|
+
scaffold's DB choice (app on a SQLite volume, or postgres + app; plus
|
|
6
|
+
redis/worker/beat when ``background_tasks`` is selected), a
|
|
7
|
+
``.dockerignore``, and ``docker-*`` Make targets. This runs for every
|
|
8
|
+
scaffold — unlike per-module recipes — because the compose service set
|
|
9
|
+
depends on the *whole* module selection, which only the scaffolder knows.
|
|
10
|
+
|
|
11
|
+
worker/beat reuse the app image with a different command: the workspace
|
|
12
|
+
venv already contains everything (``uv sync --all-packages``), so a
|
|
13
|
+
separate worker Dockerfile would only duplicate build logic.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import importlib.resources
|
|
19
|
+
import shutil
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from simple_module_cli.case import to_kebab_case
|
|
23
|
+
from simple_module_cli.recipes import ScaffoldCtx
|
|
24
|
+
|
|
25
|
+
__all__ = ["scaffold_docker_assets"]
|
|
26
|
+
|
|
27
|
+
_MAKEFILE_MARKER = "# --- docker --"
|
|
28
|
+
_PG_DB_TOKEN = "{{PG_DB}}"
|
|
29
|
+
_APP_EXTRA_ENV_TOKEN = "{{APP_EXTRA_ENV}}"
|
|
30
|
+
|
|
31
|
+
# BackgroundTasksSettings refuses a localhost broker when
|
|
32
|
+
# SM_ENVIRONMENT=production, so the app container needs the compose-network
|
|
33
|
+
# broker URLs too — not just worker/beat.
|
|
34
|
+
_APP_BROKER_ENV = (
|
|
35
|
+
" SM_BG_TASKS_BROKER_URL: redis://redis:6379/0\n"
|
|
36
|
+
" SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1\n"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _template_root() -> Path:
|
|
41
|
+
base = importlib.resources.files("simple_module_cli")
|
|
42
|
+
return Path(str(base / "templates" / "docker"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def scaffold_docker_assets(target: Path, ctx: ScaffoldCtx, *, flat: bool = False) -> None:
|
|
46
|
+
"""Emit compose + Dockerfile + .dockerignore + Make targets at ``target``.
|
|
47
|
+
|
|
48
|
+
The compose file is assembled from fragments: a per-DB base (app alone
|
|
49
|
+
on SQLite, or postgres + app), per-DB worker services when
|
|
50
|
+
``background_tasks`` is selected, and a computed ``volumes:`` block —
|
|
51
|
+
appending fragments keeps the templates plain YAML instead of merge
|
|
52
|
+
logic.
|
|
53
|
+
"""
|
|
54
|
+
templates = _template_root()
|
|
55
|
+
compose_dest = target / "docker-compose.yml"
|
|
56
|
+
dockerfile_dest = target / "docker" / "host.Dockerfile"
|
|
57
|
+
for path in (compose_dest, dockerfile_dest):
|
|
58
|
+
if path.exists():
|
|
59
|
+
raise FileExistsError(
|
|
60
|
+
f"{path} already exists — refusing to clobber. "
|
|
61
|
+
"Remove the file or run `smpy new` against an empty directory."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
dockerfile_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
src_name = "host-flat.Dockerfile" if flat else "host.Dockerfile"
|
|
66
|
+
shutil.copy2(templates / src_name, dockerfile_dest)
|
|
67
|
+
|
|
68
|
+
# The compose stack must match the scaffold's DB choice: a migration
|
|
69
|
+
# history is dialect-frozen at autogenerate time (e.g. sa.false()
|
|
70
|
+
# compiles to DEFAULT 0 on SQLite, which Postgres rejects), so pointing
|
|
71
|
+
# a sqlite-scaffolded app at a Postgres container fails on first boot.
|
|
72
|
+
db = "postgres" if ctx.db == "postgres" else "sqlite"
|
|
73
|
+
pg_db = to_kebab_case(ctx.name)
|
|
74
|
+
with_tasks = "background_tasks" in ctx.selected
|
|
75
|
+
compose = (
|
|
76
|
+
_read(templates / f"compose-base-{db}.yml.tpl")
|
|
77
|
+
.replace(_PG_DB_TOKEN, pg_db)
|
|
78
|
+
.replace(_APP_EXTRA_ENV_TOKEN, _APP_BROKER_ENV if with_tasks else "")
|
|
79
|
+
)
|
|
80
|
+
volumes = ["pgdata"] if db == "postgres" else ["appdata"]
|
|
81
|
+
if with_tasks:
|
|
82
|
+
compose += _read(templates / f"compose-tasks-{db}.yml.tpl").replace(_PG_DB_TOKEN, pg_db)
|
|
83
|
+
volumes.append("redisdata")
|
|
84
|
+
compose += "\nvolumes:\n" + "".join(f" {name}:\n" for name in volumes)
|
|
85
|
+
compose_dest.write_text(compose, encoding="utf-8")
|
|
86
|
+
|
|
87
|
+
ignore_dest = target / ".dockerignore"
|
|
88
|
+
if not ignore_dest.exists():
|
|
89
|
+
shutil.copy2(templates / "dockerignore", ignore_dest)
|
|
90
|
+
|
|
91
|
+
makefile_path = target / "Makefile"
|
|
92
|
+
existing = makefile_path.read_text(encoding="utf-8") if makefile_path.exists() else ""
|
|
93
|
+
if _MAKEFILE_MARKER not in existing:
|
|
94
|
+
snippet = _read(templates / "Makefile.snippet")
|
|
95
|
+
sep = "" if existing.endswith("\n") or not existing else "\n"
|
|
96
|
+
makefile_path.write_text(existing + sep + snippet, encoding="utf-8")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _read(path: Path) -> str:
|
|
100
|
+
return path.read_text(encoding="utf-8")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""``smpy module`` — commands for developing a module out-of-tree (own repo)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from simple_module_cli._module_host import (
|
|
11
|
+
ModuleInfo,
|
|
12
|
+
ensure_verify_host,
|
|
13
|
+
read_module_info,
|
|
14
|
+
require_binary,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
module_app = typer.Typer(help="Develop a SimpleModule module outside a host repo.")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_verify(info: ModuleInfo, *, fresh: bool = False, runner=subprocess.run) -> None:
|
|
21
|
+
"""Prove the module's TSX + CSS compile against a real scaffolded host.
|
|
22
|
+
|
|
23
|
+
Output streams straight to the terminal (CI logs want the full Vite/tsc
|
|
24
|
+
output); on failure we name the step and exit 1.
|
|
25
|
+
"""
|
|
26
|
+
host = ensure_verify_host(info, fresh=fresh)
|
|
27
|
+
client_app = host / "client_app"
|
|
28
|
+
uv, npm = require_binary("uv"), require_binary("npm")
|
|
29
|
+
steps: tuple[tuple[str, list[str], Path], ...] = (
|
|
30
|
+
# The module's own node_modules first: esbuild resolves the module
|
|
31
|
+
# tsconfig's `extends` (@simple-module-py/tsconfig) from there when
|
|
32
|
+
# compiling pages out of the editable checkout.
|
|
33
|
+
*(
|
|
34
|
+
(("npm install (module)", [npm, "install"], info.root),)
|
|
35
|
+
if (info.root / "package.json").is_file()
|
|
36
|
+
else ()
|
|
37
|
+
),
|
|
38
|
+
("uv sync", [uv, "sync"], host),
|
|
39
|
+
("npm install", [npm, "install"], client_app),
|
|
40
|
+
(
|
|
41
|
+
"gen-pages",
|
|
42
|
+
[
|
|
43
|
+
uv,
|
|
44
|
+
"run",
|
|
45
|
+
"python",
|
|
46
|
+
"-m",
|
|
47
|
+
"simple_module_hosting",
|
|
48
|
+
"gen-pages",
|
|
49
|
+
"--host-dir=client_app",
|
|
50
|
+
],
|
|
51
|
+
host,
|
|
52
|
+
),
|
|
53
|
+
("frontend build (tsc + vite)", [npm, "run", "build"], client_app),
|
|
54
|
+
)
|
|
55
|
+
for label, cmd, cwd in steps:
|
|
56
|
+
typer.echo(f"[verify] {label}")
|
|
57
|
+
if runner(cmd, cwd=cwd).returncode != 0:
|
|
58
|
+
typer.echo(f"[verify] FAILED at: {label}", err=True)
|
|
59
|
+
raise typer.Exit(code=1)
|
|
60
|
+
typer.echo("[verify] OK — module frontend builds against a scaffolded host")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@module_app.command("verify")
|
|
64
|
+
def verify_command(
|
|
65
|
+
fresh: bool = typer.Option(
|
|
66
|
+
False, "--fresh", help="Rebuild the cached .smpy/verify-host from scratch."
|
|
67
|
+
),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Build this module's frontend inside a throwaway scaffolded host."""
|
|
70
|
+
run_verify(read_module_info(Path.cwd()), fresh=fresh)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@module_app.command("build")
|
|
74
|
+
def build_command(
|
|
75
|
+
fresh: bool = typer.Option(
|
|
76
|
+
False, "--fresh", help="Rebuild the cached .smpy/verify-host from scratch."
|
|
77
|
+
),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Bundle <pkg>/assets_src/ into <pkg>/static/dist/ for static_mounts()."""
|
|
80
|
+
from simple_module_cli._module_build import run_build
|
|
81
|
+
|
|
82
|
+
run_build(read_module_info(Path.cwd()), fresh=fresh)
|
|
@@ -141,6 +141,7 @@ def new_project(
|
|
|
141
141
|
typer.echo(' make migration msg="initial schema"')
|
|
142
142
|
typer.echo(" make migrate")
|
|
143
143
|
typer.echo(" make dev")
|
|
144
|
+
typer.echo(" make docker-up # or run the full stack in containers")
|
|
144
145
|
if "background_tasks" in resolved:
|
|
145
146
|
typer.echo(" docker compose up -d redis worker beat # background jobs")
|
|
146
147
|
return
|
|
@@ -168,6 +169,7 @@ def new_project(
|
|
|
168
169
|
# errors once a second module ships its own migration branch label.
|
|
169
170
|
subprocess.run([*_ALEMBIC, "upgrade", "heads"], cwd=host_dir, check=False)
|
|
170
171
|
typer.echo("\nSetup complete. Run `make dev` in the new directory.")
|
|
172
|
+
typer.echo("To run the full stack in containers instead: make docker-up")
|
|
171
173
|
if "background_tasks" in resolved:
|
|
172
174
|
typer.echo("For background jobs, also run: docker compose up -d redis worker beat")
|
|
173
175
|
|
|
@@ -49,37 +49,26 @@ def _optional_template_root(name: str) -> Path:
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class BackgroundTasksRecipe:
|
|
52
|
-
"""Lays down run_worker.py +
|
|
52
|
+
"""Lays down run_worker.py + broker env keys + Make targets.
|
|
53
|
+
|
|
54
|
+
The compose services (redis/worker/beat) and the shared app image come
|
|
55
|
+
from the default Docker assets every scaffold gets — see
|
|
56
|
+
:mod:`simple_module_cli.docker_assets`.
|
|
57
|
+
"""
|
|
53
58
|
|
|
54
59
|
def apply(self, target: Path, ctx: ScaffoldCtx) -> None:
|
|
55
60
|
templates = _optional_template_root("background_tasks")
|
|
56
61
|
|
|
57
62
|
run_worker_dest = target / "scripts" / "run_worker.py"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
run_worker_dest,
|
|
64
|
-
compose_dest,
|
|
65
|
-
host_dockerfile_dest,
|
|
66
|
-
worker_dockerfile_dest,
|
|
67
|
-
):
|
|
68
|
-
if path.exists():
|
|
69
|
-
raise FileExistsError(
|
|
70
|
-
f"{path} already exists — refusing to clobber. "
|
|
71
|
-
"Remove the file or run `smpy new` against an empty directory."
|
|
72
|
-
)
|
|
63
|
+
if run_worker_dest.exists():
|
|
64
|
+
raise FileExistsError(
|
|
65
|
+
f"{run_worker_dest} already exists — refusing to clobber. "
|
|
66
|
+
"Remove the file or run `smpy new` against an empty directory."
|
|
67
|
+
)
|
|
73
68
|
|
|
74
69
|
run_worker_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
75
70
|
shutil.copy2(templates / "run_worker.py", run_worker_dest)
|
|
76
71
|
|
|
77
|
-
shutil.copy2(templates / "docker-compose.yml", compose_dest)
|
|
78
|
-
|
|
79
|
-
host_dockerfile_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
-
shutil.copy2(templates / "host.Dockerfile", host_dockerfile_dest)
|
|
81
|
-
shutil.copy2(templates / "worker.Dockerfile", worker_dockerfile_dest)
|
|
82
|
-
|
|
83
72
|
env_path = target / ".env.example"
|
|
84
73
|
env_text = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
85
74
|
env_path.write_text(
|