arcadedb-driver
A Python HTTP client for ArcadeDB, generated from ArcadeDB's OpenAPI contract, with a hand-written facade on top for the data plane, transactions, and the pieces of the contract the generator cannot model.
Published on PyPI as arcadedb-driver, with
attestations: every release is built and published by publish-python.yml (dispatched by
release.yml) from a clean checkout of this repository through PyPI's trusted publishing, with no
long-lived token anywhere in the chain.
Releases are cut for every package at once: bump all versions with
scripts/set-release-version.sh <version> in a reviewed PR, merge it, dispatch release.yml on
main, then review and publish the draft GitHub release it creates. Publishing the draft is what
starts the registry publish.
Requirements
- Python
>=3.10. - An ArcadeDB server at or near the version in the compatibility table below.
Installation
pip install arcadedb-driver
or, in a uv project:
uv add arcadedb-driver
Quick start
from arcadedb_driver import ArcadeDBServer, basic_auth
with ArcadeDBServer(base_url="http://localhost:2480", auth=basic_auth("root", "playwithdata")) as srv:
db = srv.db("mydb")
envelope = db.query(language="sql", command="SELECT FROM Person WHERE age > ?", params={"0": 21})
print(envelope.result)
A bearer token (for example, a session token from srv.login(); see "Sessions and API tokens")
works the same way:
from arcadedb_driver import ArcadeDBServer, bearer_auth
with ArcadeDBServer(base_url="http://localhost:2480", auth=bearer_auth("AU-...")) as srv:
...
The async facade mirrors the sync one method-for-method:
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:
db = srv.db("mydb")
envelope = await db.query(language="sql", command="SELECT FROM Person WHERE age > ?", params={"0": 21})
print(envelope.result)
asyncio.run(main())
language is a QueryLanguage: "sql", "sqlscript" (several ;-separated SQL statements run
as one script, usually through command), "cypher", "gremlin", "graphql" or "mongo".
The result envelope, and why truncated matters
query and command do not return bare rows. They return the whole response envelope:
@dataclass(frozen=True, slots=True)
class QueryEnvelope:
result: list[dict[str, Any]]
limit: int
returned: int
truncated: bool
explain: str | None = None
explain_plan: dict[str, Any] | None = None
truncated is True when the server's serializer hit its row cap while a query still had more
rows to write - result is then a partial answer, not a short-but-complete one. A caller that
reads result and ignores truncated can silently work off a partial answer, because a truncated
list and a complete one are indistinguishable by shape alone. Always check truncated before
treating result as the whole answer, and re-query with a narrower filter or a higher limit
when it is True - though raising limit is not always the fix: a result whose true size exceeds
the server's hard ceiling (arcadedb.server.httpQueryMaxResultRows) is refused outright with 413
rather than truncated, so once you are past that ceiling a narrower filter is the only way forward.
An EXPLAIN or PROFILE statement - or any statement run with profileExecution - answers with its
execution plan instead of rows: explain holds it as indented text, one line per step, and
explain_plan as the same plan in structured form. result is then empty and returned is 0,
because the plan is the answer and is not repeated as a row, so an empty result there does not
mean "no rows matched". Both fields are None on every other answer.
truncated is False used to be a client-side default rather than a server guarantee: until
26.10.1, QueryResponse declared no required fields, so the envelope's limit,
returned and truncated were all synthesised when the response omitted them. That caveat is
retired. The contract now marks all three required, the server sends all three on every
query and command, and a response missing one is a contract violation that surfaces as a
KeyError out of the generated model rather than as a quietly invented False. result stayed
optional and still defaults to [].
result also became a union in the same release: an array of rows under the default record
serializer, and a single {vertices, edges} object - plus records under studio - under the
two graph serializers. QueryEnvelope.result is a list of rows and cannot carry the second
shape, so query/command raise ArcadeDBError if it ever arrives. It cannot today: this client
sends no serializer field, so the server always picks record.
Streaming a query or command: query_stream/command_stream
query and command buffer the whole result server-side before answering. query_stream and
command_stream are a separate pair of methods for the same two endpoints, requesting
application/x-ndjson instead of a buffered JSON body and returning a generator a caller iterates
directly (the async facade's twins are async for-able instead):
for event in db.query_stream(language="sql", command="SELECT FROM Person"):
if not isinstance(event.record, Unset):
print(event.record.to_dict())
if not isinstance(event.stats, Unset):
print(f"returned {event.stats.returned}, truncated: {event.stats.truncated}")
They yield events, not rows. Each NdJsonQueryEvent carries exactly one of record (one
result row, shaped like an element of query's result list), stats, or error - never more
than one, and a caller who only ever reads event.record will silently skip both of the others.
stats is a trailer, always the last event of a complete stream, carrying the same
limit/returned/truncated QueryEnvelope reports at the top level for the buffered path.
Ignoring it loses exactly what ignoring .truncated loses above: the only way to tell a complete
answer from one the server's row cap cut short. A caller who iterates record events and stops
there has no way to know whether they saw everything.
error is a failure the server can only report after the 200 status line was already sent -
unlike the buffered path, where a failure still in progress when the response starts can be
reported as a non-2xx status, a streamed response has committed to 200 before the first row is
known to exist, and that status line cannot be taken back once the stream has started. That is why
the contract puts this failure in band, as an event, rather than as an HTTP status. This client
raises ArcadeDBError for it - exactly as query/command raise ArcadeDBError for a non-2xx
response - so both paths fail the same way; any event already yielded before the error stays
delivered to the caller.
The raised error is filled from the event field by field, so it reads like the error the buffered call would have raised for the same failure:
ArcadeDBError |
from the in-band error event |
|---|---|
status |
status - the status the buffered encoding would have answered with (503 for a retryable conflict, 409 for a duplicated key, 413 for the row cap, ...); 200 only when the event carries none (anything but a positive integer counts as none; a batch stream falls back to 500 by the same rule) |
error |
error, the classified label; message when the event has no error |
exception, detail, exception_args |
the same-named fields |
request_id |
the response's X-Request-Id header, else the event's requestId |
retry_after |
retryAfter - a header cannot be added once the stream has started, so the back-off travels in band |
commit_index |
always None: the event carries none |
Breaking change in 0.3.0: earlier releases raised this error with status 200 whatever the
event said, a rule chosen when the event carried no status at all. The contract now tells clients
to key retries on the in-band status, the batch stream below already used its own, and a
retry_after beside a 200 would be unreadable. Code that recognised a mid-stream failure by
err.status == 200 must now look at the real status instead.
query and command themselves are unchanged: they still return QueryEnvelope and still send no
Accept header. Streaming is two additional methods, not a mode either existing one can be put
into.
command_stream only ever succeeds for a read-only statement. A mutating one - UPDATE,
INSERT, DDL, UPDATE ... RETURN AFTER included - is refused before it produces a single row,
because a streamed response starts sending rows to the caller before the surrounding transaction
commits, and that commit can still roll back; the server will not let you observe rows from a
write that might never actually happen. Use the buffered command for a mutating statement - it
is unaffected by any of this. As with the ef_search/result-limit bounds above, this rule is
enforced server-side and this client does not pre-empt it by inspecting the statement first, so
the rejection surfaces as an ArcadeDBError raised from the server's response (HTTP 400), not a
local exception before the request is even sent.
You are free to stop early, and that is most of the point of a streaming API. breaking out of
the loop, or calling the generator's .close()/.aclose(), closes the response: httpx's
.stream() context manager lives inside the generator body, so the GeneratorExit that
abandonment throws into the suspended yield unwinds through it and releases the connection.
@arcadedb/driver gives the same guarantee through its own idiom, an explicit reader.cancel().
Line splitting is hand-rolled here rather than delegated to httpx's iter_lines(), and that is
load-bearing: iter_lines() follows str.splitlines(), which breaks on U+0085, U+2028 and U+2029
as well as \n - and all three are legal raw characters inside a JSON string, which ArcadeDB does
not escape. A record carrying one would be cut in half and surface as a bare json.JSONDecodeError
partway through an otherwise healthy stream. This client splits on \n alone, exactly as
@arcadedb/driver does, so such a record arrives intact.
Both the sync and async versions reach the server through the generated Client's own pooled
httpx.Client/httpx.AsyncClient, via its .stream() context manager, rather than a hand-rolled
request or a httpx.Client of their own; that is what lets them reuse the same base URL, auth
headers, and timeout every other call on this client already goes through. Anyone adding another
streaming endpoint to this package should do the same rather than standing up a new httpx.Client.
Batch loading: batch_load/batch_load_stream
POST /api/v1/batch/{database} bulk-loads vertices and edges from one ndjson payload. The
contract schematizes a single line of that payload - BatchLine, a union over BatchVertexLine
and BatchEdgeLine - but not the body, which is a newline-delimited sequence of them; text/csv
has no schema at all. So there is nothing openapi-python-client can generate a call from, and -
like db.ts.write above - both batch_load and batch_load_stream are hand-written, issuing
their request through the same generated Client's own pooled httpx.Client/httpx.AsyncClient
rather than a httpx.Client of their own:
from arcadedb_driver import VertexRow, EdgeRow
vertices: list[VertexRow] = [{"type": "Person", "id": "p1", "properties": {"name": "Alice"}}]
edges: list[EdgeRow] = [{"type": "Knows", "from_": "p1", "to": "#1:7"}]
summary = db.batch_load(vertices=vertices, edges=edges, options={"commitEvery": 5000})
EdgeRow's source endpoint is spelled from_, not from, because from is a Python keyword and
cannot be a TypedDict key. Properties live in their own properties dict rather than being
merged into the row: the server accepts a top-level key literally named properties and stores it
as an ordinary property, silently, so keeping structure and data apart in the type is what makes
that mistake impossible to make by construction. Every vertex is always sent before any edge,
regardless of the order passed in - the server resolves an edge's from_/to only against ids
declared earlier in the SAME payload, and this client enforces that ordering unconditionally
rather than exposing a way to get it wrong.
options is BatchOptions, the 17 tuning parameters the contract accepts as query parameters,
named exactly as the contract names them and sent only when present: an option a caller does not
set is left off the URL entirely rather than sent as an empty value, so the server applies its own
default instead of parsing "".
_internal/batch_rows.py owns the line format the rows are serialized into. It was established
against a live server and reported upstream as
ArcadeData/arcadedb#7570 when no schema
described it; the contract now declares it as BatchLine and the two agree field for field. That
schema covers one line rather than the whole body, so it does not remove the need for this module -
but it does mean the format is now documented upstream rather than only here. VertexRow and
EdgeRow are consequently the only way in: a hand-built payload string is not a supported input
to either method.
A load is not atomic
This is the paragraph to read before using either method. The server commits every
options["commitEvery"] records, so a load that fails partway through leaves every chunk before
the failure durably committed - an ArcadeDBError raised from a failed batch_load still
corresponds to real, already-durable data, not to a load that undid itself. Because temporary ids
are not keys, retrying the whole payload duplicates every vertex that already committed rather
than resuming cleanly; there is no server-side idempotency to lean on, and this client does not
invent one. The counters that come back with a 400 (verticesCreated and edgesCreated, beside a
partialCommit flag) are records attempted before the failure - an upper bound on what is
durable, not a count of it, and the two do not even overshoot by the same rule: vertices are
committed as the load flushes, while edges are buffered and written when it ends. Treat a failed
load as something to inspect and reconcile against the database, never as something to re-send.
Temporary ids are request-scoped, which is the other half of the same design. An id means
something only to the payload that declared it, so a vertex loaded by an earlier call cannot be
referenced by its temp id from a later one - the server will not resolve it, and "#1:7" in the
example above is the alternative: a vertex already in the database is referenced by its RID,
#bucket:position, which is exactly what the summary's idMapping returns a temp id for.
bytesRead on the summary is how a caller verifies that a chunked upload arrived whole: compare
it against the bytes sent. A body that ends before its announced length is answered 408 with
the same partial-commit counters, never a 200 carrying a truncated count - so a short bytesRead
on a 200 means the server consumed less than you believe you sent, not that it quietly accepted a
half-load.
Streaming: what the summary carries, and what it does not
batch_load_stream streams the same load as application/x-ndjson instead: a progress event as
the load proceeds, then exactly one summary event carrying the same fields the buffered call
returns, plus idMappingStreamed: true - a field BatchResponse, the buffered shape it otherwise
matches, does not declare, because a buffered load never sends it. The field is not missing from
the contract entirely, though: the TypeScript client's generated schema declares it on
NdJsonBatchEvent["summary"], the streaming path's own type - this client just has no generated
model of either shape to declare it on (see above). idMappingStreamed is sent only on a streamed
load and is distinct from idMappingOmitted (omitted means too large to return; streamed
means already delivered, piecemeal, in the progress events that preceded the summary). Each
progress event's idMapping is only the fragment that chunk resolved and is never merged across
events - accumulating it here would reintroduce, client-side, the memory cost streaming a
million-vertex load exists to avoid. An in-band error event raises ArcadeDBError instead of
being yielded, exactly like query_stream/command_stream above: a failure after the stream has
started cannot be reported as an HTTP status, because the 200 status line is already on the wire,
so it travels in band instead and fails the caller the same way a failure before the stream started
does. Any event already yielded before the error stays delivered - a partial commit is durable, and
those progress counts are how a caller learns what may have landed.
The two encodings therefore disagree about the mapping, deliberately: batch_load's summary
carries idMapping, the whole temp-id-to-RID map in one dict, while the streamed summary event
carries idMappingStreamed: true and idMappingSize, with no map at all. A caller who
genuinely needs the whole mapping from a streamed load accumulates the fragments as they arrive -
checking the total against idMappingSize, since a mapping delivered in pieces can lose one to a
truncated response without any single piece looking wrong - or calls batch_load and accepts the
memory cost, which is a good trade right up until the map stops fitting.
A streamed error event's status is exact, not a fallback: an engine failure raised after the
stream had already started carries the status the standard error mapping gives it - 409 for a
duplicated key, 503 for a retryable conflict - so branching on status works the same on both
channels. The raised ArcadeDBError also carries exception and, when the failure has any,
exception_args, plus the event's retryAfter as retry_after and its commitIndex as
commit_index - the in-band equivalents of the Retry-After and X-ArcadeDB-Commit-Index
headers a failure before the stream started carries. commit_index is a valid read-after barrier
even on a failed load (see "Two error models" below). error and exception are two more fields
the server sends that no schema declares (the contract's streamed error object declares only
commitIndex, exceptionArgs, retryAfter and status), read off the raw event here and
reported upstream on the same issue as idMappingStreamed,
ArcadeData/arcadedb#7570.
This is batch_load, not bulk_insert
arcadedb-driver-grpc's bulk_insert is a different operation -
it inserts records into one target type and knows nothing about edges or temporary ids. The gRPC
counterpart of the endpoint documented here is GraphBatchLoad, which that package reaches
through raw and wraps nowhere.
text/csv is not exposed
The endpoint accepts application/jsonl, application/x-ndjson and text/csv. Both methods
always send application/x-ndjson - the contract makes application/jsonl identical in meaning,
so there is nothing to choose between them - and neither exposes CSV. Its dialect is as
undocumented as the ndjson line format, and a rows-in API has nowhere to put a header row. Convert
a CSV file into VertexRow/EdgeRow yourself, or post the bytes through srv.raw's pooled httpx
client.
A duplex insert session: insert_session
batch_load fixes its commit policy by query parameters before the load starts. An insert
session on the server's /ws WebSocket endpoint lifts that: you send a chunk, read its
acknowledgement, and only then decide what to send next - including whether to commit() or
rollback() at all. It is the counterpart of the Java client's RemoteInsertSession.
with db.insert_session(target_type="Person") as session:
for batch in batches:
ack = session.send_chunk(batch) # the server's batchAck, as a dict
if ack["failed"]:
session.rollback()
break
else:
session.commit() # the `committed` frame: outcome, summary
AsyncArcadeDBDatabase.insert_session() returns an AsyncInsertSession with the same surface,
awaited (async with ... as session:, await session.send_chunk(...)). Records are plain
dicts; a record's own "@class" overrides target_type, and an edge names its endpoints with
"@from" / "@to". Options: session_id, target_type, transaction_mode (per_stream -
the default -, per_batch, per_row, none), conflict_mode (error, update, ignore,
abort) with key_columns / update_columns_on_conflict, validate_only,
join_current_transaction, on_batch_ack (called with every ack before send_chunk returns
it) and timeout. session_id, transaction_mode, external_transaction_id,
last_chunk_seq and is_open are readable properties.
websockets is a runtime dependency of this package (BSD-3-Clause). The connection reuses the
server's base URL and auth headers (http becomes ws, https becomes wss) and honours
verify_ssl.
Things that look like bugs and are not:
- The session id is the server's unless you choose one, and a chosen one is shared. A
client-chosen
session_idlives in ONE server-wide namespace - not per user, not per database - so two unrelated clients that both pickbatch-1collide, and the secondstartis refused. Leave it out unless the session must be nameable. - A chunk the server refuses as a whole does not consume a sequence number.
chunkSeqstarts at 1 and is managed for you; it advances only after the acknowledgement. A chunk overarcadedb.server.wsMaxInsertChunkRows, or out of sequence, raisesInsertSessionErrorwithsession_closed=False: the session is still usable,last_chunk_seqdid not move, and a smaller chunk sent next takes the same number. A row the server cannot apply is not a refusal at all: it is counted in the ack'sfailed, described inerrors, and the rest of the chunk still goes in - so always readfailed. - A chunk whose transaction fails comes back as an ack, and is replayed under the same number.
Under
per_batch- and only there, since it is the one mode where each chunk commits in a transaction of its own - a failure of that transaction (a duplicate key the engine reports on commit, for one) undoes the whole chunk. The server answers a normalbatchAck-failed == received, anerrorsentry withrowIndex-1 - and does NOT advance its watermark.send_chunkraises nothing for it andon_batch_ackstill sees it, but the returned ack carrieswhole_chunk_failed: True(a key this client adds; snake_case because the server never sends it) andlast_chunk_seqdoes not move: the nextsend_chunkis the replay of that sequence number. Moving on without fixing and resending those rows loses them. - A chunk over
arcadedb.server.wsMaxInsertFrameSizebytes is not refused, it kills the connection. The server answers an oversized frame with a WebSocket close, not anerrorframe, so the session is gone (and rolled back server-side) and the error hassession_closed=Trueandframe=None. The issue text this client was written from lists the frame cap alongside the row cap as "refused, retry reuses the seq"; against a real server only the row cap and a sequence error behave that way. Size chunks by rows, below the byte cap. - Under
per_stream, a commit-time failure arrives oncommit(). Nothing commits per chunk, so a duplicate key the engine only checks at commit fails thecommit()call withInsertSessionError, and the session - every chunk it acknowledged - is gone. - Some
errorframes end the session, others do not.session_closedtells them apart. It isFalseonly for a refusal of that one frame (the row cap, a sequence that skips ahead, a malformed record or option). It isTruefor"Insert session expired"(idle sweep),"Security error"(the grant was revoked and the session rolled back, or the principal is no longer valid and the connection is closing),"Internal error"(state unknown, treated as gone) and an"Insert session error"saying the session isnot found or expiredoris closed; the session is then marked closed and further calls raise. An error title this client does not recognise isFalse, as in the Go and TypeScript clients: if the server has in fact dropped the session, the next frame is answerednot found or expired, which closes it. - An
errorframe can arrive at any time. The server's idle sweep rolls a session back and pushes an unsolicitederror; whichever call reads it raisesInsertSessionError(session_closed=True) instead of skipping it for the answer it was waiting for. The rows already acknowledged are gone with the session. - Every wait is bounded by
timeout(default 60 s). A timeout, a dropped connection or an out-of-step answer ends the session, because a late answer could no longer be matched to its frame - the server rolls it back when the connection closes. The synchronous client's send is not bounded bytimeout(the underlyingwebsocketssync client has none); the async one's is. Interrupting a call mid-exchange (KeyboardInterrupton the sync client, task cancellation on the async one) ends the session the same way. close()rolls back. Leaving thewithblock, or callingclose(), on a session that is still open sendsrollbackand closes the connection; it is idempotent, so it is safe aftercommit(). Failures while rolling back are swallowed - the connection closing rolls the session back anyway.- Joining a transaction.
join_current_transaction=True, called on the handledb.transaction()yields, writes into that HTTP transaction and impliestransaction_mode="none"; passing any other explicit mode with it raisesInsertSessionErrorrather than being silently overridden, as in the Go and TypeScript clients. The session'scommit()androllback()then both answeroutcome == "detached": neither decides anything, thewith db.transaction()block does - the rows are durable only when it exits cleanly, and a block that raises undoes them. Calling it on a handle with no open transaction raisesInsertSessionErrorimmediately, rather than quietly opening a server-managed session. per_streamis the only mode that can undo acknowledged chunks. Afterrollback()thecommittedframe'ssummary.partialCommitsays whether anything stayed.- Not thread-safe, like the database handle: one session per thread (or per task, with one call in flight at a time). The protocol allows pipelining frames; this client deliberately does not.
- Failures are
InsertSessionError, notArcadeDBError: there is no HTTP status to carry.frameholds the server'serrorframe when there was one,session_idthe session concerned.
Sync and async
ArcadeDBServer and AsyncArcadeDBServer expose the same methods; the async one awaits them.
Both are context managers - with for the sync client, async with for the async one - because
each owns an httpx client with its own connection pool that must be released. Use the context
manager form where you can; call close() (sync) or await aclose() (async) yourself otherwise.
srv = ArcadeDBServer(base_url="http://localhost:2480")
try:
...
finally:
srv.close()
Both constructors take a timeout: httpx.Timeout | None = None, and omitting it disables
timeouts entirely - it is not "use httpx's default" (5 seconds), it is no timeout at all, because
in httpx an explicit timeout=None means exactly that. This is deliberate: the generated Client
these facades wrap defaults its own timeout to None and forwards it the same way, and
@arcadedb/driver has no default timeout either, since fetch doesn't have one. Pass an
httpx.Timeout if you want requests bounded:
srv = ArcadeDBServer(base_url="http://localhost:2480", timeout=httpx.Timeout(5.0))
Transactions
with srv.db("mydb").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"]
or, async:
async with srv.db("mydb").transaction() as tx:
await tx.command(language="sql", command="INSERT INTO Account SET balance = 100")
transaction() returns a context manager whose __enter__ (__aenter__) begins a server-side
transaction and hands back a SECOND database handle carrying its session id. Every call made
through that tx handle - not through the outer db object used to open the transaction - takes
part in it; a call made through the outer handle while a transaction is open auto-commits on its
own, outside the transaction, exactly as if no transaction were open at all.
The commit/rollback contract has three clauses:
- The block exits cleanly: the transaction commits.
- The block raises: the transaction rolls back and the block's exception propagates. If that
rollback itself also fails, the rollback's error is attached as
__cause__on the block's exception rather than replacing it - the block's own error is what the caller asked about - and the attach is silently skipped if__cause__is already set or if attaching it fails outright. - The commit itself fails: a best-effort rollback is issued first (its own failure discarded) so
the server-side session is not left open until
arcadedb.server.httpSessionExpireTimeoutreaps it, and then the commit's error is re-raised.
A rollback cannot always undo the whole block. A statement inside it that publishes a commit while
it runs - an explicit BATCH n boundary, or a statement with its own COMMIT - makes part of the
transaction durable before the block ends, and the server flags that response with
arcadedb-session-partial-commit: true. This client does not surface the header (it is reachable
through .raw), so a caller that replays a transaction block on conflict must know, from the
statements it runs, whether the block can partially commit - and must not replay one that did.
Labelling statements: query_tag
query, command, query_stream, command_stream, batch_load, batch_load_stream and
transaction() (sync and async) take an optional query_tag, sent as the
X-ArcadeDB-Query-Tag request header:
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
tx.command(language="sql", command="DELETE FROM Staging", query_tag="etl-cleanup") # its own label
The label is opaque to the server: list queries shows it, and terminate queries tag <label>
stops every statement carrying it. srv.list_queries and srv.terminate_queries_by_tag issue those
two commands for you (see "Running work" below). On transaction() it is sent only on the begin request -
not on the statements, the commit or the rollback - and the server applies it to every statement of
the transaction that carries no label of its own; a statement that passes its own query_tag
overrides it. On batch_load/batch_load_stream it is a header like everywhere else, never a
query parameter beside options. No header is sent when the tag is None or empty. The client
does not check the tag's length or characters: the contract caps it at 256 printable characters,
but that is the server's rule, and a second copy of it here would only drift from the first. Do not
count on a refusal either - a 26.11.1-SNAPSHOT server was observed accepting a 257-character tag.
Vector, hybrid and full-text search: db.vector
nearest = db.vector.search(index_name="myIndex", query_vector=[0.1, 0.2, 0.3], k=5)
fused = db.vector.hybrid(
vector_index_name="myIndex",
query_vector=[0.1, 0.2, 0.3],
fulltext_index_name="myTextIndex",
fulltext_query="cat",
)
matches = db.vector.fulltext(query_text="cat")
hybrid's fusion_strategy takes a HybridSearchRequestFusionStrategy - RRF, DBSF or
LINEAR - not a bare string. It was str | Unset until 26.10.1 turned the contract's
free-form field into an enum:
from arcadedb_driver._generated.models.hybrid_search_request_fusion_strategy import (
HybridSearchRequestFusionStrategy,
)
fused = db.vector.hybrid(
vector_index_name="myIndex",
query_vector=[0.1, 0.2, 0.3],
fusion_strategy=HybridSearchRequestFusionStrategy.RRF,
)
The enum is a str subclass, so an existing call passing "RRF" still works on the wire and only
the typechecker complains - but a call passing "rrf" was always wrong and the server always
rejected it. Reading the enum off _generated mirrors how expand and weights already work on
this method; it is not re-exported at the top level, because a name re-exported from _generated
turns every contract bump into a breaking change to this package's public API.
Full-text responses carry similarity as an enum for the same reason, BM25 or CLASSIC.
search runs a kNN query over a dense LSM_VECTOR or sparse LSM_SPARSE_VECTOR index; hybrid
fuses a vector leg with an optional full-text leg and an optional graph-expansion leg into one
ranked list; fulltext runs a Lucene-syntax query over a FULL_TEXT index. Each returns the whole
generated response model - VectorSearchResponse, HybridSearchResponse, FullTextSearchResponse
- never unwrapped to bare rows the way
QueryEnvelopeunwrapsquery/command.resultssits alongsidecount,truncated(search and hybrid only, see below),scoring, and the rest, all still reachable on the objectdb.vector.search(...)hands back.
That matters most for truncated. search and hybrid both inspect a bounded candidate window
before ranking, and truncated is True when that window was filled - meaning more matches may
exist beyond what results shows, the same hazard the result envelope's truncated documents
above for query. A caller who reads .results off a vector search and ignores .truncated works
off a partial answer without being told; raise k and search again if you need to see further.
fulltext's response, FullTextSearchResponse, has no truncated attribute at all - not
False, absent (hasattr(resp, "truncated") is False). That is the contract's shape, not a
field the server forgot to send: full-text search has no candidate-window concept to overflow the
way a vector search does, so there is nothing for a truncated flag to report either way.
ef_search (the dense-index search beam width) and each method's result-limit parameter (k for
search/hybrid, limit for fulltext) are bounded, but the bound is enforced server-side.
This client sends whatever value it is given without checking it first, so a value outside the
allowed range surfaces as an ArcadeDBError raised from the server's response, not as a local
exception before the request is even sent.
Reading a hit
Two shapes stand between you and a field on a hit, both consequences of returning the generated
response whole. VectorSearchResponse carries no required list in the contract, so every one of
its fields defaults to UNSET and results is typed list[VectorSearchResponseResultsItem] | Unset - a type checker will not let you iterate it unnarrowed. And a hit's properties is itself
a generated attrs model, VectorSearchResponseResultsItemProperties, not a dict: the record's
own fields live in its additional_properties mapping, so hit.properties.name raises
AttributeError rather than returning a value.
Neither is something the facade can strip on your behalf. Flattening results into list[dict]
the way QueryEnvelope flattens query/command rows would leave truncated, count and
scoring describing rows that no longer travel with them, and flattening only the rows would
manufacture a third shape - typed top-level fields above untyped rows - worse than either shape
this client already has (facade/vector.py's module docstring argues this at length). So the
unwrapping is two lines at the call site:
resp = db.vector.search(index_name="myIndex", query_vector=[0.1, 0.2, 0.3], k=5)
for hit in resp.results or []:
props = hit.properties.to_dict() if hit.properties else {}
print(hit.rid, hit.distance, props.get("name"))
or [] is enough to discharge the Unset: Unset.__bool__ is declared to return
Literal[False], so a type checker narrows the loop's subject to the list without an isinstance
call. to_dict() copies the additional-properties mapping into a plain dict; if you would rather
not copy, hit.properties["name"] and hit.properties.additional_properties["name"] reach the
same value directly. hybrid and fulltext hits read identically, with their own per-element
model classes.
One asymmetry with @arcadedb/driver is worth knowing if you work in both clients:
VectorSearchRequest.k, HybridSearchRequest.k, and FullTextSearchRequest.limit all carry an
OpenAPI default: 10 outside their schema's required list, and the two generators treat that
differently. openapi-python-client bakes the default straight into the generated model's own
constructor - k: int | Unset = 10 is VectorSearchRequest's actual field default here, so
db.vector.search()'s k/limit parameters stay optional with no extra work, and this facade
passes UNSET through rather than re-asserting 10 itself (see facade/vector.py's module
docstring). openapi-typescript does the opposite: it emits a property carrying a default as
required on the generated TypeScript type, so @arcadedb/driver's equivalent option types need
a hand-written widening back to optional (WithOptionalDefaults in facade/vector.ts) that this
client never needed.
Time series: a rejected db.ts.write raises TimeSeriesWriteError
POST /api/v1/ts/{database}/write answers 204 when every sample landed, and 400 with the counts
when some or all could not be applied. db.ts.write raises TimeSeriesWriteError for that 400 - a
subclass of ArcadeDBError, so except ArcadeDBError still catches it and every usual field is
filled - carrying the body:
from arcadedb_driver import TimeSeriesWriteError
try:
db.ts.write(line_protocol=payload)
except TimeSeriesWriteError as err:
print(err.written, err.dropped) # samples that landed, samples that did not
print(err.unknown_types, err.non_time_series_types, err.unavailable_types, err.undeclared_keys)
The four lists say why samples were dropped, and each is empty when the server sent none:
unknown_types: measurements naming a type that does not exist.non_time_series_types: measurements naming a type that exists but is not a time-series type.unavailable_types: measurements naming a time-series type whose storage engine failed to load (the server log says why).undeclared_keys:'<measurement>.<key> (tag|field)'entries for keys the type does not declare in that role; the samples carrying them were dropped. The server caps this list at 100 entries. A server started witharcadedb.timeSeriesUndeclaredKeys=ignorestores such samples with the undeclared keys discarded instead of dropping them.
A write is not atomic. Each measurement's batch commits as it is appended, so the written
samples are durable even though the call raised - and a rollback of a surrounding transaction does
not take them back either. Retry the dropped measurements, never the whole payload, or the written
ones land twice. written and dropped are None only if a server omits them. Every other
failure - a 401, a 413, a 400 whose body is not a JSON object - raises a plain ArcadeDBError.
Time series: db.ts.query's tags and db.ts.latest's tag are enforced server-side
A name in db.ts.query's tags body field, or in db.ts.latest's tag parameter, that is not
one of the type's declared TAG columns is not dropped from the filter - it is refused with a
400 response naming the offending tag and listing the type's declared TAG columns. Silently
ignoring it would widen the query to the whole range, and a caller has no way to tell that result
apart from a filter that legitimately matched everything. db.ts.latest's tag is also refused
the same way when it is not in name:value form - a missing : separator is an error, not a
skipped filter.
Both rules are enforced server-side, the same way ef_search and the vector/full-text
result-limit parameters are (see "Vector, hybrid and full-text search" above): this client sends
tags/tag unchanged and does not check either against a schema it does not have, so a violation
surfaces as an ArcadeDBError raised from the server's response, not a local exception before the
request is even sent.
PromQL: db.promql
db.promql is the Prometheus-compatible query surface, returning the generated models: query,
query_range, labels, label_values and series, each with an async twin. The metric name for a
time-series type is the type name itself (Sensor), not Sensor_value.
db.promql.label_values("sensor") # every value of the `sensor` tag
db.promql.label_values("sensor", start="1700000000", end="1700003600") # only values in that range
db.promql.label_values("__name__") # the time-series type names
label_values lists the values one label carries, restricted to start/end (Unix seconds, both
optional; omit both for the whole series). The name "__name__" is special: it lists the
time-series type names rather than a tag's values, since a type name is the metric name. The
server answers an empty success, not an error, for a label no series carries, so an empty
list does not mean the name was misspelled. Pass the name unescaped; the client percent-encodes it.
Two error models
The facade methods (query, command, transaction, list_databases, exists, server_info,
health, ready, the ts/grafana/promql namespaces, ...) raise ArcadeDBError on any
non-2xx response:
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, err.help_)
Two fields come from the response's headers rather than its body:
err.retry_after: seconds to wait before retrying, fromRetry-After. The server sends it on a refusal that carries a back-off - 503 when the node cannot execute the request yet (a snapshot install, for instance), 409 when an identical request is still in flight.Nonewhen the header is absent or is not a plain non-negative integer; an HTTP-dateRetry-Afteris not parsed.err.commit_index: the Raft commit index the response was produced at, fromX-ArcadeDB-Commit-Index. It is still a valid read-after barrier on an error: send it back asX-ArcadeDB-Read-After(throughArcadeDBServer(headers=...)or.raw) to read at least the state the server had committed.Noneon a standalone database, which sends no such header.
A streamed call's in-band error carries the same two fields from the event instead (see the streaming and batch sections above), since a header cannot be added once the stream has started.
A 409 has two meanings, and status alone cannot tell them apart. One is "an identical request
is still in flight": the request was not executed again, exception is
RequestStillInFlightException and retry_after is set, and retrying after that many seconds (with
the same X-Request-Id) returns the result of the execution in progress. The other is "the query
was terminated" by terminate query, terminate queries or terminate transaction: its writes
were rolled back, and it is not retryable - someone stopped it on purpose. This client retries
neither. Retry logic of your own must tell them apart by exception / retry_after, never by the
status alone.
err.help_ is spelled with a trailing underscore to match the field name on the generated
ErrorResponse model, which is reachable through .raw - help alone would shadow the Python
builtin, and picking a different spelling for the same field on the two error surfaces this
package exposes would be worse than one awkward name used consistently.
server.raw, the underlying generated client, does not raise. Every one of its operations
returns a Response whose status_code and parsed the caller inspects directly:
from arcadedb_driver._generated.api.database import list_databases
response = list_databases.sync_detailed(client=srv.raw)
if response.status_code >= 300:
... # handle it yourself; srv.raw never raises
These are two deliberately different contracts in one package. Use the facade for the ergonomics
of try/except; use .raw when you want to branch on a status code without exceptions. Mixing
assumptions about which one you're calling is the most common way to end up with an unhandled
exception or a silently ignored error.
exists cannot prove absence
present = srv.exists("mydb")
exists returns False both when the database genuinely does not exist and when it exists but
the authenticated caller is not authorized to see it - the server's response does not distinguish
the two cases, so this client cannot either. Do not treat False as proof that a database is
absent; it only means "not visible to this caller right now."
Administration: databases, users and groups
with ArcadeDBServer(base_url="http://localhost:2480", auth=basic_auth("root", "playwithdata")) as srv:
srv.create_database("tenant_a")
srv.create_user("alice", "a-long-password", databases={"tenant_a": ["admin"]}) # "*" = every database
users = srv.list_users() # [{"name": "alice", "databases": {"tenant_a": ["admin"]}}, ...]
srv.update_user("alice", password="another-password")
srv.drop_user("alice")
srv.drop_database("tenant_a")
AsyncArcadeDBServer has the same methods, awaited. All of them are root-only, and every failure
raises ArcadeDBError, as for any facade method. Users and groups have dedicated endpoints with a
structured body (/api/v1/server/users, /api/v1/server/groups): create_user, update_user,
drop_user, list_users, save_group, delete_group, list_groups. Databases have none, so
create_database and drop_database send create database <name> and drop database <name>
through POST /api/v1/server. They raise ValueError for an empty name, or one with leading or
trailing white space, before sending anything, because the server trims the name it reads out of
the command and would act on a different database than the one you named. Anything else in the
name is sent as given, embedded spaces included: one request runs one command, so a name cannot
smuggle in a second command, but validate a name built from untrusted input yourself.
"Already exists" is a 409 for users and databases alike. Both statuses were measured against a real server; they are what the e2e suite pins.
- A user that already exists is a 409 (
error"User already exists"), so a 409 fromcreate_usermeans "already there" and create-if-absent can catch it. Servers built before ArcadeData/arcadedb#10076 answer 403 "Security error" instead, which is also what a non-root caller gets; against those, do not read 403 as "already exists". - A database that already exists is a 409 too (
error"Database already exists", the name indetail). Servers built before ArcadeData/arcadedb#10079 answer 400 "Cannot execute command" instead, the status every invalid server command gets; against those, callexistsfirst to create a database only when it is missing (as root it sees every database, so itsFalseis reliable there), knowing another client can still create it in between.
update_user and save_group send only the arguments you pass: one left as None keeps the
server's current value. update_user(name, databases={}) sends the empty mapping, which removes
every assignment. list_users and list_groups return plain dicts, not generated models.
srv.server_command(command) runs any other server command and returns the whole JSON body as a
dict. It decodes the body itself rather than through the generated model, because the contract
declares an array result where administrative commands answer {"result": "ok"}, which makes
the generated from_dict raise; other commands answer shapes of their own. The command string is
sent as given.
Running work: list and terminate queries and transactions, and progress
from datetime import timedelta
from arcadedb_driver import is_query_terminated
running = srv.list_queries(tag="nightly-report") # or database=..., or neither
results = srv.terminate_queries_by_tag("nightly-report", wait=timedelta(seconds=3))
result = srv.terminate_query(running[0].id)
open_txs = srv.list_transactions()
result = srv.terminate_transaction(open_txs[0].id)
progress = srv.db("tenant_a").progress() # long-running maintenance operations, step by step
Every method has an async twin on AsyncArcadeDBServer and AsyncArcadeDBDatabase. Unlike the
administration methods above, these are open to any authenticated user: root sees and stops
every statement, another user only their own, and someone else's statement is answered
"not found", never "forbidden". They go through POST /api/v1/server, and progress through
GET /api/v1/progress/{database}. Results are the frozen dataclasses RunningQuery,
TerminateResult and OpenTransaction, with times as timezone-aware datetime and timedelta.
Each server sees only its own work. None of these is forwarded to the cluster leader. Behind a
load balancer, the server this client reaches may not be the one running the statement, and then
list_queries does not show it and terminating it answers "not found".
A statement shows up before its text does. The server lists a request the moment it is
admitted, and for a brief window that entry's language is None and its text is the request
line (POST /api/v1/command/<database>); both are filled in once the statement starts. To read the
statement itself, poll until language is set.
Each of the three terminate methods waits for the statements to stop, up to wait (omitted or zero leaves the server's
default of 5 seconds, and the server caps it at 10), then answers each with a status, typed as
QueryStatus. Which one you get decides whether you have anything to undo:
status |
meaning |
|---|---|
"terminated" |
it stopped, and what it wrote was rolled back |
"completed" |
it finished on its own before it noticed, so what it did stands |
"terminating" |
not stopped yet when the wait ended; some statements (SQL SLEEP, for one) notice only when they end, so the request's own outcome is the answer |
"not found" |
no such statement on this server, or another user's |
Terminating a command inside a transaction ends the transaction too, rolling it back, while
terminating a read leaves it open; TerminateResult.session_id and session_open say which, and
session_open is only the state at the time of the answer for a "terminating" statement. A
script counts as a command.
The stopped request raises a 409, and is_query_terminated(error) recognises it. The server
answers it 409 "Query terminated", but a 409 is also what a request still in flight under the same
X-Request-Id gets, so the status alone cannot tell "someone stopped me" from a conflict.
is_query_terminated matches the error's exception,
com.arcadedb.exception.QueryTerminatedException, which the server sends as a structured field, not
the message text. It is not retryable: somebody stopped this work on purpose.
terminate_transaction does not reach the client that owned the transaction at commit. It
rolls the transaction back, and a statement sent on it afterwards raises a 404, but a commit naming
the vanished session is answered 204, exactly like a real commit: a with db.transaction() block
that ran no statement after the terminate exits cleanly for work that is gone. Measured against
26.11.1-SNAPSHOT and pinned by the e2e suite.
Ids, session ids and the database filter must not be empty (where required) or contain a space
or control character (anything at or below U+0020), because the server reads each as one word; a
tag must not start or end with one, because the server trims it. A no-break space is not one, as
the server sees it. Each raises ValueError before anything is sent. A tag is
otherwise sent as given, spaces and quotes included: the server takes the rest of the command as the
tag.
progress() returns the generated ProgressResponse, whose result is empty when nothing is
running. The server reads a lock-free snapshot, so polling it at any rate cannot slow down the
operation being watched.
Sessions and API tokens
with ArcadeDBServer(base_url="http://localhost:2480", auth=basic_auth("root", "playwithdata")) as srv:
# Per-user isolation: log in as the user once, then keep a client holding only the token.
login = srv.login_as("alice", "a-long-password") # or srv.login(): the client's own credentials
with srv.with_token(login.token) as alice: # srv itself is unchanged
dbs = alice.list_databases() # only what alice may see
alice.logout() # the token is now refused everywhere
# 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
tokens = srv.list_api_tokens() # metadata only, never the token
srv.delete_api_token(created.token_hash) # by hash, never by token
AsyncArcadeDBServer has the same methods, awaited, except with_token, which sends nothing and
so is a plain call returning an AsyncArcadeDBServer.
login and login_as return a LoginResult carrying the session token (prefixed AU-) and
leave the client alone: login_as sends the user's credentials on that one request only.
with_token returns a new server presenting the token as a bearer token, with the original's
base URL, extra headers, timeout and TLS verification. It has its own connection pool, so close it
or use it in a with block; closing one server does not close the other. A token is a credential:
whoever holds it acts as that user until it is logged out or the server drops it, which is why
LoginResult, Session and CreatedAPIToken leave their tokens out of repr(). Keeping the
token rather than the password is the point.
The server drops a session on its own in three cases, after which the token gets a 401 like a logged-out one:
- after 30 minutes without a request (
arcadedb.server.httpAuthSessionExpireTimeout; each request restarts the clock), and, only ifarcadedb.server.httpAuthSessionAbsoluteTimeoutis set, that long after login whatever the activity; - when the same user logs in a 101st time: each user holds at most 100 sessions
(
arcadedb.server.httpAuthSessionMaxPerUser), and a further login silently evicts that user's oldest. Log in once per user and reuse the token, not once per request; - never by a count across users: past 10,000 sessions in all (
arcadedb.server.httpAuthSessionMax) a new login is refused with a 503 instead, and existing sessions are kept.
What each failure looks like, measured against a real server and pinned by the e2e suite:
- Wrong Basic credentials are a 403 "Security error", not a 401. The server answers every
failed password check that way, on every endpoint, so a
login_as403 cannot be told apart from a forbidden user (ArcadeData/arcadedb#10109 asks for 401). 401 is what an invalid, expired or logged-out bearer token gets. logoutonly ends a session token. The server answers 204 to a logout carrying Basic credentials or an API token and invalidates nothing, sologouton a client that does not present anAU-token raisesNotSessionTokenErrorwithout sending anything. Logging out a token that is no longer valid is a 401.list_sessionsis root-only (403 otherwise) and lists every user's live session token. Treat its result as you would the users' passwords. These are login sessions, not the transaction sessionstransaction()opens. Times are timezone-aware UTCdatetimes,elapsedatimedelta, and a field the server does not know (source_ip,user_agent,country,city,issuer) isNone.
API tokens:
- The plaintext is returned exactly once, in
CreatedAPIToken.token. The server stores only its SHA-256 hash, so a lost token cannot be recovered, only deleted and reissued. This client never logs or keeps it. delete_api_tokentakes the hash (token_hash, fromlist_api_tokensorcreate_api_token). Anything but 64 lowercase hexadecimal characters raisesValueErrorbefore a request is sent, because the argument travels in the query string, where proxies and access logs record it, and the likeliest mistake is the plaintext token. The error does not repeat the argument. A hash no token has is a 404, a status the contract does not list (ArcadeData/arcadedb#10110).- A taken name is a 409. The contract describes its 409 only as a replayed
X-Request-Id; the server also uses it for a duplicate name. - 412 means the transport is not confidential. With
arcadedb.server.apiTokenRequireSecureTransporton, the server refuses to send a token back over cleartext HTTP to a non-loopback client, unless a proxy listed inarcadedb.server.apiTokenTrustedProxiesterminated TLS. The setting is off by default until ArcadeDB 27.1.1, which turns it on; until then such a mint succeeds and the server logs a warning. expires_atisNonefor a token that never expires. Passed tocreate_api_token, it must be timezone-aware and after the Unix epoch, orValueErroris raised before sending: a naive time would be read in whatever timezone the client runs in, and the server reads an expiry of 0 as "never", so an epoch expiry would issue a token that never expires. An expired token authenticates nobody, and stays inlist_api_tokens(withexpiredset) until the next token change retires it.permissionstakes the shape a group'stypesmap takes.logouton a client holding an API token raisesNotSessionTokenError: an API token is revoked withdelete_api_token, not logged out.
Endpoints this client does not wrap
Two distinct things are true about parts of the contract, and they should not be confused with each other.
Not wrapped at all. POST /api/v1/ts/{database}/prom/read and
POST /api/v1/ts/{database}/prom/write (protobuf bodies) are endpoints the generator cannot model -
it has no way to describe a non-JSON request body, so it prints a warning, skips the endpoint
entirely, and exits 0. Nothing downstream notices on its own: a skipped endpoint leaves no trace in
the generated tree for git diff to flag. This package pins the exact skip set in
scripts/check_codegen_skips.py, which re-runs the generator against the committed contract and
fails if the set of skipped operations changes - so a future contract that starts describing either
endpoint in a way the generator can model, or drops one of them, cannot pass unnoticed.
db.ts.write (POST /api/v1/ts/{database}/write, InfluxDB line protocol as text/plain) and
batch_load/batch_load_stream (POST /api/v1/batch/{database}, a jsonl/ndjson/csv body) have
the same generator limitation - both endpoints are also in EXPECTED_SKIPS above - but both are
hand-written rather than left unwrapped: a time-series namespace that could query samples but never
ingest any, or a client with no way to bulk-load a graph at all, would be an odd thing to ship. See
"Batch loading" above for batch_load/batch_load_stream.
Cluster override endpoints are deliberately raw-only. acceptClusterDatabaseCopy
(POST /api/v1/cluster/accept-copy/{database}), acceptClusterDivergedDatabase
(POST /api/v1/cluster/accept-diverged/{database}) and acceptClusterStaleSnapshot
(POST /api/v1/cluster/accept-stale-snapshot) are root-only operator overrides that make a node
accept a copy of a database without a resync - a decision about which data survives, not an
operation for application code. No facade method wraps any /cluster/* endpoint; reach them through
.raw's generated operations when an operator means to.
POST /api/v1/server reached through .raw returns a body that does not conform to its declared
QueryResponse schema ({"result": "ok"} where an array is declared), so the generated model
raises on an otherwise successful call. Use srv.server_command (see "Administration" above)
instead.
Wrapped, but returning dict[str, Any] instead of a generated model. Unlike batch,
prom/read, and prom/write above, these three routes describe a JSON body the generator can
model, and it does generate operation functions and response models for them -
_generated/api/time_series/query_time_series.py, get_time_series_latest.py, and
_generated/api/grafana/query_grafana.py all exist on disk. But db.ts.query, db.ts.latest,
and db.grafana.query do not call any of those generated operations - they are hand-written, and
none of the three generated modules above is imported anywhere outside its own package. Each
method builds the request URL itself and issues it directly through the same pooled httpx client
the generated operations use (self._client.get_httpx_client(), or its async twin), returning the
parsed JSON body rather than routing the response through a generated model at all.
The reason is the same in all three: the contract types each of these responses' per-element
scalar values (a timestamp, a numeric measurement, a DataFrame cell) as "type": "object", so the
generated per-element model's from_dict calls dict(value) on every element - which raises
TypeError on an ordinary scalar like a float or an int. Had these methods routed through the
generated operations instead, db.ts.query's oneOf response parser would have caught that
TypeError and silently fallen through to the aggregated-response model, whose fields are all
optional and so "parses" anything - every raw (non-aggregated) query response would come back
mis-typed as an empty-looking aggregated one instead of raising. db.ts.latest and
db.grafana.query have no such fallback branch and would simply raise TypeError on any real
response. @arcadedb/driver, the TypeScript sibling, is unaffected only because openapi-fetch
performs no runtime validation of its own - the raw JSON passes through unexamined. Bypassing the
generated operations and returning the parsed body directly is the Python equivalent, and the only
correct behaviour, until the contract is fixed upstream to type these fields correctly; fixing the
contract is also what would let these three methods move onto the generated operations and return
generated models, the same way db.promql.* already does - db.promql.* is unaffected by any of
this today because its response schemas do not have this shape.
Contract version and compatibility
This package was generated from contracts/arcadedb-openapi-26.10.2.json, recorded in
pyproject.toml as tool.arcadedb.server-version:
[tool.arcadedb]
server-version = "26.10.2"
arcadedb-driver |
ArcadeDB server |
|---|---|
| 0.1.0 | 26.9.1 |
| 0.2.0 | 26.10.1 |
| 0.3.0 | 26.10.2 |
This table is a historical record tied to a package version, not something derived automatically:
scripts/adopt-contract-version.sh deliberately does not touch it when it retires an old contract
and adopts a new one. Adding a row is a human decision made at release time, not a side effect of
a contract bump.
The client speaks ArcadeDB's HTTP API as described by that contract. Pointing it at a server on a materially different release may work for the endpoints both versions share, but is not tested or supported.
License
Apache-2.0.
Metadata
Release files for arcadedb-driver 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| arcadedb_driver-0.3.0.tar.gz | 286.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| arcadedb_driver-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 807.8 kB
Release files / arcadedb_driver-0.3.0.tar.gz
| Download URL | arcadedb_driver-0.3.0.tar.gz |
|---|---|
| Size | 286.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a74dbba1fe073e5399094eb5f402d48668ad8248e4d742315476c93a2a5f31c
|
|
BLAKE2b-256 checksum How to use checksums |
f2b3978e8b60a3ef90a07efbb3dfe6a9c1d6253a67fb1281c0f5a186073dd0a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / arcadedb_driver-0.3.0-py3-none-any.whl
| Download URL | arcadedb_driver-0.3.0-py3-none-any.whl |
|---|---|
| Size | 521.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2f03c965b88a49586975770609cf615389d1d8c23a10e45434c8d1a2460f41d6
|
|
BLAKE2b-256 checksum How to use checksums |
89b1d99a221b68df3c6e54a5adf4f9a26c9dedbb445e777b05d0bb935a259bc5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logSecurity for arcadedb-driver 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for arcadedb_driver-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.