simple-module-cli 0.0.2__py3-none-any.whl
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/__init__.py +0 -0
- simple_module_cli/_env.py +12 -0
- simple_module_cli/app_project.py +123 -0
- simple_module_cli/case.py +30 -0
- simple_module_cli/catalog.py +107 -0
- simple_module_cli/cli.py +104 -0
- simple_module_cli/new.py +124 -0
- simple_module_cli/plugins.py +56 -0
- simple_module_cli/recipes.py +93 -0
- simple_module_cli/scaffolding.py +124 -0
- simple_module_cli/templates/host/.env.example +20 -0
- simple_module_cli/templates/host/.gitignore +19 -0
- simple_module_cli/templates/host/Makefile +24 -0
- simple_module_cli/templates/host/README.md.tpl +59 -0
- simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +12 -0
- simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +60 -0
- simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +17 -0
- simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +37 -0
- simple_module_cli/templates/host/alembic.ini +36 -0
- simple_module_cli/templates/host/client_app/app.tsx +16 -0
- simple_module_cli/templates/host/client_app/main.tsx +2 -0
- simple_module_cli/templates/host/client_app/package.json.tpl +23 -0
- simple_module_cli/templates/host/client_app/pages/Error.tsx +13 -0
- simple_module_cli/templates/host/client_app/pages.ts +47 -0
- simple_module_cli/templates/host/client_app/styles.css +7 -0
- simple_module_cli/templates/host/client_app/tsconfig.json +16 -0
- simple_module_cli/templates/host/client_app/vite.config.ts +39 -0
- simple_module_cli/templates/host/main.py +27 -0
- simple_module_cli/templates/host/migrations/env.py +80 -0
- simple_module_cli/templates/host/migrations/script.py.mako +26 -0
- simple_module_cli/templates/host/migrations/versions/.gitkeep +1 -0
- simple_module_cli/templates/host/pyproject.toml.tpl +17 -0
- simple_module_cli/templates/host/templates/index.html +12 -0
- simple_module_cli/templates/module/.github/workflows/ci.yml +32 -0
- simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +52 -0
- simple_module_cli/templates/module/.gitignore +14 -0
- simple_module_cli/templates/module/README.md.tpl +82 -0
- simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
- simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +46 -0
- simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
- simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +22 -0
- simple_module_cli/templates/module/package.json.tpl +16 -0
- simple_module_cli/templates/module/pyproject.toml.tpl +39 -0
- simple_module_cli/templates/module/tests/__init__.py +0 -0
- simple_module_cli/templates/module/tests/test_module.py.tpl +27 -0
- simple_module_cli/templates/module/tsconfig.json.tpl +11 -0
- simple_module_cli/wizard.py +48 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/.env.example +20 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/.gitignore +19 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/Makefile +24 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/README.md.tpl +59 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +12 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +60 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +17 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +37 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/alembic.ini +36 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/app.tsx +16 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/main.tsx +2 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/package.json.tpl +23 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/pages/Error.tsx +13 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/pages.ts +47 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/styles.css +7 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/tsconfig.json +16 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/vite.config.ts +39 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/main.py +27 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/env.py +80 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/script.py.mako +26 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/versions/.gitkeep +1 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/pyproject.toml.tpl +17 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/templates/index.html +12 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.github/workflows/ci.yml +32 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +52 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.gitignore +14 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/README.md.tpl +82 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +46 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +22 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/package.json.tpl +16 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/pyproject.toml.tpl +39 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tests/__init__.py +0 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tests/test_module.py.tpl +27 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tsconfig.json.tpl +11 -0
- simple_module_cli-0.0.2.dist-info/METADATA +63 -0
- simple_module_cli-0.0.2.dist-info/RECORD +92 -0
- simple_module_cli-0.0.2.dist-info/WHEEL +4 -0
- simple_module_cli-0.0.2.dist-info/entry_points.txt +3 -0
- simple_module_cli-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Host + module scaffolding via package-data templates.
|
|
2
|
+
|
|
3
|
+
* :func:`create_host` materializes a new host project from the templates
|
|
4
|
+
under ``simple_module/templates/host/``.
|
|
5
|
+
* :func:`create_module` materializes a new module package from
|
|
6
|
+
``simple_module/templates/module/``.
|
|
7
|
+
|
|
8
|
+
The frontend pages manifest + per-module JS dep discovery live in
|
|
9
|
+
:mod:`simple_module_hosting.manifest` (those need module-discovery and
|
|
10
|
+
stay in hosting).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib.resources
|
|
16
|
+
import logging
|
|
17
|
+
import shutil
|
|
18
|
+
from collections.abc import Mapping, Sequence
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from simple_module_cli.case import to_kebab_case, to_pascal_case, to_snake_case
|
|
22
|
+
|
|
23
|
+
__all__ = ["create_host", "create_module"]
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
_TEMPLATES_PACKAGE = "simple_module_cli.templates"
|
|
28
|
+
_PACKAGE_PATH_TOKEN = "__PACKAGE__"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _module_to_pypi_name(name: str) -> str:
|
|
32
|
+
return f"simple_module_{name.lower()}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _iter_template_files(template_root: Path):
|
|
36
|
+
"""Yield every file under ``template_root``. Skips ``_optional/`` paths."""
|
|
37
|
+
for path in template_root.rglob("*"):
|
|
38
|
+
if not path.is_file():
|
|
39
|
+
continue
|
|
40
|
+
if "_optional" in path.relative_to(template_root).parts:
|
|
41
|
+
continue
|
|
42
|
+
yield path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _require_empty_dest(dest: Path) -> None:
|
|
46
|
+
if dest.exists() and any(dest.iterdir()):
|
|
47
|
+
raise FileExistsError(
|
|
48
|
+
f"Destination {dest} already exists and is non-empty. "
|
|
49
|
+
"Choose a new path or remove the contents first."
|
|
50
|
+
)
|
|
51
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _resolve_template_root(subdir: str, override: Path | None) -> Path:
|
|
55
|
+
if override is not None:
|
|
56
|
+
return Path(override)
|
|
57
|
+
return Path(str(importlib.resources.files(_TEMPLATES_PACKAGE) / subdir))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _apply_template_files(
|
|
61
|
+
src_root: Path,
|
|
62
|
+
dest: Path,
|
|
63
|
+
substitutions: Mapping[str, str],
|
|
64
|
+
*,
|
|
65
|
+
path_rewrites: Mapping[str, str] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
for src in _iter_template_files(src_root):
|
|
68
|
+
rel_str = str(src.relative_to(src_root))
|
|
69
|
+
for old, new in (path_rewrites or {}).items():
|
|
70
|
+
rel_str = rel_str.replace(old, new)
|
|
71
|
+
rel_str = rel_str.removesuffix(".tpl")
|
|
72
|
+
target = dest / rel_str
|
|
73
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
if src.suffix == ".tpl":
|
|
75
|
+
text = src.read_text(encoding="utf-8")
|
|
76
|
+
for placeholder, value in substitutions.items():
|
|
77
|
+
text = text.replace(placeholder, value)
|
|
78
|
+
target.write_text(text, encoding="utf-8")
|
|
79
|
+
else:
|
|
80
|
+
shutil.copy2(src, target)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_host(
|
|
84
|
+
dest: Path,
|
|
85
|
+
name: str,
|
|
86
|
+
modules: Sequence[str],
|
|
87
|
+
template_root: Path | None = None,
|
|
88
|
+
) -> Path:
|
|
89
|
+
dest = Path(dest)
|
|
90
|
+
_require_empty_dest(dest)
|
|
91
|
+
module_dep_lines = "\n".join(f' "{_module_to_pypi_name(m)}>=0.1,<1.0",' for m in modules)
|
|
92
|
+
_apply_template_files(
|
|
93
|
+
_resolve_template_root("host", template_root),
|
|
94
|
+
dest,
|
|
95
|
+
{"{{HOST_NAME}}": name, "{{MODULE_DEPS}}": module_dep_lines},
|
|
96
|
+
)
|
|
97
|
+
logger.info(
|
|
98
|
+
"Scaffolded host '%s' at %s (modules: %s)", name, dest, ", ".join(modules) or "<none>"
|
|
99
|
+
)
|
|
100
|
+
return dest
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def create_module(
|
|
104
|
+
dest: Path,
|
|
105
|
+
name: str,
|
|
106
|
+
template_root: Path | None = None,
|
|
107
|
+
) -> Path:
|
|
108
|
+
dest = Path(dest)
|
|
109
|
+
_require_empty_dest(dest)
|
|
110
|
+
display_name = to_pascal_case(name)
|
|
111
|
+
slug = to_kebab_case(name)
|
|
112
|
+
package_name = to_snake_case(name)
|
|
113
|
+
_apply_template_files(
|
|
114
|
+
_resolve_template_root("module", template_root),
|
|
115
|
+
dest,
|
|
116
|
+
substitutions={
|
|
117
|
+
"{{MODULE_NAME}}": display_name,
|
|
118
|
+
"{{MODULE_SLUG}}": slug,
|
|
119
|
+
"{{PACKAGE_NAME}}": package_name,
|
|
120
|
+
},
|
|
121
|
+
path_rewrites={_PACKAGE_PATH_TOKEN: package_name},
|
|
122
|
+
)
|
|
123
|
+
logger.info("Scaffolded module '%s' at %s (package: %s)", display_name, dest, package_name)
|
|
124
|
+
return dest
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Database — defaults to a local SQLite file.
|
|
2
|
+
# For PostgreSQL use: postgresql+asyncpg://user:pass@host:5432/dbname
|
|
3
|
+
SM_DATABASE_URL=sqlite+aiosqlite:///./app.db
|
|
4
|
+
|
|
5
|
+
# Environment: development | production
|
|
6
|
+
SM_ENVIRONMENT=development
|
|
7
|
+
|
|
8
|
+
# Secret key for session middleware — change before deploying.
|
|
9
|
+
SM_SECRET_KEY=change-me-in-production
|
|
10
|
+
|
|
11
|
+
# Vite dev server URL (only used in development).
|
|
12
|
+
SM_VITE_DEV_URL=http://localhost:5050
|
|
13
|
+
|
|
14
|
+
# Optional: JSON array to restrict which installed modules load at boot.
|
|
15
|
+
# SM_MODULES_ENABLED=["Auth","Products"]
|
|
16
|
+
|
|
17
|
+
# First-boot admin seed (optional). Only applied when the users table is empty.
|
|
18
|
+
# Leave unset and use `uv run sm-users create-admin` instead if you prefer.
|
|
19
|
+
# SM_USERS_BOOTSTRAP_EMAIL=admin@example.com
|
|
20
|
+
# SM_USERS_BOOTSTRAP_PASSWORD=changeme
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.venv/
|
|
5
|
+
|
|
6
|
+
uv.lock
|
|
7
|
+
|
|
8
|
+
node_modules/
|
|
9
|
+
|
|
10
|
+
.env
|
|
11
|
+
|
|
12
|
+
*.db
|
|
13
|
+
*.sqlite3
|
|
14
|
+
|
|
15
|
+
static/dist/
|
|
16
|
+
|
|
17
|
+
# Auto-generated by the host at boot / `sm gen-pages`.
|
|
18
|
+
client_app/modules.manifest.json
|
|
19
|
+
client_app/modules.generated.ts
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.PHONY: install dev dev-api dev-ui build migrate gen-pages
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync
|
|
5
|
+
cd client_app && npm install
|
|
6
|
+
|
|
7
|
+
dev: gen-pages
|
|
8
|
+
@echo "Starting API and UI dev servers..."
|
|
9
|
+
$(MAKE) -j2 dev-api dev-ui
|
|
10
|
+
|
|
11
|
+
dev-api:
|
|
12
|
+
uv run uvicorn main:app --reload --port 8000
|
|
13
|
+
|
|
14
|
+
dev-ui:
|
|
15
|
+
cd client_app && npm run dev
|
|
16
|
+
|
|
17
|
+
build:
|
|
18
|
+
cd client_app && npm run build
|
|
19
|
+
|
|
20
|
+
migrate:
|
|
21
|
+
uv run alembic upgrade head
|
|
22
|
+
|
|
23
|
+
gen-pages:
|
|
24
|
+
uv run sm gen-pages --host-dir=client_app
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# {{HOST_NAME}}
|
|
2
|
+
|
|
3
|
+
A SimpleModule host application, generated by `sm create-host`.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install Python deps (framework + any modules listed in pyproject.toml)
|
|
9
|
+
uv sync
|
|
10
|
+
|
|
11
|
+
# Copy example env and adjust
|
|
12
|
+
cp .env.example .env
|
|
13
|
+
|
|
14
|
+
# First time only — initialize the migration history for the modules you
|
|
15
|
+
# picked. Inspect the generated file, then apply it.
|
|
16
|
+
alembic revision --autogenerate -m "initial schema"
|
|
17
|
+
alembic upgrade head
|
|
18
|
+
|
|
19
|
+
# Run the API
|
|
20
|
+
python main.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Dev UI + API together (when you have a `client_app/` alongside this file):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
make dev # if you ship a Makefile; otherwise run vite + uvicorn in two shells
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Adding a module
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 1) install the module package (e.g. from PyPI)
|
|
33
|
+
uv add simple_module_my_module
|
|
34
|
+
|
|
35
|
+
# 2) generate & apply the migration (new tables + any schema changes)
|
|
36
|
+
alembic revision --autogenerate -m "add my-module"
|
|
37
|
+
alembic upgrade head
|
|
38
|
+
|
|
39
|
+
# 3) restart the host — the module's routes, menu items, permissions,
|
|
40
|
+
# feature flags, events, and health checks register automatically
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Disabling a module without uninstalling it
|
|
44
|
+
|
|
45
|
+
Set `SM_MODULES_ENABLED` to a JSON array of the modules you want loaded:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
SM_MODULES_ENABLED=["Auth","Products"]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Unlisted modules stay installed (so the existing DB schema is untouched)
|
|
52
|
+
but don't contribute any routes, registries, or lifecycle work.
|
|
53
|
+
|
|
54
|
+
## Upgrading the framework
|
|
55
|
+
|
|
56
|
+
Modules declare `requires_framework` in their `Meta`. If a module can't
|
|
57
|
+
work with your installed `simple_module_core` version, the host refuses
|
|
58
|
+
to boot with `FrameworkVersionError` listing the offending modules.
|
|
59
|
+
Upgrade or pin as appropriate.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# --- background_tasks ----------------------------------------------------
|
|
2
|
+
.PHONY: worker beat worker-docker
|
|
3
|
+
|
|
4
|
+
worker: ## Run a Celery worker locally against $(SM_BG_TASKS_BROKER_URL)
|
|
5
|
+
uv run celery -A scripts.run_worker:celery worker -l info
|
|
6
|
+
|
|
7
|
+
beat: ## Run the Celery beat scheduler locally
|
|
8
|
+
uv run celery -A scripts.run_worker:celery beat -l info
|
|
9
|
+
|
|
10
|
+
worker-docker: ## Build + run the worker + beat services in docker
|
|
11
|
+
docker compose up --build worker beat
|
|
12
|
+
# --- end background_tasks ------------------------------------------------
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
services:
|
|
2
|
+
redis:
|
|
3
|
+
image: redis:7-alpine
|
|
4
|
+
ports:
|
|
5
|
+
- "6379:6379"
|
|
6
|
+
volumes:
|
|
7
|
+
- redisdata:/data
|
|
8
|
+
healthcheck:
|
|
9
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
10
|
+
interval: 5s
|
|
11
|
+
timeout: 3s
|
|
12
|
+
retries: 10
|
|
13
|
+
|
|
14
|
+
worker:
|
|
15
|
+
build:
|
|
16
|
+
context: .
|
|
17
|
+
dockerfile: docker/worker.Dockerfile
|
|
18
|
+
env_file: .env
|
|
19
|
+
environment:
|
|
20
|
+
SM_BG_TASKS_BROKER_URL: redis://redis:6379/0
|
|
21
|
+
SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1
|
|
22
|
+
depends_on:
|
|
23
|
+
redis:
|
|
24
|
+
condition: service_healthy
|
|
25
|
+
command:
|
|
26
|
+
- "uv"
|
|
27
|
+
- "run"
|
|
28
|
+
- "celery"
|
|
29
|
+
- "-A"
|
|
30
|
+
- "scripts.run_worker:celery"
|
|
31
|
+
- "worker"
|
|
32
|
+
- "-l"
|
|
33
|
+
- "info"
|
|
34
|
+
- "--concurrency=4"
|
|
35
|
+
|
|
36
|
+
beat:
|
|
37
|
+
build:
|
|
38
|
+
context: .
|
|
39
|
+
dockerfile: docker/worker.Dockerfile
|
|
40
|
+
env_file: .env
|
|
41
|
+
environment:
|
|
42
|
+
SM_BG_TASKS_BROKER_URL: redis://redis:6379/0
|
|
43
|
+
SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1
|
|
44
|
+
depends_on:
|
|
45
|
+
redis:
|
|
46
|
+
condition: service_healthy
|
|
47
|
+
worker:
|
|
48
|
+
condition: service_started
|
|
49
|
+
command:
|
|
50
|
+
- "uv"
|
|
51
|
+
- "run"
|
|
52
|
+
- "celery"
|
|
53
|
+
- "-A"
|
|
54
|
+
- "scripts.run_worker:celery"
|
|
55
|
+
- "beat"
|
|
56
|
+
- "-l"
|
|
57
|
+
- "info"
|
|
58
|
+
|
|
59
|
+
volumes:
|
|
60
|
+
redisdata:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Entry point for the Celery worker and beat services.
|
|
2
|
+
|
|
3
|
+
Both the web process and the worker go through the same
|
|
4
|
+
``background_tasks.celery_app.build_celery`` factory so the broker
|
|
5
|
+
config, autodiscovered tasks, and signal handlers stay in lockstep.
|
|
6
|
+
|
|
7
|
+
Run locally:
|
|
8
|
+
uv run celery -A scripts.run_worker:celery worker -l info
|
|
9
|
+
uv run celery -A scripts.run_worker:celery beat -l info
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from background_tasks.celery_app import build_celery
|
|
15
|
+
from background_tasks.settings import BackgroundTasksSettings
|
|
16
|
+
|
|
17
|
+
celery = build_celery(BackgroundTasksSettings())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Celery worker image for the BackgroundTasks module.
|
|
2
|
+
# Serves both the worker and beat services in docker-compose — they
|
|
3
|
+
# differ only by command.
|
|
4
|
+
|
|
5
|
+
FROM python:3.12-slim AS base
|
|
6
|
+
|
|
7
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
8
|
+
PYTHONUNBUFFERED=1 \
|
|
9
|
+
UV_LINK_MODE=copy \
|
|
10
|
+
UV_COMPILE_BYTECODE=1 \
|
|
11
|
+
UV_SYSTEM_PYTHON=1
|
|
12
|
+
|
|
13
|
+
RUN apt-get update \
|
|
14
|
+
&& apt-get install -y --no-install-recommends \
|
|
15
|
+
curl \
|
|
16
|
+
ca-certificates \
|
|
17
|
+
build-essential \
|
|
18
|
+
&& rm -rf /var/lib/apt/lists/* \
|
|
19
|
+
&& pip install --no-cache-dir uv
|
|
20
|
+
|
|
21
|
+
WORKDIR /app
|
|
22
|
+
|
|
23
|
+
COPY pyproject.toml uv.lock ./
|
|
24
|
+
COPY scripts/ scripts/
|
|
25
|
+
COPY client_app/ client_app/
|
|
26
|
+
|
|
27
|
+
RUN uv sync --frozen --no-dev
|
|
28
|
+
|
|
29
|
+
RUN useradd --system --uid 10001 --home /app --shell /usr/sbin/nologin worker \
|
|
30
|
+
&& chown -R worker:worker /app
|
|
31
|
+
USER worker
|
|
32
|
+
|
|
33
|
+
ENV CELERY_APP=scripts.run_worker:celery
|
|
34
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
35
|
+
CMD uv run celery -A $CELERY_APP inspect ping -d celery@$HOSTNAME || exit 1
|
|
36
|
+
|
|
37
|
+
CMD ["uv", "run", "celery", "-A", "scripts.run_worker:celery", "worker", "-l", "info"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = migrations
|
|
3
|
+
sqlalchemy.url =
|
|
4
|
+
|
|
5
|
+
[loggers]
|
|
6
|
+
keys = root,sqlalchemy,alembic
|
|
7
|
+
|
|
8
|
+
[handlers]
|
|
9
|
+
keys = console
|
|
10
|
+
|
|
11
|
+
[formatters]
|
|
12
|
+
keys = generic
|
|
13
|
+
|
|
14
|
+
[logger_root]
|
|
15
|
+
level = WARN
|
|
16
|
+
handlers = console
|
|
17
|
+
|
|
18
|
+
[logger_sqlalchemy]
|
|
19
|
+
level = WARN
|
|
20
|
+
handlers =
|
|
21
|
+
qualname = sqlalchemy.engine
|
|
22
|
+
|
|
23
|
+
[logger_alembic]
|
|
24
|
+
level = INFO
|
|
25
|
+
handlers =
|
|
26
|
+
qualname = alembic
|
|
27
|
+
|
|
28
|
+
[handler_console]
|
|
29
|
+
class = StreamHandler
|
|
30
|
+
args = (sys.stderr,)
|
|
31
|
+
level = NOTSET
|
|
32
|
+
formatter = generic
|
|
33
|
+
|
|
34
|
+
[formatter_generic]
|
|
35
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
36
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createInertiaApp } from '@inertiajs/react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { resolvePage } from './pages';
|
|
4
|
+
|
|
5
|
+
createInertiaApp({
|
|
6
|
+
resolve: async (name) => {
|
|
7
|
+
return await resolvePage(name);
|
|
8
|
+
},
|
|
9
|
+
setup({ el, App, props }) {
|
|
10
|
+
createRoot(el).render(<App {...props} />);
|
|
11
|
+
},
|
|
12
|
+
progress: {
|
|
13
|
+
color: '#4B5563',
|
|
14
|
+
delay: 150,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{HOST_NAME}}-client-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "tsc && vite build",
|
|
8
|
+
"preview": "vite preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@inertiajs/react": "^2.0.0",
|
|
12
|
+
"react": "^19.0.0",
|
|
13
|
+
"react-dom": "^19.0.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^22.0.0",
|
|
17
|
+
"@types/react": "^19.0.0",
|
|
18
|
+
"@types/react-dom": "^19.0.0",
|
|
19
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
20
|
+
"typescript": "^5.7.0",
|
|
21
|
+
"vite": "^6.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type ErrorProps = {
|
|
2
|
+
status: number;
|
|
3
|
+
message?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export default function Error({ status, message }: ErrorProps) {
|
|
7
|
+
return (
|
|
8
|
+
<main style={{ padding: '2rem', maxWidth: '40rem', margin: '0 auto' }}>
|
|
9
|
+
<h1>{status}</h1>
|
|
10
|
+
<p>{message || 'Something went wrong.'}</p>
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inertia page resolver.
|
|
3
|
+
*
|
|
4
|
+
* Module pages are discovered via a generated file (modules.generated.ts)
|
|
5
|
+
* emitted by the Python host at boot, or manually via `sm gen-pages`. Each
|
|
6
|
+
* installed module contributes an import.meta.glob() call with an absolute
|
|
7
|
+
* path, so pages shipped inside pip-installed module wheels resolve.
|
|
8
|
+
*
|
|
9
|
+
* Host-level pages live in client_app/pages/{PageName}.tsx and are
|
|
10
|
+
* registered under just "{PageName}" (e.g. "Error").
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { moduleGlobs } from './modules.generated';
|
|
14
|
+
|
|
15
|
+
type PageModule = { default: React.ComponentType<Record<string, unknown>> };
|
|
16
|
+
type PageLoader = () => Promise<PageModule>;
|
|
17
|
+
|
|
18
|
+
const hostPages = import.meta.glob<PageModule>('./pages/*.tsx');
|
|
19
|
+
|
|
20
|
+
const pages: Record<string, PageLoader> = {};
|
|
21
|
+
|
|
22
|
+
for (const [moduleName, globEntries] of Object.entries(moduleGlobs)) {
|
|
23
|
+
for (const [filePath, loader] of Object.entries(globEntries)) {
|
|
24
|
+
const match = filePath.match(/\/pages\/(\w+)\.tsx$/);
|
|
25
|
+
if (match) {
|
|
26
|
+
pages[`${moduleName}/${match[1]}`] = loader;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const [filePath, loader] of Object.entries(hostPages)) {
|
|
32
|
+
const match = filePath.match(/\.\/pages\/(\w+)\.tsx$/);
|
|
33
|
+
if (match) {
|
|
34
|
+
pages[match[1]] = loader;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function resolvePage(
|
|
39
|
+
name: string,
|
|
40
|
+
): Promise<React.ComponentType<Record<string, unknown>>> {
|
|
41
|
+
const loader = pages[name];
|
|
42
|
+
if (!loader) {
|
|
43
|
+
throw new Error(`Page "${name}" not found. Available pages: ${Object.keys(pages).join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
const module = await loader();
|
|
46
|
+
return module.default;
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["vite/client", "node"],
|
|
13
|
+
"allowImportingTsExtensions": false
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts", "**/*.tsx"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import { defineConfig } from 'vite';
|
|
5
|
+
|
|
6
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
// Load the module pages manifest written by the Python host at boot.
|
|
9
|
+
// Each entry points at an absolute pages/ directory — typically inside a
|
|
10
|
+
// pip-installed module wheel. Vite needs these in server.fs.allow so the
|
|
11
|
+
// dev server can read files outside the host root.
|
|
12
|
+
const manifestPath = path.resolve(__dirname, 'modules.manifest.json');
|
|
13
|
+
const moduleFsAllow: string[] = [];
|
|
14
|
+
if (fs.existsSync(manifestPath)) {
|
|
15
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as Record<string, string>;
|
|
16
|
+
for (const pagesDir of Object.values(manifest)) {
|
|
17
|
+
moduleFsAllow.push(path.dirname(pagesDir));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
plugins: [react()],
|
|
23
|
+
root: __dirname,
|
|
24
|
+
build: {
|
|
25
|
+
outDir: '../static/dist',
|
|
26
|
+
manifest: true,
|
|
27
|
+
rollupOptions: {
|
|
28
|
+
input: path.resolve(__dirname, 'main.tsx'),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
server: {
|
|
32
|
+
port: 5050,
|
|
33
|
+
strictPort: true,
|
|
34
|
+
origin: 'http://localhost:5050',
|
|
35
|
+
fs: {
|
|
36
|
+
allow: [projectRoot, ...moduleFsAllow],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Host application entry point.
|
|
2
|
+
|
|
3
|
+
This file was generated by `sm 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
|
+
settings = Settings()
|
|
11
|
+
|
|
12
|
+
setup_logging(
|
|
13
|
+
level=settings.log_level,
|
|
14
|
+
json_format=settings.log_format == "json",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
app = create_app(settings)
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
import uvicorn
|
|
21
|
+
|
|
22
|
+
uvicorn.run(
|
|
23
|
+
"main:app",
|
|
24
|
+
host="0.0.0.0",
|
|
25
|
+
port=8000,
|
|
26
|
+
reload=settings.is_development,
|
|
27
|
+
)
|