SDK reference overview
This article refers to SDK version v0.5.0. The current SDK version is N/A.
Official Python SDK for the Barbara Edge AI platform API. Typed, synchronous and asynchronous clients for managing nodes, clusters, applications, models, and related resources.
What Barbara manages
Before using the SDK it helps to understand the domain it operates on, because the resource tree (client.nodes, client.clusters, client.applications, ...) mirrors it directly.
Barbara is a management platform for Edge AI: instead of running inference or data processing in a central cloud, the workload runs physically close to where the data is produced: a factory floor, a retail store, a vehicle, a piece of industrial equipment. Barbara's role is to be the control plane for the machines running there.
A few terms recur throughout the API and this SDK, matching how they appear elsewhere in Academy:
Node. A physical or virtual machine running the Barbara agent: anything from a small industrial gateway to a server-grade box. Operators in the Barbara Panel usually refer to it by a human-readable name, the Barbara ID (for example factory-line-3-cam01). A node reports telemetry (status, resource usage, firmware version) and is the unit onto which applications and models get deployed. See Node lifecycle.
Cluster. A group of nodes managed and deployed to as a unit. Rather than pushing an application to ten nodes one at a time, a cluster lets you push it once and have it land on every member. Clusters have their own global secrets and global configuration, independent of any single node's. See High availability.
Application. A packaged, versioned piece of software (typically a Docker-based service) that can be installed on a node or across a cluster. Barbara distinguishes between applications you author yourself (Docker apps) and applications published in Barbara's marketplace (Marketplace apps); the SDK reflects this with separate create_docker_workload / create_marketplace_workload methods. See App Library.
Workload. An application actually running on a specific node, or, at cluster scope, across every node in a cluster. Barbara's product docs use Workload at both scopes: there's no separate "stack" concept in Panel, and the SDK matches that, with client.nodes.workloads at node scope and client.clusters.workloads at cluster scope. Both expose the same lifecycle operations (start, stop, logs) once deployed.
Model. A machine learning model artifact (for example, an ONNX file), versioned the same way applications are, and deployed to nodes to run inference at the edge. See Models.
Group. A named collection of nodes used for organizing and filtering, independent of clusters (a group does not imply a shared deployment).
Every resource in BarbaraClient corresponds to one of these concepts, and the nesting mirrors reality: workloads live under nodes (client.nodes.workloads) because a workload only exists in the context of a node running it; workloads live under clusters (client.clusters.workloads) for the same reason at cluster scope.
Authentication
The Barbara API uses the OAuth2 password grant — the SDK exchanges a set of credentials for a short-lived bearer token and refreshes it automatically. See Authenticate in the Getting started guide for the four Barbara API Credentials and how to export them.
If you'd rather not rely on environment variables, for example when credentials come from a secrets manager, construct a BarbaraConfig explicitly:
from barbara import BarbaraClient, BarbaraConfig
config = BarbaraConfig(
client_id="...",
client_secret="...",
username="...",
password="...",
)
client = BarbaraClient(config)
Three more environment variables control where the client connects, and rarely need to change:
| Variable | Description | Default |
|---|---|---|
BBR_API_URL | Barbara API base URL | https://prod.bap.barbara.tech |
BBR_AUTH_URL | Barbara auth server base URL | https://prod.auth.barbara.tech/auth |
BBR_REALM | Authentication realm | bbr_prod |
Access tokens issued by the Barbara auth server expire after roughly one hour. You do not need to handle this yourself: every request the SDK makes checks token freshness first, and a request that still comes back 401 is retried exactly once with a freshly fetched token. If it fails a second time, the original error is raised — at that point the credentials themselves are the likely problem, not an expired token.
Async usage
AsyncBarbaraClient mirrors BarbaraClient method for method — only await differs. Reach for it when you're already inside an async application (a web service, an event loop) rather than as a default choice for one-off scripts.
import asyncio
from barbara import AsyncBarbaraClient
async def main():
async with AsyncBarbaraClient.from_env() as client:
nodes = await client.nodes.list()
asyncio.run(main())
The rest of this page shows the synchronous client; every example works identically on the async one by adding await and using async with.
Both clients accept http2=True (needs the optional h2 dependency, pip install barbara-api-sdk[http2]) to multiplex many concurrent requests over one connection, useful together with AsyncBarbaraClient and asyncio.gather; it's not worth turning on for purely sequential calls. max_retries (default 0) retries a request that fails with a connection error or a 429/502/503/504 response, with exponential backoff honoring Retry-After on a 429 — this is on top of, not instead of, the unconditional single 401-retry-with-fresh-token described above. See the Client reference for both parameters.
Identifying nodes: internal ID vs. Barbara ID
Every node is addressed internally by an _id, a 24-character hexadecimal string, but nobody remembers those. The Nodes list in Panel shows nodes by their human-assigned name instead (the Barbara ID, e.g. factory-line-3-cam01). This distinction comes up constantly once you start scripting against the API.
node = client.nodes.resolve("factory-line-3-cam01")
print(node.id) # the internal _id, e.g. "651f3a2e9b1c4d00123abcde"
resolve() looks the name up via a search and returns the matching node, so downstream calls that require an ID (get, reboot, create_global_secrets, ...) have one to use.
If you already have the internal _id, for instance saved from a previous run, skip straight to client.nodes.get(node_id). Reserve resolve() for the case where you only have the human-readable name, since it costs an extra search request.
Usage
Nodes
See Node management for the equivalent Panel actions.
nodes = client.nodes.list(search="sensor")
node = client.nodes.get("<node-id>")
node = client.nodes.resolve("my-node-01") # look up by node name
client.nodes.reboot("<node-id>")
client.nodes.poweroff("<node-id>")
Node global secrets
Global Secrets (Wi-Fi pre-shared keys, API tokens injected into a workload, etc.) are Barbara-managed, stored per node, and always base64-encoded by the underlying API. They are distinct from a Marketplace app's own App Secrets and from Docker-native Swarm Secrets. See Node & cluster secrets and Secrets for the equivalent Panel card.
client.nodes.create_global_secrets("<node-id>", {"wifi-psk": "s3cr3t"})
secrets = client.nodes.list_global_secrets("<node-id>")
client.nodes.delete_global_secret("<node-id>", "<secret-id>")
The SDK handles the base64 encoding for you: pass plain strings in, get plain strings (or their metadata) back out. You never need to encode a value yourself.
Node global configuration
Application configuration is a free-form JSON document attached to a node, typically read by a workload at startup to adjust its behavior without rebuilding it. This corresponds to what Panel calls Global Config — configuration scoped to the node rather than to a single workload (see Application configuration types for the full picture).
client.nodes.set_global_config("<node-id>", config={"threshold": 5})
config = client.nodes.get_global_config("<node-id>")
Docker credentials
client.nodes.create_docker_credentials(
"<node-id>", [{"user": "bob", "password": "s3cr3t", "server": "docker.io"}]
)
Node identity: name, tags, location, safety actions
See General info for the same fields (name, location, tags) as they appear in Panel.
client.nodes.update_name("<node-id>", "floor-2-sensor-01")
client.nodes.add_tag("<node-id>", "production")
production_nodes = client.nodes.list_by_tags(["production"])
client.nodes.set_location("<node-id>", lat=40.4168, lng=-3.7038, city="Madrid")
location = client.nodes.get_location("<node-id>")
client.nodes.update_safety_actions(
"<node-id>", trigger_threshold=90, stop_apps=True, prune_volumes=True
)
update_safety_actions configures an automatic reaction to disk pressure: above trigger_threshold (a percentage, strictly between 0.1 and 99.9), the node can stop its apps and/or prune docker resources on its own, without a human watching a dashboard. At least one action must be enabled.
list_by_tags(tags) returns every node carrying at least one of the given tags. There's no server-side tag filter on the underlying list endpoint, so this pages through the whole fleet and filters locally: fine for selecting a target set of nodes, not meant for a hot path against a very large fleet.
get_location is returned as a raw dict rather than a typed object, since node location payloads vary in shape.
Barbara Core updates
See Firmware updates.
client.nodes.update_barbara_core("<node-id>", "update")
client.nodes.update_barbara_core(
"<node-id>", "schedule", schedule_timestamp="2026-01-01T03:00:00Z"
)
client.nodes.cancel_barbara_core_update("<node-id>")
Docker maintenance and volumes
client.nodes.prune_docker("<node-id>", "prunevolumes")
client.nodes.prune_docker_all("<node-id>")
client.nodes.restart_docker_daemon("<node-id>")
client.nodes.create_docker_volume("<node-id>", "shared-cache")
volumes = client.nodes.list_docker_volumes("<node-id>")
client.nodes.delete_docker_volume("<node-id>", volumes[0]["_id"])
See Volumes.
create_docker_volume doesn't return an id. Use list_docker_volumes to look one up before calling delete_docker_volume — device volumes have no dedicated list endpoint of their own.
Telemetry
See Telemetry.
latency = client.nodes.get_telemetry_latency("<node-id>")
client.nodes.set_telemetry_latency("<node-id>", 30)
telemetry = client.nodes.get_last_telemetry("<node-id>")
print(telemetry["disk"], telemetry["alive"])
get_last_telemetry and get_telemetry_latency both return raw dicts, with nested fields for disk, network, and containers. Read the keys you need directly.
Node networking
client.nodes.network covers the Networking card on a node's detail page: Ethernet, WiFi, and Mobile interfaces, VLANs, hostname, VPN, Proxy, Standalone Mode, IPTables, and NTP servers.
network = client.nodes.network.get("<node-id>")
client.nodes.network.update_ethernet_interface(
"<node-id>", "eno1", dhcp=False, ip="10.0.0.5", gateway="10.0.0.1"
)
client.nodes.network.enable_vpn("<node-id>")
client.nodes.network.start_vpn("<node-id>")
See Node networking for the full walkthrough, field by field.
Node workloads
A workload is an application instance running on one specific node. Deploying one means pointing at an application version and, for Marketplace applications, describing which services and ports it exposes (its Compose Config). See Docker apps and Marketplace apps.
client.nodes.workloads.create_docker_workload(
"<node-id>",
app_version_id="<app-version-id>",
application_id="<application-id>",
)
client.nodes.workloads.create_marketplace_workload(
"<node-id>",
app_version_id="<app-version-id>",
application_id="<application-id>",
name="my-workload",
compose_config=[{"name": "modelservice", "ports": {"PORT_NUMBER": "9083"}}],
)
client.nodes.workloads.start("<node-id>", "<workload-id>")
client.nodes.workloads.stop("<node-id>", "<workload-id>")
logs = client.nodes.workloads.get_logs("<node-id>", "<workload-id>")
Creation and update calls do not return the resulting workload state: the API acknowledges the request but does not echo back the deployed object. Call client.nodes.workloads.get(...) afterwards if your script needs to inspect the result (its final status, assigned ports, and so on).
Model workloads
A model workload is the same idea as a Marketplace workload, but deploying a model application version instead — for example, a TensorFlow Serving or Triton container. It uses the same body shape as Marketplace workloads, with one extra constraint: compose_config must exactly match the service template declared by the selected model application version. See Models.
client.nodes.workloads.create_model_workload(
"<node-id>",
app_version_id="<model-app-version-id>",
application_id="<model-application-id>",
name="my-model-workload",
compose_config=[{"name": "modelservice", "ports": {"PORT_NUMBER": "8501"}}],
)
Clusters
A cluster groups nodes so they can be configured and deployed to together, without repeating the same call once per node. See Clusters.
clusters = client.clusters.list()
cluster = client.clusters.get("<cluster-id>")
client.clusters.update("<cluster-id>", "new-name")
client.clusters.create_global_secrets("<cluster-id>", {"db-password": "s3cr3t"})
client.clusters.set_global_config("<cluster-id>", config={"threshold": 5})
Creating a cluster and managing membership
Creating a cluster means describing the swarm/VRRP networking for a primary node and, optionally, a set of secondary nodes that join it at creation time.
client.clusters.create(
"floor-2-cluster",
primary_node={
"nodeId": "<node-id>",
"labels": "eyJ6b25lIjogImZsb29yLTIifQ==", # base64 JSON: {"zone": "floor-2"}
"restrictSwarmTrafficToInterface": False,
"advertiseAddr": "10.0.0.5",
},
enable_cluster_volumes=True,
)
primary_node and secondary_nodes take the cluster networking configuration as dicts matching the API schema (nodeId, labels as base64-encoded JSON, advertiseAddr, ...).
Nodes already in a cluster can be moved through its lifecycle states, or join/leave altogether:
client.clusters.join_node(
"<cluster-id>",
"<node-id>",
labels={"zone": "floor-2"},
restrict_swarm_traffic_to_interface=False,
advertise_addr="10.0.0.6",
)
client.clusters.pause_node("<cluster-id>", "<node-id>")
client.clusters.drain_node("<cluster-id>", "<node-id>")
client.clusters.set_node_active("<cluster-id>", "<node-id>")
client.clusters.leave_node("<cluster-id>", "<node-id>")
Swarm-native configs and secrets
Beyond Barbara's own Global Config/Global Secrets, a cluster app can declare its own Docker Swarm configs and secrets directly in its docker-compose.yaml. Panel surfaces these read-only on the Swarm Config and Swarm Secrets cards; the SDK mirrors that with cleanup-only methods:
client.clusters.delete_swarm_config("<cluster-id>", "<swarm-config-id>")
client.clusters.delete_swarm_secret("<cluster-id>", "<swarm-secret-id>")
Cluster workloads
The cluster-level equivalent of node workloads — deploy an application across every node in a cluster in one call, rather than iterating node by node. Barbara's product docs use "Workload" at both scopes, there's no separate "stack" concept in Panel. See Add applications.
client.clusters.workloads.create_docker_workload(
"<cluster-id>",
app_version_id="<app-version-id>",
application_id="<application-id>",
)
client.clusters.workloads.delete("<cluster-id>", "<workload-id>")
Model workloads work the same way, using create_model_workload — the cluster-level equivalent of the node-scope call, with the same "compose_config must match the model's template" constraint:
client.clusters.workloads.create_model_workload(
"<cluster-id>",
app_version_id="<model-app-version-id>",
application_id="<model-application-id>",
name="my-model-workload",
compose_config=[{"name": "modelservice", "ports": {"PORT_NUMBER": "8501"}}],
)
Applications
Applications and their versions form the catalog that workloads are deployed from, at either scope. Creating an application registers its metadata; creating a version uploads the actual installable artifact (typically a Docker image bundle). See App Library.
apps = client.applications.list()
client.applications.create(
"edge-app", "Long description", "Barbara", docker=True, icon_path="./icon.png"
)
client.applications.create_version(
"<application-id>", "./app-v1.tar", "1.0.0", ["amd64"], ["Initial release"]
)
create and create_version upload files (icon, installable artifact) as multipart/form-data. Pass a local file path: the SDK reads the file from disk and builds the multipart request for you, so there is no need to open the file or set headers yourself.
Models
The model catalog works the same way as the application catalog, but for ML model artifacts deployed to run inference at the edge. See Models.
models = client.models.list()
client.models.create(
"anomaly-detector", "Long description", "Barbara", model_type=0, engine=0
)
client.models.create_version("<model-id>", "./model.onnx", "1.0.0", ["Initial release"])
sha256 and size for a model version are computed automatically from the artifact, so you don't need to hash or measure the file yourself before calling create_version.
Config Repository
Reusable, named configuration documents that can be referenced by ID elsewhere in the API — useful when the same configuration document needs to be attached to many nodes, clusters, or workloads without duplicating it in every call. See Config repository.
config_doc = client.configs.create(
name="sensor-thresholds",
description="Per-node alert thresholds",
config={"temperature_max": 80},
)
Groups
Groups organize nodes independently of clusters: a group is a label for filtering and reporting, and does not imply a shared deployment target the way a cluster does. See Nodes list for group management in Panel.
group = client.groups.create(
name="floor-2-sensors",
description="All floor 2 nodes",
node_ids=["<node-id-1>", "<node-id-2>"],
)
Users
Company users, read-only through this API. See Organization.
users = client.users.list()
page = client.users.paginate(offset=0, size=50)
paginate exists because list() on some resources caps how many results the API returns in a single call. Use paginate when a company has more users (or, on other resources, more of whatever entity) than fits in one page.
Alerts
See the Alert Manager app.
alerts = client.alerts.list()
client.alerts.ack("<alert-id>")
events = client.alerts.list_events(node_id="<node-id>")
Roles and permissions
Every Barbara API token carries a role (read, edit, edit_plus, or admin), decoded from the JWT issued at login. These roles are not strictly hierarchical: admin does not automatically imply every permission edit_plus has. If a call fails with a permission error, check which role the authenticated user actually holds in Panel rather than assuming a "higher" role covers it. See Roles and permissions.
from barbara import BarbaraPermissionError
try:
client.nodes.reboot("<node-id>")
except BarbaraPermissionError:
print("The authenticated user's role does not allow this action")
Error handling
All API errors raise a subclass of BarbaraApiError, so you can catch broadly or narrow down to a specific failure mode:
from barbara import BarbaraApiError, BarbaraAuthError, BarbaraNotFoundError, BarbaraPermissionError
try:
client.nodes.resolve("unknown-node")
except BarbaraNotFoundError:
...
except BarbaraPermissionError:
...
except BarbaraApiError as e:
print(e.status, e.body)
A 404 on an optional sub-resource (for example, a node with no secrets configured yet) is often expected, not an error condition worth aborting a script over. Decide, call by call, whether BarbaraNotFoundError should stop your program or just be logged and skipped.
exc.bodybody on BarbaraApiError is the server's raw error response, never the request you sent, so it can't contain your credentials. It can still contain data about the resource you were operating on (for example, a validation error echoing back a field value). Avoid logging exc.body indiscriminately in a context you don't control — log str(exc)/exc.status instead unless you specifically need the body for debugging.
Architecture
- One client, one resource tree.
BarbaraClientandAsyncBarbaraClientexpose the same resources (.nodes,.clusters,.applications, ...) with identical method signatures. - Automatic token refresh. A request that receives a
401is retried once with a freshly fetched token. - Typed models. Response entities are plain dataclasses. Every entity keeps the original API payload in
.raw, so a field the SDK hasn't typed yet is still reachable. - Typed exceptions.
BarbaraNotFoundError,BarbaraAuthError, andBarbaraPermissionErrorsubclassBarbaraApiErrorso callers can handle specific failure modes without inspecting status codes by hand. - An escape hatch for everything else. Every resource method calls
client.request(method, path, ...)internally: the same authenticated, token-refreshing request method is available directly for any endpoint not yet wrapped by a typed resource. When buildingpathyourself, percent-encode any value that isn't a fixed literal (urllib.parse.quote(value, safe="")); every typed resource method does this for its own id parameters, butclient.request(...)takespathas-is.
# Calling an endpoint the SDK doesn't wrap yet, using the same authenticated
# request method every typed resource is built on:
response = client.request("GET", "/v1/some/future/endpoint")
Resource index
| Resource | Description |
|---|---|
client.nodes | Node lifecycle, global secrets, global configuration, docker credentials, and actions (reboot, provision, ...) |
client.nodes.network | Physical interfaces, VLANs, hostname, VPN, Proxy, Standalone Mode, IPTables, and NTP servers |
client.nodes.workloads | Applications deployed on a node |
client.clusters | Cluster lifecycle, global secrets, global configuration, and docker credentials |
client.clusters.workloads | Applications deployed across a cluster |
client.applications | Application catalog and versions |
client.models | Model catalog and versions |
client.configs | Reusable Config Repository documents |
client.groups | Node groups |
client.users | Company users (read-only) |
client.alerts | Alerts and alert events |
Use the sidebar for the full generated reference — every method, parameter, and return type — plus the client, config & auth, exceptions, and models pages. For the underlying HTTP API itself, see the API Specification.
Roadmap
The following areas of the Barbara API are not yet covered by this SDK:
- VPN peer management
License
Distributed under the MIT License. See LICENSE for details.