tricoredb
Official Python client for TriCoreDB — SQL, documents, vectors, graphs and cache over one native connection.
- No dependencies — only the Python standard library
- Fully typed — ships
py.typed, checked withmypy --strict - Server-side parameters — values never become part of the SQL text
- Exact values —
Decimal,datetime,UUIDand binary data convert exactly - Transactions, connection pooling, TLS and mutual TLS
Contents
- Requirements
- Installation
- Running a server
- Quick start
- Connecting
- SQL
- Transactions
- Connection pool
- Documents
- Vectors
- Graphs
- Cache
- LLM context
- Admin
- Errors
- Timeouts and cancellation
- TLS
- Security
- Compatibility
Requirements
- Python 3.9 or later
- A TriCoreDB server speaking protocol 1.0 (
tricore-server0.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)returnsRowsand may only runSELECT. Every cell is a string — convert to the type you need.execute(sql, args=None)returns aResponse, 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
Decimalnever passes through afloat, so no digit is lost.NaNandInfinityare 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 ofsqlor(sql, args)entries as oneBEGIN … COMMITscript. Its arguments are rendered into the script on the client withbind_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_transactionis 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 thansize.pool.get(timeout=10.0)borrows a connection for the block, or raisesPoolTimeout.- 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
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.