Basics¶
This Quick Start builds a small task API on the msflib core package, one page at a time. The same Task model carries through all four pages:
- Basics (this page): install, settings, engine and a running app.
- Models, data and tables: define
Taskand create its table. - Actions and CRUD: read and write tasks through a
ModelAction. - Endpoints and routers: expose the actions over HTTP and test them.
You need Python 3.10 or later. The snippets were run on Python 3.12 with FastAPI 0.136, SQLModel 0.0.22 and Pydantic 2.12.
Install¶
Create a project and add the core package. With Poetry, in pyproject.toml:
[tool.poetry.dependencies]
python = ">=3.10,<3.15"
msflib = { git = "https://github.com/msflib/fastapi.git", subdirectory = "core", rev = "core-v0.2.1" }
then run poetry install. With pip, the equivalent is:
pip install "msflib @ git+https://github.com/msflib/fastapi.git@core-v0.2.1#subdirectory=core"
msflib brings FastAPI, SQLModel, Pydantic, Uvicorn and httpx with it. For the tests on the last page, add pytest to your dev dependencies. Use a release tag such as core-v0.2.1 in place of dev for anything you deploy.
Project layout¶
tasks/
.env
app/
__init__.py
settings.py
db.py
main.py
Later pages add models.py, actions.py, deps.py and router.py next to these. Create an empty app/__init__.py now.
Settings¶
MSFLib settings are grouped into namespaces, one per package. The core's namespace is CORE. A host app puts the namespaces it uses into one settings class and reads each through settings.scope("NAMESPACE"). There are two ways to build that class, and both work with everything in this Quick Start. Pick one; the tabs below show each.
Inherit the settings classes you use, together with SettingsBase. Fields are top-level, so environment variables use the plain field names.
# app/settings.py
from msflib.core.config import CoreSettings, SettingsBase
class Settings(CoreSettings, SettingsBase):
pass
settings = Settings()
When you add a module, add its settings class to the bases, for example class Settings(AuthSettings, CoreSettings, SettingsBase). Override a default in the class body (PROJECT_NAME: str = "Tasks").
# .env
PROJECT_NAME=Tasks
SQLITE_DATABASE_URI=sqlite:///./tasks.db
SECRET_KEY=change-me
Generate a class with one nested field per namespace. Environment variables are nested with a double underscore.
# app/settings.py
from msflib.core.config import CoreSettings, SettingsBase, compose_settings_model
Settings = compose_settings_model("Settings", SettingsBase, [CoreSettings])
settings = Settings()
When you add a module, append its settings class to the list, for example [CoreSettings, AuthSettings].
# .env
CORE__PROJECT_NAME=Tasks
CORE__SQLITE_DATABASE_URI=sqlite:///./tasks.db
CORE__SECRET_KEY=change-me
Values come from environment variables and a .env file in the directory you run from. The rest of this guide reads settings with settings.scope("CORE"), which works the same in both layouts. See Tiered configuration for how the two compare.
CoreSettings defaults to SQLite (USE_SQLITE=True) at sqlite:///./tmp/msflib.db. SQLite does not create the tmp directory for you, which is why the .env above points at a file in the project root. SECRET_KEY defaults to a random value generated each time the process starts; set it explicitly for anything beyond a local experiment. Configuration in the core page lists the other CORE keys, including the PostgreSQL ones.
Known issue (#276)
Constructing the settings creates an uploads/ directory in the working directory, because STORAGE_METHOD defaults to file. Set STORAGE_PATH (CORE__STORAGE_PATH in a composed host) if you want it elsewhere.
Engine and session¶
The core does not create your engine. Build it from the settings, and turn it into a FastAPI dependency with get_session_factory:
# app/db.py
from msflib.api.deps import get_session_factory
from msflib.db.sqlite import enable_savepoints
from sqlmodel import create_engine
from .settings import settings
core = settings.scope("CORE")
engine = enable_savepoints(
create_engine(core.SQLITE_DATABASE_URI, connect_args={"check_same_thread": False})
)
get_session = get_session_factory(engine)
get_session yields one SQLModel Session per request and closes it afterwards. enable_savepoints is for SQLite only: pysqlite delays BEGIN, so releasing a savepoint commits the work before it. MSFLib features that use savepoints, such as on_model_write callbacks, depend on the wrapped engine, so wrap every SQLite engine you build. To use PostgreSQL instead, set the POSTGRES_* values (or SQLALCHEMY_DATABASE_URI directly) in a subclassed host, or CORE__POSTGRES_* (or CORE__SQLALCHEMY_DATABASE_URI) in a composed host, and build the engine with create_engine(str(core.SQLALCHEMY_DATABASE_URI)) and no wrapper. USE_SQLITE is only a flag, read by the seed CLI and by your own engine code; the core does not switch databases for you.
Known issue (#276)
enable_savepoints does not check the database dialect. Do not wrap a PostgreSQL (or any non-SQLite) engine with it.
The app¶
# app/main.py
from fastapi import FastAPI
from msflib.eventbus import bind_app_emitter
from .settings import settings
core = settings.scope("CORE")
app = FastAPI(title=core.PROJECT_NAME, openapi_url=f"{core.API_V1_STR}/openapi.json")
app_emitter = bind_app_emitter(app)
@app.get(f"{core.API_V1_STR}/health")
def health() -> dict[str, str]:
return {"status": "ok", "project": core.PROJECT_NAME}
Two MSFLib details here. core.API_V1_STR (default /api/v1) is the prefix the host app gives to everything it mounts. bind_app_emitter(app) creates one event emitter for this app and installs the middleware that makes it current during each request. Every ModelAction write emits events, and modules register their listeners on this emitter, so bind it once at app creation even if you do not use events yet. See Event bus.
Run it¶
uvicorn app.main:app --reload
curl localhost:8000/api/v1/health
# {"status":"ok","project":"Tasks"}
Open http://localhost:8000/docs for the interactive API docs. The app has no tables or task routes yet.
Troubleshooting¶
ModuleNotFoundError: No module named 'app'. Run uvicorn from the project root (the directory that contains app/), not from inside app/.
sqlite3.OperationalError: unable to open database file. The directory in SQLITE_DATABASE_URI does not exist. The default points into ./tmp; set SQLITE_DATABASE_URI (CORE__SQLITE_DATABASE_URI in a composed host) as shown above or create the directory.
A .env value is ignored. The .env file is read from the current working directory, so start the app from the directory that holds it. Also check the variable names match your layout: plain names (PROJECT_NAME) for a subclassed host, CORE__-prefixed names (CORE__PROJECT_NAME) for a composed one.
Next: Models, data and tables.