simple-module-hosting 0.0.1__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_hosting/__init__.py +7 -0
- simple_module_hosting/_error_handlers.py +54 -0
- simple_module_hosting/_hydrate_step.py +39 -0
- simple_module_hosting/_inertia_setup.py +73 -0
- simple_module_hosting/_inertia_shared.py +61 -0
- simple_module_hosting/_observability.py +108 -0
- simple_module_hosting/_phase_helpers.py +160 -0
- simple_module_hosting/app_builder.py +281 -0
- simple_module_hosting/bootstrap_settings.py +55 -0
- simple_module_hosting/cli.py +292 -0
- simple_module_hosting/health.py +79 -0
- simple_module_hosting/host_settings.py +33 -0
- simple_module_hosting/i18n_deps.py +25 -0
- simple_module_hosting/i18n_manifest.py +202 -0
- simple_module_hosting/i18n_middleware.py +95 -0
- simple_module_hosting/inertia_deps.py +27 -0
- simple_module_hosting/inertia_utils.py +31 -0
- simple_module_hosting/logging.py +91 -0
- simple_module_hosting/manifest.py +250 -0
- simple_module_hosting/middleware.py +272 -0
- simple_module_hosting/migrations.py +65 -0
- simple_module_hosting/permissions.py +75 -0
- simple_module_hosting/py.typed +0 -0
- simple_module_hosting/redirects.py +45 -0
- simple_module_hosting/scaffolding.py +294 -0
- simple_module_hosting/settings.py +10 -0
- simple_module_hosting/templates/host/.env.example +20 -0
- simple_module_hosting/templates/host/.gitignore +19 -0
- simple_module_hosting/templates/host/Makefile +24 -0
- simple_module_hosting/templates/host/README.md.tpl +59 -0
- simple_module_hosting/templates/host/alembic.ini +36 -0
- simple_module_hosting/templates/host/client_app/app.tsx +16 -0
- simple_module_hosting/templates/host/client_app/main.tsx +2 -0
- simple_module_hosting/templates/host/client_app/package.json.tpl +23 -0
- simple_module_hosting/templates/host/client_app/pages/Error.tsx +13 -0
- simple_module_hosting/templates/host/client_app/pages.ts +47 -0
- simple_module_hosting/templates/host/client_app/styles.css +7 -0
- simple_module_hosting/templates/host/client_app/tsconfig.json +16 -0
- simple_module_hosting/templates/host/client_app/vite.config.ts +39 -0
- simple_module_hosting/templates/host/main.py +27 -0
- simple_module_hosting/templates/host/migrations/env.py +80 -0
- simple_module_hosting/templates/host/migrations/script.py.mako +26 -0
- simple_module_hosting/templates/host/migrations/versions/.gitkeep +1 -0
- simple_module_hosting/templates/host/pyproject.toml.tpl +17 -0
- simple_module_hosting/templates/host/templates/index.html +12 -0
- simple_module_hosting/templates/module/.github/workflows/ci.yml +32 -0
- simple_module_hosting/templates/module/.github/workflows/publish.yml.tpl +52 -0
- simple_module_hosting/templates/module/.gitignore +14 -0
- simple_module_hosting/templates/module/README.md.tpl +82 -0
- simple_module_hosting/templates/module/__PACKAGE__/__init__.py +0 -0
- simple_module_hosting/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- simple_module_hosting/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
- simple_module_hosting/templates/module/__PACKAGE__/module.py.tpl +46 -0
- simple_module_hosting/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
- simple_module_hosting/templates/module/__PACKAGE__/services.py.tpl +22 -0
- simple_module_hosting/templates/module/package.json.tpl +16 -0
- simple_module_hosting/templates/module/pyproject.toml.tpl +39 -0
- simple_module_hosting/templates/module/tests/__init__.py +0 -0
- simple_module_hosting/templates/module/tests/test_module.py.tpl +27 -0
- simple_module_hosting/templates/module/tsconfig.json.tpl +11 -0
- simple_module_hosting-0.0.1.dist-info/METADATA +93 -0
- simple_module_hosting-0.0.1.dist-info/RECORD +65 -0
- simple_module_hosting-0.0.1.dist-info/WHEEL +4 -0
- simple_module_hosting-0.0.1.dist-info/entry_points.txt +3 -0
- simple_module_hosting-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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"}
|
|
@@ -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/
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# simple_module_{{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
The `{{MODULE_NAME}}` module for SimpleModule hosts.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install simple_module_{{PACKAGE_NAME}}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
A host that installs this package picks it up automatically via
|
|
12
|
+
`entry_points`. No additional wiring is required.
|
|
13
|
+
|
|
14
|
+
## Development
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv sync --extra dev
|
|
18
|
+
uv run pytest
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Frontend assets (optional)
|
|
22
|
+
|
|
23
|
+
If this module ships TSX pages:
|
|
24
|
+
|
|
25
|
+
1. Drop them in `{{PACKAGE_NAME}}/pages/` (the scaffold created the
|
|
26
|
+
directory). One file per Inertia page — e.g. `Browse.tsx`,
|
|
27
|
+
`Detail.tsx`.
|
|
28
|
+
2. Build them with your bundler of choice (Vite/esbuild/etc.) into
|
|
29
|
+
`{{PACKAGE_NAME}}/static/dist/`. This directory is **gitignored** —
|
|
30
|
+
only the wheel carries it.
|
|
31
|
+
3. Before releasing (`uv build`), rebuild the assets. Your
|
|
32
|
+
`publish.yml` CI should run the bundler before `uv build`; add
|
|
33
|
+
a step after "Install project + dev deps" such as
|
|
34
|
+
`npm ci && npm run build`.
|
|
35
|
+
|
|
36
|
+
Hosts mount the bundle automatically at
|
|
37
|
+
`/modules/{{MODULE_SLUG}}/static` — the generated
|
|
38
|
+
`{{MODULE_NAME}}Module.static_mounts()` method returns the directory
|
|
39
|
+
when it exists, an empty dict otherwise (so dev without a build step
|
|
40
|
+
doesn't fail).
|
|
41
|
+
|
|
42
|
+
## Continuous integration
|
|
43
|
+
|
|
44
|
+
Two workflows live under `.github/workflows/`:
|
|
45
|
+
|
|
46
|
+
- **`ci.yml`** — runs on every push to `main` and on every pull request.
|
|
47
|
+
Installs deps, runs `ruff check`, then `pytest`.
|
|
48
|
+
- **`publish.yml`** — runs only when a tag matching `v*` is pushed.
|
|
49
|
+
Re-runs tests, builds the wheel + sdist, and uploads to PyPI via
|
|
50
|
+
trusted publishing (no API token).
|
|
51
|
+
|
|
52
|
+
## Publishing
|
|
53
|
+
|
|
54
|
+
One-time PyPI setup (do this before your first release):
|
|
55
|
+
|
|
56
|
+
1. Create the project's Trusted Publisher entry at
|
|
57
|
+
<https://pypi.org/manage/account/publishing/>:
|
|
58
|
+
- PyPI project name: `simple_module_{{PACKAGE_NAME}}`
|
|
59
|
+
- Owner / Repository: your GitHub org + this repo
|
|
60
|
+
- Workflow filename: `publish.yml`
|
|
61
|
+
- Environment (recommended): `pypi`
|
|
62
|
+
2. On GitHub → Settings → Environments, create `pypi`. Add required
|
|
63
|
+
reviewers if you want a manual approval gate before every release.
|
|
64
|
+
|
|
65
|
+
Every release after that is three commands:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 1) bump the version in pyproject.toml, commit
|
|
69
|
+
# 2) tag
|
|
70
|
+
git tag v0.2.0
|
|
71
|
+
git push --tags
|
|
72
|
+
# 3) done — the publish workflow uploads to PyPI
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
No `PYPI_API_TOKEN` secret exists anywhere. Trusted publishing mints a
|
|
76
|
+
short-lived OIDC token scoped to this repo + workflow + environment.
|
|
77
|
+
|
|
78
|
+
## API-version contract
|
|
79
|
+
|
|
80
|
+
The `Meta.requires_framework` field declares which `simple_module_core`
|
|
81
|
+
versions this module supports. Update the spec on each framework major
|
|
82
|
+
bump after verifying compatibility.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""API routes for the {{MODULE_NAME}} module."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
router = APIRouter()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@router.get("/")
|
|
9
|
+
async def list_items() -> dict:
|
|
10
|
+
"""Placeholder endpoint. Replace with real resources."""
|
|
11
|
+
return {"module": "{{MODULE_NAME}}", "items": []}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""{{MODULE_NAME}} module — generated by `sm create-module`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, FastAPI
|
|
9
|
+
from simple_module_core import ModuleBase, ModuleMeta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class {{MODULE_NAME}}Module(ModuleBase):
|
|
13
|
+
meta = ModuleMeta(
|
|
14
|
+
name="{{MODULE_NAME}}",
|
|
15
|
+
route_prefix="/api/{{MODULE_SLUG}}",
|
|
16
|
+
view_prefix="/{{MODULE_SLUG}}",
|
|
17
|
+
depends_on=[],
|
|
18
|
+
version="0.1.0",
|
|
19
|
+
requires_framework=">=1.0,<2.0",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None:
|
|
23
|
+
from {{PACKAGE_NAME}}.endpoints.api import router as api
|
|
24
|
+
|
|
25
|
+
api_router.include_router(api)
|
|
26
|
+
|
|
27
|
+
def register_settings(self, app: FastAPI) -> None:
|
|
28
|
+
from {{PACKAGE_NAME}}.services import {{MODULE_NAME}}Services
|
|
29
|
+
from {{PACKAGE_NAME}}.settings import {{MODULE_NAME}}Settings
|
|
30
|
+
|
|
31
|
+
app.state.{{PACKAGE_NAME}} = {{MODULE_NAME}}Services(settings={{MODULE_NAME}}Settings())
|
|
32
|
+
|
|
33
|
+
def static_mounts(self) -> dict[str, Path]:
|
|
34
|
+
"""Expose pre-built frontend assets when they exist.
|
|
35
|
+
|
|
36
|
+
Returns an empty dict when ``static/dist/`` hasn't been built yet,
|
|
37
|
+
so ``uv run pytest`` works during development. In production, the
|
|
38
|
+
wheel ships the built bundle via ``force-include`` in
|
|
39
|
+
``pyproject.toml`` and the host mounts it at
|
|
40
|
+
``/modules/{{MODULE_SLUG}}/static``.
|
|
41
|
+
"""
|
|
42
|
+
pkg_root = Path(str(importlib.resources.files("{{PACKAGE_NAME}}")))
|
|
43
|
+
dist = pkg_root / "static" / "dist"
|
|
44
|
+
if not dist.is_dir():
|
|
45
|
+
return {}
|
|
46
|
+
return {"/modules/{{MODULE_SLUG}}/static": dist}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Keep this directory; TSX pages go here (e.g. Browse.tsx, Detail.tsx).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Module-scoped state container.
|
|
2
|
+
|
|
3
|
+
Stored as ``app.state.{{PACKAGE_NAME}}`` by
|
|
4
|
+
:meth:`{{MODULE_NAME}}Module.register_settings`.
|
|
5
|
+
|
|
6
|
+
Not frozen — ``on_startup`` may set fields that depend on the DB or
|
|
7
|
+
other framework services. Convention: set once during boot, treat as
|
|
8
|
+
read-only after.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from {{PACKAGE_NAME}}.settings import {{MODULE_NAME}}Settings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class {{MODULE_NAME}}Services:
|
|
20
|
+
"""{{MODULE_NAME}} module singletons."""
|
|
21
|
+
|
|
22
|
+
settings: {{MODULE_NAME}}Settings
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simple-module-py/{{MODULE_SLUG}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Frontend assets for the {{MODULE_NAME}} module",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"react": "^19.0.0",
|
|
8
|
+
"react-dom": "^19.0.0",
|
|
9
|
+
"@inertiajs/react": "^2.0.0",
|
|
10
|
+
"@simple-module-py/ui": "*"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@simple-module-py/tsconfig": "*"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {}
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "simple_module_{{PACKAGE_NAME}}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "{{MODULE_NAME}} module for SimpleModule hosts"
|
|
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
|
+
"sqlalchemy>=2.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.entry-points.simple_module]
|
|
14
|
+
{{PACKAGE_NAME}} = "{{PACKAGE_NAME}}.module:{{MODULE_NAME}}Module"
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8.0",
|
|
19
|
+
"pytest-asyncio>=0.24",
|
|
20
|
+
# Shared fixtures (fake_event_bus, build_test_app, etc.) for testing
|
|
21
|
+
# modules in isolation. The pytest11 entry_point auto-registers them.
|
|
22
|
+
"simple_module_testing>=0.1,<1.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["{{PACKAGE_NAME}}"]
|
|
31
|
+
|
|
32
|
+
# Ship the built frontend bundle + per-module JS dep manifest inside the wheel.
|
|
33
|
+
# static/dist/ is gitignored, so force-include picks up the working-tree build
|
|
34
|
+
# during `uv build` — run your bundler (vite/esbuild) first. package.json lives
|
|
35
|
+
# at the module root so npm workspaces see it; copying it into <pkg>/ lets the
|
|
36
|
+
# host discover JS deps via importlib.resources after a pip install.
|
|
37
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
38
|
+
"{{PACKAGE_NAME}}/static/dist" = "{{PACKAGE_NAME}}/static/dist"
|
|
39
|
+
"package.json" = "{{PACKAGE_NAME}}/package.json"
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Smoke tests for the {{MODULE_NAME}} module.
|
|
2
|
+
|
|
3
|
+
The ``build_test_app`` and ``fake_event_bus`` fixtures come from the
|
|
4
|
+
``simple_module_testing`` pytest plugin (registered via entry_points when
|
|
5
|
+
that package is installed). No conftest.py is required.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from {{PACKAGE_NAME}}.module import {{MODULE_NAME}}Module
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestMeta:
|
|
14
|
+
def test_meta_name(self):
|
|
15
|
+
assert {{MODULE_NAME}}Module.meta.name == "{{MODULE_NAME}}"
|
|
16
|
+
|
|
17
|
+
def test_meta_requires_framework(self):
|
|
18
|
+
assert {{MODULE_NAME}}Module.meta.requires_framework is not None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestRoutes:
|
|
22
|
+
async def test_app_boots_with_module(self, build_test_app):
|
|
23
|
+
"""Module registers cleanly into a minimal FastAPI host."""
|
|
24
|
+
app = build_test_app({{MODULE_NAME}}Module)
|
|
25
|
+
paths = {getattr(r, "path", None) for r in app.routes}
|
|
26
|
+
# The placeholder GET / endpoint lives under the route_prefix.
|
|
27
|
+
assert any(p and p.startswith("/api/{{MODULE_SLUG}}") for p in paths)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@simple-module-py/tsconfig/base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"paths": {
|
|
6
|
+
"@/*": ["./{{PACKAGE_NAME}}/*"],
|
|
7
|
+
"@simple-module-py/ui/*": ["../../packages/ui/src/*"]
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"include": ["{{PACKAGE_NAME}}/**/*.ts", "{{PACKAGE_NAME}}/**/*.tsx"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_module_hosting
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding
|
|
5
|
+
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
|
+
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
7
|
+
Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Anto Subash <antosubash@live.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: fastapi,inertia,scaffolding,simple-module,starlette,uvicorn
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: click>=8.1
|
|
25
|
+
Requires-Dist: fastapi-inertia>=1.0
|
|
26
|
+
Requires-Dist: fastapi>=0.115
|
|
27
|
+
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: jinja2>=3.1
|
|
29
|
+
Requires-Dist: simple-module-core==0.0.1
|
|
30
|
+
Requires-Dist: simple-module-db==0.0.1
|
|
31
|
+
Requires-Dist: starlette>=0.44
|
|
32
|
+
Requires-Dist: tomlkit>=0.13
|
|
33
|
+
Requires-Dist: uvicorn[standard]>=0.34
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# simple_module_hosting
|
|
37
|
+
|
|
38
|
+
FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline, exposes the `sm` / `simple-module` CLI, and ships the project scaffolder.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install simple_module_hosting
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For a new project, most users run the generator instead:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uvx simple-module new my-app
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What it provides
|
|
53
|
+
|
|
54
|
+
- `create_app(settings)` — returns a fully-wired `FastAPI` instance with all discovered modules registered.
|
|
55
|
+
- Middleware pipeline (execution order): CorrelationId → RequestLogging → SecurityHeaders → Session → `<module middleware>` → Tenant (opt-in) → Locale → InertiaLayoutData → app.
|
|
56
|
+
- Inertia wiring — shared props (`auth`, `menus`, `i18n`), `InertiaDep`, page-route lookup.
|
|
57
|
+
- CLI entry points: both `sm` and `simple-module` are installed and alias the same Click tree.
|
|
58
|
+
- Scaffolders — `sm create-host`, `sm create-module`, `sm new` (greenfield app with users + dashboard + permissions pre-wired), `sm gen-pages`.
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
Minimal `main.py`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from simple_module_hosting import create_app
|
|
66
|
+
from simple_module_hosting.settings import Settings
|
|
67
|
+
|
|
68
|
+
settings = Settings() # reads SM_* env vars
|
|
69
|
+
app = create_app(settings) # discovers + registers every installed module
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
import uvicorn
|
|
73
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
CLI:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
simple-module new my-app # scaffold a new project
|
|
80
|
+
simple-module doctor # diagnostic codes (SM001-SM017)
|
|
81
|
+
simple-module gen-pages # regenerate client_app/modules.generated.ts
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`sm` works identically to `simple-module`.
|
|
85
|
+
|
|
86
|
+
## Depends on
|
|
87
|
+
|
|
88
|
+
- `simple_module_core`, `simple_module_db`
|
|
89
|
+
- `fastapi`, `fastapi-inertia`, `starlette`, `uvicorn`, `click`, `jinja2`, `httpx`
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
|