Skip to content

Dependency injection

MSFLib modules do not import your app's sessions, settings or authentication. Instead, each module that needs request-scoped dependencies ships a factory in its deps.py. You call the factory once, at app setup, passing in your own callables, and it returns a DependencyNamespace holding ready-made FastAPI dependencies. This keeps modules decoupled from your app and lets you swap any piece.

DependencyNamespace

DependencyNamespace (from msflib.api) is an attribute bag with a few dict helpers. It holds callables you use with Depends(...).

from msflib.api import DependencyNamespace

ns = DependencyNamespace(a=1, b=2)
ns.a                 # 1
ns["b"]              # 2
ns.as_dict()         # {'a': 1, 'b': 2}
list(ns)             # ['a', 'b']

It is deliberately mutable, so a downstream app can wrap or replace one member (for example to add logging around get_current_account) and pass the namespace on to a router.

The convention

Every deps.py factory follows the same shape:

  1. Keyword-only parameters for what the module needs from the host app: a session dependency, settings, model classes, other modules' dependencies.
  2. Inner functions that declare their own Depends(...) on those parameters.
  3. A returned DependencyNamespace of those functions, one entry per distinct shape.

Examples in the tree:

Factory Module Takes Returns (selection)
get_account_dependencies msflib.auth.deps AccountModel, oauth_token_url, secret_key, session_dep, keystore_dep get_current_account, get_current_account_or_none, get_current_active_account, get_current_active_superuser, RoleCheck
get_user_dependencies msflib.auth.deps UserModel, WorkspaceModel, session_dep, the account_dependencies namespace, optional workspace_resolver get_current_workspace, get_current_workspace_anonymous, get_current_user, get_current_active_user, WorkspaceRoleCheck
get_tenant_dependencies msflib.tenancy.deps session_dep, optional settings get_current_tenant
get_scope_dependencies msflib.scope the get_current_* callables above get_account_scope, get_workspace_scope, get_tenant_scope
get_provider_dependencies msflib.ai_core.deps settings, get_session, identity callables, optional get_policy_resolver get_resolved_ai_settings, get_registry_service, user_profiles_enabled, ...

Check each factory's docstring for its exact parameters and return members; some members only exist when you pass certain inputs. For instance get_scope_dependencies omits get_workspace_scope on a site with no workspace dependency.

Wiring a host app

Create the namespaces once at import time or in your app factory, then reference their members in endpoints. The dependencies resolve per request.

from fastapi import Depends, FastAPI
from msflib.scope import ScopeEnvelope, get_scope_dependencies

# Account, get_session, get_keystore and settings are your app's own objects.
from msflib.auth.deps import get_account_dependencies

account_deps = get_account_dependencies(
    AccountModel=Account,
    oauth_token_url="/api/v1/login/access-token",
    secret_key=settings.scope("CORE").SECRET_KEY,
    session_dep=get_session,
    keystore_dep=get_keystore,
)
scope_deps = get_scope_dependencies(
    get_current_account=account_deps.get_current_active_account,
)

app = FastAPI()


@app.get("/whoami")
def whoami(scope: ScopeEnvelope = Depends(scope_deps.get_account_scope)):
    return dict(scope.dimensions)

Module routers take these callables as arguments in the same spirit. The workspace-config router, for example, is built as router(get_session, settings, get_current_workspace, get_current_workspace_anonymous, get_current_active_user, workspace_role_check, ...), with each argument coming from your namespaces.

Writing your own factory

Follow the convention for your own app code. This example runs as written:

from types import SimpleNamespace

from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from msflib.api import DependencyNamespace


def get_site_dependencies(*, get_current_account):
    def get_greeting(account=Depends(get_current_account)) -> str:
        return f"hello {account.id}"

    return DependencyNamespace(get_greeting=get_greeting)


def current_account():
    return SimpleNamespace(id=7)


deps = get_site_dependencies(get_current_account=current_account)

app = FastAPI()


@app.get("/greeting")
def greeting(text: str = Depends(deps.get_greeting)):
    return text


print(TestClient(app).get("/greeting").json())  # hello 7

In tests you can pass fake callables like current_account above instead of real authentication, which is the main practical benefit of the pattern.

Why a factory per module

  • One factory means every consumer shares the same dependency function objects. FastAPI caches a dependency's result per request by function identity, so a settings or scope dependency used by several other dependencies is computed once per request.
  • Choosing the identity inputs at the factory (account only, or workspace plus membership) picks the right set of dependencies up front, instead of a single dependency full of optional behavior.

Pitfalls

  • Call a factory once, not inside an endpoint or per request. Calling it again creates new function objects, so FastAPI's per-request caching no longer deduplicates them.
  • Pass the same namespace member to every place that needs it. Two separately built get_current_account functions are two dependencies.
  • Factories validate their inputs at call time and raise ValueError for contradictory combinations (for example get_current_account together with get_current_active_user). That happens at startup, which is the intent.
  • Some namespaces mix kinds of members. get_ai_dependencies in ai_core returns plain callables with runtime arguments, not request-scoped dependencies, and RoleCheck and WorkspaceRoleCheck in the auth namespaces are classes you instantiate with a role list. Read the factory docstring before assuming a member is usable in Depends(...) directly.