Skip to content

Cloud Storage Backends

Enterprise storage backends for distributed systems: AWS S3, Google Cloud Storage, Azure Blob, and SQL databases.

Installation

# AWS S3
pip install truthound[s3]

# Google Cloud Storage
pip install truthound[gcs]

# Azure Blob Storage
pip install truthound[azure]

# SQL Database (SQLAlchemy)
pip install truthound[db]

# All cloud backends
pip install truthound[all-stores]

S3 Store

AWS S3 backend using boto3.

Basic Usage

from truthound.stores.backends.s3 import S3Store

store = S3Store(
    bucket="my-validation-bucket",
    prefix="validations/",
    region="us-east-1",
)

# Save and retrieve
run_id = store.save(result)
result = store.get(run_id)

Using the Factory

from truthound.stores import get_store

store = get_store(
    "s3",
    bucket="my-bucket",
    prefix="validations/",
    region="us-east-1",
)

Configuration

from truthound.stores.backends.s3 import S3Config

config = S3Config(
    bucket="my-bucket",                    # S3 bucket name
    prefix="truthound/",                   # Key prefix
    namespace="default",                   # Namespace for organization
    region="us-east-1",                    # AWS region
    endpoint_url=None,                     # Custom endpoint (MinIO, LocalStack)
    use_compression=True,                  # Gzip compression
    storage_class="STANDARD",              # S3 storage class
    server_side_encryption="AES256",       # SSE setting
    kms_key_id=None,                       # KMS key for aws:kms
    tags={"env": "prod"},                  # Object tags
)

Configuration Options

Option Type Default Description
bucket str "" S3 bucket name (required)
prefix str truthound/ Key prefix for objects
region str \| None None AWS region name
endpoint_url str \| None None Custom endpoint URL
use_compression bool True Compress with gzip
storage_class str STANDARD S3 storage class
server_side_encryption str \| None None SSE type (AES256, aws:kms)
kms_key_id str \| None None KMS key ID
tags dict[str, str] {} Object tags

Conditional publication and recovery

S3Store(config=S3Config(...)) captures the supplied settings, including nested metadata and object tags. Individual constructor settings also accept JSON or safe YAML (serialization_format) and a native compression name such as compression="zstd"; True retains gzip and False disables it. Pickle and unknown settings fail before provider work. Configured metadata supplies record defaults, and each record's own metadata takes precedence. Object tags use URL encoding for both names and values.

Initialization loads the index and validates the actual record inventory, identities and metadata; new indices also retain SHA-256 and opaque ETag receipts. Legacy indices without hashes remain readable when their records agree. Reads close native response bodies. max_object_bytes (default 128 MiB) bounds encoded objects/indexes and checks decoded record size; the codec may allocate before the decoded-size check. Query errors propagate, limit=0 is empty, and sorting honors run_time, data_asset or status. Only a provider not-found response means absence; access/connection failures remain errors.

Record and index writes use native conditional publication with confirmed ETags; new keys use IfNoneMatch="*" and replacements use the prior IfMatch receipt. Deletion uses the record's ETag and requires HTTP 204. A changed index blocks an already stale owner before its next record write. A failed or unconfirmed write fences further mutation. These are separate object operations: concurrent races can leave unconfirmed objects and are not an atomic multi-object transaction or an exclusive remote lease. Use one active writer per namespace and reconcile before reusing uncertain state.

rebuild_index() is explicit and can run even when initialization rejects a corrupt or inconsistent index. It validates every current selected-format record before replacing the index conditionally. Malformed records remain errors and are never silently omitted. Rebuilding adopts the current valid record inventory, including unindexed records, and drops entries for missing records; inspect the namespace before choosing this recovery. Closing releases the owned native client, while a close failure retains its handle for an explicit retry. Approved MCP S3 store factories use the same native paths, including rebuild_index, and withhold provider keys, tags and private metadata from the client projection. S3-compatible services must implement the required conditional write and delete contracts. Current development evidence uses the actual botocore request model and persistent synthetic objects through worker restart; live cloud services remain separately unverified.

S3-Compatible Services

# MinIO
store = S3Store(
    bucket="my-bucket",
    endpoint_url="http://localhost:9000",
    region="us-east-1",
)

# LocalStack
store = S3Store(
    bucket="my-bucket",
    endpoint_url="http://localhost:4566",
    region="us-east-1",
)

Server-Side Encryption

# SSE-S3
store = S3Store(
    bucket="my-bucket",
    server_side_encryption="AES256",
)

# SSE-KMS
store = S3Store(
    bucket="my-bucket",
    server_side_encryption="aws:kms",
    kms_key_id="arn:aws:kms:us-east-1:123456789012:key/abc123",
)

GCS Store

Google Cloud Storage backend using google-cloud-storage.

Basic Usage

from truthound.stores.backends.gcs import GCSStore

store = GCSStore(
    bucket="my-validation-bucket",
    prefix="validations/",
    project="my-gcp-project",
)

run_id = store.save(result)
result = store.get(run_id)

Configuration

from truthound.stores.backends.gcs import GCSConfig

config = GCSConfig(
    bucket="my-bucket",                    # GCS bucket name
    prefix="truthound/",                   # Object prefix
    namespace="default",                   # Namespace
    project="my-project",                  # GCP project ID
    credentials_path=None,                 # Service account JSON path
    use_compression=True,                  # Gzip compression
)

Configuration Options

Option Type Default Description
bucket str "" GCS bucket name (required)
prefix str truthound/ Object name prefix
project str \| None None GCP project ID
credentials_path str \| None None Service account JSON path
use_compression bool True Compress with gzip

Generation receipts and recovery

GCSStore(config=GCSConfig(...)) captures settings and nested record metadata. Individual settings accept serialization_format="json" or "yaml" and native codec names such as compression="zstd"; True means gzip and False disables compression. Unknown settings and pickle are rejected. Record metadata overrides configured defaults. Connections remain lazy until an operation needs the SDK.

GCS uses the same Core record/index, query, checksum and explicit rebuild logic as S3. New publications use if_generation_match=0; updates, index replacement and deletion use the exact prior generation. Metadata is reloaded before media reads, and raw_download=True plus a pinned generation preserves stored bytes: Google's HTTP decoder and Core must not both decompress the same gzip object. Missing generation receipts cannot commit an index and fence further mutations. Read failures and corrupt indices remain errors; access denial is not absence. rebuild_index() explicitly validates/adopts the current valid record inventory, including orphaned records, before a conditional index replacement. Historical blob_name index entries without hashes/generations remain readable when their identities and metadata agree; corrupt records are never silently skipped.

max_object_bytes defaults to 128 MiB and checks encoded objects/indexes and decoded records; it is not a codec heap sandbox. Query sorting and limit=0 follow the shared object-store contract. Close releases the actual native client and retains a failed cleanup handle for explicit retry. Use one writer per namespace; separate record/index operations can leave uncertain state and do not form a multi-object transaction or exclusive remote lease. Reconcile before reuse. Approved MCP factories expose the same native lifecycle/rebuild behavior, while provider keys, project/credential details and private metadata stay out of the client projection. Development evidence executes actual Google SDK JSON/media serialization and gzip decoding against an owned persistent HTTP transport, including installed-worker restart; it does not establish live GCS verification. See Google's generation preconditions and Blob API.

Authentication

# Default credentials (GOOGLE_APPLICATION_CREDENTIALS env var)
store = GCSStore(bucket="my-bucket")

# Explicit service account
store = GCSStore(
    bucket="my-bucket",
    credentials_path="/path/to/service-account.json",
)

# With project ID
store = GCSStore(
    bucket="my-bucket",
    project="my-gcp-project",
)

Azure Blob Store

Azure Blob Storage backend using azure-storage-blob.

Basic Usage

from truthound.stores.backends.azure_blob import AzureBlobStore

store = AzureBlobStore(
    container="my-container",
    connection_string="DefaultEndpointsProtocol=https;...",
)

run_id = store.save(result)
result = store.get(run_id)

Configuration

from truthound.stores.backends.azure_blob import AzureBlobConfig

config = AzureBlobConfig(
    container="my-container",              # Container name
    prefix="truthound/",                   # Blob name prefix
    namespace="default",                   # Namespace
    connection_string=None,                # Connection string
    account_url=None,                      # Account URL
    account_name=None,                     # Account name
    account_key=None,                      # Account key
    sas_token=None,                        # SAS token
    use_compression=True,                  # Gzip compression
    content_type="application/json",       # Content type
    access_tier="Hot",                     # Access tier
    metadata={"env": "prod"},              # Blob metadata
)

Configuration Options

Option Type Default Description
container str "" Container name (required)
prefix str truthound/ Blob name prefix
connection_string str \| None None Connection string
account_url str \| None None Account URL
account_name str \| None None Account name
account_key str \| None None Account key
sas_token str \| None None SAS token
use_compression bool True Compress with gzip
access_tier str \| None None Hot, Cool, Cold, Archive
metadata dict[str, str] {} Blob metadata

Authentication Methods

# Connection string (recommended for development)
store = AzureBlobStore(
    container="my-container",
    connection_string="DefaultEndpointsProtocol=https;...",
)

# Account URL with SAS token
store = AzureBlobStore(
    container="my-container",
    account_url="https://myaccount.blob.core.windows.net",
    sas_token="sv=2021-06-08&ss=b&srt=sco&sp=rwdlacx&...",
)

# Account name and key
store = AzureBlobStore(
    container="my-container",
    account_name="myaccount",
    account_key="...",
)

# Managed identity (DefaultAzureCredential)
store = AzureBlobStore(
    container="my-container",
    account_url="https://myaccount.blob.core.windows.net",
)

Access Tiers

store = AzureBlobStore(
    container="my-container",
    connection_string="...",
    access_tier="Cool",  # Hot, Cool, Cold, Archive
)

# Change tier for existing result
store.set_access_tier("run-123", "Archive")

ETag receipts, recovery and client ownership

Install truthound[azure] for both Blob Storage and azure-identity. Select one connection source: connection string, account URL, or account name. A URL/name without a key or SAS uses an owned DefaultAzureCredential; credential discovery failures never fall back to anonymous access. Set anonymous=True explicitly for public containers. Conflicting authentication settings are rejected.

AzureBlobStore(config=AzureBlobConfig(...)) captures nested configuration. metadata contains ASCII Azure blob headers; record_metadata contains defaults for the persisted validation record. JSON and safe YAML support configured codecs such as gzip and zstd. Reads disable the SDK's automatic gzip decoding so Core restores stored bytes exactly once. max_object_bytes bounds stored and restored record/index bytes; it is not a process heap limit.

Azure shares Core's S3/GCS inventory, record validation, SHA-256, query and index rebuild logic. New blobs use If-None-Match: *; updates, reads and deletes use confirmed ETags. Missing write receipts, concurrent replacement, unreadable indexed records and corrupt indexes fence further writes. Result and index publication are separate conditional operations, not a multi-blob transaction or remote lease. Prefer one writer per namespace. After investigating an interrupted publication, use rebuild_index() explicitly on a fresh owner; it validates every candidate before publishing the new index. Legacy blob_name indexes remain readable when the record inventory and metadata agree. Corruption never becomes an empty store.

access_tier accepts Hot, Cool, Cold and Archive and is supplied with the native upload. Tier rejection fails the save. set_access_tier(run_id, tier) requires a conditional native acknowledgement; get_access_tier(run_id) returns observed tier and archive_status. An acknowledgement does not mean archive rehydration has completed. See the Azure BlobClient API.

Call close() to release both the owned SDK client and default credential. Cleanup failure retains the failed handle and blocks new work until an explicit successful close retry. Offline tests exercise the real SDK pipeline with synthetic service responses; they do not establish live-account authorization or rehydration timing.

Database Store

SQL database backend using SQLAlchemy. Supports PostgreSQL, MySQL, SQLite, and other SQLAlchemy-compatible databases.

Basic Usage

from truthound.stores.backends.database import DatabaseStore

# SQLite
store = DatabaseStore(
    connection_url="sqlite:///validations.db",
)

# PostgreSQL
store = DatabaseStore(
    connection_url="postgresql://user:pass@localhost/validations",
)

run_id = store.save(result)
result = store.get(run_id)

Configuration

from truthound.stores.backends.database import DatabaseConfig, PoolingConfig

config = DatabaseConfig(
    connection_url="postgresql://user:pass@localhost/db",
    namespace="default",                   # Namespace
    table_prefix="",                       # Table name prefix
    pool_size=5,                           # Connection pool size
    max_overflow=10,                       # Max overflow connections
    echo=False,                            # Echo SQL statements
    create_tables=True,                    # Auto-create tables
    use_pool_manager=True,                 # Use enterprise pool manager
    pooling=PoolingConfig(                 # Advanced pooling
        pool_size=5,
        max_overflow=10,
        pool_timeout=30.0,
        pool_recycle=3600,
        pool_pre_ping=True,
        enable_circuit_breaker=True,
        enable_health_checks=True,
        enable_retry=True,
        max_retries=3,
        retry_base_delay=0.1,
    ),
)

Configuration Options

Option Type Default Description
connection_url str sqlite:///.truthound/store.db SQLAlchemy connection URL
table_prefix str "" Table name prefix
pool_size int 5 Connection pool size
max_overflow int 10 Max overflow connections
echo bool False Echo SQL statements
create_tables bool True Auto-create tables
use_pool_manager bool True Use enterprise pool manager

Connection URLs

# SQLite
DatabaseStore(connection_url="sqlite:///data.db")
DatabaseStore(connection_url="sqlite:///:memory:")

# PostgreSQL
DatabaseStore(connection_url="postgresql://user:pass@host:5432/db")
DatabaseStore(connection_url="postgresql+psycopg2://user:pass@host/db")

# MySQL
DatabaseStore(connection_url="mysql+pymysql://user:pass@host:3306/db")

# SQL Server
DatabaseStore(connection_url="mssql+pyodbc://user:pass@host/db?driver=ODBC+Driver+17")

Enterprise Pooling

The database store includes enterprise-grade connection pooling:

from truthound.stores.backends.database import DatabaseStore, PoolingConfig

store = DatabaseStore(
    connection_url="postgresql://user:pass@localhost/db",
    pooling=PoolingConfig(
        pool_size=10,
        max_overflow=20,
        pool_timeout=30.0,
        pool_recycle=3600,
        pool_pre_ping=True,
        enable_circuit_breaker=True,
        enable_health_checks=True,
        enable_retry=True,
        max_retries=3,
    ),
)

# Check health
if store.is_healthy:
    print("Database connection is healthy")

# Get pool metrics
metrics = store.pool_metrics
if metrics:
    print(f"Active connections: {metrics.active_connections}")

# Get comprehensive status
status = store.get_pool_status()

# Manual connection recycling
recycled = store.recycle_connections()

Retry with Exponential Backoff

from sqlalchemy import text

def insert_and_query(session):
    session.execute(text("INSERT INTO my_table ..."))
    return session.execute(text("SELECT * FROM my_table")).fetchall()

# Automatic retry on transient errors
result = store.execute_with_retry(insert_and_query)

Common Features

Compression

All cloud stores support gzip compression (enabled by default):

# Enabled (default for cloud stores)
store = S3Store(bucket="my-bucket", compression=True)

# Disabled
store = S3Store(bucket="my-bucket", compression=False)

Namespace Organization

# Development namespace
dev_store = S3Store(bucket="my-bucket", namespace="development")

# Production namespace
prod_store = S3Store(bucket="my-bucket", namespace="production")

Error Handling

from truthound.stores.base import (
    StoreConnectionError,
    StoreNotFoundError,
    StoreReadError,
    StoreWriteError,
)

try:
    store.initialize()
except StoreConnectionError as e:
    print(f"Connection failed: {e}")

try:
    result = store.get("nonexistent")
except StoreNotFoundError as e:
    print(f"Not found: {e.item_id}")

Backend Comparison

Feature S3 GCS Azure Blob Database
Compression Yes Yes Yes No
Server-side encryption Yes Yes Yes N/A
Access tiers Yes No Yes N/A
Custom metadata Tags No Yes Yes
Connection pooling N/A N/A N/A Yes
Circuit breaker No No No Yes
Retry with backoff No No No Yes

Next Steps

S3 and GCS also retain failed close handles and reject new IO until an explicit close retry succeeds. An unconfirmed shutdown cannot be reused as a healthy client.