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,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
|
+
});
|
simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/package.json.tpl
ADDED
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Alembic environment for a SimpleModule host.
|
|
2
|
+
|
|
3
|
+
Discovery + metadata aggregation lives in `simple_module_db.migrations` so
|
|
4
|
+
this file stays small and identical across all hosts. Don't add module-
|
|
5
|
+
specific imports here — install the module package and re-run autogenerate.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from logging.config import fileConfig
|
|
12
|
+
|
|
13
|
+
from alembic import context
|
|
14
|
+
from simple_module_db import build_module_metadata, make_include_object, render_item
|
|
15
|
+
from simple_module_hosting.settings import Settings
|
|
16
|
+
from sqlalchemy import engine_from_config, pool
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("alembic.env")
|
|
19
|
+
|
|
20
|
+
config = context.config
|
|
21
|
+
|
|
22
|
+
if config.config_file_name is not None:
|
|
23
|
+
fileConfig(config.config_file_name)
|
|
24
|
+
|
|
25
|
+
target_metadata = build_module_metadata()
|
|
26
|
+
include_object = make_include_object(target_metadata)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_url() -> str:
|
|
30
|
+
"""Read database URL from settings, convert async to sync driver."""
|
|
31
|
+
settings = Settings()
|
|
32
|
+
url = settings.database_url
|
|
33
|
+
url = url.replace("+aiosqlite", "")
|
|
34
|
+
url = url.replace("+asyncpg", "+psycopg2")
|
|
35
|
+
return url
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_migrations_offline() -> None:
|
|
39
|
+
"""Run migrations in 'offline' mode — generates SQL without a live DB."""
|
|
40
|
+
url = _get_url()
|
|
41
|
+
context.configure(
|
|
42
|
+
url=url,
|
|
43
|
+
target_metadata=target_metadata,
|
|
44
|
+
literal_binds=True,
|
|
45
|
+
dialect_opts={"paramstyle": "named"},
|
|
46
|
+
include_object=include_object,
|
|
47
|
+
render_item=render_item,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
with context.begin_transaction():
|
|
51
|
+
context.run_migrations()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_migrations_online() -> None:
|
|
55
|
+
"""Run migrations against a live database."""
|
|
56
|
+
configuration = config.get_section(config.config_ini_section, {})
|
|
57
|
+
configuration["sqlalchemy.url"] = _get_url()
|
|
58
|
+
|
|
59
|
+
connectable = engine_from_config(
|
|
60
|
+
configuration,
|
|
61
|
+
prefix="sqlalchemy.",
|
|
62
|
+
poolclass=pool.NullPool,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
with connectable.connect() as connection:
|
|
66
|
+
context.configure(
|
|
67
|
+
connection=connection,
|
|
68
|
+
target_metadata=target_metadata,
|
|
69
|
+
include_object=include_object,
|
|
70
|
+
render_item=render_item,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with context.begin_transaction():
|
|
74
|
+
context.run_migrations()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if context.is_offline_mode():
|
|
78
|
+
run_migrations_offline()
|
|
79
|
+
else:
|
|
80
|
+
run_migrations_online()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
import sqlalchemy as sa
|
|
11
|
+
from alembic import op
|
|
12
|
+
${imports if imports else ""}
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = ${repr(up_revision)}
|
|
16
|
+
down_revision: str | None = ${repr(down_revision)}
|
|
17
|
+
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
|
|
18
|
+
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
${upgrades if upgrades else "pass"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def downgrade() -> None:
|
|
26
|
+
${downgrades if downgrades else "pass"}
|
simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/versions/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Keep this directory in git
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{HOST_NAME}}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SimpleModule host application"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"simple_module_core>=1.0,<2.0",
|
|
8
|
+
"simple_module_db>=1.0,<2.0",
|
|
9
|
+
"simple_module_hosting>=1.0,<2.0",
|
|
10
|
+
"alembic>=1.13",
|
|
11
|
+
"uvicorn[standard]>=0.34",
|
|
12
|
+
{{MODULE_DEPS}}
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>SimpleModule</title>
|
|
7
|
+
{% inertia_head %}
|
|
8
|
+
</head>
|
|
9
|
+
<body class="antialiased">
|
|
10
|
+
{% inertia_body %}
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Fast feedback for pull requests and main-branch pushes.
|
|
2
|
+
# No publishing here — see publish.yml.
|
|
3
|
+
|
|
4
|
+
name: CI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
pull_request:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v3
|
|
19
|
+
with:
|
|
20
|
+
version: latest
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
run: uv python install 3.12
|
|
24
|
+
|
|
25
|
+
- name: Install project + dev deps
|
|
26
|
+
run: uv sync --extra dev
|
|
27
|
+
|
|
28
|
+
- name: Lint
|
|
29
|
+
run: uv run ruff check .
|
|
30
|
+
|
|
31
|
+
- name: Tests
|
|
32
|
+
run: uv run pytest
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Publish simple_module_{{PACKAGE_NAME}} to PyPI on tagged releases.
|
|
2
|
+
#
|
|
3
|
+
# Setup (one-time, in the PyPI project settings → "Publishing"):
|
|
4
|
+
# 1. Create the project on https://pypi.org/manage/account/publishing/
|
|
5
|
+
# with: PyPI project name = simple_module_{{PACKAGE_NAME}}
|
|
6
|
+
# Owner = <your GitHub org/user>
|
|
7
|
+
# Repository name = <this repo's name>
|
|
8
|
+
# Workflow filename = publish.yml
|
|
9
|
+
# Environment = pypi (optional but recommended)
|
|
10
|
+
# 2. In GitHub: Settings → Environments → New environment "pypi"
|
|
11
|
+
# with required reviewers if you want a manual approval gate.
|
|
12
|
+
#
|
|
13
|
+
# After setup, pushing a tag like `v0.1.0` builds the wheel + sdist and
|
|
14
|
+
# uploads via OIDC — no API token needs to exist anywhere.
|
|
15
|
+
|
|
16
|
+
name: Publish
|
|
17
|
+
|
|
18
|
+
on:
|
|
19
|
+
push:
|
|
20
|
+
tags:
|
|
21
|
+
- "v*"
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
build-and-publish:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment: pypi
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write # required for PyPI trusted publishing
|
|
29
|
+
contents: read
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- name: Install uv
|
|
35
|
+
uses: astral-sh/setup-uv@v3
|
|
36
|
+
with:
|
|
37
|
+
version: latest
|
|
38
|
+
|
|
39
|
+
- name: Set up Python
|
|
40
|
+
run: uv python install 3.12
|
|
41
|
+
|
|
42
|
+
- name: Install project + dev deps
|
|
43
|
+
run: uv sync --extra dev
|
|
44
|
+
|
|
45
|
+
- name: Run tests
|
|
46
|
+
run: uv run pytest
|
|
47
|
+
|
|
48
|
+
- name: Build distribution
|
|
49
|
+
run: uv build
|
|
50
|
+
|
|
51
|
+
- name: Publish to PyPI
|
|
52
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.venv/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.db
|
|
8
|
+
*.sqlite3
|
|
9
|
+
.env
|
|
10
|
+
|
|
11
|
+
# Built frontend assets — shipped inside the wheel via force-include but
|
|
12
|
+
# NOT source-controlled. Re-run `npm run build` (or `vite build`) before
|
|
13
|
+
# `uv build` to repopulate.
|
|
14
|
+
*/static/dist/
|