Plugin Authoring Guide
The iMednet SDK ships two first-party optional plugins:
imednet-streamlit — interactive Streamlit reporting dashboards.
imednet-workflows— opinionated data-extraction and query-management workflows (see the workflows API reference).
The built-in imednet-workflows package is the reference implementation for
third-party plugins. Any third-party package can register its own plugin using
the standard Python entry-point mechanism.
Streamlit Dashboard Plugin (imednet-streamlit)
The imednet-streamlit package provides a multi-page Streamlit reporting
dashboard that consumes the core SDK. It is intentionally isolated so that
users deploying the SDK in serverless or batch environments are not forced to
install the large streamlit and altair binaries.
For the full end-user walkthrough and configuration reference, see the Streamlit plugin guide.
Installation
pip install imednet-streamlit
Or, when working inside the monorepo:
pip install ./packages/plugins-streamlit
Launching the dashboard
Once installed, the imednet dashboard CLI command becomes available:
imednet dashboard
This boots a local Streamlit server (default port 8501) and opens the dashboard in your default browser.
Optional flags:
imednet dashboard --port 8080 # use a custom port
imednet dashboard --no-browser # suppress automatic browser launch
If the plugin is not installed, the command exits with code 1 and
prints a helpful message:
Dashboard plugin not found. Install it with:
pip install imednet-streamlit
You can also launch the app directly with Streamlit:
streamlit run "$(python -c 'import imednet_streamlit.app as app; print(app.__file__)')"
Dashboard pages
Page |
Description |
|---|---|
Home |
Connection status and navigation guide |
Query Status |
Open/closed query breakdown with trend charts |
Subject Enrollment |
Enrollment funnel and site-level summaries |
Site Performance |
Site-level query rate metrics and rankings |
Data Completeness |
Record status heatmap and form-level summaries |
Credential management
Credentials are entered in the sidebar at runtime. The API Key and Security
Key fields use type="password" so they are masked in the browser. Values
are stored in st.session_state only for the lifetime of the browser
session and are never logged, written to disk, or exposed in plaintext.
After clicking Connect, the credential keys are removed from
st.session_state and only the constructed ImednetSDK instance is
retained for subsequent API calls.
Architecture rules
SDK-only data access — dashboard pages never call
requestsorhttpxdirectly; all network calls go throughImednetSDK, which provides the built-in retry policies and error handling.Caching — every
_fetch_*function is decorated with@st.cache_data(ttl=600)to prevent API rate-limiting on re-renders.Zero core bloat —
streamlit,altair, andpandasdo not appear in thepackages/coredependency list.
Workbench MVP developer guide
The Reporting Foundation MVP pages (Setup Wizard, Review Workbench, Publisher Wizard, Data Lineage) are designed so teams can extend templates and schema mapping safely without changing the core SDK package.
Extending templates and schema mappings
Use imednet.models.study_config.StudyConfiguration as the single
source of truth for extension points:
mappingsdefines canonical-field to raw-form mappings.terminology_lookupsdefines normalization tables used before validation.widgetsdefines dashboard template blocks rendered by the MVP pages.
When introducing new mappings or widgets, validate behavior in both:
tests/unit/streamlit/for UI interaction and page-state checks.tests/unit/workflows/for schema profiling/extraction behavior.
Headless Streamlit testing pattern (AppTest)
Use Streamlit’s official AppTest runner to test full-page flows without
manual sys.modules monkeypatching:
from pathlib import Path
from streamlit.testing.v1 import AppTest
from unittest.mock import patch
APP_PATH = (
Path(__file__).resolve().parents[3]
/ "packages"
/ "plugins-streamlit"
/ "src"
/ "imednet_streamlit"
/ "app.py"
)
def test_dashboard_login_flow() -> None:
with patch("imednet_streamlit.auth.ImednetSDK") as mock_sdk:
mock_sdk.return_value = object()
at = AppTest.from_file(str(APP_PATH))
at.run()
at.sidebar.text_input(key="_imednet_api_key").input("test-api-key")
at.sidebar.text_input(key="_imednet_security_key").input("test-security-key")
at.sidebar.text_input(key="_imednet_study_key").input("PROT-100")
at.sidebar.button[0].click()
at.run()
assert at.sidebar.success[0].value == "Connected ✓"
Optional-plugin CLI graceful-degradation verification
CLI commands that depend on optional plugins should be tested in two modes:
Plugin present: patch
importlib.util.find_spec(or module import) to return a module and assert the command runs.Plugin absent: patch discovery to return
None(or raiseImportError) and assert the command exits with code1and prints install guidance.
See tests/unit/cli/test_cli.py and tests/unit/cli/test_cli_export.py
for repository examples of this pattern.
Plugin authoring reference
How it works
During ImednetSDK (and
AsyncImednetSDK) initialisation the SDK queries the
imednet.plugins entry-point group for an entry named "workflows". If
exactly one is found the SDK loads the factory callable and calls it with
itself as the argument. The returned object is stored as sdk.workflows.
If the plugin is not installed, sdk.workflows is a placeholder that
raises ImportError with a helpful message on first attribute access.
If the plugin is installed but fails to load (broken import, wrong
interface, …) a PluginLoadError is raised at SDK
construction time.
Plugin contract
A plugin package must expose a single factory callable. The factory must
satisfy PluginProtocol:
from typing import Protocol
from imednet.plugins import PluginProtocol, WorkflowsNamespaceProtocol
class PluginProtocol(Protocol):
def __call__(self, sdk_instance: Any) -> WorkflowsNamespaceProtocol: ...
The object returned by the factory must satisfy
WorkflowsNamespaceProtocol — it must expose at
least the following attributes (any type):
data_extractionquery_managementrecord_mapperrecord_updatesubject_data
Minimal working example
# myplugin/namespace.py
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from imednet.sdk import ImednetSDK
class MyDataExtraction:
def __init__(self, sdk: "ImednetSDK") -> None:
self._sdk = sdk
def run(self, study_key: str) -> list[Any]:
return self._sdk.records.list(study_key=study_key)
class MyWorkflows:
def __init__(self, sdk: "ImednetSDK") -> None:
self.data_extraction = MyDataExtraction(sdk)
self.query_management = None # implement as needed
self.record_mapper = None
self.record_update = None
self.subject_data = None
def create_workflows(sdk: "ImednetSDK") -> MyWorkflows:
"""Entry-point factory — called by ImednetSDK at initialisation."""
return MyWorkflows(sdk)
Registering the entry point
Add the following to your package’s pyproject.toml:
[tool.poetry.plugins."imednet.plugins"]
workflows = "myplugin.namespace:create_workflows"
Or, when using setuptools, in setup.cfg:
[options.entry_points]
imednet.plugins =
workflows = myplugin.namespace:create_workflows
Once installed (e.g. pip install myplugin), the SDK will discover and
load the plugin automatically:
from imednet import ImednetSDK
sdk = ImednetSDK(api_key="...", security_key="...")
records = sdk.workflows.data_extraction.run("MY_STUDY")
Error handling
PluginLoadError is raised when:
More than one
"workflows"plugin is registered in theimednet.pluginsentry-point group.The entry point fails to import (e.g. broken dependency).
The loaded object is not callable.
The factory raises
TypeErrorwhen called with the SDK instance.
PluginLoadError is a subclass of
ImednetError so it can be caught uniformly:
from imednet import ImednetSDK
from imednet.errors import ImednetError
try:
sdk = ImednetSDK(api_key="...", security_key="...")
except ImednetError as exc:
print(f"SDK initialisation failed: {exc}")
API reference
- class imednet.plugins.PluginProtocol[source]
Bases:
ProtocolProtocol that every iMednet plugin factory must satisfy.
A conforming factory is a callable that accepts an SDK instance and returns an object implementing
WorkflowsNamespaceProtocol.Example:
from imednet.plugins import PluginProtocol from imednet.sdk import ImednetSDK class MyWorkflows: def __init__(self, sdk: ImednetSDK) -> None: self.data_extraction = ... self.query_management = ... self.record_mapper = ... self.record_update = ... self.subject_data = ... def create_workflows(sdk: ImednetSDK) -> MyWorkflows: return MyWorkflows(sdk) # Verify conformance at import time (optional, for development use): assert isinstance(create_workflows, PluginProtocol)
- __init__(*args, **kwargs)
- class imednet.plugins.WorkflowsNamespaceProtocol[source]
Bases:
ProtocolMinimal interface that a workflows namespace object must expose.
Each attribute should be a workflow class instance wired to the SDK.
- __init__(*args, **kwargs)
-
data_extraction:
Any
-
query_management:
Any
-
record_mapper:
Any
-
record_update:
Any
-
subject_data:
Any
- exception imednet.errors.PluginLoadError[source]
Bases:
ImednetErrorRaised when a plugin fails to load or does not satisfy the
PluginProtocol.