Skip to main content

tricoredb

Official Python client for TriCoreDB — SQL, documents, vectors, graphs and cache over one native connection.

PyPI Python License

  • No dependencies — only the Python standard library
  • Fully typed — ships py.typed, checked with mypy --strict
  • Server-side parameters — values never become part of the SQL text
  • Exact values — Decimal, datetime, UUID and binary data convert exactly
  • Transactions, connection pooling, TLS and mutual TLS

Contents

Requirements

  • Python 3.9 or later
  • A TriCoreDB server speaking protocol 1.0 (tricore-server 0.1.0-rc.1 or later) — see Running a server

Installation

pip install tricoredb

Running a server

The quickest way is the official Docker image, trinesh14/tricoredb.

Local development (no TLS, no encryption — this machine only). Set TRICORE_ADMIN_PASSWORD in your shell first, then create the admin and start the server:

docker run --rm -v tricoredb-dev:/var/lib/tricoredb -e TRICORE_ADMIN_PASSWORD --entrypoint /usr/local/bin/tricore trinesh14/tricoredb:0.1.0-rc.1-r2 auth init-admin --user admin --password-env TRICORE_ADMIN_PASSWORD --data-dir /var/lib/tricoredb/data
docker run -d --name tricoredb-dev -p 127.0.0.1:8427:8427 -e TRICORE_TLS=off -e TRICORE_ENCRYPTION=off -e TRICORE_MODULES=all -v tricoredb-dev:/var/lib/tricoredb trinesh14/tricoredb:0.1.0-rc.1-r2

Anything else: the image's default is TLS on and an encrypted data volume. Follow the quick start on the Docker Hub page to create the certificate and key, then connect with TLS.

TRICORE_MODULES=all enables every data model. The image's default is sql, document and cache; a call to a disabled model fails with an error whose code is engine.disabled.

Quick start

import os
from tricoredb import TriCore

with TriCore.connect("127.0.0.1", 8427, user="admin", secret=os.environ["TRICORE_ADMIN_PASSWORD"]) as db:
    db.execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name TEXT)")
    db.execute("INSERT INTO users VALUES (?, ?)", [1, "Ada"])

    rows = db.query("SELECT id, name FROM users WHERE id = ?", [1])
    print(rows.dicts())   # [{'id': '1', 'name': 'Ada'}]

Connecting

db = TriCore.connect(host, port, user=..., secret=...)
Argument Default Description
host "127.0.0.1" Server host
port 8427 Server port
user None Username. Omit to skip authentication.
secret None Password
client_name "tricoredb-python" Name reported to the server
timeout 10.0 Seconds for connect, TLS, handshake and authentication
tls None A TlsOptions; see TLS
read_timeout None Socket deadline for each reply, in seconds
features FEATURES Capabilities to request during the handshake
follow_leader True Send a not_leader refusal again, to the leader; see Cluster redirects
leader_retries 5 How many times a refused request is sent again
leader_retry_delay 0.25 First pause, in seconds, before asking again; doubles up to 2

A connection is a context manager; close() it yourself otherwise.

db.ping()            # transport round trip
db.features          # Features(server_params=..., session_txn=..., correlation_id=...)
db.close()

A connection is not thread-safe: it is one request/response stream. Use one connection per thread, or a Pool.

Every data method takes an optional keyword argument database, which defaults to "main".

SQL

rows = db.query("SELECT id, name FROM users WHERE name = ?", ["Ada"])
rows.columns     # ['id', 'name']
rows.rows        # [['1', 'Ada']]
rows.dicts()     # [{'id': '1', 'name': 'Ada'}]

resp = db.execute("UPDATE users SET name = ? WHERE id = ?", ["Grace", 1])
resp.warnings    # non-fatal server warnings
  • query(sql, args=None) returns Rows and may only run SELECT. Every cell is a string — convert to the type you need.
  • execute(sql, args=None) returns a Response, for writes and DDL.

Parameters

Use ? placeholders. query() and execute() send values separately from the SQL text and the server binds them, so a value can never change what the statement means. If the server did not grant server-side parameters, the call fails with a clear error before anything is sent.

Python value Sent as Use for
None NULL
bool boolean BOOL
int integer, exactly INT, BIGINT
float number DOUBLE
str text TEXT
Decimal its exact digits, as text a DECIMAL column parses exactly DECIMAL
datetime, date, time ISO-8601 text TIMESTAMP and friends
UUID text
bytes, bytearray 0x… hex BLOB
  • A Decimal never passes through a float, so no digit is lost. NaN and Infinity are refused by name; precision and scale limits are checked by the server.
  • Other types are sent unchanged, so an unsupported value gets a clear refusal from the server rather than being silently turned into text.
  • Identifiers (table and column names) cannot be parameters — validate them against an allow-list.

Transactions

Two shapes:

# 1. Every statement known up front: one request, one round trip.
db.transaction([
    ("INSERT INTO accounts VALUES (?, ?)", [1, 100]),
    ("INSERT INTO accounts VALUES (?, ?)", [2, 50]),
])

# 2. A later statement depends on an earlier read: a session block on this connection.
with db.transaction_block() as tx:
    balance = int(tx.query("SELECT balance FROM accounts WHERE id = ?", [1]).rows[0][0])
    tx.execute("UPDATE accounts SET balance = ? WHERE id = ?", [balance - 10, 1])
# committed here; an exception inside rolls back and re-raises
  • transaction(statements) runs a list of sql or (sql, args) entries as one BEGIN … COMMIT script. Its arguments are rendered into the script on the client with bind_params. It works on every node.
  • begin(), commit(), rollback() open and close a block bound to this connection; transaction_block() wraps them. Use the connection it yields (tx) for every statement inside.
  • If the server did not grant session transactions, begin() fails by name and sends nothing — it never silently autocommits.
  • db.in_transaction is true while a block is open.

A schema change (for example CREATE TABLE) that commits while a block is open aborts that block; roll back and retry.

Connection pool

from tricoredb import Pool

with Pool("127.0.0.1", 8427, user="admin", secret=secret, size=8) as pool:
    with pool.get() as db:
        db.execute("INSERT INTO users VALUES (?, ?)", [2, "Grace"])
  • Pool(host, port, user=None, secret=None, size=8, timeout=10.0, tls=None, read_timeout=None, follow_leader=True, leader_retries=5, leader_retry_delay=0.25) opens connections lazily, never more than size.
  • pool.get(timeout=10.0) borrows a connection for the block, or raises PoolTimeout.
  • A connection never returns to the pool with a transaction open: it is rolled back, and get() raises to say so.

Documents

from tricoredb import Filter, Stage, GroupBy, Acc

db.doc_create_collection("people")
db.doc_insert("people", {"name": "Ada", "city": "London", "visits": 3}, id="p1")
db.doc_get("people", "p1")                        # dict, or None
db.doc_find("people", Filter.and_(Filter.gt("visits", 1), Filter.eq("city", "London")))
db.doc_update_one("people", "p1", set={"city": "Paris"}, inc={"visits": 1})
db.doc_aggregate("people", [
    Stage.match(Filter.eq("city", "Paris")),
    Stage.group(GroupBy.field("city"), [Stage.acc("n", Acc.count())]),
    Stage.sort(("n", True)),
])

Filters: all, eq, ne, gt, gte, lt, lte, in_, contains, and_. Stages: match, group, sort, skip, limit, project, count.

Also: doc_update, doc_update_many, doc_delete, doc_list_collections, doc_drop_collection, doc_create_index, doc_drop_index, doc_list_indexes, doc_analyze.

Batches, options and transactions

These need server 0.1.0-rc.3 or later; an older server answers them with an error.

from tricoredb import Bulk

ids = db.doc_insert_many("people", [{"name": "Lin"}, {"_id": "kay", "name": "Kay"}])
db.doc_replace("people", "kay", {"name": "Kay", "city": "Oslo"}, return_="After")

# One atomic batch: every item lands or none does.
db.doc_bulk_write("people", [
    Bulk.insert({"name": "Tim"}, id="tim"),
    Bulk.update_one("kay", inc={"visits": 1}),
    Bulk.delete(ids[0]),
])

# A transaction spans requests on this connection until it commits or rolls back.
db.doc_begin_transaction()            # sets db.txn
try:
    db.doc_update("people", "kay", {"city": "Bergen"})
    db.doc_commit_transaction()
except Exception:
    db.doc_rollback_transaction()     # a refused commit leaves it open
    raise
Group Methods
Writes doc_replace, doc_insert_many, doc_delete_many, doc_bulk_write (items from Bulk: insert, update_one, replace, delete)
Transactions doc_begin_transaction, doc_commit_transaction, doc_rollback_transaction
Collections doc_rename_collection, doc_set_collection_options, doc_get_collection_options (schema, defaults, versioning), doc_purge_expired
Indexes and plans doc_rebuild_index, doc_explain

Vectors

db.vector_create_collection("docs", dimension=3, metric="cosine")   # "cosine" | "dot" | "l2"
db.vector_upsert("docs", "a", [1.0, 0.0, 0.0], {"kind": "note"})
hits = db.vector_search("docs", [0.9, 0.1, 0.0], top_k=5, filter={"kind": "note"})
hits.ids()                                                          # ['a'], best first

Higher scores are closer for every metric (l2 scores are negated distances).

Also: vector_get, vector_delete, vector_list_collections, vector_describe_collection, vector_list_vectors, vector_drop_collection.

Batches, named vectors and more searches

These need server 0.1.0-rc.3 or later; an older server answers them with an error.

db.vector_upsert_many("embeddings", [
    {"id": "doc-2", "vector": [0.3, 0.1, 0.9], "metadata": {"lang": "en"}},
    {"id": "doc-3", "vector": [0.2, 0.2, 0.2]},
])
per_query = db.vector_search_batch("embeddings", [[0.1, 0.2, 0.3], [0.9, 0, 0]], 5, filter={"lang": "en"})
similar = db.vector_search_by_examples("embeddings", ["doc-1"], 5)
english = db.vector_count("embeddings", {"lang": "en"})

db.vector_add_named_vector("embeddings", "title", 2, "dot")
db.vector_upsert_named("embeddings", "doc-1", {"default": [0.1, 0.2, 0.3], "title": [1, 2]})
by_title = db.vector_search("embeddings", [1, 2], 5, options={"using": "title"})
Group Methods
Writes vector_upsert_many, vector_upsert_text, vector_upsert_named, vector_delete_many, vector_delete_by_filter, vector_update_by_filter
Searches vector_search_batch, vector_search_multi, vector_search_by_examples, and the options of vector_search (using, offset, exact, namespace, ...)
Reads vector_count, vector_list_partitions, vector_embed, vector_list_aliases
Collections vector_add_named_vector, vector_set_alias, vector_set_collection_config, vector_copy_collection, vector_rename_collection
Index vector_rebuild_index, vector_index_stats, vector_measure_recall

Graphs

db.graph_create("social")
db.graph_add_node("social", "ada", labels=["Person"], properties={"name": "Ada"})
db.graph_add_node("social", "grace", labels=["Person"])
db.graph_add_edge("social", "e1", "ada", "grace", "KNOWS", {"since": 1843})

db.graph_neighbors("social", "ada", direction="outgoing")
db.graph_shortest_path("social", "ada", "grace").node_path          # ['ada', 'grace']
db.graph_query("social", "MATCH (p:Person) RETURN p.name").rows

"No path" is not an error: graph_shortest_path returns a GraphPath with found=False.

Also: graph_get_node, graph_get_edge, graph_delete_node, graph_delete_edge, graph_degree, graph_traverse, graph_weighted_shortest_path, graph_list_nodes, graph_list_edges, graph_list, graph_drop.

Batches, algorithms, schema and Cypher writes

These need server 0.1.0-rc.3 or later; an older server answers them with an error.

db.graph_add_many("social",
    nodes=[{"id": "lin", "labels": ["person"]}, {"id": "kay", "labels": ["person"]}],
    edges=[{"id": "e2", "from": "lin", "to": "kay", "label": "knows"}])
ranks = db.graph_page_rank("social", limit=10)      # {"ranks": [{"id", "score"}], ...}
parts = db.graph_connected_components("social")

db.graph_create_constraint("social", "unique_name", "person", "name", "unique")
db.graph_execute("social", "CREATE (n:person {name: 'Tim'})")

db.graph_begin_transaction("social")   # sets db.txn; graph_execute is queued until the commit
db.graph_execute("social", "CREATE (n:person {name: 'Una'})")
db.graph_commit_transaction("social")
Group Methods
Writes graph_add_many, graph_remove_many, graph_update_node, graph_update_edge, graph_import_csv, graph_copy, graph_execute (Cypher writes)
Transactions graph_begin_transaction, graph_commit_transaction, graph_rollback_transaction
Walks and paths graph_dfs, graph_subgraph, graph_k_shortest_paths, graph_all_paths
Algorithms graph_page_rank, graph_centrality, graph_communities, graph_connected_components, graph_triangle_count, graph_node_similarity, graph_detect_cycle
Schema graph_create_node_index, graph_create_constraint, graph_drop_schema_object, graph_list_schema_objects, graph_find_nodes
Inspection graph_stats, graph_schema, graph_check_consistency

Cache

Values are bytes; *_text helpers exist for text.

db.cache_set("sessions", "user:1", b'{"id": 1}', ttl_ms=60_000)
db.cache_get("sessions", "user:1")          # bytes, or None
db.cache_get_text("sessions", "user:1")     # str, or None
db.cache_incr("counters", "page:home")      # new value
db.cache_delete("sessions", "user:1")       # True if it existed
Group Methods
Keys cache_set, cache_set_nx, cache_get, cache_get_text, cache_delete, cache_exists, cache_ttl, cache_expire, cache_persist, cache_incr, cache_keys, cache_clear_namespace
Lists cache_lpush, cache_rpush, cache_lpop, cache_rpop, cache_lrange, cache_llen, cache_lindex
Sets cache_sadd, cache_srem, cache_sismember, cache_scard, cache_smembers
Hashes cache_hset, cache_hset_text, cache_hget, cache_hdel, cache_hgetall, cache_hexists, cache_hlen
Streams cache_xadd, cache_xadd_text, cache_xlen, cache_xrange, cache_xread, cache_xdel, cache_xtrim

The groups below need server 0.1.0-rc.3 or later; an older server answers them with an error.

from tricoredb import Expiry

db.cache_mset("sessions", [("user:1", b"a"), ("user:2", b"b")])
values = db.cache_mget("sessions", ["user:1", "user:9"])     # [b"a", None]
db.cache_set_if("sessions", "user:1", b"c", "IfPresent", Expiry.keep())

db.cache_zadd("scores", "board", [(b"ada", 12), (b"lin", 7)])
top = db.cache_zrange_with_scores("scores", "board", 0, 9, rev=True)

lock = db.cache_lock_acquire("locks", "report", "worker-1", 30_000)
if lock:
    # pass lock.token to whatever the lock protects, then:
    db.cache_lock_release("locks", "report", "worker-1")
Group Methods
Keys cache_mget, cache_mset, cache_set_if, cache_compare_and_swap, cache_get_del, cache_get_set, cache_get_ex, cache_append, cache_expire_at
Freshness cache_set_fresh, cache_get_fresh, cache_set_missing, cache_warm
Sorted sets cache_zadd, cache_zrem, cache_zscore, cache_zcard, cache_zrank, cache_zrange, cache_zrange_with_scores, cache_zrangebyscore, cache_zrangebyscore_with_scores, cache_zincrby
Locks cache_lock_acquire, cache_lock_renew, cache_lock_release
Bitmaps cache_set_bit, cache_get_bit, cache_bit_count, cache_bit_pos, cache_bit_op
HyperLogLog cache_pfadd, cache_pfcount, cache_pfmerge
Lists cache_lmove, cache_ltrim, cache_blpop, cache_brpop
Sets and hashes cache_sunion, cache_sinter, cache_sdiff, cache_srandmember, cache_hincrby
Batches and scripts cache_pipeline, cache_transaction, cache_script_load, cache_script_run
Pub/Sub cache_publish, cache_poll
Administration cache_stats, cache_hot_keys, cache_set_namespace_limit, cache_get_namespace_limit, cache_purge_expired, cache_flush_database

cache_pipeline, cache_transaction and cache_script_load take operations in the server's wire form, such as {"Incr": {"namespace": "n", "key": "k", "by": 1}}, and each result is {"ok": <response data>} or {"error": "..."}. cache_flush_database and cache_purge_expired act on the whole database named in their last argument. cache_hot_keys is refused unless the server sets [cache] hot_keys.

LLM context

Build compact, read-only context for a language model from your data:

from tricoredb import SqlSource, DocumentSource

context = db.llm_context(
    [SqlSource("SELECT * FROM orders LIMIT 20"), DocumentSource("people", limit=10)],
    output_format="json",      # "toon" | "markdown" | "json"
)
schema = db.llm_schema(output_format="toon")

Admin

db.admin_ping()      # needs the Admin permission and the cluster module
db.admin_status()    # dict

Errors

Everything raised derives from TriCoreError:

Class When
TriCoreError The server refused a request, or a general failure
AuthError Authentication failed
ProtocolError The server sent something the driver cannot interpret
ConnectionFailed Could not connect, or the TLS handshake failed (also an OSError)
Timeout A deadline elapsed (also an OSError)
PoolTimeout No pooled connection became free in time

Branch on e.code, never on the message text:

from tricoredb import TriCoreError

try:
    db.execute("INSERT INTO users VALUES (?, ?)", [1, "Ada"])
except TriCoreError as e:
    if e.code == "state.conflict":
        ...  # a concurrent transaction won — retry
    else:
        raise
Code Meaning
perm.denied Not authorized for this operation or database
request.invalid The server refused the request (bad SQL, type mismatch, …)
request.malformed The request could not be interpreted
engine.disabled That data model is disabled on the server
limit.exceeded A resource limit was reached
state.conflict Transaction conflict — retrying the transaction may succeed
not_leader This cluster node cannot serve the request (see below)
outcome_unknown Handed to the leader, answer lost: may have been applied
internal Server fault

code is None when the server sent none, or when the driver raised the error itself before sending.

Cluster redirects

On a replicated cluster only the leader accepts writes. A follower refuses with not_leader, and the driver handles that itself (follow_leader=True, the default):

  • when the refusal names the leader, the driver opens a connection there, authenticated as before, sends the request again, and keeps that connection;
  • while an election is in progress (no leader named), it waits and asks the same node again, pausing 0.25 s, then 0.5 s, up to 2 s;
  • after leader_retries (5) it raises the last refusal.

This is safe because a not_leader refusal was applied nowhere. The driver never resends inside a transaction block (the block belongs to its connection and ends with the refusal), and never resends OUTCOME_UNKNOWN: a follower handed that request to the leader and lost the answer, so it may have been applied. Check before sending it again.

Following uses the address the server reports, so it must be reachable from your application; with TLS, the leader's certificate is checked against that host name. When it is not reachable, the driver keeps its connection and waits. db.address is the host:port a connection talks to now.

To handle redirects yourself, pass follow_leader=False: the error then has e.code == NOT_LEADER, e.is_redirect true, and e.leader_hint, the leader's host:port or None during an election.

Timeouts and cancellation

db = TriCore.connect(host, port, user=user, secret=secret, timeout=10.0, read_timeout=30.0)
db.request_timeout_ms = 5_000     # ask the server to stop any request after 5 s

# From a second connection, stop a statement the first one is running:
other.cancel(db.last_request_id)

A connection whose read deadline expired is unusable afterwards; open a new one.

TLS

from tricoredb import TriCore, TlsOptions

tls = TlsOptions(ca_file="tricoredb-ca.crt", server_name="localhost")
db = TriCore.connect("localhost", 8427, user="admin", secret=secret, tls=tls)
Option Description
ca_file PEM CA bundle to trust
server_name Name expected in the server certificate (SNI); default "localhost"
client_cert_file, client_key_file Client certificate and key, for mutual TLS (set both)
danger_accept_invalid_certs Skips certificate verification. Development only.

TLS is off unless you pass tls. With no ca_file the trust store is empty: the driver never falls back to operating-system root certificates, so a wrong path fails instead of trusting an unrelated CA.

Security

  • Load credentials from the environment or a secret manager — never commit them.
  • Use TLS whenever traffic leaves a trusted network.
  • Always pass user input as parameters, never by string formatting.
  • Connect with a least-privilege user.

To report a vulnerability, use private vulnerability reporting on GitHub rather than a public issue.

Compatibility

tricoredb Python TriCoreDB protocol
0.1.x, 0.2.x 3.9 – 3.14 1.0
0.3.x 3.9 – 3.14 1.0

0.3 adds methods for the operations of server 0.1.0-rc.3. They need that server or a later one; the methods of earlier releases work against older servers as before.

This package follows semantic versioning. See the changelog for release notes.

License

Apache-2.0

Metadata

Release files for tricoredb 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tricoredb 0.3.0
File Size Uploaded Provenance Actions
tricoredb-0.3.0.tar.gz 98.6 kB Without provenance Details for tricoredb-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for tricoredb 0.3.0
File Interpreter ABI Platform Actions
tricoredb-0.3.0-py3-none-any.whl Python 3 none any Details for tricoredb-0.3.0-py3-none-any.whl

Total release size: 157.6 kB

Release files / tricoredb-0.3.0.tar.gz

Download URL tricoredb-0.3.0.tar.gz
Size 98.6 kB
Tags Source
SHA-256 checksum
How to use checksums
00b41bc159095d99bdd363ab242e8eaabd2b1b5bd368c0b50736d669eabcfb30
BLAKE2b-256 checksum
How to use checksums
a83bc886870153cfc1337301e5a33d593bac6f278ed2dba9f39416f597835d06
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release files / tricoredb-0.3.0-py3-none-any.whl

Download URL tricoredb-0.3.0-py3-none-any.whl
Size 59.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a68db5682afe11167945c864ee60355b7567e4c742bba7eafa1117144cca1225
BLAKE2b-256 checksum
How to use checksums
0c72834edd369a80633e0364ea9342aeb9f829510dcfb423edc4b49375acec31
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Security for tricoredb 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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A 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.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page