Metadata-Version: 2.5
Name: tricoredb
Version: 0.3.0
Summary: TriCoreDB client for Python, speaking the native tricore protocol (SQL, cache, document, vector, graph)
Project-URL: Homepage, https://github.com/trinesh14/tricoredb-sdk-python
Project-URL: Repository, https://github.com/trinesh14/tricoredb-sdk-python
Project-URL: Issues, https://github.com/trinesh14/tricoredb-sdk-python/issues
Project-URL: Changelog, https://github.com/trinesh14/tricoredb-sdk-python/blob/main/CHANGELOG.md
Author: Trinesh Kumar
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: cache,database,document,driver,graph,sql,tricoredb,vector
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# tricoredb

Official Python client for [TriCoreDB](https://hub.docker.com/r/trinesh14/tricoredb) —
SQL, documents, vectors, graphs and cache over one native connection.

[![PyPI](https://img.shields.io/pypi/v/tricoredb?cacheSeconds=3600)](https://pypi.org/project/tricoredb/)
[![Python](https://img.shields.io/pypi/pyversions/tricoredb?cacheSeconds=86400)](https://pypi.org/project/tricoredb/)
[![License](https://img.shields.io/pypi/l/tricoredb?cacheSeconds=86400)](https://github.com/trinesh14/tricoredb-sdk-python/blob/main/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](#requirements)
- [Installation](#installation)
- [Running a server](#running-a-server)
- [Quick start](#quick-start)
- [Connecting](#connecting)
- [SQL](#sql)
- [Transactions](#transactions)
- [Connection pool](#connection-pool)
- [Documents](#documents)
- [Vectors](#vectors)
- [Graphs](#graphs)
- [Cache](#cache)
- [LLM context](#llm-context)
- [Admin](#admin)
- [Errors](#errors)
- [Timeouts and cancellation](#timeouts-and-cancellation)
- [TLS](#tls)
- [Security](#security)
- [Compatibility](#compatibility)

## 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](#running-a-server)

## Installation

```bash
pip install tricoredb
```

## Running a server

The quickest way is the official Docker image,
[`trinesh14/tricoredb`](https://hub.docker.com/r/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:

```bash
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](https://hub.docker.com/r/trinesh14/tricoredb) to create the
certificate and key, then connect with [TLS](#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

```python
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

```python
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](#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](#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.

```python
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`](#connection-pool).

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

## SQL

```python
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:

```python
# 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

```python
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

```python
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.

```python
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

```python
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.

```python
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

```python
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.

```python
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.

```python
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.

```python
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:

```python
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

```python
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:

```python
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

```python
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

```python
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](https://github.com/trinesh14/tricoredb-sdk-python/security/advisories/new)
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](https://semver.org). See the
[changelog](https://github.com/trinesh14/tricoredb-sdk-python/blob/main/CHANGELOG.md)
for release notes.

## License

[Apache-2.0](https://github.com/trinesh14/tricoredb-sdk-python/blob/main/LICENSE)
