diff --git a/CHANGELOG.md b/CHANGELOG.md index 160318854..81c525755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,18 @@ All notable changes to this project are documented in this file. The format is b ## [Unreleased] ### Added +- `AuditLink.for_model(Model, url_template=..., ...)` fills `entity_type` (class + name) and `table_name` from the model; every module's `register_audit_links` + now uses it. `simple_module_db.independent_session(app)` is an async context + manager for out-of-request sessions that commits on success and rolls back on + error (`permissions` startup replay and `api_tokens` last-used write use it) + (#523). +- Framework pagination helper (#513): `simple_module_db.PageParams`, `Page` and + `async paginate(session, stmt, params)` (clamps to the last page, never a + negative offset, counts through soft-delete/tenant filters), `clamp_page` / + `total_pages`; `simple_module_hosting.pagination` adds the strict + `page_params(n)` and clamping `lenient_page_params(n)` dependencies; and a + `PaginationBar` component in `@simple-module-py/ui` (`ui.pagination.*` keys). - New `mail` module (`simple_module_mail`) owns outgoing mail: the SMTP/console transport, its settings (`/admin/settings/`, package `mail`) and the on-demand `mail.mailer` health check behind "Test mailer connection". Modules send through @@ -39,6 +51,35 @@ All notable changes to this project are documented in this file. The format is b it, which removes the `users` ↔ `audit_log` import cycle. ### Changed +- Bound FastAPI to `>=0.140,<0.157` in framework and module dependencies; a + contract test guards the route guard's private FastAPI/Starlette APIs (#516). +- Declare and document the optional `file_storage[tasks]` extra for the lockstep + background-tasks package; document `DurableTransport` as the durable-event + extension point and verify uploads without background tasks (#502). +- Users, file_storage, background_tasks (executions and outbox), audit_log, + settings and tenants lists now share the pagination helpers instead of + per-module copies (#513). Browser view routes (`background_tasks` + including the outbox page, `file_storage`, `tenants` admin, `users` admin) clamp + `?page=0`/negative/garbage to page 1 rather than reject; every JSON list API + (`background_tasks` executions and outbox, `audit_log`, `file_storage`, + `users` admin) answers 422 for `page` outside 1..1,000,000 and `per_page` + outside 1..200, where some used to accept `page=0` or an unbounded `per_page` + or forward a huge page to SQL as an out-of-range offset. The outbox API now + reports the clamped page when asked past the end. The audit log, executions + and outbox tables use `PaginationBar`; their private `showing/previous/next` + locale keys are removed in favour of `ui.pagination.*`. +- One module scaffolder (#509). `make new-module` now calls the CLI's + `create_module` (`framework_repo=True` overlay) instead of ~980 lines of + f-string templates in `scripts/_templates_*.py`, which are deleted, so + `smpy create-module` / `smpy new` and `make new-module` share one set of + `templates/module/` files. Both now emit `constants.py`, `models.py`, + `contracts/`, `service.py`, `deps.py`, permission-guarded REST + Inertia + endpoints, Browse/Create/Edit pages, a menu item with `label_key`, + `register_permissions`, `locale_dirs` and `locales/en.json` (the CLI sample + page `Index.tsx` is replaced by `Browse.tsx`). `make new-module` also adds + the module to the `publish-pypi` matrix and runs `npm install` and + `make gen-i18n`; a fresh module passes `make lint`. The scaffold tests live in + `framework/cli/tests/` only. - `audit_log` and `dashboard` no longer depend on `Users` (meta `depends_on` and package dependency); `background_tasks` drops its unused `Users` dependency (#501). `api_tokens` moves `Users` to `optional_depends_on` and diff --git a/CLAUDE.md b/CLAUDE.md index fdc1e9204..9bb0ed149 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,13 +131,13 @@ The Python package name (`feature_flags`) keys the i18n namespace, the namespace is the meta name. **Module layout** (`make new-module` scaffolds the files below except -`constants.py`, `tasks.py` and the CSS, plus `pyproject.toml`, `README.md`, -`LICENSE`, `package.json`, `tsconfig.json`, `tests/`): +`tasks.py` and the CSS, plus `pyproject.toml`, `README.md`, `LICENSE`, +`package.json`, `tsconfig.json`, `tests/`): ``` modules/// ├── module.py # ModuleBase subclass, meta = ModuleMeta(...) -├── constants.py # add it: PERM_*, ROLE_*, _PAGE_*, _MODULE_* — see § Conventions +├── constants.py # PERM_*, PAGE_*, MENU_*, ROLE_*, _MODULE_* — see § Conventions (scaffolded) ├── models.py # SQLModel tables (table=True) ├── contracts/ # SQLModel DTOs — the public surface; add a Protocol only for real extension points ├── settings.py # DbBackedSettings subclass @@ -371,8 +371,9 @@ development and testing (`session_cookie_secure`). durable handlers, which have no app). The `mail` module owns SMTP and its settings; without it the sender only logs (`delivers_email` is false). Tests use the `captured_mail` fixture. -- **Audit links / design packs**: `register_audit_links` takes - `AuditLink(entity_type=Model.__name__, …)` (class name, not table name, or it +- **Audit links / design packs**: `register_audit_links` builds + links with `AuditLink.for_model(Model, url_template=…, …)` (it fills + `entity_type=Model.__name__` and `table_name`; a table-name `entity_type` silently never matches); `register_design_packs` slugs must match `^[a-z0-9][a-z0-9-]*$` and the host must still import the CSS. - **i18n.** Ship `/locales/.json`, declare in `locale_dirs()` with @@ -407,23 +408,35 @@ development and testing (`session_cookie_secure`). ## Adding a module or package -`make new-module name=` scaffolds the package, wires it into -`host/pyproject.toml` and the root `testpaths`/`extra-paths`, and runs -`uv sync`. Expect `make lint` to fail right after: the scaffold still emits a -literal `inertia.render("/Create")` and permission and menu strings -without constants or `label_key`. Fix those first. It also does **not**: - -1. Run `npm install`, so regenerate and commit `package-lock.json` (CI uses - `npm ci`). Commit the `uv.lock` change the scaffold's - `uv sync` makes too (CI uses `uv sync --locked`). -2. Add `simple_module_` to the `publish-pypi` matrix in - `.github/workflows/release.yml`; a test and `check_metadata.py` fail until - you do. `pyproject.toml` needs description, `readme`, `license = "MIT"`, - keyword `simple-module`, `urls.Repository`; `README.md` needs ≥ 500 bytes, an - H1 and Install/Usage sections. +There is **one scaffolder**: `make new-module name=` runs +`scripts/new_module.py`, a thin wrapper over the CLI's +`simple_module_cli.scaffolding.create_module(..., framework_repo=True)`, the same +templates `smpy create-module` / `smpy new` use +(`framework/cli/simple_module_cli/templates/module/`). Change a convention there, +once. The `framework_repo` variant is a template overlay +(`_optional/framework_repo/`: workspace-sourced `pyproject.toml` with the +`check_metadata.py` fields, `LICENSE`, `README.md`, i18n-keyed pages). + +It generates a CRUD sample (model, DTOs, service, REST + Inertia endpoints, +Browse/Create/Edit pages, `locales/en.json`, tests) whose permissions, page keys +and menu item come from `constants.py` (`label_key` set, routes guarded), so a +fresh module passes `make lint`. Then +`simple_module_cli.framework_repo.wire_module` adds it to `host/pyproject.toml`, +the root `testpaths`/`extra-paths` and the `publish-pypi` matrix in +`release.yml`, and the target runs `uv sync --all-packages`, `npm install` and +`make gen-i18n`. Commit the resulting `uv.lock`, `package-lock.json` and +`packages/i18n/src/{keys.generated,generated-resources}.ts` changes (CI uses +`--locked` / `npm ci` and diffs the i18n files). It does **not**: + +1. Create a migration for the sample table: edit `models.py`, then follow + § Database (`--head