Discovery & entry points
Modules are Python packages registered via the simple_module entry-point group. There is no module registry file, no INSTALLED_MODULES list in settings — if the package is installed in the environment and declares the entry point, it's a module.
Declaring a module
Every module's pyproject.toml ends with:
[project.entry-points.simple_module]
orders = "orders.module:OrdersModule"- The key (
orders) is the entry-point name. It's used for reporting only — the framework authoritative name isModuleMeta.name. - The value is
<import_path>:<class_name>. The class must inherit fromsimple_module_core.module.ModuleBaseand declare a non-nullmeta = ModuleMeta(...)class attribute.
Entry points are registered at install time, not at import time. After editing a module's pyproject.toml, re-run uv sync --all-packages (or make install) so the installer writes the new metadata to the venv's *.dist-info/entry_points.txt.
Discovery algorithm
simple_module_core.discovery.discover_modules():
- Iterates
importlib.metadata.entry_points(group="simple_module"). - For each entry point, calls
.load()to import the module'smodule.pyand pull out the class. - Validates: the loaded object must be a
ModuleBasesubclass with ametaattribute of typeModuleMeta. - If
SM_MODULES_ENABLEDis set, filters to only those listed. - Topologically sorts by
ModuleMeta.depends_onusing a stable tiebreaker (name). - Returns a tuple of
ModuleBaseinstances (one per module, constructed with no args).
Failures:
- Import error (e.g. syntax error in
module.py) — lenient mode: logged + skipped. Strict mode: raisesInvalidModuleError. - Missing
meta— emitsSM001. Strict mode: raises. - Duplicate
meta.name— emitsSM008(error). Always fails boot, even in dev, because names are used as DB schemas/prefixes. - Cycle in
depends_on— raisesCircularDependencyError. Always fails boot.
ModuleMeta
from simple_module_core.module import ModuleMeta
meta = ModuleMeta(
name="Orders", # PascalCase, globally unique
route_prefix="/api/orders", # where the API router mounts
view_prefix="/orders", # where the view router mounts
depends_on=["Products"], # hard ordering requirements
version="1.0.0", # semver for the module
)name
The authoritative identifier. It's used for:
- Postgres schema name (lowercased →
orders). - SQLite table-name prefix (
orders_*). - Inertia page namespace (
"Orders/Browse"). - Diagnostic reporter name.
- Feature-flag and permission grouping.
Rule of thumb: use PascalCase and keep it stable. Renaming a module after release requires a data migration and a coordinated client-side rename.
depends_on
A list of other modules' meta.name values that must be loaded first. The framework topo-sorts on this before invoking lifecycle hooks. Use this when:
- Your module's middleware must sit inside/outside another module's middleware. See Middleware pipeline.
- Your module's
register_routesreads another module's service to compose endpoints. - Your module subscribes to events published by another module.
depends_on is not a dependency-injection mechanism. It only controls order — you still import the services you need.
version
Semver string. Used in diagnostic output and can be surfaced in /admin/health to help operators correlate deployed module versions with bug reports. Not currently parsed for automated version-range checks.
The simple_module group
Entry points are the same mechanism that ships with importlib.metadata:
.venv/lib/python3.12/site-packages/orders-1.0.0.dist-info/entry_points.txt:
[simple_module]
orders = orders.module:OrdersModuleYou can inspect what the framework sees with:
uv run python -c "
from importlib.metadata import entry_points
for ep in entry_points(group='simple_module'):
print(ep.name, '->', ep.value)
"Disabling modules at runtime
Set SM_MODULES_ENABLED to a comma-separated allow-list of entry-point names. Useful for:
- Running tests against a minimal world (
SM_MODULES_ENABLED=users,orders). - Temporarily disabling a misbehaving module in production without a redeploy.
- Sharding a large app into multiple deployments (e.g. background-worker process loads only the
background_tasksmodule).
Disabling via env doesn't remove the module's database tables. Re-enabling picks up where it left off.
Strict-mode checklist
Before flipping to SM_ENVIRONMENT=production, verify:
- [ ] Every module's
pyproject.tomldeclares thesimple_moduleentry point. - [ ] Every
ModuleBasesubclass hasmeta = ModuleMeta(...). - [ ]
make doctorreports zero errors (SM001,SM008,SM009,SM010,SM016are all ERROR-level). - [ ]
alembic upgrade headhas been run against the production DB. - [ ]
SM_SECRET_KEYis notchange-me-in-production.
Failure on any of the above produces a clean boot-time exception rather than a silent misbehavior.