Python SDK for building federated analyses on the FLAME/PrivateAIM platform.
An analysis container instantiates FlameCoreSDK, which bootstraps the connections to the platform's
sidecar services (MessageBroker, PO service, ResultService/Storage, DataAPI) and exposes methods for
inter-node messaging, result exchange and data source access.
Install it into your analysis image straight from the repository:
pip install git+https://github.com/PrivateAIM/flame-python-sdk.gitWhen working on the SDK itself (dependencies are managed with Poetry, the virtualenv is kept
in-project via poetry.toml):
poetry install # with dev dependencies
poetry install --without dev # runtime onlyRuntime requirements: Python >= 3.9, httpx, fastapi, uvicorn.
Every FLAME analysis starts by connecting itself to the other components of the FLAME platform and
starting an analysis REST API. All of this is done by instantiating a FlameCoreSDK object.
from flamesdk import FlameCoreSDK
def main():
flame = FlameCoreSDK()
# Your code here
flame.analysis_finished()
if __name__ == "__main__":
main()analysis_finished() signals all participating nodes to finish and flags this node as executed,
which lets the platform shut the container down.
FlameCoreSDK(
aggregator_requires_data: bool = False,
default_requires_data: bool = True,
stream_log_level: int = 20,
silent: bool = False,
status_sync: Optional[tuple[Literal['executed', 'stopped', 'failed']]] = ('executed', 'stopped', 'failed'),
)aggregator_requires_data— also connect the DataAPI on aggregator nodes (by default only nodes withnode_role == 'default'connect to it).default_requires_data— ifFalse, a default node whose DataAPI connection fails is accepted as a proxy node (its role is set to"proxy") instead of failing startup.stream_log_level— minimum log level submitted to the hub (see Logging).silent— suppress console output.status_sync— terminal states the analysis REST API advertises to partner nodes.
Constructing FlameCoreSDK() performs the platform handshake in a fixed order:
- Read the node config from the environment (see below).
- Wait until the local nginx sidecar (
nginx-<DEPLOYMENT_NAME>) is reachable. - Connect to MessageBroker → PO service → ResultService/Storage → DataAPI (the latter only when
required, see
aggregator_requires_data). - Start the
FlameAPI(FastAPI + uvicorn) on a background thread for incoming messages, health checks, token refresh and partner status updates. - If all clients and the API thread came up, the analysis status is flipped to
executing.
Each connection is wrapped individually — a partial startup is allowed, failures are logged and the
missing client is stored as None.
Read by NodeConfig and injected by the platform:
| Variable | Purpose |
|---|---|
ANALYSIS_ID |
id of the analysis |
PROJECT_ID |
id of the project |
KEYCLOAK_TOKEN |
bearer token for the sidecar services |
DATA_SOURCE_TOKEN |
token for the data sources |
DEPLOYMENT_NAME |
used to derive the nginx sidecar host name |
node_id, node_type and node_role are not read from the environment — they are filled in by the
MessageBroker handshake.
get_id() -> str— node id of this node.get_analysis_id() -> str/get_project_id() -> strget_type() -> Literal['default', 'aggregator']— the node type."aggregator"means the node may callsubmit_final_result(...).get_role() -> str/set_role(role) -> str— the node role; equal to the node type right after startup unless set manually (or set to"proxy", seedefault_requires_data).get_participants() -> list[dict[str, str]]— configs of all other participating nodes.get_participant_ids() -> list[str]get_aggregator_id() -> Optional[str]— node id of the node dedicated as aggregator.get_self_node_index() -> int/get_node_index(node_id) -> Optional[int]— index of a node in the alphanumerically sorted list of all analysis node ids.partner_role_call(node_ids, max_attempts=1, timeout=None, attempt_timeout=10) -> dict[str, Optional[str]]— ask the given partner nodes for their role (Noneper node on timeout/unknown id).full_role_call(max_attempts=1, timeout=None, attempt_timeout=10) -> dict[str, Optional[str]]—partner_role_callfor all participants.ready_check(nodes='all', attempt_interval=30, timeout=None) -> dict[str, bool]— repeatedly ping the given nodes until they respond or the timeout is reached.node_has_data() -> bool— whether this node is connected to the DataAPI.analysis_finished() -> bool— tell all nodes to finish, then mark this node asexecuted.
flame.flame_log("training round 3 done")
flame.flame_log("connecting...", end='', halt_submission=True)
flame.flame_log("success", append=True)flame_log(msg, sep='', end='', log_type='info', append=False, halt_submission=False, hidden_error_msg=None)
prints to the console and submits the log to the hub (logs created before the PO service connection
is up are queued). Log types are defined by LogTypeLiteral in
flamesdk/resources/utils/constants.py: debug (10), info (20), notice (25), warn (30),
alert (33), emerg (36), error (40), crit (50). Only logs at or above stream_log_level are
submitted. log_type='error' raises the error in addition to logging it; hidden_error_msg keeps a
stacktrace in the local log without submitting it to the hub. halt_submission=True defers
submission until the following log call, which is how "…success" / "…failed" end up on one line.
get_progress() -> intset_progress(progress: Union[int, float]) -> None— floats are truncated to integers; finishing the node sets it to 100.
set_checkpoint(kwargs: dict[str, Any], file_paths: Optional[list[str]] = None) -> None— saves the given kwargs plus the diff of the working directory (files created since startup, extended byfile_paths) to local storage under the tagcheckpoint-<n>-end.load_checkpoint(index: int) -> Optional[dict[str, Any]]— restores the kwargs and recreates the saved files/directories. ReturnsNoneif the checkpoint does not exist or is ambiguous.
The checkpoint- tag prefix is reserved — save_intermediate_data refuses tags containing it.
AnalysisStatus (flamesdk/resources/utils/constants.py) defines starting, started, stuck,
stopping, stopped, executing, executed, failed. Always pass AnalysisStatus.<NAME>.value
rather than a hardcoded string.
The Message Broker is used for control flow and small data exchange between nodes. Volume data (ML models, datasets) should be exchanged via the Result Service instead.
send_message(receivers, message_category, message, max_attempts=1, timeout=None, attempt_timeout=10) -> tuple[list[str], list[str]]— returns the node ids that acknowledged and those that did not.await_messages(senders, message_category, message_id=None, timeout=None) -> dict[str, Optional[list[Message]]]send_message_and_wait_for_responses(receivers, message_category, message, max_attempts=1, timeout=None, attempt_timeout=10) -> dict[str, Optional[list[Message]]]get_messages(status='unread') -> list[Message]delete_messages(message_ids) -> intclear_messages(status='read', min_age=None) -> int— delete messages by status, optionally only those older thanmin_ageseconds.
A Message carries the user payload in message.body. The meta field is reserved and added by the
SDK — a message body must not contain it.
{
"meta": {
"type": "outgoing | incoming",
"category": "str",
"id": "str",
"akn_id": "str | null",
"status": "unread | read",
"sender": "node_id",
"created_at": "datetime",
"arrived_at": "datetime | null",
"number": "int"
},
"...": "your payload"
}The message categories role_call, role_call_answer, ready_check, analysis_finished and
intermediate_data are used internally by the SDK.
Saves and exchanges results between the nodes of one analysis and locally between analyses of the same project.
submit_final_result(result, output_type='str', multiple_results=False, filename=None, local_dp=None) -> Union[dict[str, str], list[dict[str, str]]]— sends the final result to the hub for analysts to download. Only available on nodes whoseget_type()is"aggregator".output_typeis'str','bytes'or'pickle'(or a list of those whenmultiple_results=True);local_dptakesLocalDifferentialPrivacyParams({"epsilon": float, "sensitivity": float})and applies to floating-point results only.save_intermediate_data(data, location, remote_node_ids=None, tag=None) -> Optional[dict]—location='local'stores in the node,location='global'stores in the central MinIO instance. Global saves requireremote_node_ids(the recipients' public keys are used for encryption) and return one dict per recipient.get_intermediate_data(location, query=None, tag=None, tag_option='all') -> Any—tag_optionis'all','last'or'first'when multiple entries share a tag.get_local_tags(filter=None) -> list[str]send_intermediate_data(receivers, data, message_category='intermediate_data', max_attempts=1, timeout=None, attempt_timeout=10) -> tuple[list[str], list[str]]— saves the data globally and notifies the receivers via the Message Broker.await_intermediate_data(senders, message_category='intermediate_data', timeout=None) -> dict[str, Any]— waits for such notifications and retrieves the referenced data; senders that did not deliver in time map toNone.
flame.send_intermediate_data(receivers=flame.get_participant_ids(), data={"weights": weights})
partner_data = flame.await_intermediate_data(senders=flame.get_participant_ids(), timeout=300)Access to the FHIR and S3 stores linked to the project. All of these return None and log a warning
when the node is not connected to the DataAPI (check with node_has_data()).
get_data_sources() -> Optional[list[dict[str, Any]]]get_data_client(data_id) -> Optional[AsyncClient]— raw httpx client for a specific store.get_fhir_data(fhir_queries=[]) -> Optional[list[dict[str, Union[str, dict]]]]get_s3_data(s3_keys=[]) -> Optional[list[dict[str, bytes]]]fhir_to_csv(fhir_data, col_key_seq, value_key_seq, input_resource, row_key_seq=None, row_id_filters=None, col_id_filters=None, row_col_name='', separator=',', output_type='file') -> Optional[Union[StringIO, dict]]— pivots a FHIR bundle into CSV (output_type='file'returns aStringIO,'dict'a nested dict).
The background FlameAPI thread exposes:
| Route | Purpose |
|---|---|
GET /healthz |
health check, reports the current analysis status |
POST /webhook |
incoming messages from the Message Broker |
POST /partner_status |
partner node status updates |
POST /token_refresh |
refreshes the Keycloak token on all clients |
flamesdk/
flame_core.py # FlameCoreSDK — the public API
resources/
node_config.py # env var config, node id/type/role
rest_api.py # FlameAPI (FastAPI)
client_apis/
message_broker_api.py # high-level API layer: orchestration,
storage_api.py # retries, timeouts, type conversion
data_api.py
po_api.py
clients/*_client.py # thin httpx transports per sidecar service
utils/
logging.py # FlameLogger
constants.py # AnalysisStatus, LogTypeLiteral
fhir.py, utils.py
tests/
unit_test/ # pytest unit tests
test_images/ # end-to-end scripts run inside containers
When adding a capability, extend the *_api.py layer and keep clients/*_client.py limited to
request/response plumbing.
poetry run pytest # unit tests (tests/test_images is ignored)
poetry run pytest tests/unit_test/test_util.py # a single test module
poetry run ruff check . # lint
poetry run ruff format . # format
poetry run pre-commit run --all-filesRun pytest from the repository root — fixtures reference their data files relative to it.
Commits are checked by conventional-pre-commit, so use Conventional Commits (feat:, fix:,
chore:, refactor: …).
Apache-2.0 — see LICENSE.