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.
Files changed (92) hide show
  1. simple_module_cli/__init__.py +0 -0
  2. simple_module_cli/_env.py +12 -0
  3. simple_module_cli/app_project.py +123 -0
  4. simple_module_cli/case.py +30 -0
  5. simple_module_cli/catalog.py +107 -0
  6. simple_module_cli/cli.py +104 -0
  7. simple_module_cli/new.py +124 -0
  8. simple_module_cli/plugins.py +56 -0
  9. simple_module_cli/recipes.py +93 -0
  10. simple_module_cli/scaffolding.py +124 -0
  11. simple_module_cli/templates/host/.env.example +20 -0
  12. simple_module_cli/templates/host/.gitignore +19 -0
  13. simple_module_cli/templates/host/Makefile +24 -0
  14. simple_module_cli/templates/host/README.md.tpl +59 -0
  15. simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +12 -0
  16. simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +60 -0
  17. simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +17 -0
  18. simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +37 -0
  19. simple_module_cli/templates/host/alembic.ini +36 -0
  20. simple_module_cli/templates/host/client_app/app.tsx +16 -0
  21. simple_module_cli/templates/host/client_app/main.tsx +2 -0
  22. simple_module_cli/templates/host/client_app/package.json.tpl +23 -0
  23. simple_module_cli/templates/host/client_app/pages/Error.tsx +13 -0
  24. simple_module_cli/templates/host/client_app/pages.ts +47 -0
  25. simple_module_cli/templates/host/client_app/styles.css +7 -0
  26. simple_module_cli/templates/host/client_app/tsconfig.json +16 -0
  27. simple_module_cli/templates/host/client_app/vite.config.ts +39 -0
  28. simple_module_cli/templates/host/main.py +27 -0
  29. simple_module_cli/templates/host/migrations/env.py +80 -0
  30. simple_module_cli/templates/host/migrations/script.py.mako +26 -0
  31. simple_module_cli/templates/host/migrations/versions/.gitkeep +1 -0
  32. simple_module_cli/templates/host/pyproject.toml.tpl +17 -0
  33. simple_module_cli/templates/host/templates/index.html +12 -0
  34. simple_module_cli/templates/module/.github/workflows/ci.yml +32 -0
  35. simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +52 -0
  36. simple_module_cli/templates/module/.gitignore +14 -0
  37. simple_module_cli/templates/module/README.md.tpl +82 -0
  38. simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  39. simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  40. simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
  41. simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +46 -0
  42. simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
  43. simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +22 -0
  44. simple_module_cli/templates/module/package.json.tpl +16 -0
  45. simple_module_cli/templates/module/pyproject.toml.tpl +39 -0
  46. simple_module_cli/templates/module/tests/__init__.py +0 -0
  47. simple_module_cli/templates/module/tests/test_module.py.tpl +27 -0
  48. simple_module_cli/templates/module/tsconfig.json.tpl +11 -0
  49. simple_module_cli/wizard.py +48 -0
  50. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/.env.example +20 -0
  51. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/.gitignore +19 -0
  52. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/Makefile +24 -0
  53. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/README.md.tpl +59 -0
  54. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +12 -0
  55. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +60 -0
  56. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +17 -0
  57. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +37 -0
  58. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/alembic.ini +36 -0
  59. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/app.tsx +16 -0
  60. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/main.tsx +2 -0
  61. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/package.json.tpl +23 -0
  62. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/pages/Error.tsx +13 -0
  63. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/pages.ts +47 -0
  64. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/styles.css +7 -0
  65. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/tsconfig.json +16 -0
  66. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/vite.config.ts +39 -0
  67. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/main.py +27 -0
  68. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/env.py +80 -0
  69. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/script.py.mako +26 -0
  70. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/versions/.gitkeep +1 -0
  71. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/pyproject.toml.tpl +17 -0
  72. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/templates/index.html +12 -0
  73. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.github/workflows/ci.yml +32 -0
  74. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +52 -0
  75. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.gitignore +14 -0
  76. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/README.md.tpl +82 -0
  77. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  78. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  79. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
  80. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +46 -0
  81. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
  82. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +22 -0
  83. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/package.json.tpl +16 -0
  84. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/pyproject.toml.tpl +39 -0
  85. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tests/__init__.py +0 -0
  86. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tests/test_module.py.tpl +27 -0
  87. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tsconfig.json.tpl +11 -0
  88. simple_module_cli-0.0.2.dist-info/METADATA +63 -0
  89. simple_module_cli-0.0.2.dist-info/RECORD +92 -0
  90. simple_module_cli-0.0.2.dist-info/WHEEL +4 -0
  91. simple_module_cli-0.0.2.dist-info/entry_points.txt +3 -0
  92. 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
+ });
@@ -0,0 +1,2 @@
1
+ import './styles.css';
2
+ import './app';
@@ -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,7 @@
1
+ body {
2
+ margin: 0;
3
+ font-family:
4
+ system-ui,
5
+ -apple-system,
6
+ sans-serif;
7
+ }
@@ -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"}
@@ -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/