Python — HTTP Driver
arcadedb-driver is ArcadeDB’s Python HTTP client, generated from the server’s OpenAPI contract.
It wraps queries, commands and transactions in a small facade over
httpx, plus a matching asyncio facade for async applications.
Connect and query
import httpx
from arcadedb_driver import ArcadeDBServer, basic_auth
with ArcadeDBServer(
base_url="http://localhost:2480",
auth=basic_auth("root", "playwithdata"),
timeout=httpx.Timeout(5.0),
) as srv:
db = srv.db("mydb")
envelope = db.query(language="sql", command="SELECT FROM Person WHERE age > ?", params={"0": 21})
print(envelope.result, envelope.returned, envelope.truncated)
[{'@rid': '#1:0', '@type': 'Person', '@cat': 'v', 'name': 'Alice', 'age': 30}, {'@rid': '#1:1', '@type': 'Person', '@cat': 'v', 'name': 'Bob', 'age': 25}] 2 False
|
Positional parameters are zero-indexed — Positional parameters are keyed by their zero-based index as a string. For the single placeholder
above, the key is
The driver’s own README shows |
basic_auth("root", "playwithdata") builds the Authorization header for HTTP basic auth;
bearer_auth("AU-…") substitutes directly for it wherever you already hold a session token
from srv.login() (see Sessions and API tokens), or any other bearer
credential.
An EXPLAIN or PROFILE statement answers with its execution plan instead of rows: the
envelope’s explain holds it as indented text and explain_plan as the same plan in structured
form, while result is empty and returned is 0. An empty result there is not "no rows
matched".
Sync and async
Every facade method has an async twin. AsyncArcadeDBServer mirrors ArcadeDBServer method for
method, just await-ed:
import asyncio
from arcadedb_driver import AsyncArcadeDBServer, basic_auth
async def main() -> None:
async with AsyncArcadeDBServer(
base_url="http://localhost:2480", auth=basic_auth("root", "playwithdata")
) as srv:
envelope = await srv.db("mydb").query(language="sql", command="SELECT FROM Person")
print(envelope.result)
asyncio.run(main())
[{'@rid': '#1:0', '@type': 'Person', '@cat': 'v', 'name': 'Alice', 'age': 30}, {'@rid': '#1:1', '@type': 'Person', '@cat': 'v', 'name': 'Bob', 'age': 25}]
Beyond query, command and transaction, a database handle also exposes db.ts, db.grafana
and db.promql for time-series ingestion and querying (including db.promql.label_values), and
db.vector for vector, hybrid and full-text search — see the package README linked below for
those. Everything in the sections below has the same async twin.
Close your client
Both ArcadeDBServer and AsyncArcadeDBServer are context managers, because each owns an
httpx client with its own connection pool that must be released. Prefer with /
async with. When you cannot use one — a long-lived client held across requests, say — call
close() on the sync class or await aclose() on the async one yourself.
|
Timeouts are off by default. Omitting |
Transactions
with db.transaction() as tx:
tx.command(language="sql", command="INSERT INTO Account SET balance = 100")
total = tx.query(language="sql", command="SELECT sum(balance) AS total FROM Account").result[0]["total"]
print("total:", total)
total: 100
db.transaction() returns a context manager whose enter gives you a second database
handle — here, tx — that carries the transaction’s session id. The contract:
-
If the block exits cleanly, the transaction commits.
-
If the block raises, the transaction rolls back and the block’s own exception propagates. If the rollback itself also fails, that failure is attached as the exception’s
causerather than replacing it — the block’s error is what you asked about. -
If the commit itself fails, a best-effort rollback is issued first, so the server-side session is not left open for
arcadedb.server.httpSessionExpireTimeoutto reap it.
The trap: calls made through the outer db handle while a transaction is open do not join that
transaction — they auto-commit individually, outside it. Only calls made through the handle
transaction() gives you (tx above) take part.
Two error models
Facade methods raise ArcadeDBError on any non-2xx response, carrying status, error,
detail and request_id. This holds for query, command and transaction, and equally for
the ts, grafana and promql namespaces — every one of them raises the same
ArcadeDBError rather than letting an httpx exception or anything else through:
from arcadedb_driver import ArcadeDBError
try:
db.query(language="sql", command="SELECT FROM NoSuchType")
except ArcadeDBError as err:
print(err.status, err.error, err.detail, err.request_id)
500 Internal error Type with name 'NoSuchType' was not found 7f43ebd2c31c5c12-24
request_id is the server’s X-Request-Id for that call and is different on every request; use
it to correlate a failure with the server log. Two more fields come from the response’s headers:
retry_after, the seconds the server asked you to wait before retrying (on a 503, or a 409 for an
identical request still in flight), and commit_index, the Raft index to send back as
X-ArcadeDB-Read-After on a later read. Both are None when the server sent none.
|
A 409 has two meanings. It is either "an identical request is still in flight", which is
retryable after |
A rejected time-series write raises TimeSeriesWriteError, a subclass of ArcadeDBError that
says how many samples were written and dropped and why. A write is not atomic: the written
samples landed, so re-sending the whole batch writes them twice.
srv.raw takes the opposite approach: it never raises. It is not a second facade with its own
query and command methods — it is the generated openapi-python-client Client object, the
value you hand to the generated endpoint functions as their client= argument. Each of those
functions returns a response whose status_code and parsed body you inspect yourself, the same
shape you get for a successful call. Use raw when you would rather branch on a status code than
catch an exception:
from arcadedb_driver import ArcadeDBServer, basic_auth
from arcadedb_driver._generated.api.database import list_databases
with ArcadeDBServer(base_url="http://localhost:2480", auth=basic_auth("root", "playwithdata")) as srv:
response = list_databases.sync_detailed(client=srv.raw)
print(response.status_code, response.parsed.result)
200 ['mydb']
Labelling work with a query tag
query, command, query_stream, command_stream, batch_load, batch_load_stream and
transaction() take an optional query_tag, sent as the X-ArcadeDB-Query-Tag header, so you can
list and terminate the work by it:
db.query(language="sql", command="SELECT FROM Event", query_tag="nightly-report")
with db.transaction(query_tag="etl-run-42") as tx:
tx.command(language="sql", command="INSERT INTO Event SET n = 1") # labelled etl-run-42
A transaction’s tag is sent on the begin request only; the server applies it to every statement in the transaction that carries no tag of its own.
Duplex insert session
db.insert_session() opens an insert session on the server’s /ws endpoint. Unlike batch_load,
whose commit policy is fixed before the load starts, a session lets you read each chunk’s
acknowledgement before deciding what to send next, and whether to commit at all:
with db.insert_session(target_type="Person") as session:
for batch in batches:
ack = session.send_chunk(batch) # nothing is durable yet
if ack["failed"]:
session.rollback()
break
else:
session.commit()
Leaving the with block with the session still open rolls it back.
Administration
create_database, drop_database, create_user, update_user, drop_user, list_users,
save_group, delete_group and list_groups are on the server handle, and server_command runs
any other server command. All of them are root-only:
srv.create_database("tenant_a")
srv.create_user("alice", "a-long-password", databases={"tenant_a": ["admin"]})
users = srv.list_users()
srv.drop_user("alice")
srv.drop_database("tenant_a")
A user or database that already exists is a 409 on ArcadeDB 26.10.2 and later. Older servers answer 403 for a duplicate user and 400 for a duplicate database.
Sessions and API tokens
# Log in as each user once, then keep a client that holds only that user's token.
login = srv.login_as("alice", "a-long-password") # or srv.login()
with srv.with_token(login.token) as alice: # srv is unchanged
alice.list_databases() # only what alice may see
alice.logout()
# A long-lived machine credential (root only).
created = srv.create_api_token("ci", database="tenant_a")
store(created.token) # the only time the plaintext exists
srv.delete_api_token(created.token_hash) # by hash, never by token
list_sessions and list_api_tokens list what exists. Each user holds at most 100 sessions, and a
further login silently evicts the oldest, so log in once per user and reuse the token rather than
logging in per request. Wrong Basic credentials are answered 403, not 401; 401 is what an invalid,
expired or logged-out token gets.
Running work
from datetime import timedelta
from arcadedb_driver import is_query_terminated
running = srv.list_queries(tag="nightly-report") # or database=...
results = srv.terminate_queries_by_tag("nightly-report", wait=timedelta(seconds=3))
srv.terminate_query(running[0].id)
open_txs = srv.list_transactions()
srv.terminate_transaction(open_txs[0].id)
progress = srv.db("mydb").progress() # long-running maintenance operations
Any authenticated user may call these: root sees and stops every statement, another user only
their own. Each terminate answer carries a status, and which one you get decides whether you have
anything to undo:
status |
Meaning |
|---|---|
|
It stopped, and what it wrote was rolled back. |
|
It finished before it noticed, so what it did stands. |
|
It had not stopped when the wait ended; the request’s own outcome is the answer. |
|
No such statement on this server, or another user’s. |
|
Choosing a protocol from Python
ArcadeDB can be queried from Python over three protocols. For small results (a few rows per call) they perform the same; for large results the choice matters.
| HTTP API | Bolt (neo4j driver) |
gRPC (grpcio) |
|
|---|---|---|---|
Install |
|
|
|
Server setup |
enabled by default (port 2480) |
add the |
add the |
Authentication |
HTTP Basic |
user and password |
call metadata |
Transactions |
yes ( |
yes |
yes |
Streams a large result |
no, one JSON body |
yes, in |
yes, in |
633K vertex ids |
0.54 s |
2.59 s |
0.30 s |
633K PageRank scores |
0.98 s |
2.81 s |
0.79 s |
2M edge rows |
1.61 s |
8.55 s |
1.34 s |
Few rows per call (a count) |
fast |
fast |
not measured |
Times are for the whole result consumed in Python (warm median of 3). Bolt is shown with the whole result in one PULL (fetch_size=-1); with the driver’s default of 1,000 rows per PULL the 2M-row call took about 15 s. gRPC is shown with 100,000 rows per batch; 10,000 is nearly as fast, while 1,000 is slower than HTTP on wider rows (1.8 s and 2.9 s for the last two calls).
Recommendation
-
Large results from Python: use gRPC streaming with a batch size of 10,000 or more, or the HTTP API when you want the simplest setup.
-
Use Bolt when you need Neo4j tooling and driver compatibility. From Python, set a large
fetch_sizefor big results. The Python driver is the limit here, not the server: the official Java driver reads the same 2M rows over Bolt in 1.5 s. -
Small results, or many small calls: any of the three.
Measurement notes
-
Server: ArcadeDB 26.10.2-SNAPSHOT image, with the Bolt buffering fix (#9165).
-
Machine: Docker Desktop on macOS, on battery power, so only the ratios are meaningful. The benchmark script is
scripts/grpc_transfer_bench.py, and the details are inArcadeDB-release-progress.md. -
Rows are narrow (one or two numeric columns). Wide rows, strings and whole vertices or edges were not measured.
-
Docker Desktop adds about 1.3 ms to each client-server round trip, which penalizes small batches. A native Linux server will show less of that penalty.
Next steps
-
Native Drivers — the result envelope and why
truncatedmatters, and how this driver compares to the others. -
Python — gRPC Driver — the gRPC counterpart for throughput-sensitive server-to-server work.
-
The package README for the full method list and every failure mode of the methods above, including
db.ts,db.grafana,db.promqlanddb.vector.