simple-module-cli 0.0.13__tar.gz → 0.0.15__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.13 → simple_module_cli-0.0.15}/PKG-INFO +1 -1
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/pyproject.toml +1 -1
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/case.py +5 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/scaffolding.py +21 -11
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/vite.config.ts +17 -7
- simple_module_cli-0.0.15/simple_module_cli/templates/host/main.py +46 -0
- simple_module_cli-0.0.15/tests/test_case.py +63 -0
- simple_module_cli-0.0.15/tests/test_env_helper.py +54 -0
- simple_module_cli-0.0.15/tests/test_scaffold_rollback.py +73 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_scaffolding_host.py +96 -0
- simple_module_cli-0.0.13/simple_module_cli/templates/host/main.py +0 -30
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/.gitignore +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/LICENSE +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/README.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/app_project.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/cli.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/new.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/workspace/Makefile +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_cli_new.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_cli_new_regressions.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/tests/test_scaffolding_module.py +0 -0
- {simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/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.15
|
|
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
|
|
@@ -41,6 +41,11 @@ def to_snake_case(name: str) -> str:
|
|
|
41
41
|
s = re.sub(r"[\s\-]+", "_", name)
|
|
42
42
|
s = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", s)
|
|
43
43
|
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
|
|
44
|
+
# Collapse runs of underscores that the boundary regexes can introduce
|
|
45
|
+
# when the input already contained a separator (e.g. ``My Feature`` →
|
|
46
|
+
# ``My_Feature`` → ``My__Feature``). Without this the PyPI slug emits a
|
|
47
|
+
# double hyphen.
|
|
48
|
+
s = re.sub(r"_+", "_", s)
|
|
44
49
|
return s.lower()
|
|
45
50
|
|
|
46
51
|
|
|
@@ -193,20 +193,30 @@ def create_module(
|
|
|
193
193
|
template_root: Path | None = None,
|
|
194
194
|
) -> Path:
|
|
195
195
|
dest = Path(dest)
|
|
196
|
+
existed_before = dest.exists()
|
|
196
197
|
_require_empty_dest(dest)
|
|
197
198
|
display_name = to_pascal_case(name)
|
|
198
199
|
slug = to_kebab_case(name)
|
|
199
200
|
package_name = to_snake_case(name)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
201
|
+
try:
|
|
202
|
+
_apply_template_files(
|
|
203
|
+
_resolve_template_root("module", template_root),
|
|
204
|
+
dest,
|
|
205
|
+
substitutions={
|
|
206
|
+
"{{MODULE_NAME}}": display_name,
|
|
207
|
+
"{{MODULE_SLUG}}": slug,
|
|
208
|
+
"{{PACKAGE_NAME}}": package_name,
|
|
209
|
+
"{{PACKAGE_NAME_UPPER}}": package_name.upper(),
|
|
210
|
+
},
|
|
211
|
+
path_rewrites={_PACKAGE_PATH_TOKEN: package_name},
|
|
212
|
+
)
|
|
213
|
+
except Exception:
|
|
214
|
+
# Rollback so a half-scaffolded directory doesn't leave the user
|
|
215
|
+
# with an unparseable Python package and the impression that a
|
|
216
|
+
# retry won't work because ``dest`` is now non-empty. We only
|
|
217
|
+
# nuke the directory we created — never one we found pre-existing.
|
|
218
|
+
if not existed_before and dest.is_dir():
|
|
219
|
+
shutil.rmtree(dest, ignore_errors=True)
|
|
220
|
+
raise
|
|
211
221
|
logger.info("Scaffolded module '%s' at %s (package: %s)", display_name, dest, package_name)
|
|
212
222
|
return dest
|
|
@@ -68,7 +68,6 @@ if (fs.existsSync(manifestPath)) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
const fsRootPrefix = fsRoot + path.sep;
|
|
72
71
|
const fakeWorkspaceImporter = path.join(fsRoot, 'package.json');
|
|
73
72
|
|
|
74
73
|
// CJS-only deps like `clsx`, `tailwind-merge`, `class-variance-authority`
|
|
@@ -158,11 +157,13 @@ function collectOptimizeIncludes(): string[] {
|
|
|
158
157
|
|
|
159
158
|
// Cross-package bare imports from module pages (`maplibre-gl`, `pmtiles`,
|
|
160
159
|
// `@inertiajs/react`, …) live in fsRoot/node_modules after `npm install`.
|
|
161
|
-
// But
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
160
|
+
// But Vite's resolver walks up from the importer looking for node_modules
|
|
161
|
+
// and doesn't always reach fsRoot/node_modules — true for wheel installs
|
|
162
|
+
// under `.venv/.../site-packages/<pkg>/pages/` (outside fsRoot) AND for
|
|
163
|
+
// workspace-member modules at `modules/<name>/<pkg>/pages/` whose upward
|
|
164
|
+
// walk hits intermediate dirs without node_modules before reaching the
|
|
165
|
+
// hoisted workspace root. Resolution fails with: "Failed to resolve import
|
|
166
|
+
// … Does the file exist?".
|
|
166
167
|
//
|
|
167
168
|
// This plugin recovers by retrying any unresolved bare import from a
|
|
168
169
|
// module-pages importer as if the importer lived at fsRoot, which puts
|
|
@@ -184,7 +185,6 @@ function moduleBareImportResolver(): Plugin {
|
|
|
184
185
|
return null;
|
|
185
186
|
}
|
|
186
187
|
const importerPath = importer.split('?')[0];
|
|
187
|
-
if (importerPath.startsWith(fsRootPrefix)) return null;
|
|
188
188
|
if (!modulePagesPrefixes.some((prefix) => importerPath.startsWith(prefix))) {
|
|
189
189
|
return null;
|
|
190
190
|
}
|
|
@@ -205,6 +205,16 @@ export default defineConfig({
|
|
|
205
205
|
optimizeDeps: {
|
|
206
206
|
entries: ['main.tsx', 'pages/**/*.tsx', ...moduleOptimizeEntries],
|
|
207
207
|
include: collectOptimizeIncludes(),
|
|
208
|
+
// Vite's dep scanner runs esbuild against every entry above. When the
|
|
209
|
+
// entry sits outside the workspace (a wheel-installed module's
|
|
210
|
+
// pages/, or sometimes even a workspace module's pages/), esbuild's
|
|
211
|
+
// upward node_modules walk from the importer does not always reach
|
|
212
|
+
// the hoisted node_modules at fsRoot. Seeding it as NODE_PATH-style
|
|
213
|
+
// fallback ensures bare specifiers like `maplibre-gl` resolve during
|
|
214
|
+
// scan-imports. See GitHub issue #152.
|
|
215
|
+
esbuildOptions: {
|
|
216
|
+
nodePaths: [path.join(fsRoot, 'node_modules')],
|
|
217
|
+
},
|
|
208
218
|
},
|
|
209
219
|
build: {
|
|
210
220
|
outDir: '../static/dist',
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from simple_module_core.dotenv import load_dotenv_into_environ
|
|
11
|
+
|
|
12
|
+
# Resolve the workspace root from this file's location so the web process
|
|
13
|
+
# behaves the same regardless of where uvicorn was launched (the scaffolded
|
|
14
|
+
# Makefile uses ``cd host && uvicorn main:app``, but ``uv run --project host``
|
|
15
|
+
# or a wheel deployment may run from elsewhere). chdir up front so cwd-relative
|
|
16
|
+
# paths in ``.env`` (e.g. ``sqlite+aiosqlite:///./host/app.db``) resolve
|
|
17
|
+
# consistently; load ``.env`` into ``os.environ`` so framework code reading
|
|
18
|
+
# ``os.environ.get("SM_…")`` directly sees the same values pydantic does.
|
|
19
|
+
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
20
|
+
os.chdir(_REPO_ROOT)
|
|
21
|
+
load_dotenv_into_environ(_REPO_ROOT / ".env")
|
|
22
|
+
|
|
23
|
+
from simple_module_hosting import Settings, create_app
|
|
24
|
+
from simple_module_hosting.logging import setup_logging
|
|
25
|
+
|
|
26
|
+
from routes import router as host_router
|
|
27
|
+
|
|
28
|
+
settings = Settings()
|
|
29
|
+
|
|
30
|
+
setup_logging(
|
|
31
|
+
level=settings.log_level,
|
|
32
|
+
json_format=settings.log_format == "json",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
app = create_app(settings)
|
|
36
|
+
app.include_router(host_router)
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
import uvicorn
|
|
40
|
+
|
|
41
|
+
uvicorn.run(
|
|
42
|
+
"main:app",
|
|
43
|
+
host="0.0.0.0",
|
|
44
|
+
port=8000,
|
|
45
|
+
reload=settings.is_development,
|
|
46
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Direct unit tests for the identifier case helpers.
|
|
2
|
+
|
|
3
|
+
Every scaffolder pipes a user-supplied module name through these — a typo
|
|
4
|
+
that emits ``u_r_l_path`` from ``URLPath`` would propagate into the PyPI
|
|
5
|
+
slug *and* the display name. Existing tests touch ``to_pascal_case`` only
|
|
6
|
+
indirectly via ``test_helpers.py``; we pin the snake/kebab forms too,
|
|
7
|
+
including the acronym edge cases the docstring promises.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from simple_module_cli.case import to_kebab_case, to_pascal_case, to_snake_case
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestToSnakeCase:
|
|
17
|
+
@pytest.mark.parametrize(
|
|
18
|
+
("raw", "expected"),
|
|
19
|
+
[
|
|
20
|
+
("MyFeature", "my_feature"),
|
|
21
|
+
("my-feature", "my_feature"),
|
|
22
|
+
("my_feature", "my_feature"),
|
|
23
|
+
("My Feature", "my_feature"),
|
|
24
|
+
("MY_FEATURE", "my_feature"),
|
|
25
|
+
("URLPath", "url_path"),
|
|
26
|
+
("APIClient", "api_client"),
|
|
27
|
+
("HTTPServer2", "http_server2"),
|
|
28
|
+
("simple", "simple"),
|
|
29
|
+
("simple-thing-name", "simple_thing_name"),
|
|
30
|
+
("Already_Snake_Mixed", "already_snake_mixed"),
|
|
31
|
+
("trailing-", "trailing_"),
|
|
32
|
+
],
|
|
33
|
+
)
|
|
34
|
+
def test_canonicalises(self, raw, expected):
|
|
35
|
+
assert to_snake_case(raw) == expected
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestToKebabCase:
|
|
39
|
+
@pytest.mark.parametrize(
|
|
40
|
+
("raw", "expected"),
|
|
41
|
+
[
|
|
42
|
+
("MyFeature", "my-feature"),
|
|
43
|
+
("my_feature", "my-feature"),
|
|
44
|
+
("URLPath", "url-path"),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
def test_canonicalises(self, raw, expected):
|
|
48
|
+
assert to_kebab_case(raw) == expected
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestToPascalCase:
|
|
52
|
+
@pytest.mark.parametrize(
|
|
53
|
+
("raw", "expected"),
|
|
54
|
+
[
|
|
55
|
+
("my-feature", "MyFeature"),
|
|
56
|
+
("my_feature", "MyFeature"),
|
|
57
|
+
("MyFeature", "MyFeature"),
|
|
58
|
+
("URLPath", "UrlPath"), # consequence of the snake-cased pipeline
|
|
59
|
+
("__name__", "Name"), # empty parts dropped
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
def test_canonicalises(self, raw, expected):
|
|
63
|
+
assert to_pascal_case(raw) == expected
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""``set_env_key`` is the single helper that edits scaffold-time .env files.
|
|
2
|
+
|
|
3
|
+
A regression here writes a duplicate ``KEY=`` line or, worse, leaves the old
|
|
4
|
+
value unstripped — both manifest as "my recipe didn't take effect" which is
|
|
5
|
+
hard to debug downstream.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from simple_module_cli._env import set_env_key
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_appends_to_empty_body():
|
|
14
|
+
assert set_env_key("", "FOO", "bar") == "FOO=bar\n"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_replaces_existing_key():
|
|
18
|
+
body = "FOO=old\nBAR=keep\n"
|
|
19
|
+
out = set_env_key(body, "FOO", "new")
|
|
20
|
+
# Replaced line lives at the bottom (append-after-strip strategy).
|
|
21
|
+
assert "FOO=old" not in out
|
|
22
|
+
assert "FOO=new\n" in out
|
|
23
|
+
assert "BAR=keep" in out
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_unrelated_lines_preserved_in_order():
|
|
27
|
+
body = "A=1\nB=2\nC=3\n"
|
|
28
|
+
out = set_env_key(body, "Z", "9")
|
|
29
|
+
lines = out.splitlines()
|
|
30
|
+
assert lines[0] == "A=1"
|
|
31
|
+
assert lines[1] == "B=2"
|
|
32
|
+
assert lines[2] == "C=3"
|
|
33
|
+
assert lines[-1] == "Z=9"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_idempotent_when_key_already_at_value():
|
|
37
|
+
body = "FOO=bar\n"
|
|
38
|
+
once = set_env_key(body, "FOO", "bar")
|
|
39
|
+
twice = set_env_key(once, "FOO", "bar")
|
|
40
|
+
assert once == twice == "FOO=bar\n"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_prefix_match_is_exact():
|
|
44
|
+
"""``KEY=`` must not match ``KEY_LONGER=``."""
|
|
45
|
+
body = "FOO_BAR=keep_me\n"
|
|
46
|
+
out = set_env_key(body, "FOO", "new")
|
|
47
|
+
assert "FOO_BAR=keep_me" in out
|
|
48
|
+
assert "FOO=new" in out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_output_always_ends_with_newline():
|
|
52
|
+
body = "X=1" # no trailing newline
|
|
53
|
+
out = set_env_key(body, "Y", "2")
|
|
54
|
+
assert out.endswith("\n")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Scaffold rollback on partial failure.
|
|
2
|
+
|
|
3
|
+
Before the rollback added to ``create_module``, a mid-pipeline error left
|
|
4
|
+
the user with a non-empty destination directory — the next ``smpy new``
|
|
5
|
+
invocation against the same path would then refuse to overwrite, but the
|
|
6
|
+
files already written wouldn't form a valid Python package either.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from simple_module_cli import scaffolding
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_create_module_rolls_back_on_template_failure(tmp_path, monkeypatch):
|
|
18
|
+
"""An exception during ``_apply_template_files`` must clear ``dest``."""
|
|
19
|
+
dest = tmp_path / "broken_module"
|
|
20
|
+
|
|
21
|
+
def boom(*_args, **_kwargs):
|
|
22
|
+
# Simulate a mid-write error after the dest directory exists but
|
|
23
|
+
# before all files have been laid down.
|
|
24
|
+
dest.mkdir(exist_ok=True)
|
|
25
|
+
(dest / "half_written.py").write_text("# truncated", encoding="utf-8")
|
|
26
|
+
raise RuntimeError("simulated template engine failure")
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr(scaffolding, "_apply_template_files", boom)
|
|
29
|
+
|
|
30
|
+
with pytest.raises(RuntimeError, match="simulated template engine failure"):
|
|
31
|
+
scaffolding.create_module(dest, "my_thing")
|
|
32
|
+
|
|
33
|
+
assert not dest.exists(), (
|
|
34
|
+
"Partial scaffold left on disk — rollback didn't fire. Subsequent "
|
|
35
|
+
"smpy new attempts at this path would refuse to overwrite."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_rollback_does_not_delete_pre_existing_directory(tmp_path, monkeypatch):
|
|
40
|
+
"""A pre-existing (empty) destination must stay on disk on rollback.
|
|
41
|
+
|
|
42
|
+
We can't tell from inside ``create_module`` whether ``dest`` was made
|
|
43
|
+
by us or by the caller, but the directory's *prior existence* is a
|
|
44
|
+
reliable signal: if the caller mkdir'd it, leaving their dir alone is
|
|
45
|
+
the conservative choice. (The half-written scaffold contents inside
|
|
46
|
+
are an unavoidable consequence — preventing those needs a transactional
|
|
47
|
+
file system, which we don't have.)
|
|
48
|
+
"""
|
|
49
|
+
dest = tmp_path / "owned_by_caller"
|
|
50
|
+
dest.mkdir()
|
|
51
|
+
|
|
52
|
+
def boom(*_args, **_kwargs):
|
|
53
|
+
raise RuntimeError("simulated failure before any files written")
|
|
54
|
+
|
|
55
|
+
monkeypatch.setattr(scaffolding, "_apply_template_files", boom)
|
|
56
|
+
|
|
57
|
+
with pytest.raises(RuntimeError):
|
|
58
|
+
scaffolding.create_module(dest, "thing")
|
|
59
|
+
|
|
60
|
+
# The dir survives — we didn't make it.
|
|
61
|
+
assert dest.exists()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_successful_scaffold_keeps_dest():
|
|
65
|
+
"""Sanity check: the rollback path only fires on failure."""
|
|
66
|
+
import tempfile
|
|
67
|
+
|
|
68
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
69
|
+
dest = Path(tmp) / "real_module"
|
|
70
|
+
scaffolding.create_module(dest, "real_module")
|
|
71
|
+
assert dest.exists()
|
|
72
|
+
# The template materialises at least the package directory.
|
|
73
|
+
assert any(dest.iterdir())
|
|
@@ -155,6 +155,28 @@ class TestCreateHost:
|
|
|
155
155
|
assert "make_include_object" in env_py
|
|
156
156
|
assert "for mod in modules:" not in env_py
|
|
157
157
|
|
|
158
|
+
async def test_main_py_loads_dotenv_before_settings(self, tmp_path):
|
|
159
|
+
"""Regression for #158: scaffolded main.py must populate ``os.environ``
|
|
160
|
+
from ``.env`` *before* ``Settings`` is imported, otherwise framework
|
|
161
|
+
code reading ``os.environ`` directly (e.g. users.bootstrap's dotenv
|
|
162
|
+
fallback under uvicorn launched from ``host/``) silently misses values
|
|
163
|
+
that pydantic-settings would have picked up.
|
|
164
|
+
"""
|
|
165
|
+
from simple_module_cli.scaffolding import create_host
|
|
166
|
+
|
|
167
|
+
dest = tmp_path / "demo"
|
|
168
|
+
create_host(dest, name="demo", modules=[])
|
|
169
|
+
main_py = (dest / "main.py").read_text(encoding="utf-8")
|
|
170
|
+
|
|
171
|
+
assert "load_dotenv_into_environ" in main_py
|
|
172
|
+
assert "os.chdir" in main_py
|
|
173
|
+
# The load_dotenv call must precede the first ``Settings`` import so
|
|
174
|
+
# ``BootstrapSettings``' ``env_file=".env"`` lookup and any direct
|
|
175
|
+
# ``os.environ.get(...)`` reads see the same view of the environment.
|
|
176
|
+
dotenv_idx = main_py.index("load_dotenv_into_environ(")
|
|
177
|
+
settings_import_idx = main_py.index("from simple_module_hosting import")
|
|
178
|
+
assert dotenv_idx < settings_import_idx
|
|
179
|
+
|
|
158
180
|
async def test_cli_create_host_runs_end_to_end(self, tmp_path):
|
|
159
181
|
"""The Click `smpy create-host` command produces a working scaffold."""
|
|
160
182
|
from simple_module_cli.cli import app
|
|
@@ -171,3 +193,77 @@ class TestCreateHost:
|
|
|
171
193
|
assert "simple_module_dashboard" in (tmp_path / "out" / "pyproject.toml").read_text(
|
|
172
194
|
encoding="utf-8"
|
|
173
195
|
)
|
|
196
|
+
|
|
197
|
+
async def test_scaffold_vite_config_includes_node_paths_fallback(self, tmp_path):
|
|
198
|
+
"""vite.config.ts must seed optimizeDeps.esbuildOptions.nodePaths
|
|
199
|
+
with the workspace node_modules so esbuild's scan-imports pass can
|
|
200
|
+
resolve cross-package bare imports from module pages whose importers
|
|
201
|
+
sit outside the host's client_app (e.g. wheel-installed modules).
|
|
202
|
+
|
|
203
|
+
Regression test for GitHub issue #152.
|
|
204
|
+
"""
|
|
205
|
+
from simple_module_cli.scaffolding import create_host
|
|
206
|
+
|
|
207
|
+
dest = tmp_path / "demo"
|
|
208
|
+
create_host(name="demo", dest=dest, modules=[])
|
|
209
|
+
|
|
210
|
+
vite_config = (dest / "client_app" / "vite.config.ts").read_text(encoding="utf-8")
|
|
211
|
+
|
|
212
|
+
# The scanner fallback must be configured under optimizeDeps so it
|
|
213
|
+
# applies to both dev pre-bundling and `vite build`'s pre-bundle pass.
|
|
214
|
+
assert "esbuildOptions" in vite_config, (
|
|
215
|
+
"vite.config.ts must configure optimizeDeps.esbuildOptions"
|
|
216
|
+
)
|
|
217
|
+
assert "nodePaths" in vite_config, (
|
|
218
|
+
"vite.config.ts must seed optimizeDeps.esbuildOptions.nodePaths "
|
|
219
|
+
"with the workspace node_modules (GH issue #152)."
|
|
220
|
+
)
|
|
221
|
+
# The seeded path must reference fsRoot — the dir that contains the
|
|
222
|
+
# hoisted node_modules — not a hardcoded literal that breaks in
|
|
223
|
+
# flat-vs-workspace layouts.
|
|
224
|
+
assert "fsRoot" in vite_config, (
|
|
225
|
+
"nodePaths must reference fsRoot (not a hardcoded literal) so the "
|
|
226
|
+
"fallback works in both flat and workspace layouts (GH issue #152)."
|
|
227
|
+
)
|
|
228
|
+
assert "node_modules" in vite_config, (
|
|
229
|
+
"nodePaths entry must include 'node_modules' (GH issue #152)."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def test_scaffold_vite_resolver_does_not_skip_workspace_modules(self, tmp_path):
|
|
233
|
+
"""The moduleBareImportResolver plugin must NOT short-circuit on
|
|
234
|
+
``fsRootPrefix`` containment.
|
|
235
|
+
|
|
236
|
+
In an npm-workspaces scaffold, ``fsRoot`` resolves to the workspace
|
|
237
|
+
root, which means workspace-member modules at ``modules/<name>/`` sit
|
|
238
|
+
*under* ``fsRoot``. An early-return gating on ``fsRootPrefix`` skips
|
|
239
|
+
them, leaving cross-package bare imports (`maplibre-gl`, `pmtiles`,
|
|
240
|
+
...) unresolved during dev-mode resolveId.
|
|
241
|
+
|
|
242
|
+
The plugin should guard only on the module-pages prefix set — the
|
|
243
|
+
condition that actually identifies module-page importers regardless of
|
|
244
|
+
whether they sit inside or outside ``fsRoot``.
|
|
245
|
+
|
|
246
|
+
Regression test for GitHub issue #156.
|
|
247
|
+
"""
|
|
248
|
+
from simple_module_cli.scaffolding import create_host
|
|
249
|
+
|
|
250
|
+
dest = tmp_path / "demo"
|
|
251
|
+
create_host(name="demo", dest=dest, modules=[])
|
|
252
|
+
|
|
253
|
+
vite_config = (dest / "client_app" / "vite.config.ts").read_text(encoding="utf-8")
|
|
254
|
+
|
|
255
|
+
# The resolver must still exist — the fix shouldn't remove the plugin.
|
|
256
|
+
assert "moduleBareImportResolver" in vite_config, (
|
|
257
|
+
"vite.config.ts must register the cross-package bare-import resolver."
|
|
258
|
+
)
|
|
259
|
+
# The buggy early-return must be gone (GH issue #156).
|
|
260
|
+
assert "startsWith(fsRootPrefix)" not in vite_config, (
|
|
261
|
+
"vite.config.ts must not early-return on fsRootPrefix containment — "
|
|
262
|
+
"in npm-workspaces mode workspace-member module pages live under "
|
|
263
|
+
"fsRoot and would be incorrectly skipped (GH issue #156)."
|
|
264
|
+
)
|
|
265
|
+
# The workspace-root re-resolution must still run.
|
|
266
|
+
assert "fakeWorkspaceImporter" in vite_config, (
|
|
267
|
+
"vite.config.ts must re-resolve unresolved bare imports against the "
|
|
268
|
+
"workspace root so hoisted node_modules wins (GH issue #156)."
|
|
269
|
+
)
|
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
from simple_module_hosting import Settings, create_app
|
|
8
|
-
from simple_module_hosting.logging import setup_logging
|
|
9
|
-
|
|
10
|
-
from routes import router as host_router
|
|
11
|
-
|
|
12
|
-
settings = Settings()
|
|
13
|
-
|
|
14
|
-
setup_logging(
|
|
15
|
-
level=settings.log_level,
|
|
16
|
-
json_format=settings.log_format == "json",
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
app = create_app(settings)
|
|
20
|
-
app.include_router(host_router)
|
|
21
|
-
|
|
22
|
-
if __name__ == "__main__":
|
|
23
|
-
import uvicorn
|
|
24
|
-
|
|
25
|
-
uvicorn.run(
|
|
26
|
-
"main:app",
|
|
27
|
-
host="0.0.0.0",
|
|
28
|
-
port=8000,
|
|
29
|
-
reload=settings.is_development,
|
|
30
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/.env.example
RENAMED
|
File without changes
|
{simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/.gitignore
RENAMED
|
File without changes
|
{simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/Makefile
RENAMED
|
File without changes
|
{simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/README.md.tpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/alembic.ini
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/host/routes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/module/.gitignore
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.13 → simple_module_cli-0.0.15}/simple_module_cli/templates/workspace/Makefile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|