Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

MINT SDK (Python)

SDK for building analysis plugins that integrate with the MINT platform.

Full Documentation: See the MINT SDK docs for detailed API reference and guides.

Installation

# From PyPI (when published) — runtime core
uv add mint-sdk

# Development tooling: the `mint` CLI (typer) and in-process serving (uvicorn)
uv add --dev 'mint-sdk[cli,server]'

# From git
uv add git+https://github.com/MorscherLab/MINT#subdirectory=packages/sdk-python

Extras

Extra Adds Needed for
cli typer the mint command
server uvicorn[standard] mint dev, mint platform foreground, serving a plugin in-process
local-db SQLModel, SQLAlchemy, aiosqlite, greenlet SQL-backed plugin storage
s3 boto3 S3 object storage

mint-sdk alone installs only what a plugin process needs to serve requests (FastAPI, Pydantic, HTTPX, jsonschema, python-multipart, packaging). Running mint without the cli extra prints the install hint instead of a traceback.

MINT SDK 1.2 requires Pydantic >=2.12.0 and supplies it transitively. Plugins must not declare a separate Pydantic range.

SDK-owned runtime dependencies

A plugin declares mint-sdk plus packages unique to its scientific or service domain. The SDK owns FastAPI, Pydantic, HTTPX, jsonschema, python-multipart and packaging at runtime, and Typer/Uvicorn through the cli/server extras; plugin code may import them but must not list them again in pyproject.toml — ask for the extra instead (mint-sdk[cli,server] in the dev group). For SQL-backed plugin storage, depend on mint-sdk[local-db] instead of declaring SQLModel, SQLAlchemy, aiosqlite, or greenlet directly.

The plugin uv.lock selects exact versions for standalone development. An in-process plugin follows the platform lock, so removing duplicate constraints prevents a plugin from blocking a compatible platform patch.

Quick Start

Platform and standalone plugin HTTP errors share code, message, status, request_id, details, and the legacy detail field. Existing platform error codes and SDK exception error values remain valid. The REST client's APIError exposes status as an alias of status_code, with code, request_id, details, and the original body. Its subclasses are AuthError (401 and 403), NotFoundError, ConflictError and NetworkError; any other error status raises APIError itself. NotFoundError and ConflictError retain their SDK runtime exception inheritance, a 403 is also a PermissionException and a 422 is also a ValidationException. JSON error responses are decoded even when the request expects a binary download, and X-Request-ID is used when the body has no request ID.

Create a Python-only plugin whose standard UI is supplied by the SDK:

mint init my-plugin --mode generated --yes
cd my-plugin
mint dev

Generated mode uses typed Pydantic inputs with ui = "generated" on the plugin's Metadata and @job. It does not create a Vue project and mint build does not require Bun.

Use standard mode when the plugin needs a custom Vue page:

mint init my-plugin --mode standard --yes
cd my-plugin
mint sdk generate
mint dev

Standard mode starts with a small PluginWorkspaceView, a working form, and a generated typed client. That workspace is starter code, not a required layout; it can be replaced or removed without making mint doctor reject the mode. mint init --yes defaults to generated mode. Pass --mode standard when the plugin needs a custom Vue frontend.

mint doctor --strict is a pre-commit and CI validation step, not a startup requirement for mint dev.

mint sdk generate writes the frontend contract and typed client from backend routes and Pydantic schemas, so Vue code can call plugin endpoints without hand-writing route prefixes or request/response types. Use mint docs contract inside a plugin to inspect the generated endpoint and client-call contract without writing files. Use mint docs python job or mint docs frontend ExperimentSelectorModal to see the installed API contract together with its recommended usage. Add --json to receive structured examples for tooling. Use mint sdk generate --check --json in CI or editor tasks when you need machine-readable drift status. Use mint doctor --json for machine-readable project health checks and safe-fix status. When adding backend pieces, pass --generate to supported mint add commands to refresh the generated client in the same step.

Declarative plugin contracts

A plugin declares its options in one Metadata subclass, passes it to @plugin, and subclasses Plugin:

from pydantic import BaseModel

from mint_sdk import (
    Health,
    Metadata,
    Plugin,
    SettingsChange,
    health,
    on,
    plugin,
)

class PeakQcSettings(BaseModel):
    threshold: float = 0.05

class PeakQc(Metadata):
    domain = "metabolomics"
    settings = PeakQcSettings
    settings_access = "configure"

@plugin(PeakQc)
class PeakQcPlugin(Plugin[PeakQcSettings]):
    def routers(self):
        return []

    async def shutdown(self) -> None:
        pass

    @health(timeout=2.0)
    async def probe(self) -> Health:
        return Health(message="ready")

    @on("settings", fields={"threshold"})
    def threshold_changed(self, change: SettingsChange[PeakQcSettings]) -> None:
        self._threshold = change.current.threshold

Package identity comes from PEP 621 metadata and the sole mint.plugins entry point; the route prefix defaults to "/" + <entry-point key>. A Metadata subclass declares only runtime behavior, and an unknown attribute, a wrong type or an unknown value raises TypeError when the class is defined. writes (default {"artifacts"}) declares the platform data the plugin writes, auth (default True) whether its routes need a signed-in user, and settings_access (default "admin") who may change its settings. Plugin[S] is optional; @plugin checks that S is Metadata.settings. @health accepts sync or async handlers; sync handlers run in a worker thread, and timeouts or exceptions produce an unhealthy result. The SDK serves the result at GET /__mint/health, which the platform polls, so a plugin does not write its own /health route. @on("settings") handlers are synchronous, run after the settings commit in declaration order, and receive only actual top-level field changes. @on("experiment.before_save") handlers return None to allow the save and raise EventVetoException to refuse it.

Access levels

access says who may use an @endpoint, @job, MCP Tool or nav page. The platform owns the three levels; a plugin cannot name permissions or define roles.

access Platform requirement
"use" (default) plugins.use and a role that sees the plugin
"configure" "use" plus plugins.configure
"admin" a platform administrator
from mint_sdk import endpoint

class MethodRoutes:
    @endpoint.delete("/methods/{method_id}", access="configure")
    async def delete_method(self, method_id: str) -> None: ...

An endpoint with auth=False takes only access="use". Standalone mode skips the check. actor.can(access) gives the same answer in plugin code. access does not replace CurrentExperiment or repository ownership and visibility checks.

Notifications and calendar feeds

Async plugin methods may return typed notification or calendar values. The decorators validate each result and publish it when the plugin instance is bound to an integrated platform context:

from datetime import UTC, datetime

from mint_sdk import Plugin
from mint_sdk.integrations import (
    CalendarEvent,
    NotificationEvent,
    NotificationSeverity,
    calendar_event,
    notify,
)


class InstrumentPlanner(Plugin):
    @notify(channels={"email", "teams"})
    async def report_stop(self, run_id: str) -> NotificationEvent:
        return NotificationEvent(
            event_key=f"run:{run_id}:stopped",
            severity=NotificationSeverity.CRITICAL,
            title="Acquisition stopped",
            message=f"Run {run_id} stopped before completion.",
            occurred_at=datetime.now(UTC),
        )

    @calendar_event
    async def schedule(
        self,
        run_id: str,
        user_id: int,
        start: datetime,
        end: datetime,
    ) -> CalendarEvent:
        return CalendarEvent(
            event_key=f"run:{run_id}",
            title=f"Run {run_id}",
            start=start,
            end=end,
            participant_user_ids=(user_id,),
        )

An instrument probe may call a normal, plugin-owned API and then invoke an async @notify method. The plugin owns and validates the probe token; MINT does not issue, store, rotate, or read it. MINT receives only the method's structured notification result.

Only unacknowledged Critical notifications are delivered externally. MINT selects email recipients and owns the SMTP, Teams, and Slack targets. Calendar events use stable keys for upsert or cancellation and appear in read-only ICS feeds. Each decorator accepts at most 100 typed results per call.

An unbound or standalone direct call returns the method result without publishing. An integrated host-bound direct call can publish. Stacking @notify with @job requests completed/failed owner email only for a managed Job; calling that function directly does not create a terminal Job email. See the plugin development guide and 1.2 migration guide for the full contracts.

Backend-only generated UI

Plugins with simple controls and results can omit frontend source. Declare an optional Pydantic settings model with Metadata.settings, calculations with @job, and ui = "generated" on the Metadata. MINT then serves a standard workspace in standalone and integrated modes. An existing plugin Vue frontend always takes precedence.

from mint_sdk import Metadata, Plugin, TableResult, job, plugin
from pydantic import BaseModel, Field

class Inputs(BaseModel):
    threshold: float = Field(1.0, ge=0.0)

class PluginSettings(BaseModel):
    method: str = "default"

class SimpleAnalysis(Metadata):
    domain = "test"
    title = "Simple analysis"
    settings = PluginSettings
    ui = "generated"

@plugin(SimpleAnalysis)
class SimpleAnalysisPlugin(Plugin):
    @job()
    def run(self, inputs: Inputs) -> TableResult:
        return TableResult(
            columns=["threshold"],
            rows=[{"threshold": inputs.threshold}],
        )

Input v1 supports typed scalar fields, Pydantic models, enums, toggles, string arrays, and ordinary Path file/directory inputs. Result v1 supports text, JSON, tables/DataFrames, images, artifacts, Matplotlib figures, and interactive Plotly figures. Artifact v1 embeds its download bytes. Arbitrary HTML, JavaScript, and frontend callbacks are rejected.

The generic workspace cannot infer domain-specific part batching. ui = "generated" therefore rejects a class containing a staged job (a Job spec with parts); use a standard/custom frontend and its generated useJobs() client for that lifecycle.

Job options go on a Job subclass, and @job(Spec) references it. @job() takes every default. The SDK checks the options when the subclass is created and raises TypeError for an unknown option, a wrong type, or options of two execution modes. The handler sets the mode: async def is a service job (timeout, concurrency); def is a process job (cpu), staged when parts is set (min_parts, max_parts, max_bytes). The job id defaults to the method name in kebab-case and the title to the id in title case.

Call a decorated job like an ordinary synchronous Python method. The platform schedules the same job through its HTTP routes. @job, Job, JobContext, JobManager, and the result types are stable SDK APIs. JobContext.report(percent, message, *, stage=...) publishes percent/stage/message plus structured current-file progress; JobContext.warn() records bounded non-fatal warnings without replacing the progress message. JobContext.check() raises asyncio.CancelledError once the job is cancelled; the scheduler records that job as cancelled, not failed.

When one definition processes different datasets, set name and folders on the spec to derive the persisted Job name and source folders from each validated submission:

from mint_sdk import Job, job

class Analyze(Job):
    title = "Analyze"
    name = lambda inputs: inputs.dataset_name
    folders = lambda inputs: inputs.dataset_ids

@job(Analyze)
def analyze(inputs: Inputs) -> TableResult:
    ...

The callbacks are synchronous and receive a detached copy of the validated input. name returns a string, which the SDK cuts to 255 characters; an empty name falls back to the job title. The canonical in-memory Job state remains available after clients refresh, but it is not restored after a daemon restart.

The generated workspace schema is versioned but remains experimental in this minor release. See the backend-only example.

Runtime configuration and concurrency

Metadata.settings = PluginSettings is the single settings-model declaration for standalone and integrated settings. Metadata.settings_access defaults to "admin": integrated mutation and reset routes then require a platform administrator. Set it to "configure" when users with plugins.configure may change the settings; reads and standalone operation keep their existing behavior. Custom form schemas and out-of-process runtime metadata inherit the same declaration. Effective values resolve in this order:

model defaults < saved store < explicit .env < process environment < startup override

Environment variables use MINT_PLUGIN_<NORMALIZED_PLUGIN_NAME>__<FIELD>; nested fields add another double underscore. For example, MINT_PLUGIN_MY_ANALYSIS__DATABASE__HOST. Values supplied by .env, the process environment, or startup overrides remain effective. Writes still replace the saved backing value; that value appears in stored_settings and is marked inactive until the higher-priority runtime override is removed. Secret fields are never returned or persisted by the public settings API.

Full replacement uses optimistic concurrency. Pass the opaque revision that backs the candidate; do not parse or calculate it:

candidate = self.settings.model_copy(update={"threshold": 0.1})
await self.update_settings(candidate, replace=True, revision=self.settings_revision)

A full save makes one compare-and-swap attempt. It does not merge a stale candidate and does not retry it, because the SDK cannot infer which fields the caller intended to replace. A stale revision raises SettingsConflictError with committed=False; reload before rebuilding the candidate.

For a partial update, use await self.update_settings({"threshold": 0.1}). A patch shallow-merges into the latest durable backing value and retries compare-and-swap conflicts up to three times, so concurrent requests changing different fields do not overwrite each other. A patch does not take an expected_revision. Validation or preflight failure does not change durable or in-memory state. A preflight may run again during a patch retry, so it must be repeatable and side-effect free; @on("settings") handlers run once, only after a successful commit. A post-commit handler failure uses SettingsTransactionError with committed=True. If a CAS response is lost, the SDK reads the authoritative snapshot before deciding the result. A matching target is treated as committed. If neither the commit nor the readback can be confirmed, the error has commit_state="unknown" and committed=None; settings reads and new writes are fenced until an authoritative read succeeds, hot-applies the exact snapshot, and runs the @on("settings") handlers once.

The settings GET response carries the same opaque typed-settings content token as revision in its JSON body and as a quoted ETag. Each managed GET reads a fresh snapshot from authoritative persistence, but it does not hot-apply that snapshot to the running plugin or run @on("settings") handlers. Full HTTP writes (PUT /settings and POST /settings/reset) return 428 Precondition Required without If-Match: "<revision>", and 409 Conflict when that revision is stale. The token is a CAS value, not a monotonic generation: equal durable content can produce the same token, so it does not detect an A-to-B-to-A transition. An unconfirmed commit returns 503 Service Unavailable with committed: null; reload before deciding whether to retry the mutation.

SettingsTransactionError.commit_state is committed, not_committed, or unknown; committed remains its True, False, or None compatibility projection. Readback uses strict JSON identity, so true and 1 are distinct, and non-finite numbers are rejected before persistence.

PUT {} and reset clear the durable typed backing instead of materializing model defaults. The effective runtime model still receives those defaults with DEFAULT provenance, so a later plugin release can change a default without an older saved copy shadowing it.

Every resolver-backed runtime uses the synchronous host protocol mint_sdk.host.SettingsCompareAndSwapAdapter: standalone apps use JsonSettingsStore, in-process platform plugins use the central platform store, and isolated or Docker plugins use RemotePlatformContext to reach that same store. An integrated host subclasses mint_sdk.host.HostContext and implements get_plugin_settings_cas_adapter() with the complete plugin-owned key scope. Missing or unreachable CAS support fails app startup before plugin initialization instead of falling back to a best-effort read-modify-write.

Config models use a strict, fail-fast storage contract. Supported fields are named BaseModel fields composed from the standard scalar types, dates and times, Decimal, UUID, Path, Enum, Pydantic secret types, nested BaseModel values, explicit mappings/sequences, Literal, unions, and Annotated with ordinary Field constraints or discriminators. Enum values must survive a JSON round trip without type drift. Mapping keys must be str or string-valued Literal annotations; JSON object storage does not preserve integer or Enum key identity. Validators may normalize values, but the returned runtime shape must still match the annotation. Dynamic Any values are limited to string-keyed JSON-shaped trees and recognized secret leaves. Do not union SecretStr or SecretBytes with UUID, date/time, Decimal, Path, bytes, or another scalar that uses the same JSON string representation; use a small BaseModel with separate public identity and secret fields.

MINT rejects RootModel, TypedDict, dataclasses, Pydantic dataclasses, NamedTuple, extra="allow", arbitrary types, unsafe mapping keys, and serialization-changing features. The latter include field/model serializers, json_encoders, GetPydanticSchema, SerializeAsAny, plain/wrap serializers, and field exclude/exclude_if. Broad Pydantic helper types such as Json and ImportString are not config scalars; use a supported storage type plus a validator. Numeric ser_json_temporal modes are rejected. Legacy ser_json_timedelta="float" is supported, while non-UTF-8 byte serialization requires the same ser_json_bytes and val_json_bytes mode. See the 1.2 migration guide for the complete migration list.

The platform owns allowed_experiment_types; a decorator config model and a plugin-supplied owned_keys scope cannot claim it. The value must be list[str] | None, and corrupt persisted policy denies every experiment type until repaired. Typed and platform-owned fields are updated separately.

The user-facing plugin-config GET returns platform-owned public fields plus a fresh, sanitized typed snapshot only when the loaded provider, plugin name, resolver, and exact decorator model all match. The typed snapshot is read from authoritative persistence for each request without hot-applying it to the running plugin. It does not fall back to raw stored values for unloaded, untyped, external, or Docker-hosted plugins. The response revision and ETag cover only the plugin-owned typed snapshot; they do not cover allowed_experiment_types or other platform-owned fields. The plugin-scoped internal transport can still read raw backing values for an isolated runtime, and every write through that transport must supply explicit owned_keys without platform-owned fields.

Integrated plugin frontends should let the injected plugin.settings_api capability choose the settings route. Isolated subprocess, Docker-managed, external HTTP, and dev-proxy runtimes receive "plugin" and use their proxied plugin-local /settings API. In-process plugins receive "platform" and use the platform config facade; an absent field keeps that legacy platform behavior. The frontend SDK composables perform this selection automatically.

Full saves and shallow patches prepare a secret-free backing value together with its exact effective snapshot. Environment/startup overrides are included in preflight and the live commit but are not materialized into storage. A patch replaces only its explicit top-level keys and uses bounded compare-and-swap retries. Preflight and host persistence callbacks may be synchronous or return one awaitable; nested awaitables fail the transaction instead of leaving work running after an uncommitted result.

Secret-bearing list items need at least one stable non-secret field, such as an ID or a discriminating Literal, so masked values can be associated after a reorder. If a positional item becomes entirely secret after masking while other items remain, a public full save returns 422 instead of storing an ambiguous placeholder. Use a small BaseModel item with a stable public field instead of mixing bare secret values with ordinary list values. A locked secret override also keeps the pre-existing lower backing branch; unlike a locked non-secret scalar, its masked effective branch is never copied down. If a different locked branch occurs inside a positional collection, public full replacement returns 422 instead of silently discarding sibling edits.

Each browser tab gets an ephemeral session. Jobs are independent and may run concurrently across a user's tabs. Process and staged jobs snapshot input and effective settings at submission time. Service jobs snapshot input and expose the submitted settings as JobContext.settings, but run against the initialized host object: self.settings, plugin attributes, and services are live when the handler executes. Sessions and jobs are not persisted and do not survive a daemon restart.

@job works for both custom Vue and generated-UI plugins. Generated contracts expose typed useJobs(definition) for definition inputs, job handles, and outputs, plus refresh, delete, clear, pause, resume, and idempotent dispose lifecycle helpers. Use ordinary Path parameters for file or directory inputs; HTTP submissions stage opaque owner/session-scoped uploads into per-job writable workspaces, while direct Python calls keep the caller's local paths. Integrated generated UI may stage an accessible file-backed analysis artifact into the same workspace by experiment and artifact database ID; the copy and permission checks occur only when the user submits the job. mint_sdk.testing.Harness runs the real scheduled path without test-owned sessions, actors, polling, or temporary-file plumbing. Harness(plugin, settings=..., context=...) overrides the plugin settings and gives the plugin a platform context such as RecordingContext; harness.client is a TestClient on the plugin's own app.

Set access = "configure" or access = "admin" on the Job spec for maintenance or provisioning work that must not be submitted by ordinary platform users. Integrated submission and all associated Path/staged upload and seal operations then require plugins.configure or a platform administrator ("configure"), or a platform administrator only ("admin"). jobs.manage_all alone does not grant admission. Direct Python calls and standalone mode remain local operations, and the original job owner may still cancel or delete an existing job after losing that access. Generated clients bind Path uploads to the selected definition automatically; custom callers can pass the optional definition ID to uploadPath(input, definitionId).

For large outputs, write beneath JobContext.output_path() and return FileResult. The host adopts and hashes the file without base64 or a worker-IPC copy. It either exposes an expiring authenticated download or runs a trusted finalizer with ManagedFile, the original actor, validated input and settings snapshots, and a stable retry idempotency key. @finalize(Spec, attempts=3, timeout=30.0) binds the finalizer to the job declared with @job(Spec); a job without a spec cannot have one. The finalizer takes (result: ManagedFile, *, context: FinalizeContext[InputsT, SettingsT]); the SDK checks the written type arguments against the job's input model and the plugin's settings model. The finalizer's return annotation becomes the typed value inside the generated wire-result envelope. Finalization is async and cannot be cancelled by clients. Each attempt has a configured timeout; blocking libraries must run through FinalizeContext.run_blocking() so timeout cancellation drains the thread before the actor scope, managed file, quota, or live plugin services are released. A thread that never returns can therefore extend wall-clock teardown beyond that timeout. Platform mutation APIs remain responsible for current authorization. See the 1.2 migration guide.

The shared scheduler enforces CPU slots globally and a concurrent-job limit per user across all of that user's sessions. Each synchronous job runs in a fresh worker process. The pool defaults to one less than the host CPU count. A plugin's explicit Job.cpu request is reduced to the pool size when the deployment has fewer CPUs, and the effective value is available as JobContext.cpu and applied to common Python and Rust thread-pool environment variables. Configure the two primary limits with MINT_JOB_CPU_SLOTS and MINT_JOB_MAX_CONCURRENT_PER_USER, or the matching mint daemon flags.

The in-memory runtime also bounds queued work, retained terminal jobs, serialized payloads, and result lifetime. Configure these limits with MINT_JOB_MAX_QUEUED, MINT_JOB_MAX_QUEUED_PER_USER, MINT_JOB_MAX_SESSIONS, MINT_JOB_MAX_SESSIONS_PER_USER, MINT_JOB_MAX_RETAINED, MINT_JOB_MAX_RETAINED_PER_USER, MINT_JOB_MAX_INPUT_BYTES, MINT_JOB_MAX_TEMP_BYTES_TOTAL, MINT_JOB_MAX_TEMP_BYTES_PER_USER, MINT_JOB_MAX_RESULT_BYTES, MINT_JOB_MAX_MANAGED_RESULT_BYTES, MINT_JOB_MAX_RESULT_BYTES_TOTAL, MINT_JOB_MAX_RESULT_BYTES_PER_USER, and MINT_JOB_RESULT_TTL_HOURS.

The temporary-byte limits are shared service-wide across plugins, ordinary path storage, staged parts, and multipart request spools. During upload adoption, both the request spool and retained workspace copy may count.

Process workers reconstruct the zero-argument plugin class and inject the submission-time settings snapshot. Jobs should depend on typed input, self.settings, and JobContext. The trusted worker snapshot contains effective secret values, but job state, events, and the public settings API never expose them.

For Docker, Linux, and WSL, run one foreground host process:

mint daemon \
  --app mint_sdk.app:create_app \
  --port 8000 \
  --cpu-slots 4 \
  --max-concurrent-per-user 2

Forwarded headers are trusted only from loopback by default. When a reverse proxy runs on another address, pass its explicit IP or CIDR with --forwarded-allow-ips; do not use a wildcard on an exposed deployment.

--app defaults to the plugin runtime. The daemon serves the plugin in the current directory; use mint -C DIR daemon for another directory. The MINT platform has its own commands in the platform checkout: mint-platform serve and mint-platform daemon ....

Monorepo plugins must declare non-standard project paths so every CLI command uses the same frontend and generated client:

[tool.mint]
frontend_dir = "packages/ui"
generated_dir = "packages/contracts/client"

Both values are project-relative. generated_dir defaults to <frontend_dir>/src/generated when omitted. Conventional single-package plugins need no path settings.

mint doctor can also inspect split Python packages and several plugin projects from one repository root:

[tool.mint]
python_source_dirs = ["packages/leaf"]

[tool.mint.workspace]
plugin_members = ["plugins/*"]

[[tool.mint.doctor.import_rules]]
source = "leaf.analyzer"
forbid = ["mint_sdk", "fastapi", "leaf.api"]

Workspace members are explicit globs. Directories without a mint.plugins entry point are reported as skipped, and each plugin receives a separate text/JSON result group. Run mint doctor --strict when warnings must also fail CI. Import rules are static checks of normal Python imports; they do not import project code or guess dynamic imports.

For R-backed analyses:

mint init drp-r --mode standard
mint add r-analysis drp-fit --page
mint doctor --only r --explain
mint sdk generate

This creates an RAnalysisBridge service, FastAPI route, typed frontend composable, optional starter page, and a small mint_bridge.R helper for reading inputs, writing outputs, accessing the current experiment id, and writing analysis artifacts while keeping Python/Pydantic as the frontend contract source of truth.

For experiment object files, let the platform choose local, S3, or OpenStack Swift storage and keep only the returned reference in your experiment data. Standalone plugin runs use a local store next to the plugin's SQLite database, or under ~/.mint/plugins/<plugin-name>/objects.

class MyPlugin(Plugin):
    async def save_report(self, experiment_id: int, payload: bytes) -> dict:
        store = self.context.objects(experiment_id)
        ref = await store.put(
            "reports/report.json",
            payload,
            media_type="application/json",
            metadata={"kind": "qc-report"},
        )
        return ref.to_dict()

The platform stores object bytes under {server.dataPath}/objects by default. Admins can switch the object backend between storage.objects.backend = "local" "s3", and "swift" and can set storage.objects.localPath for local storage. When the backend is S3, the same SDK calls write to storage.s3.objectBucket under storage.s3.objectPrefix. When the backend is Swift, they write to storage.swift.objectContainer under storage.swift.objectPrefix. The platform owns endpoint, region, access key, secret key, session token, SSL, path-style, Keystone auth URL, project/domain, and Swift password settings; saved credentials are encrypted at rest and redacted from admin config responses. Use Admin -> Configuration -> Object Storage to choose Local Path, S3 Bucket, or OpenStack Swift, and the Test Connection button to validate provider access. Isolated plugin uploads of a Path or a file object use multipart transfer instead of base64 JSON.

S3-compatible provider settings may also come from environment variables. The platform reads endpoint/region names such as MINT_S3_ENDPOINT_URL, MINT_S3_REGION_NAME, S3_ENDPOINT_URL, and AWS_ENDPOINT_URL_S3; credential names such as AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN; and MINT-specific MINT_S3_* / MINT_STORAGE__S3__* variants for SSL and path-style addressing. Native Swift settings may come from MINT_SWIFT_* / MINT_STORAGE__SWIFT__* variables or standard OpenStack variables such as OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, OS_PROJECT_NAME, OS_USER_DOMAIN_NAME, OS_PROJECT_DOMAIN_NAME, OS_AUTH_VERSION, and OS_REGION_NAME.

Scripts can use MINTClient.objects for the same storage API by passing plugin_id explicitly. get_s3_connector() and platform S3 credential access were removed in 1.1. Integrated plugins call self.context.objects(experiment_id). A standalone script that needs S3 directly uses boto3 with its own credentials.

For first-class analysis artifacts, use the read-only client.experiments.artifacts resource:

artifacts = client.experiments.artifacts
rows = artifacts.list(42)
detail = artifacts.resolve(
    42,
    plugin_id="drp-analysis",
    artifact_key="qc-report",
)
artifacts.download_file(
    42,
    "qc-report.pdf",
    plugin_id="drp-analysis",
    artifact_key="qc-report",
)

resolve() uses (experiment_id, plugin_id, artifact_key) as the stable identity; artifact_id is only an optional hint. get_file_bytes() buffers a file in memory with a 256 MiB default limit. download_file() streams through a temporary file and atomically replaces the target after validation, so it is the appropriate choice for larger files. Downloads have no resume support and restart from byte zero after failure.

Both download methods accept on_progress with the SDK's DataTransferProgress callback. Reads verify the file reference's size and, when present, the managed artifact's metadata.mint_result_sha256; malformed checksum provenance is an error. Archived artifacts remain opt-in with include_archived=True. Mid-stream failures are not replayed automatically, and progress observers do not change the transfer outcome if they raise an exception.

At the Python layer, plugins subclass Plugin:

from mint_sdk import Metadata, Plugin, plugin
from fastapi import APIRouter

router = APIRouter()

@router.get("/hello")
async def hello():
    return {"message": "Hello from my plugin!"}

class MyMetadata(Metadata):
    domain = "metabolomics"

@plugin(MyMetadata)
class MyPlugin(Plugin):
    def routers(self):
        return [(router, "")]

    async def shutdown(self):
        pass

Plugin Package Structure

mint-plugin-example/
├── pyproject.toml
├── README.md
└── src/mint_plugin_example/
    ├── __init__.py
    └── plugin.py

pyproject.toml

[project]
name = "mint-plugin-example"
version = "0.1.0"
dependencies = ["mint-sdk>=1.4.0b1,<1.5"]

[dependency-groups]
dev = ["mint-sdk[cli,server]>=1.4.0b1,<1.5"]

[project.entry-points."mint.plugins"]
example = "mint_plugin_example.plugin:MyPlugin"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/mint_plugin_example"]

The entry point mint.plugins is how the platform discovers your plugin.

Platform Context

self.context is the plugin's PlatformContext. It is never None: without the platform it is a StandaloneContext. It provides:

  • Typed request actors (CurrentActor / Actor)
  • Capability-scoped data namespaces: design, artifacts, experiments, users, instruments, objects(experiment_id), files and db() (self.design and self.artifacts are shortcuts)
  • publish(name, payload), which delivers a plugin event to the plugin's own @on handlers
  • Persisted configuration for the current plugin
  • Typed notification and calendar publication

The context does not expose full platform settings or platform secrets. Use self.settings for the plugin's typed configuration and self.context.objects(experiment_id) for experiment files. Standalone, calls that need the platform raise UnavailableException (HTTP 503); objects() and db() use local storage.

async def initialize(self) -> None:
    if self.is_standalone:
        return
    experiments = await self.context.experiments.list(status="ongoing")
await self.design.save(experiment_id, {"plate": "A"})
await self.artifacts.save(experiment_id, {"rows": rows})          # key "default"
await self.artifacts.save(experiment_id, report_path, "report")   # a file needs a key
summary = await self.artifacts.get(experiment_id, model=Summary)

Installation Commands

These commands are for a developer environment or the trusted in-process plugin path. In-process installation retains normal package, Git, editable/local-source, wheel, and source-distribution support.

# Install from GitHub
uv add git+https://github.com/org/mint-plugin-example

# Install specific version
uv add git+https://github.com/org/mint-plugin-example@v1.0.0

# Install from PyPI
uv add mint-plugin-example

# Install local plugin for development
uv add --editable ./my-plugin

Subprocess installation

A subprocess plugin source must be an administrator-trusted, existing local .whl. MINT rejects package requirements, Git URLs, editable installs, source trees, source archives, and source distributions for this path. Dependencies must have compatible binary wheels; source builds are disabled.

Keep the selected wheel at its persisted path. If it is missing at startup, MINT leaves the plugin disabled and does not use a registry, network download, or source fallback. This release does not include a trusted wheel builder or a content-hash artifact cache.

The virtual environment and separate process isolate dependencies and ordinary failures, not hostile code. They are not an OS sandbox. Deploy untrusted code through an external HTTP or Docker-managed runtime with explicit operating system or container controls. Snapshot rollback can remove packages added after the snapshot, but MINT refuses to restore missing or changed packages without immutable trusted wheels.

For an external HTTP runtime, the deployment operator sets MINT_EXTERNAL_PLUGIN_<NORMALIZED_NAME>_TOKEN on MINT and passes the same secret to the external service as MINT_PLUGIN_TOKEN. The normalized name is uppercase with runs of non-alphanumeric characters replaced by _. Use at least 32 non-whitespace characters. MINT reads the value at startup and does not register the runtime when it is missing or weak. It does not store, expose, or rotate this secret. Normalized token keys must be unique; all colliding runtimes remain disabled. Targets must be absolute http or https URLs with a hostname and valid optional port, without userinfo, query strings, or fragments. Plain HTTP is accepted only for localhost, 127.0.0.0/8, or ::1; remote targets require HTTPS. Tokens used by an instrument to authenticate to a plugin-owned API are separate credentials owned entirely by that plugin.

Metadata

Release files for mint-sdk 1.4.0b2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mint-sdk 1.4.0b2
File Size Uploaded Provenance
mint_sdk-1.4.0b2.tar.gz 2.7 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mint-sdk 1.4.0b2
File Interpreter ABI Platform
mint_sdk-1.4.0b2-py3-none-any.whl Python 3 none any Details

Total release size: 5.2 MB

Release files / mint_sdk-1.4.0b2.tar.gz

Download URL mint_sdk-1.4.0b2.tar.gz
Size 2.7 MB
Tags Source
SHA-256 checksum
How to use checksums
1a267eb5b3195cf1154bc33abf1d4af1bd679ad7ae1cb02030cd653345c77c6b
BLAKE2b-256 checksum
How to use checksums
c20283f3f7e612871fa07b92503b3f62323207ab556301df4c965702f7a6cd00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / mint_sdk-1.4.0b2-py3-none-any.whl

Download URL mint_sdk-1.4.0b2-py3-none-any.whl
Size 2.4 MB
Tags Python 3
SHA-256 checksum
How to use checksums
e539bf8f5210a241e92e89fa985f200200694a60f4efeee66f85719a874ce5ec
BLAKE2b-256 checksum
How to use checksums
f3d20105d0e001bd3f5d5c1f0cd45b74e34c6c48394ee1e30a12be5bcb4921cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for mint-sdk 1.4.0b2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page