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={"1": 21})
print(envelope.result)
A bearer token (for example, a session token returned by /api/v1/login) 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={"1": 21})
print(envelope.result)
asyncio.run(main())
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
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.
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, with a status of 200 - so both paths fail the same way; any event already yielded
before the error stays delivered to the caller.
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. error and exception are two more fields the server sends that no schema
declares (the contract's streamed error object declares only commitIndex, status and
exceptionArgs), 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.
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.httpTxExpireTimeoutreaps it, and then the commit's error is re-raised.
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: 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.
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_)
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."
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.
POST /api/v1/server (administrative commands) is likewise not wrapped by the facade, and 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.
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.1.json, recorded in
pyproject.toml as tool.arcadedb.server-version:
[tool.arcadedb]
server-version = "26.10.1"
arcadedb-driver |
ArcadeDB server |
|---|---|
| 0.1.0 | 26.9.1 |
| 0.2.0 | 26.10.1 |
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.2.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.2.0.tar.gz | 202.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| arcadedb_driver-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 665.0 kB
Release files / arcadedb_driver-0.2.0.tar.gz
| Download URL | arcadedb_driver-0.2.0.tar.gz |
|---|---|
| Size | 202.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0128a791724a6900c79b0f34a8bd545d8d6ae0f1304377146e5f9882316b6f10
|
|
BLAKE2b-256 checksum How to use checksums |
e3d4d5d74eabaf244897160e84424d3efb08da6c00fea8dbc48487dfc04c0698
|
| 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 6, 2026.
Transparency logRelease files / arcadedb_driver-0.2.0-py3-none-any.whl
| Download URL | arcadedb_driver-0.2.0-py3-none-any.whl |
|---|---|
| Size | 463.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
30a8ffedf0fe15987ffe231fd970a326c5d4c96e3731dbd2c14a851a5e164baf
|
|
BLAKE2b-256 checksum How to use checksums |
e17253517e95fe611e80ffdac61178464a042d3188aefb89316fd54c4f84fc1f
|
| 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 6, 2026.
Transparency logSecurity for arcadedb-driver 0.2.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.2.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 6, 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.