Barbara API SDK for Python¶
Official Python SDK for the Barbara Edge AI platform API.
Typed, synchronous and asynchronous clients for managing nodes, clusters, applications, models, and related resources.
Source{ .md-button } PyPI{ .md-button }
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. See also the Barbara Academy platform documentation for the concepts as they appear in the Panel UI.
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, Barbara Core version) and is the unit onto which applications and models get deployed. See Node lifecycle in Academy.
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 secrets and configuration, independent of any single node's. See High availability in Academy.
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 applications) and applications published in Barbara's marketplace (Marketplace applications); the SDK reflects this with separate create_docker_workload / create_marketplace_workload methods. See App Library in Academy.
Workload. An application actually running — either on one specific node, or (the cluster-level equivalent) across every node in a cluster. Barbara's product docs use this one term for both scopes; the SDK mirrors that with client.nodes.workloads and client.clusters.workloads. Both expose 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 in Academy.
Group. A named collection of nodes used for organizing and filtering, independent of clusters (a group does not imply a shared deployment).
Why this matters for the SDK
Every resource in BarbaraClient corresponds to one of these concepts, and the nesting mirrors reality: node-scoped workloads live under nodes (client.nodes.workloads), and cluster-scoped workloads live under clusters (client.clusters.workloads) — same vocabulary, same operations, different scope.
Requirements¶
- Python 3.9 or later
Installation¶
pip install barbara-api-sdk
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. You need four values, referred to throughout Barbara's documentation as the Barbara API Credentials:
| Credential | Description |
|---|---|
BBR_API_USERNAME |
Your Barbara Panel username |
BBR_API_PASSWORD |
Your Barbara Panel password |
BBR_API_CLIENT_ID |
OAuth2 client ID, provided by Barbara |
BBR_API_CLIENT_SECRET |
OAuth2 client secret, provided by Barbara |
Where these come from
BBR_API_USERNAME and BBR_API_PASSWORD are the same credentials used to log into the Barbara Panel. Create a free account at onboarding.barbara.tech if you don't have one. BBR_API_CLIENT_ID and BBR_API_CLIENT_SECRET are issued separately by Barbara support; they identify the OAuth2 client, not a personal account.
The simplest way to authenticate is to export all four as environment variables and let the client pick them up:
export BBR_API_USERNAME="..."
export BBR_API_PASSWORD="..."
export BBR_API_CLIENT_ID="..."
export BBR_API_CLIENT_SECRET="..."
from barbara import BarbaraClient
client = BarbaraClient.from_env()
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 |
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)
Token lifetime is short
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.
Quick start¶
from barbara import BarbaraClient
with BarbaraClient.from_env() as client:
for node in client.nodes.list():
print(node.node_name, node.status)
node = client.nodes.resolve("my-node-01")
BarbaraClient is a context manager: using with ensures the underlying HTTP connection pool is closed when you're done. It is not required (a client left open simply keeps the pool alive until garbage collected), but it is good practice in scripts.
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.
Identifying nodes: internal ID vs. Barbara ID¶
Every node is addressed internally by an _id (a 24-character hexadecimal string), but nobody remembers those, and the Barbara Panel shows nodes by their human-assigned name (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.
Tip
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 in Academy 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¶
Barbara-managed secrets (Wi-Fi pre-shared keys, API tokens injected into a workload, etc.), shared by every workload on the node — distinct from a Marketplace app's own App Secrets and from Docker-native Swarm Secrets. Always base64-encoded by the underlying API. See Secrets in Academy.
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>")
Note
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¶
A free-form JSON document attached to a node, typically read by every workload on it at startup to adjust its behavior without rebuilding it. This corresponds to what the Panel calls Global Config: configuration scoped to the node rather than to a single workload (see Application configuration types for the full picture — a workload's own App Config is set through client.nodes.workloads, below).
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"}]
)
creds = client.nodes.list_docker_credentials("<node-id>")
print(creds[0].server, creds[0].user) # password is never returned by the API
Node identity: name, tags, location, safety actions¶
See General info in Academy for the same fields (name, location, tags) as they appear in the Panel.
client.nodes.update_name("<node-id>", "floor-2-sensor-01")
client.nodes.add_tag("<node-id>", "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 Panel's "Safety Actions": above trigger_threshold (a percentage, strictly between 0.1 and 99.9), the node can stop its apps/models and/or prune docker resources on its own, without a human watching a dashboard. At least one action must be enabled.
Note
get_location is returned as a raw dict rather than a typed object, since node location payloads vary in shape.
Warning
set_location currently returns a 500 Internal Server Error regardless of the payload sent. Use the Panel to update a node's location until this is resolved.
Barbara Core updates¶
Barbara Core is the single versioned package that bundles a node's OS and Node Manager together (e.g. Barbara Core 1.10.1.471) — Panel's own update modal shows one version number, not two separate firmwares. See Barbara Core updates in Academy.
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 in Academy.
Note
create_docker_volume doesn't return an id. Use list_docker_volumes to look one up before calling delete_docker_volume, since node volumes have no dedicated list endpoint of their own.
Telemetry¶
See Telemetry in Academy.
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"])
Tip
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 workloads¶
A workload is an application instance running on one specific node: a Docker app, a Marketplace app, or a Model. Deploying one means pointing at an application version and, for Marketplace/Model apps, describing which services and ports it exposes (its Compose Config). See Docker apps and Marketplace apps in Academy.
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>")
Warning
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).
Note
compose_config is a Marketplace/Model-only concept — it's the workload's Compose Config (ports/env/volumes), rewritten into its docker-compose.yml at deploy time. Docker workloads control their own compose file directly, so create_docker_workload/update_docker_workload take no compose_config argument.
A workload's own App Config (as opposed to the node-scoped Global Config above):
client.nodes.workloads.set_app_config("<node-id>", "<workload-id>", config={"threshold": 5})
config = client.nodes.workloads.get_app_config("<node-id>", "<workload-id>")
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, with one extra constraint: compose_config must exactly match the service template declared by the selected model application version. See Models in Academy.
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 in Academy.
clusters = client.clusters.list()
cluster = client.clusters.get("<cluster-id>")
cluster = client.clusters.resolve("floor-2-cluster") # look up by cluster name
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,
)
Note
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>")
Cluster-wide Docker volumes¶
Unlike node-level Docker volumes, these give a cluster's workloads Swarm-managed high availability — a workload that needs a volume to survive a node failing over must use one of these.
client.clusters.create_swarm_volume("<cluster-id>", "shared-cache")
client.clusters.delete_swarm_volume("<cluster-id>", "<volume-id>")
Docker-native objects declared in an app's own docker-compose.yml (Swarm Config/Swarm Secrets, distinct from the Barbara-managed Global Config/Global Secrets above) are cleaned up the same way: delete_all_swarm_configs, delete_swarm_config, delete_all_swarm_secrets, delete_swarm_secret.
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" for both scopes — there's no separate "stack" concept in Panel. See Add applications in Academy.
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 create_model_workload on client.nodes.workloads, 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 in Academy.
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"]
)
Tip
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; there is no need to open the file or set headers yourself.
Deploying a Marketplace/Model app from its published template¶
A Marketplace or Model workload's compose_config/app_secrets must echo back every service declared in the app version's template, or the API rejects the request — default_services builds that starting point from the template's own defaults, ready to hand to create_marketplace_workload/create_model_workload as-is or after overriding individual ports/volumes/env:
app_version = client.applications.get_version("<application-id>", "<app-version-id>")
compose_config, app_secrets = client.applications.default_services(app_version)
client.nodes.workloads.create_marketplace_workload(
"<node-id>",
app_version_id="<app-version-id>",
application_id="<application-id>",
name="my-workload",
compose_config=compose_config,
app_secrets=app_secrets,
)
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 in Academy.
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"])
Tip
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 — typed application or global — that a workload's own App Config or a node's/cluster's own Global Config can reference by id (config_id), rather than being one itself: useful when the same document needs to be attached to many nodes, clusters, or workloads without duplicating it in every call. See Application configuration types in Academy.
config = client.configs.create(
name="sensor-thresholds",
description="Per-node alert thresholds",
config={"temperature_max": 80},
)
print(config.config_type) # "application" or "global"
client.nodes.set_global_config("<node-id>", config_id=config.id)
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 in Academy for group management in the 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 in Academy.
users = client.users.list()
page = client.users.paginate(offset=0, size=50)
print(page.items[0].role) # UserRole.ADMINISTRATOR / SUPERVISOR / EDITOR / VIEWER, or None
Note
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 in Academy.
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 map to the labels Panel shows in the Organization view — Viewer, Editor, Supervisor, Administrator, respectively — but they 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 the Panel rather than assuming a "higher" role covers it. See Roles and permissions in Academy.
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)
Note
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.
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. - Product naming throughout. Classes, methods, and fields follow Barbara's own product terminology (Node, Workload, App Config vs. Global Config, Barbara Core, ...) rather than internal API/wire jargon — see the Barbara Academy links throughout this page for the concepts behind each resource.
- An escape hatch for everything else. Every resource method calls
client.request(method, path, ...)internally, and 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")
API reference¶
| Resource | Description |
|---|---|
client.nodes |
Node lifecycle, global secrets, global config, docker credentials, Barbara Core updates, and actions (reboot, provision, ...) |
client.nodes.workloads |
Docker/Marketplace/Model applications deployed on a node |
client.clusters |
Cluster lifecycle, global secrets, global config, docker credentials, and Swarm-managed volumes |
client.clusters.workloads |
Docker/Marketplace/Model applications deployed across a cluster |
client.applications |
Application catalog and versions |
client.models |
Model catalog and versions |
client.configs |
Config Repository — reusable, named configuration documents |
client.groups |
Node groups |
client.users |
Company users (read-only) |
client.alerts |
Alerts and alert events |
Use the navigation sidebar for the full generated reference: every method, parameter, and return type. For the underlying HTTP API itself, see the Barbara API documentation.
Examples¶
The examples/ directory has complete, runnable scripts for common use cases:
| Script | Description |
|---|---|
hello_world.py |
The first script to run: confirm your credentials work and list your nodes |
node_info.py |
Read a node's configuration and latest telemetry |
check_barbara_core_updates.py |
Check nodes for an outdated Barbara Core, optionally update them |
clone_node.py |
Clone a node's workloads, config, and docker volumes onto another node |
Each script also demonstrates calling an endpoint through client.request(...) directly (the same low-level method every typed resource is built on) for functionality this SDK doesn't wrap yet.
See the examples README for what each one covers, how to configure and run it, and ideas for extending it.
Roadmap¶
The following areas of the Barbara API are not yet covered by this SDK:
- Node network configuration (interfaces, NTP, proxy, VPN, iptables)
- Node standalone mode and VPN peer management
- App Secrets (Marketplace-only, per-app secrets, distinct from a node's Global Secrets)
License¶
Distributed under the MIT License. See LICENSE for details.