How to extend MSFLib¶
The msflib-* packages are libraries, not an application. You write the FastAPI app, choose the database, and decide what is mounted where. The packages supply building blocks that you wire together. This page describes those building blocks, what your app owns, and which guide covers each kind of change.
Install¶
Add the core and the modules you need. Core is required by all modules.
pip install "msflib @ git+https://github.com/msflib/fastapi.git#subdirectory=core"
pip install "msflib-auth @ git+https://github.com/msflib/fastapi.git#subdirectory=modules/auth"
pip install "msflib-account @ git+https://github.com/msflib/fastapi.git#subdirectory=modules/account"
All packages share the msflib import namespace, so msflib.auth, msflib.account and msflib.workspaces come from different distributions. Several package __init__ files export little or nothing (msflib.auth, msflib.account), so import from the subpackage that defines the name, as the examples in these guides do.
What the packages provide¶
| Building block | Shape | Where it is extended |
|---|---|---|
| Settings | A ModuleSettingsBase subclass per module, with a namespace |
You compose the ones you use into one Settings class, and add your own. See below. |
| Models | *Base classes and *Create, *Update, *Read schemas |
You define the table classes from the bases. See Overriding models. |
| Actions | ModelAction[Model, Create, Update] and module subclasses such as AccountAction |
You subclass or instantiate them with your model types. See Subclassing actions. |
| Routers | Factory functions such as msflib.auth.router.router(...) that return an APIRouter |
You call the factory with your models, dependencies and settings, then include the result. See Mounting routes. |
| Dependencies | Factory functions such as get_account_dependencies(...) that return a DependencyNamespace |
You call them once in your deps.py. See Authentication. |
| Events | An app-scoped event bus, with lifecycle events emitted by actions and routers | You register listeners. See Subclassing actions and Event bus. |
Router and dependency factories take the session, the keystore, the current-account dependency and the settings as arguments instead of importing them. That is what makes the pieces replaceable.
What your app owns¶
- The
FastAPIinstance, its middleware, and the final URL layout. - The database engine and the session dependency.
msflib.api.deps.get_session_factory(engine)builds a session dependency from an engine you create. - Table models for the account and profile (the account module deliberately ships only the bases) and for anything you add.
- The settings class and where its values come from.
- Schema migrations for every table you define or extend. See
msflib.db.migrationsin the core page.
Known issue (#276)
msflib.db.session creates an engine by importing app.core.config.settings, and msflib.db.migrations.run_alembic_env is documented to be used the same way. No feature module imports msflib.db.session. If your project is not laid out as an app package with app/core/config.py, build your own engine, as the examples here do.
A host layout¶
This is the layout the guides use. It mirrors the repository's reference host app in testsite/app.
myapp/
settings.py # composed Settings and the settings instance
models.py # Account, Profile and your own tables
actions.py # action instances (and your subclasses)
deps.py # engine, session, keystore, auth dependencies
routers/
notes.py # your own routers
main.py # FastAPI app, include_router calls
Writing custom code fills in every file.
Composing settings¶
Each module declares its settings as a class with a namespace. You combine the classes you use, plus CoreSettings and SettingsBase, into one class, either by inheriting them (subclassed, shown here) or with compose_settings_model (composed; see Tiered configuration). Override defaults in the class body.
from msflib.account.config import AccountSettings
from msflib.auth.config import AuthSettings
from msflib.core.config import CoreSettings, SettingsBase
class Settings(AccountSettings, AuthSettings, CoreSettings, SettingsBase):
SECRET_KEY: str = "change-me"
USERS_OPEN_REGISTRATION: bool = True
settings = Settings()
settings.scope("CORE").API_V1_STR # "/api/v1"
settings.scope("AUTH").ACCESS_TOKEN_EXPIRE_MINUTES # 30
Pass settings to every router and action factory. They read their own values with settings.scope("AUTH"), settings.scope("ACCOUNT") and so on, so one object serves all modules when the settings use this subclassed style. With it, an environment variable carries the plain field name (ACCESS_TOKEN_EXPIRE_MINUTES=5).
Known issue (#287)
The auth router reads settings.SECRET_KEY from the top level of the settings object, so composed (nested) settings fail in it with AttributeError on login and password recovery. Use the subclassed style shown here.
Behaviour may change (#280)
The NAMESPACE__KEY environment variable style (for example AUTH__ACCESS_TOKEN_EXPIRE_MINUTES) only works for composed (field) settings, not for subclassed hosts like this one.
For per-tenant, per-workspace and per-user values, see Tiered configuration.
Set real secrets
SECRET_KEY defaults to a random value generated at import time, so tokens stop validating after a restart and differ between workers. Set it explicitly. AccountSettings also defaults FIRST_SUPERUSER_PASSWORD to password; override it in any shared environment.
Choosing an extension point¶
| Change | Mechanism |
|---|---|
| Add your own feature (table, rules, endpoints) | New model, action and router in your app. Writing custom code |
| Add a column to the account, profile or workspace table | Define the table from the module's base class and add the field. Overriding models |
| Accept or return extra fields on a module endpoint | Pass your own schema types to the router factory. Overriding models |
| Change what happens on create, update or delete | Subclass the action, or listen to its lifecycle events. Subclassing actions |
| Put a module's endpoints under another prefix, behind more checks, or replace one | include_router options, or declare your route first. Mounting routes |
| Add claims to the access token, or change how the workspace is found | access_token_claims_decorator and WorkspaceResolver. Authentication |
What is not supported¶
These come up often enough to state plainly. Each was tried against the current source.
- Subclassing a module's concrete table class to add columns.
class MyWorkspace(Workspace, table=True)whereWorkspaceismsflib.workspaces.models.workspace.WorkspaceraisesValueErrorat class definition. Define your table fromWorkspaceBaseinstead (see the known issue below). - Defining your own table with the same name as a module table that is also imported. A second
accounttable raisesInvalidRequestError: Table 'account' is already defined. Define your table and do not import the module's concrete one. - Adding a column to a module's table after the fact. Columns come from the class definition. Relationships can be attached later with
ModelBase.add_relationship, but not columns. - Editing a module router's routes through a hook. Routers expose no callbacks for their routes. You can shadow a route, wrap the whole router in extra dependencies, or filter routes out. See Mounting routes.
Known issue (#271)
Subclassing a module's concrete table class raises the ValueError described above. Separately, msflib.workspace_notifications.service.notification imports the module's concrete msflib.account.models.account.Account table, so a host that defines its own Account table and also imports that module fails with the "already defined" error above.