simple-module-cli 0.0.36__tar.gz → 0.0.37__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.36 → simple_module_cli-0.0.37}/.gitignore +1 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/PKG-INFO +1 -1
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/pyproject.toml +1 -1
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/_env.py +8 -1
- simple_module_cli-0.0.37/simple_module_cli/_local_env.py +83 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/add_cmd.py +36 -5
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/app_project.py +34 -11
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/cli.py +26 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/docker_assets.py +3 -2
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/new.py +1 -0
- simple_module_cli-0.0.37/simple_module_cli/openapi_cmd.py +81 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/pyproject_edit.py +19 -10
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-doctor/SKILL.md +3 -1
- simple_module_cli-0.0.37/simple_module_cli/templates/docker/Makefile.snippet +25 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/docker/compose-base-postgres.yml.tpl +5 -3
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/docker/compose-tasks-postgres.yml.tpl +3 -3
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/docker/compose-tasks-sqlite.yml.tpl +1 -1
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/.env.example +3 -2
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/.gitignore +1 -3
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/Makefile +21 -2
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/README.md.tpl +1 -1
- simple_module_cli-0.0.37/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +24 -0
- simple_module_cli-0.0.37/simple_module_cli/templates/host/client_app/pages/Error.tsx +1 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/migrations/env.py +9 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/routes.py +1 -1
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +1 -1
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/workspace/.env.example +3 -2
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/workspace/.gitignore +1 -3
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/workspace/Makefile +19 -6
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/workspace/README.md.tpl +9 -10
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/workspace/pyproject.toml.tpl +3 -0
- simple_module_cli-0.0.37/simple_module_cli/uv_workspace.py +57 -0
- simple_module_cli-0.0.37/tests/test_cli_add_workspace_member.py +112 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_docker_assets.py +82 -2
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_new.py +55 -19
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_new_scaffold_layout.py +32 -0
- simple_module_cli-0.0.37/tests/test_cli_openapi_export.py +32 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_recipes.py +16 -2
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_module_css_build.py +3 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_scaffolding_host.py +2 -0
- simple_module_cli-0.0.36/simple_module_cli/templates/docker/Makefile.snippet +0 -13
- simple_module_cli-0.0.36/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -26
- simple_module_cli-0.0.36/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -13
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/LICENSE +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/README.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/_module_build.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/_module_host.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/_templating.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/git_source.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/module_cmd.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/pins.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/pypi.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/requirements.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/scaffolding.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/docker/compose-base-sqlite.yml.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/docker/dockerignore +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/docker/host-flat.Dockerfile +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/docker/host.Dockerfile +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/module-assets.ts +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/client_app/vite.dev-url.ts +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/__PACKAGE__/endpoints/views.py.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/__PACKAGE__/pages/Index.tsx.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/_optional/standalone/package.json.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/_optional/standalone/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/tests/test___PACKAGE__.py.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/update_cmd.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/conftest.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_add_cmd.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_add_spec_parse.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_git_scan.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_new_git_module.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_new_migration_bootstrap.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_new_regressions.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_package_update_pins.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_pyproject_edit.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_update_sources.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_vite_port_env.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_cli_wizard_git.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_create_module_ci_skip.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_module_cmd_build.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_module_cmd_e2e.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_module_cmd_verify.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_module_css_assets.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_module_css_subdir_sources.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_module_npm_aliases.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_module_pages_manifest.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_scaffolded_host_setup_wizard.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_scaffolding_module.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_scaffolding_module_js.py +0 -0
- {simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/tests/test_skills_cmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.37
|
|
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
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__all__ = ["set_env_key"]
|
|
5
|
+
__all__ = ["env_values", "set_env_key"]
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def set_env_key(text: str, key: str, value: str) -> str:
|
|
@@ -10,3 +10,10 @@ def set_env_key(text: str, key: str, value: str) -> str:
|
|
|
10
10
|
lines = [ln for ln in text.splitlines() if not ln.startswith(f"{key}=")]
|
|
11
11
|
lines.append(f"{key}={value}")
|
|
12
12
|
return "\n".join(lines) + "\n"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def env_values(text: str) -> dict[str, str]:
|
|
16
|
+
"""Parse the ``KEY=VALUE`` lines of an env-style file body (comments skipped)."""
|
|
17
|
+
lines = text.splitlines()
|
|
18
|
+
pairs = (ln.split("=", 1) for ln in lines if "=" in ln and not ln.lstrip().startswith("#"))
|
|
19
|
+
return {key.strip(): value for key, value in pairs}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Split a new app's env file into a committed template and a local ``.env``.
|
|
2
|
+
|
|
3
|
+
``.env.example`` is committed, so it only ever carries placeholders. The
|
|
4
|
+
secrets ``smpy new`` generates go to ``.env``, which every scaffold's
|
|
5
|
+
``.gitignore`` excludes (GH #441). An existing ``.env`` is never overwritten:
|
|
6
|
+
only keys it does not define yet are added.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import secrets
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from simple_module_cli._env import env_values, set_env_key
|
|
16
|
+
|
|
17
|
+
__all__ = ["add_postgres_password_placeholder", "comment_out_env_key", "write_local_env"]
|
|
18
|
+
|
|
19
|
+
_PG_PASSWORD_NOTE = (
|
|
20
|
+
"# Compose puts this into SM_DATABASE_URL unencoded: use only URL-safe\n"
|
|
21
|
+
"# characters (letters, digits, - _ . ~). `make docker-up` rejects others (GH #472).\n"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_SECRET_NOTE = (
|
|
25
|
+
"# Secrets — `smpy new` generated these into .env (gitignored); keep them\n"
|
|
26
|
+
"# out of this committed file. Set real values wherever you deploy."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def comment_out_env_key(text: str, key: str) -> str:
|
|
31
|
+
"""Turn ``KEY=...`` into a bare ``# KEY=`` placeholder, in place if present."""
|
|
32
|
+
lines = text.splitlines()
|
|
33
|
+
placeholder = f"# {key}="
|
|
34
|
+
hits = [i for i, ln in enumerate(lines) if ln.startswith((f"{key}=", placeholder))]
|
|
35
|
+
if hits:
|
|
36
|
+
lines[hits[0]] = placeholder
|
|
37
|
+
lines = [ln for i, ln in enumerate(lines) if i not in hits[1:]]
|
|
38
|
+
else:
|
|
39
|
+
if _SECRET_NOTE.splitlines()[0] not in lines:
|
|
40
|
+
lines += ["", *_SECRET_NOTE.splitlines()]
|
|
41
|
+
lines.append(placeholder)
|
|
42
|
+
return "\n".join(lines) + "\n"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def add_postgres_password_placeholder(text: str) -> str:
|
|
46
|
+
"""Add an empty ``POSTGRES_PASSWORD=`` to ``.env.example``, with its URL-safe note."""
|
|
47
|
+
text = set_env_key(text, "POSTGRES_PASSWORD", "")
|
|
48
|
+
return text.replace("POSTGRES_PASSWORD=\n", _PG_PASSWORD_NOTE + "POSTGRES_PASSWORD=\n", 1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def write_local_env(
|
|
52
|
+
target: Path, *, secret_keys: Iterable[str], overrides: dict[str, str] | None = None
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Write ``target/.env`` from ``.env.example`` plus generated secrets.
|
|
55
|
+
|
|
56
|
+
``secret_keys`` get a fresh random value each; ``overrides`` are written
|
|
57
|
+
verbatim (e.g. a database URL carrying a generated password). Overrides
|
|
58
|
+
belong together, so an existing ``.env`` defining any of them gets none:
|
|
59
|
+
a fresh URL must not disagree with a kept ``POSTGRES_PASSWORD``.
|
|
60
|
+
"""
|
|
61
|
+
env_path = target / ".env"
|
|
62
|
+
existing = env_path.exists()
|
|
63
|
+
source = env_path if existing else target / ".env.example"
|
|
64
|
+
text = source.read_text(encoding="utf-8") if source.exists() else ""
|
|
65
|
+
defined = env_values(text).keys()
|
|
66
|
+
overrides = overrides or {}
|
|
67
|
+
if existing and defined & overrides.keys():
|
|
68
|
+
overrides = {}
|
|
69
|
+
values = {key: secrets.token_urlsafe(32) for key in secret_keys} | overrides
|
|
70
|
+
for key, value in values.items():
|
|
71
|
+
if existing and key in defined:
|
|
72
|
+
continue
|
|
73
|
+
placeholder = f"# {key}="
|
|
74
|
+
lines = text.splitlines()
|
|
75
|
+
if placeholder in lines:
|
|
76
|
+
# Line-based, so a placeholder on the last line (no trailing
|
|
77
|
+
# newline) is handled too.
|
|
78
|
+
lines[lines.index(placeholder)] = f"{key}={value}"
|
|
79
|
+
text = "\n".join(lines) + "\n"
|
|
80
|
+
else:
|
|
81
|
+
text = set_env_key(text, key, value)
|
|
82
|
+
env_path.write_text(text, encoding="utf-8")
|
|
83
|
+
env_path.chmod(0o600) # holds generated secrets
|
|
@@ -11,6 +11,7 @@ sync-js-deps`` → entry-point verification, unless ``--no-sync``.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import os
|
|
14
15
|
import subprocess
|
|
15
16
|
import tempfile
|
|
16
17
|
from collections.abc import Callable
|
|
@@ -35,7 +36,9 @@ from simple_module_cli.pyproject_edit import (
|
|
|
35
36
|
write_dependency,
|
|
36
37
|
write_git_source,
|
|
37
38
|
write_path_source,
|
|
39
|
+
write_workspace_source,
|
|
38
40
|
)
|
|
41
|
+
from simple_module_cli.uv_workspace import is_workspace_member
|
|
39
42
|
|
|
40
43
|
__all__ = ["ExecRunner", "add_module", "post_install", "run_add"]
|
|
41
44
|
|
|
@@ -137,18 +140,46 @@ def _add_pypi(doc, pyproject: Path, raw: str, no_sync: bool, exec_runner: ExecRu
|
|
|
137
140
|
return [name]
|
|
138
141
|
|
|
139
142
|
|
|
143
|
+
def _resolve_local_path(project_dir: Path, spec_path: Path) -> tuple[Path, str]:
|
|
144
|
+
"""Resolve a local-path spec to ``(directory, text written to pyproject)``.
|
|
145
|
+
|
|
146
|
+
A relative path is resolved against the ``--path`` project (the pyproject
|
|
147
|
+
being edited, which is what ``[tool.uv.sources]`` paths are relative to).
|
|
148
|
+
If nothing exists there but the path exists relative to the cwd, the cwd
|
|
149
|
+
one is used and the written path is re-expressed relative to the project.
|
|
150
|
+
"""
|
|
151
|
+
if spec_path.is_absolute():
|
|
152
|
+
return spec_path, str(spec_path)
|
|
153
|
+
root = project_dir / spec_path
|
|
154
|
+
if not root.exists() and (Path.cwd() / spec_path).exists():
|
|
155
|
+
root = Path.cwd() / spec_path
|
|
156
|
+
return root, os.path.relpath(root.resolve(), project_dir.resolve())
|
|
157
|
+
return root, str(spec_path)
|
|
158
|
+
|
|
159
|
+
|
|
140
160
|
def _add_path(
|
|
141
161
|
doc, pyproject: Path, spec_path: Path, no_sync: bool, exec_runner: ExecRunner
|
|
142
162
|
) -> list[str]:
|
|
143
|
-
root =
|
|
163
|
+
root, written = _resolve_local_path(pyproject.parent, spec_path)
|
|
144
164
|
found = [m for m in scan_modules(root) if m.subdirectory is None]
|
|
145
165
|
if not found:
|
|
146
|
-
raise _fail(
|
|
166
|
+
raise _fail(
|
|
167
|
+
f"{root} has no [project.entry-points.simple_module] entry point at its root "
|
|
168
|
+
f"(relative paths resolve against the --path project, {pyproject.parent}, "
|
|
169
|
+
"falling back to the current directory)"
|
|
170
|
+
)
|
|
147
171
|
mod = found[0]
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
if is_workspace_member(pyproject.parent, root):
|
|
173
|
+
# uv refuses a path source for a workspace member; the scaffold's own
|
|
174
|
+
# sample module is wired the same way (unpinned + workspace = true).
|
|
175
|
+
write_dependency(doc, mod.dist_name, "")
|
|
176
|
+
write_workspace_source(doc, mod.dist_name)
|
|
177
|
+
typer.echo(f"Added {mod.dist_name} (uv workspace member {written}).")
|
|
178
|
+
else:
|
|
179
|
+
write_dependency(doc, mod.dist_name, derive_range(mod.version))
|
|
180
|
+
write_path_source(doc, mod.dist_name, path=written)
|
|
181
|
+
typer.echo(f"Added {mod.dist_name} (editable path {written}).")
|
|
150
182
|
save_pyproject(pyproject, doc)
|
|
151
|
-
typer.echo(f"Added {mod.dist_name} (editable path {spec_path}).")
|
|
152
183
|
if not no_sync:
|
|
153
184
|
post_install(
|
|
154
185
|
pyproject.parent,
|
|
@@ -20,7 +20,12 @@ from pathlib import Path
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
22
|
from simple_module_cli._env import set_env_key
|
|
23
|
-
from simple_module_cli.
|
|
23
|
+
from simple_module_cli._local_env import (
|
|
24
|
+
add_postgres_password_placeholder,
|
|
25
|
+
comment_out_env_key,
|
|
26
|
+
write_local_env,
|
|
27
|
+
)
|
|
28
|
+
from simple_module_cli.case import to_kebab_case, to_pascal_case, to_snake_case
|
|
24
29
|
from simple_module_cli.catalog import CATALOG, PRESETS, expand_deps
|
|
25
30
|
from simple_module_cli.docker_assets import scaffold_docker_assets
|
|
26
31
|
from simple_module_cli.recipes import RECIPES, ScaffoldCtx
|
|
@@ -52,6 +57,8 @@ _APP_PY_DEV_DEPS = [
|
|
|
52
57
|
"pytest>=8.0",
|
|
53
58
|
"pytest-asyncio>=0.24",
|
|
54
59
|
"pytest-playwright>=0.7.2",
|
|
60
|
+
# Floor: unpinned, uv backtracks playwright to 1.18 (GH #445).
|
|
61
|
+
"playwright>=1.45",
|
|
55
62
|
"ruff>=0.8",
|
|
56
63
|
"ty>=0.0.29",
|
|
57
64
|
]
|
|
@@ -150,19 +157,33 @@ def create_app_project(
|
|
|
150
157
|
py_deps.append(_SAMPLE_MODULE_PKG)
|
|
151
158
|
workspace_sources.append(_SAMPLE_MODULE_PKG)
|
|
152
159
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
# Generated secrets go to the gitignored .env, never the committed
|
|
161
|
+
# .env.example (GH #441) — see write_local_env below.
|
|
162
|
+
secret_keys = ["SM_SECRET_KEY"]
|
|
156
163
|
if "users" in resolved:
|
|
157
164
|
# UsersSettings refuses its placeholder token secrets when
|
|
158
165
|
# SM_ENVIRONMENT=production — the mode the compose containers run
|
|
159
|
-
# in — so generate real ones, same as SM_SECRET_KEY
|
|
160
|
-
|
|
166
|
+
# in — so generate real ones, same as SM_SECRET_KEY.
|
|
167
|
+
secret_keys += [
|
|
161
168
|
"SM_USERS_RESET_PASSWORD_TOKEN_SECRET",
|
|
162
169
|
"SM_USERS_VERIFICATION_TOKEN_SECRET",
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
]
|
|
171
|
+
db_name = to_snake_case(name)
|
|
172
|
+
env_path = target / ".env.example"
|
|
173
|
+
env_text = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
|
|
174
|
+
for key in secret_keys:
|
|
175
|
+
env_text = comment_out_env_key(env_text, key)
|
|
176
|
+
local_overrides: dict[str, str] = {}
|
|
177
|
+
if db == "postgres":
|
|
178
|
+
# The compose `postgres` service takes its password from .env, and
|
|
179
|
+
# the dev SM_DATABASE_URL must match it (GH #444).
|
|
180
|
+
env_text = add_postgres_password_placeholder(env_text)
|
|
181
|
+
password = _secrets.token_urlsafe(24)
|
|
182
|
+
local_overrides = {
|
|
183
|
+
"POSTGRES_PASSWORD": password,
|
|
184
|
+
"SM_DATABASE_URL": _db_url(db, db_name, flat=flat, password=password),
|
|
185
|
+
}
|
|
186
|
+
env_text = set_env_key(env_text, "SM_DATABASE_URL", _db_url(db, db_name, flat=flat))
|
|
166
187
|
env_text = set_env_key(env_text, "SM_MULTI_TENANT", "true" if tenancy else "false")
|
|
167
188
|
env_path.write_text(env_text, encoding="utf-8")
|
|
168
189
|
|
|
@@ -198,6 +219,8 @@ def create_app_project(
|
|
|
198
219
|
RECIPES[recipe_key].apply(target, ctx)
|
|
199
220
|
|
|
200
221
|
scaffold_docker_assets(target, ctx, flat=flat)
|
|
222
|
+
# Last, so .env picks up every key the recipes added to .env.example.
|
|
223
|
+
write_local_env(target, secret_keys=secret_keys, overrides=local_overrides)
|
|
201
224
|
|
|
202
225
|
return host_dir, preserved
|
|
203
226
|
|
|
@@ -234,9 +257,9 @@ def _seed_static_dist_placeholder(static_dist: Path) -> None:
|
|
|
234
257
|
(static_dist / ".gitkeep").touch()
|
|
235
258
|
|
|
236
259
|
|
|
237
|
-
def _db_url(db: str,
|
|
260
|
+
def _db_url(db: str, db_name: str, *, flat: bool, password: str = "<POSTGRES_PASSWORD>") -> str:
|
|
238
261
|
if db == "postgres":
|
|
239
|
-
return f"postgresql+asyncpg://postgres:
|
|
262
|
+
return f"postgresql+asyncpg://postgres:{password}@localhost:5432/{db_name}"
|
|
240
263
|
# Workspace mode keeps the SQLite file next to host/'s alembic.ini so
|
|
241
264
|
# `cd host && uvicorn` and `cd host && alembic` resolve the same path.
|
|
242
265
|
return "sqlite+aiosqlite:///./app.db" if flat else "sqlite+aiosqlite:///./host/app.db"
|
|
@@ -14,6 +14,7 @@ group are mounted as named subgroups (e.g. ``smpy host gen-pages``).
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import os
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Annotated
|
|
19
20
|
|
|
@@ -23,6 +24,7 @@ from simple_module_cli.add_cmd import add_module, run_add
|
|
|
23
24
|
from simple_module_cli.case import to_kebab_case
|
|
24
25
|
from simple_module_cli.module_cmd import module_app
|
|
25
26
|
from simple_module_cli.new import new_project
|
|
27
|
+
from simple_module_cli.openapi_cmd import openapi_app
|
|
26
28
|
from simple_module_cli.package_update import package_update
|
|
27
29
|
from simple_module_cli.plugins import discover_and_mount
|
|
28
30
|
from simple_module_cli.scaffolding import create_host as _create_host
|
|
@@ -30,6 +32,7 @@ from simple_module_cli.scaffolding import create_module as _create_module
|
|
|
30
32
|
from simple_module_cli.scaffolding import is_inside_existing_repo, resolve_framework_version
|
|
31
33
|
from simple_module_cli.skills_cmd import app as skills_app
|
|
32
34
|
from simple_module_cli.update_cmd import update_modules
|
|
35
|
+
from simple_module_cli.uv_workspace import find_workspace_root, is_workspace_member
|
|
33
36
|
|
|
34
37
|
app = typer.Typer(
|
|
35
38
|
help="SimpleModule developer CLI.",
|
|
@@ -43,6 +46,7 @@ app.command("update")(update_modules)
|
|
|
43
46
|
app.command("package-update")(package_update)
|
|
44
47
|
app.add_typer(skills_app, name="skills")
|
|
45
48
|
app.add_typer(module_app, name="module")
|
|
49
|
+
app.add_typer(openapi_app, name="openapi")
|
|
46
50
|
|
|
47
51
|
|
|
48
52
|
@app.command("create-host")
|
|
@@ -103,6 +107,27 @@ def create_host(
|
|
|
103
107
|
typer.echo(" python main.py")
|
|
104
108
|
|
|
105
109
|
|
|
110
|
+
def _echo_workspace_hint(target: Path) -> None:
|
|
111
|
+
"""Tell the user how to wire a workspace-member module into the host (GH #442)."""
|
|
112
|
+
root = find_workspace_root(target.parent)
|
|
113
|
+
if root is None:
|
|
114
|
+
return
|
|
115
|
+
host = root / "host"
|
|
116
|
+
if not (host / "pyproject.toml").is_file() or not is_workspace_member(host, target):
|
|
117
|
+
return
|
|
118
|
+
rel = os.path.relpath(target.resolve(), host.resolve())
|
|
119
|
+
if not rel.startswith("."):
|
|
120
|
+
rel = f"./{rel}" # parse_add_spec reads a bare "a/b" as a PyPI requirement
|
|
121
|
+
host_arg = os.path.relpath(host.resolve(), Path.cwd())
|
|
122
|
+
if host_arg.startswith(".."):
|
|
123
|
+
host_arg = str(host.resolve())
|
|
124
|
+
typer.echo(
|
|
125
|
+
f"\nThis module is a uv workspace member. Wire it into the host with:\n"
|
|
126
|
+
f" smpy add {rel} --path {host_arg}"
|
|
127
|
+
" # adds it as { workspace = true }"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
106
131
|
@app.command("create-module")
|
|
107
132
|
def create_module(
|
|
108
133
|
name: Annotated[str, typer.Argument(help="Module name (any case).")],
|
|
@@ -149,6 +174,7 @@ def create_module(
|
|
|
149
174
|
"Skipped .github/ workflows: this module is inside an existing repo, "
|
|
150
175
|
"where nested workflows never run. Use --standalone to emit them."
|
|
151
176
|
)
|
|
177
|
+
_echo_workspace_hint(target)
|
|
152
178
|
typer.echo("\nNext steps:")
|
|
153
179
|
typer.echo(f" cd {target}")
|
|
154
180
|
typer.echo(" uv sync --extra dev")
|
|
@@ -19,7 +19,7 @@ import importlib.resources
|
|
|
19
19
|
import shutil
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
|
-
from simple_module_cli.case import
|
|
22
|
+
from simple_module_cli.case import to_snake_case
|
|
23
23
|
from simple_module_cli.recipes import ScaffoldCtx
|
|
24
24
|
|
|
25
25
|
__all__ = ["scaffold_docker_assets"]
|
|
@@ -70,7 +70,8 @@ def scaffold_docker_assets(target: Path, ctx: ScaffoldCtx, *, flat: bool = False
|
|
|
70
70
|
# compiles to DEFAULT 0 on SQLite, which Postgres rejects), so pointing
|
|
71
71
|
# a sqlite-scaffolded app at a Postgres container fails on first boot.
|
|
72
72
|
db = "postgres" if ctx.db == "postgres" else "sqlite"
|
|
73
|
-
|
|
73
|
+
# snake_case: a kebab name needs quoting in every psql call (GH #443).
|
|
74
|
+
pg_db = to_snake_case(ctx.name)
|
|
74
75
|
with_tasks = "background_tasks" in ctx.selected
|
|
75
76
|
compose = (
|
|
76
77
|
_read(templates / f"compose-base-{db}.yml.tpl")
|
|
@@ -170,6 +170,7 @@ def new_project(
|
|
|
170
170
|
for path in preserved:
|
|
171
171
|
rel = path.relative_to(target) if path.is_relative_to(target) else path
|
|
172
172
|
typer.echo(f" {rel}")
|
|
173
|
+
typer.echo("\nGenerated secrets were written to .env (gitignored), not .env.example.")
|
|
173
174
|
typer.echo("\nNext steps:")
|
|
174
175
|
typer.echo(f" cd {target}")
|
|
175
176
|
if no_install:
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""``smpy openapi`` — export the host's OpenAPI document without a server (#456).
|
|
2
|
+
|
|
3
|
+
The app is built in process with ``create_app`` and its schema is written to a
|
|
4
|
+
file, so CI can generate API clients from a checkout. ``simple_module_hosting``
|
|
5
|
+
is imported inside the command: the CLI package depends only on typer and
|
|
6
|
+
tomlkit (``test_no_framework_deps``), and the hosting stack exists only in a
|
|
7
|
+
host's environment (run via ``uv run smpy openapi export``).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
openapi_app = typer.Typer(help="OpenAPI schema tools.", no_args_is_help=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _norm(name: str) -> str:
|
|
23
|
+
"""``feature_flags`` and the ``FeatureFlags`` tag compare equal."""
|
|
24
|
+
return name.replace("_", "").replace("-", "").lower()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _filter_modules(schema: dict, modules: list[str]) -> dict:
|
|
28
|
+
"""Keep operations whose first tag is one of ``modules`` (case and underscores ignored)."""
|
|
29
|
+
wanted = {_norm(m) for m in modules}
|
|
30
|
+
paths: dict = {}
|
|
31
|
+
for path, item in schema.get("paths", {}).items():
|
|
32
|
+
kept = {
|
|
33
|
+
method: op
|
|
34
|
+
for method, op in item.items()
|
|
35
|
+
if isinstance(op, dict) and _norm((op.get("tags") or [""])[0]) in wanted
|
|
36
|
+
}
|
|
37
|
+
if kept:
|
|
38
|
+
paths[path] = kept
|
|
39
|
+
return {**schema, "paths": paths}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@openapi_app.command("export")
|
|
43
|
+
def export(
|
|
44
|
+
out: Annotated[Path, typer.Option("--out", help="File to write.")] = Path("openapi.json"),
|
|
45
|
+
module: Annotated[
|
|
46
|
+
list[str] | None,
|
|
47
|
+
typer.Option("--module", help="Only include this module's operations. Repeatable."),
|
|
48
|
+
] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Write the host's OpenAPI document (``/api/**`` only) to ``--out``."""
|
|
51
|
+
project_root = Path(os.environ.get("SM_PROJECT_ROOT") or Path.cwd())
|
|
52
|
+
os.environ.setdefault("SM_PROJECT_ROOT", str(project_root))
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from simple_module_core.dotenv import load_dotenv_into_environ
|
|
56
|
+
from simple_module_hosting import create_app, merge_host_settings
|
|
57
|
+
from simple_module_hosting._preapp_config import load_host_overrides
|
|
58
|
+
from simple_module_hosting.bootstrap_settings import BootstrapSettings
|
|
59
|
+
except ImportError:
|
|
60
|
+
typer.echo(
|
|
61
|
+
"error: simple_module_hosting is not installed — run this from a host "
|
|
62
|
+
"project (`uv run smpy openapi export`).",
|
|
63
|
+
err=True,
|
|
64
|
+
)
|
|
65
|
+
raise typer.Exit(code=1) from None
|
|
66
|
+
|
|
67
|
+
load_dotenv_into_environ(project_root / ".env")
|
|
68
|
+
if not load_host_overrides(BootstrapSettings().database_url):
|
|
69
|
+
typer.echo(
|
|
70
|
+
"warning: no host settings found in the database (unreachable, not migrated or "
|
|
71
|
+
"empty); using environment and defaults for api_title / api_version.",
|
|
72
|
+
err=True,
|
|
73
|
+
)
|
|
74
|
+
settings = merge_host_settings()
|
|
75
|
+
schema = create_app(settings).openapi()
|
|
76
|
+
if module:
|
|
77
|
+
schema = _filter_modules(schema, module)
|
|
78
|
+
|
|
79
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
out.write_text(json.dumps(schema, indent=2, sort_keys=False) + "\n", encoding="utf-8")
|
|
81
|
+
typer.echo(f"wrote {out} ({len(schema.get('paths', {}))} paths)")
|
|
@@ -24,6 +24,7 @@ __all__ = [
|
|
|
24
24
|
"write_dependency",
|
|
25
25
|
"write_git_source",
|
|
26
26
|
"write_path_source",
|
|
27
|
+
"write_workspace_source",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
|
|
@@ -90,6 +91,15 @@ def _uv_sources(doc: tomlkit.TOMLDocument) -> Table:
|
|
|
90
91
|
return uv.setdefault("sources", tomlkit.table())
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
def _set_source(doc: tomlkit.TOMLDocument, dist_name: str, src: Any) -> bool:
|
|
95
|
+
"""Write ``src`` under ``[tool.uv.sources]``; False when already identical."""
|
|
96
|
+
sources = _uv_sources(doc)
|
|
97
|
+
if dist_name in sources and dict(sources[dist_name]) == dict(src):
|
|
98
|
+
return False
|
|
99
|
+
sources[dist_name] = src
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
93
103
|
def write_git_source(
|
|
94
104
|
doc: tomlkit.TOMLDocument,
|
|
95
105
|
dist_name: str,
|
|
@@ -104,22 +114,21 @@ def write_git_source(
|
|
|
104
114
|
src[ref_info.kind] = ref_info.value
|
|
105
115
|
if subdirectory:
|
|
106
116
|
src["subdirectory"] = subdirectory
|
|
107
|
-
|
|
108
|
-
if dist_name in sources and dict(sources[dist_name]) == dict(src):
|
|
109
|
-
return False
|
|
110
|
-
sources[dist_name] = src
|
|
111
|
-
return True
|
|
117
|
+
return _set_source(doc, dist_name, src)
|
|
112
118
|
|
|
113
119
|
|
|
114
120
|
def write_path_source(doc: tomlkit.TOMLDocument, dist_name: str, *, path: str) -> bool:
|
|
115
121
|
src = tomlkit.inline_table()
|
|
116
122
|
src["path"] = path
|
|
117
123
|
src["editable"] = True
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
return _set_source(doc, dist_name, src)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def write_workspace_source(doc: tomlkit.TOMLDocument, dist_name: str) -> bool:
|
|
128
|
+
"""``dist = { workspace = true }`` — required for uv workspace members."""
|
|
129
|
+
src = tomlkit.inline_table()
|
|
130
|
+
src["workspace"] = True
|
|
131
|
+
return _set_source(doc, dist_name, src)
|
|
123
132
|
|
|
124
133
|
|
|
125
134
|
def _sources_dict(doc: tomlkit.TOMLDocument) -> dict[str, Any]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: simple-module-doctor
|
|
3
|
-
description: Use when interpreting a simple_module_python diagnostic code (SM001–
|
|
3
|
+
description: Use when interpreting a simple_module_python diagnostic code (SM001–SM029) printed at boot or by the diagnostics runner — what the code means, whether it's blocking, and the concrete fix. Triggers on any "SMnnn" code in logs, "InvalidModuleError", "module silently doesn't load", or "production failed boot".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# simple_module_python: diagnostics
|
|
@@ -33,6 +33,8 @@ You can also call `run_diagnostics(...)` from `simple_module_core` programmatica
|
|
|
33
33
|
| **SM019** | WARNING | A module registers view routes (non-empty `view_prefix` + overrides `register_routes`) but overrides neither `register_menu_items` nor `register_permissions` — its pages exist with no sidebar entry and no role-editor visibility | Add `register_menu_items()` (sidebar) or `register_permissions()` (role editor), or clear `view_prefix` if the module is API-only |
|
|
34
34
|
| **SM020** | ERROR | More than one auth provider module is installed | Install exactly one auth provider (e.g. `users` OR `keycloak`, not both) |
|
|
35
35
|
| **SM021** | WARNING | No auth provider module is installed | Install an auth provider module (e.g. `simple-module-users` or `simple-module-keycloak`) |
|
|
36
|
+
| **SM028** | WARNING | A module's `tasks.py` imports `torch`, `tensorflow`, `ultralytics`, `osgeo`, `rasterio`, `cv2` or `sklearn` at module level | Import the package inside the task function, so only a worker that runs the task loads it |
|
|
37
|
+
| **SM029** | INFO | A module overrides `db_extensions()` (Postgres extensions like `ltree`) but the database is SQLite, which has none; `ensure_db_extensions` raises `UnsupportedDatabaseError` at migrate time | Point `SM_DATABASE_URL` at PostgreSQL |
|
|
36
38
|
|
|
37
39
|
`SM002`, `SM005`, `SM006` don't exist in the implementation — they're reserved/retired and never emitted. `SM012` is the one code that doesn't come from the core diagnostics runner: it's raised from the hosting layer (`check_settings_registration`, after Phase 4 settings registration) and so is only ever seen at **dev boot**, not via `make doctor`.
|
|
38
40
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# --- docker --------------------------------------------------------------
|
|
2
|
+
.PHONY: docker-build docker-up docker-down
|
|
3
|
+
|
|
4
|
+
docker-build: ## Build the app image
|
|
5
|
+
docker compose build
|
|
6
|
+
|
|
7
|
+
docker-up: ## Run the full stack in containers
|
|
8
|
+
@test -f .env || { cp .env.example .env; \
|
|
9
|
+
echo "Created .env from .env.example: set POSTGRES_PASSWORD (Postgres apps) and the"; \
|
|
10
|
+
echo "commented SM_* secrets in .env; production containers refuse to boot without them."; }
|
|
11
|
+
@# compose interpolates POSTGRES_PASSWORD raw into the SM_DATABASE_URL, so it must be URL-safe.
|
|
12
|
+
@# The value is read the way compose reads it: last definition wins, CRLF, an inline
|
|
13
|
+
@# trailing comment and surrounding quotes are not part of the password.
|
|
14
|
+
@pw=$$(sed -n 's/^POSTGRES_PASSWORD=//p' .env | tail -n 1 | tr -d '\r' \
|
|
15
|
+
| sed -e 's/[[:space:]]#.*$$//' -e 's/[[:space:]]*$$//' -e 's/^"\(.*\)"$$/\1/' -e "s/^'\(.*\)'$$/\1/"); \
|
|
16
|
+
if printf '%s' "$$pw" | grep -q '[^A-Za-z0-9._~-]'; then \
|
|
17
|
+
echo "POSTGRES_PASSWORD in .env must be URL-safe (letters, digits, - _ . ~ only):" >&2; \
|
|
18
|
+
echo "compose puts it into SM_DATABASE_URL unencoded. Pick another password." >&2; \
|
|
19
|
+
exit 1; \
|
|
20
|
+
fi
|
|
21
|
+
docker compose up --build
|
|
22
|
+
|
|
23
|
+
docker-down: ## Stop the compose stack
|
|
24
|
+
docker compose down
|
|
25
|
+
# --- end docker ----------------------------------------------------------
|
|
@@ -4,9 +4,11 @@ services:
|
|
|
4
4
|
environment:
|
|
5
5
|
POSTGRES_DB: "{{PG_DB}}"
|
|
6
6
|
POSTGRES_USER: postgres
|
|
7
|
-
POSTGRES_PASSWORD:
|
|
7
|
+
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}"
|
|
8
8
|
ports:
|
|
9
|
-
|
|
9
|
+
# Loopback only: reachable as a local dev database, never from the
|
|
10
|
+
# network. The app reaches it over the compose network (GH #444).
|
|
11
|
+
- "127.0.0.1:5432:5432"
|
|
10
12
|
volumes:
|
|
11
13
|
- pgdata:/var/lib/postgresql/data
|
|
12
14
|
healthcheck:
|
|
@@ -26,7 +28,7 @@ services:
|
|
|
26
28
|
# Containers serve the built bundle; development mode would emit
|
|
27
29
|
# asset tags pointing at the (absent) Vite dev server.
|
|
28
30
|
SM_ENVIRONMENT: production
|
|
29
|
-
SM_DATABASE_URL: postgresql+asyncpg://postgres
|
|
31
|
+
SM_DATABASE_URL: "postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/{{PG_DB}}"
|
|
30
32
|
{{APP_EXTRA_ENV}} ports:
|
|
31
33
|
- "8000:8000"
|
|
32
34
|
depends_on:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
redis:
|
|
3
3
|
image: redis:7-alpine
|
|
4
4
|
ports:
|
|
5
|
-
- "6379:6379"
|
|
5
|
+
- "127.0.0.1:6379:6379"
|
|
6
6
|
volumes:
|
|
7
7
|
- redisdata:/data
|
|
8
8
|
healthcheck:
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
required: false
|
|
22
22
|
environment:
|
|
23
23
|
SM_ENVIRONMENT: production
|
|
24
|
-
SM_DATABASE_URL: postgresql+asyncpg://postgres
|
|
24
|
+
SM_DATABASE_URL: "postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/{{PG_DB}}"
|
|
25
25
|
SM_BG_TASKS_BROKER_URL: redis://redis:6379/0
|
|
26
26
|
SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1
|
|
27
27
|
working_dir: /app
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
required: false
|
|
49
49
|
environment:
|
|
50
50
|
SM_ENVIRONMENT: production
|
|
51
|
-
SM_DATABASE_URL: postgresql+asyncpg://postgres
|
|
51
|
+
SM_DATABASE_URL: "postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/{{PG_DB}}"
|
|
52
52
|
SM_BG_TASKS_BROKER_URL: redis://redis:6379/0
|
|
53
53
|
SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1
|
|
54
54
|
working_dir: /app
|
{simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/.env.example
RENAMED
|
@@ -5,8 +5,9 @@ SM_DATABASE_URL=sqlite+aiosqlite:///./app.db
|
|
|
5
5
|
# Environment: development | production
|
|
6
6
|
SM_ENVIRONMENT=development
|
|
7
7
|
|
|
8
|
-
# Secret key for session middleware
|
|
9
|
-
|
|
8
|
+
# Secret key for session middleware. Leave unset to have one generated and
|
|
9
|
+
# stored at first boot; `smpy new` writes a generated one into .env.
|
|
10
|
+
# SM_SECRET_KEY=
|
|
10
11
|
|
|
11
12
|
# Vite dev server URL (only used in development).
|
|
12
13
|
SM_VITE_DEV_URL=http://localhost:5050
|
{simple_module_cli-0.0.36 → simple_module_cli-0.0.37}/simple_module_cli/templates/host/.gitignore
RENAMED
|
@@ -3,8 +3,6 @@ __pycache__/
|
|
|
3
3
|
*.egg-info/
|
|
4
4
|
.venv/
|
|
5
5
|
|
|
6
|
-
uv.lock
|
|
7
|
-
|
|
8
6
|
node_modules/
|
|
9
7
|
|
|
10
8
|
.env
|
|
@@ -14,6 +12,6 @@ node_modules/
|
|
|
14
12
|
|
|
15
13
|
static/dist/
|
|
16
14
|
|
|
17
|
-
# Auto-generated by the host at boot / `
|
|
15
|
+
# Auto-generated by the host at boot / `make gen-pages`.
|
|
18
16
|
client_app/modules.manifest.json
|
|
19
17
|
client_app/modules.generated.ts
|