Skip to content

PlatformContext

PlatformContext is the single object the platform hands to a plugin. Through it, the plugin reaches every platform-side service: experiments, plugin data, users, plugin roles, the platform config, and a database session scoped to the plugin's schema.

python
from mint_sdk import PlatformContext

class MyPlugin(AnalysisPlugin):
    async def initialize(self, context: PlatformContext | None = None):
        self._context = context   # may be None in standalone mode

When standalone, context is None; the plugin uses LocalDatabase (a mint-sdk-managed local SQLite) instead. When integrated, context is a real PlatformContext instance and every accessor below is live.

Accessors

AccessorReturnsNotes
is_authenticated (property)boolTrue if the active request has an authenticated user
get_current_user_dependency()FastAPI DependsInject as user = Depends(context.get_current_user_dependency())
get_optional_user_dependency()FastAPI DependsAs above but None-tolerant
get_user_repository()UserRepository | NoneUser lookups (read-only)
get_experiment_repository()ExperimentRepository | NoneExperiment access, type-gated by PluginType
get_plugin_data_repository()PluginDataRepository | NoneSave/load DesignData and PluginAnalysisResult
get_plugin_role_repository()PluginRoleRepository | NonePer-plugin user roles
require_plugin_role(*roles)FastAPI DependsRoute guard — see below
get_config()dict (PlatformConfig)Platform configuration view (filtered)
get_shared_db_session()async context managerAsync SQLAlchemy session scoped to your plugin's schema

Repository access is still bounded by the platform deployment and plugin type. STATIC and ANALYSIS get a read-only experiment wrapper, EXPERIMENT_DESIGN gets a design-scoped wrapper, and FULL gets full experiment access. get_shared_db_session() requires shared-database setup; declare requires_shared_database=True when your plugin owns tables.

Authentication and current user

Two FastAPI dependencies cover the common cases. Because routes are usually declared in separate modules and PlatformContext is attached during initialize(), build auth-dependent routers from the plugin instance:

python
from fastapi import APIRouter, Depends
from mint_sdk import PlatformContext

class MyPlugin(AnalysisPlugin):
    async def initialize(self, context: PlatformContext | None = None):
        self._context = context

    def get_routers(self):
        return [(create_router(self), "")]

async def _allow_standalone():
    return None

def create_router(plugin: MyPlugin) -> APIRouter:
    router = APIRouter()
    context = getattr(plugin, "_context", None)
    current_user = (
        context.get_current_user_dependency()
        if context is not None
        else _allow_standalone
    )

    @router.get("/me")
    async def me(user=Depends(current_user)):
        return {"user": user}

    return router

Standalone mode falls through to the stub. Integrated mode uses the platform auth dependency and request-scoped user context.

Plugin role guard

require_plugin_role(*roles) returns a dependency that:

  • Resolves the current user via the same auth dependency
  • Reads the user's plugin role from PluginRoleRepository
  • Allows the request through only if the role is in roles
  • Bypasses the check for platform admins automatically
python
from fastapi import APIRouter, Depends


async def _allow_standalone():
    return None


def create_admin_router(plugin: MyPlugin) -> APIRouter:
    router = APIRouter(tags=["admin"])
    context = getattr(plugin, "_context", None)
    admin_or_owner = (
        context.require_plugin_role("admin", "owner")
        if context is not None
        else Depends(_allow_standalone)
    )

    @router.get("/admin/settings", dependencies=[admin_or_owner])
    async def settings():
        return {"settings": "..."}

    return router

See Recipes → Route permissions for the full pattern.

Database session

get_shared_db_session() is the canonical way for a plugin to talk to its own tables. The session has its search_path set to the plugin's schema, so unqualified table names resolve correctly:

python
from sqlalchemy import select

class MyPlugin(AnalysisPlugin):
    async def list_panels(self):
        async with self._context.get_shared_db_session() as session:
            result = await session.execute(select(PanelModel))
            return result.scalars().all()

Standalone mode has its own equivalent — AnalysisPlugin.get_plugin_db_session() (an instance method on the plugin itself, not the context) routes to LocalDatabase when no context is present:

python
class MyPlugin(AnalysisPlugin):
    async def list_panels(self):
        async with self.get_plugin_db_session() as session:
            ...   # works in both modes

Prefer self.get_plugin_db_session() over the context method directly — it gives you mode-portable plugin code.

Convenience methods on AnalysisPlugin

For the most common operations on DesignData and PluginAnalysisResult, the plugin base class wraps PluginDataRepository:

MethodWhat it doesStandalone?
await self.save_design(experiment_id, data)Save / update design dataReturns None
await self.load_design(experiment_id)Load design dataReturns None
await self.save_analysis(experiment_id, result)Save / update analysis result for this pluginReturns None
await self.load_analysis(experiment_id, fields=None)Load analysis result for this plugin, optionally projecting top-level result keysReturns None
await self.load_artifacts(experiment_id)Load only result["artifacts"] from this plugin's resultReturns None
await self.load_analyses(experiment_id, include_others=False)Load analysis results; defaults to this plugin's own resultReturns []
await self.save(experiment_id, design=..., analysis=...)Save both at onceReturns (None, None)
await self.load(experiment_id)Load bothReturns (None, None)
await self.delete_design(experiment_id)Delete designReturns False
await self.delete_analysis(experiment_id)Delete analysisReturns False

These are the daily authoring API. Drop down to context.get_plugin_data_repository() only when you need bulk operations or have multiple plugin IDs to coordinate.

For cross-plugin readers, call load_analyses(experiment_id, include_others=True) or repo.get_analysis_results(experiment_id, include_others=True) explicitly. The default is intentionally scoped to the calling plugin so ordinary analysis plugins do not accidentally consume another plugin's result payloads.

What PlatformContext is not

  • Not a request-scoped object you Depends-inject. It's a long-lived object set during initialize() and stored on the plugin instance.
  • Not a synchronous interface. Every accessor returning data uses async I/O. The shared-mode and isolated-mode variants both honor this.
  • Not a container for user state. The user comes from the FastAPI auth dependency (get_current_user_dependency()), not from the context object directly.

Standalone fallback pattern

For mode-portable code:

python
class MyPlugin(AnalysisPlugin):
    async def initialize(self, context=None):
        self._context = context
        if context is None:
            self._setup_standalone_db()      # base class helper

    def get_experiment_id_from_request(self, request_body):
        # Use convenience methods — they no-op cleanly when standalone
        return request_body.get("experiment_id", 1)

    async def fetch(self, experiment_id):
        # Works in both modes
        return await self.load_design(experiment_id)

The convenience methods (save_design, load_design, …) return None/False in standalone mode rather than raising — your plugin can carry on with empty results in development without branching code.

Next

Data model — what the repos return → Recipes → Reading experiments — concrete ExperimentRepository patterns → Recipes → Route permissions — using the plugin role guard

MINT is open source. Made by the Morscher Lab.