Skip to main content

Official Python driver for xyzDB — a thin, zero-dependency TCP client.

Project description

xyzDB — Python driver

A thin, zero-dependency Python client for xyzDB. It speaks the binary wire protocol directly (pure standard library), with a fluent query API, parameter binding, and bearer-token auth.

  • Requires: Python ≥ 3.9 and a running xyzdb-server (default localhost:2505).
  • Dependencies: none at runtime.

The engine itself is developed at the official xyzDB repo: github.com/Tunolabs/xyzdb.

Install

From a checkout of the xyzdb-clients repo (not yet published to PyPI):

pip install ./python          # from the repo root; or: pip install -e ./python

Quickstart

import xyzdb

with xyzdb.connect("127.0.0.1", 2505) as db:
    # Schema (optional — lobes auto-create on first PUT)
    db.create_lobe("clients", hint="Customer records")
    db.create_anchor("rfc", unique_in="clients")

    # Write
    db.put("clients", {"rfc": "ACME-001", "name": "Acme Corp", "region": "US-West"})

    # Read one (anchor lookup, O(1))
    acme = db.find("clients", where={"rfc": "ACME-001"}).execute()
    print(acme.name)                      # -> "Acme Corp"

    # Read many (top-N)
    top = list(
        db.scan("clients", where={"region": "US-West"})
          .order_by("revenue", descending=True)
          .limit(10)
    )

connect() returns a Client. Use it as a context manager (auto-closes) or call db.close() yourself. A client owns one TCP connection and is not thread-safe — use one per thread.

Connecting & auth

db = xyzdb.connect(host="127.0.0.1", port=2505)

If the server runs with --auth-token, set XYZDB_TOKEN in the environment; the driver sends the bearer token automatically on connect:

export XYZDB_TOKEN="my-secret-token"

Prefer 127.0.0.1 over localhost under high concurrency (avoids the dual IPv4/IPv6 connection budget).

Writing

# Single record. Prefix a field with * to make it a gravity field
# (records sharing the value are co-located on disk).
db.put("creditos", {"*rfc": "ACME-001", "_type": "Credit", "monto": 50000})

# Linked write (relationship to a parent record).
db.put("creditos",
       {"*rfc": "ACME-001", "_type": "Payment", "amount": 1000},
       link_to={"lobe": "clients", "where": {"rfc": "ACME-001"}, "as": "owner"})

# Upsert on the anchor.
db.put("clients", {"rfc": "ACME-001", "name": "Acme Inc"}, on_conflict="update")

# Atomic batch (≤ 10,000 records per call).
db.put_batch("clients", [
    {"rfc": "A-1", "name": "One"},
    {"rfc": "A-2", "name": "Two"},
])

on_conflict only supports "update".

Reading

find() and scan() return a QueryBuilder. Chain trailing clauses and pipeline steps, then terminate with .execute(), iteration, or a terminal verb. Clauses are assembled in the order the engine requires (WHERE → ORDER BY → LIMIT → CURSOR, then | steps) regardless of call order.

# Graph fetch: a record plus its linked descendants.
tree = db.find("clients", where={"rfc": "ACME-001"}).pull(depth=2).execute()

# Update in place.
db.find("clients", where={"rfc": "ACME-001"}).set(status="inactive")

# Delete matching records.
db.scan("creditos", where={"status": "cancelled"}).delete()

# Grouped aggregate.
r = (db.scan("creditos", where={"_type": "Credit"})
       .group_by("status")
       .aggregate("count()", "sum(monto)"))
print(r.count, r.sum_monto)

# Semantic top-k. The engine never embeds — declare the vector field, store
# embeddings you produced, then NEAREST over them (pass a query vector from the
# same model, or a REF to a stored record's vector).
db.create_lobe("mem")
db.create_vector("emb", "mem")            # VECTOR emb IN "mem"
db.put("mem", {"id": "a", "conv": "c1", "emb": [0.1, 0.2, 0.3]})
hits = (db.scan("mem", where={"conv": "c1"})
          .limit(10000)
          .nearest("emb", query_vector, k=10, metric="cosine")
          .execute())

Metrics for nearest(): "cosine", "dot", "l2".

Filters (where=)

where= takes a dict (an AND conjunction) or a raw predicate string:

# Equality (AND-combined)
db.scan("fintech", where={"status": "overdue", "region": "US-West"})

# List → IN; operator dicts → comparisons / set / null tests
db.scan("fintech", where={"status": ["overdue", "defaulted"]})   # status IN [...]
db.scan("fintech", where={"amount": {">": 5000}})
db.scan("clients", where={"tags": {"contains": "tech"}})
db.scan("data", where={"score": {"is_not_null": True}})

# Raw string → the full OR / NOT / parenthesised tree (build from trusted text;
# for untrusted values use execute() with $params)
db.scan("fintech", where='(status = "overdue" OR status = "defaulted") AND amount > 5000')

FIND is AND-only (anchor/gravity fast path); use SCAN for OR/NOT.

More reads: FETCH, FOLLOW, SHAPE, TAKE

# FETCH — several co-located lobes in ONE call (shared key). Returns one
# envelope whose fields are a named section per lobe.
env = db.fetch(["clientes", "creditos", "operaciones"],
               where={"rfc": "ACME-001"},
               as_names=["cliente", "creditos", "operaciones"])

# FOLLOW — jump across lobes: each message's cited_doc resolved in "documents".
docs = (db.find("messages", where={"thread": "T-1"})
          .follow("cited_doc", to="documents", on="doc_id").execute())

# SHAPE — project each record down to the fields you need.
rows = (db.scan("clients", where={"tier": "gold"})
          .order_by("score", descending=True).limit(10)
          .shape("name", "score").execute())

# TAKE — top-N over an aggregate (O(N) when a metric-ordered ghost exists),
# or a plain stream truncation.
from xyzdb import metric
top = (db.scan("creditos", where={"_type": "Credit", "status": ["active", "overdue"]})
         .group_by("rfc")
         .agg(metric("sum(monto)", as_="exposicion"), "count()")
         .take(100, by="exposicion").execute())

top5 = db.scan("clients").limit(1000).take(5).execute()   # truncate the stream

Use the non-terminal .agg(...) step (not the .aggregate(...) terminal) when a TAKE follows the aggregate.

Conditional aggregates (per-metric filter)

Each metric may carry its own alias and WHERE, computing several conditional aggregates in one pass. Build them with metric():

from xyzdb import metric

r = (db.scan("credits").group_by("empresa_id").aggregate(
        metric("count()", as_="total"),
        metric("count()", as_="active", where={"status": "active"}),
        metric("sum(amount)", as_="overdue_amount", where={"status": "overdue"}),
     ))

Deleting & purging

DELETE requires a selection — a where= (or a pipeline that produced records). Calling .delete() on an unfiltered scan raises ValueError and points you to purge(), so you never empty a lobe by accident. There is no cascade.

db.scan("creditos", where={"status": "cancelled"}).delete()   # matched rows only
db.purge("scratch")                                           # empty the whole lobe (deliberate)

Pagination

SCAN without .limit() caps at 1,000 rows; the hard ceiling is 10,000. Page beyond it by round-tripping the opaque cursor token:

resp = db.execute('SCAN "creditos" WHERE rfc = $r LIMIT 1000', {"r": "ACME-001"})
while resp.get("has_more"):
    resp = (db.scan("creditos", where={"rfc": "ACME-001"})
              .limit(1000).cursor(resp["cursor"]).execute())

Cursor pagination is not valid together with order_by, aggregate, or ghost routing.

Parameter binding (injection-safe)

Never interpolate untrusted input into a statement. Pass it through bound $name placeholders — substituted server-side, so the text never becomes syntax:

db.execute('FIND "clients" WHERE name = $n', {"n": user_input})

execute() is also the escape hatch for any xyTalk the fluent API doesn't cover.

Result types

  • Record — one row. Access fields by attribute (rec.name) or key (rec["name"]). .to_dict(), .to_json(), .to_compact(), .get(k, default). For PULL results, iterate the record to walk linked children.
  • AggregateResult — metrics by attribute or key; parenthesised keys are normalised (sum(monto)result.sum_monto).

Errors

from xyzdb import XyzDBError, ConnectionError

try:
    db.put("clients", {"rfc": "dupe"})
except XyzDBError as e:
    print(e.code, e.message)        # server-side rejection
except ConnectionError as e:
    print("transport:", e)          # connect/send/recv failure

ConnectionError is a subclass of XyzDBError.

Tests

Integration tests need a running server on localhost:2505 (override with XYZDB_PORT):

pip install -e './python[test]'
pytest python/tests

Versioning & license

The driver is versioned independently from the engine and released under the py-X.Y.Z tag (see ../VERSIONING.md). Licensed under Apache-2.0 (the clients are permissive; only the engine is BUSL-1.1). For the full language and operator docs see docs/usage/ and the MCP integration guide at docs/mcp-integration.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

xyzdb-1.0.0.tar.gz (36.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

xyzdb-1.0.0-py3-none-any.whl (27.8 kB view details)

Uploaded Python 3

File details

Details for the file xyzdb-1.0.0.tar.gz.

File metadata

  • Download URL: xyzdb-1.0.0.tar.gz
  • Upload date:
  • Size: 36.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.3

File hashes

Hashes for xyzdb-1.0.0.tar.gz
Algorithm Hash digest
SHA256 b80a2d1de40bd88b3ff00c367557860330650bf6a94155c568b926fc6657cfeb
MD5 b709070fe8d67d918dc890aef8915fa0
BLAKE2b-256 84e57e1d5b07ee2064bbadd68e4de5e0c2f7a08e7b3f3ce5e76c0f2ac4e52d7b

See more details on using hashes here.

File details

Details for the file xyzdb-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: xyzdb-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 27.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.3

File hashes

Hashes for xyzdb-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 43de05e9303ae7113f828f1b34b1b50636c0a45755a772ba6fef7f8b668d6a16
MD5 72c3668273150ebd0fc2fcd5fc94fefe
BLAKE2b-256 85851f4390bce9519559488673977424bc2d95528d54a544a429aea1877a5775

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page