Skip to main content

3tears Core

Three-tier caching library for Python applications. Provides collections (L1 SQLite -> L2 NATS KV -> L3 PostgreSQL) with subscript access, entity proxy objects, and configurable flush strategies.

Architecture

L1 (SQLite, in-process, sync)  ->  L2 (NATS KV, shared, async)  ->  L3 (PostgreSQL, persistent, async)
  • L1: In-memory SQLite via WAL mode. Sync access. Used by entity attribute reads and writes.
  • L2: NATS KV shared cache. Async. Cross-pod consistency for multi-instance deployments.
  • L3: PostgreSQL (or PostGIS, YugabyteDB, etc.). Async. Source of truth.

Reads promote up the stack (L3 miss -> L2 miss -> L1 hit on next access). Writes flow down (L1 -> L2 -> L3, with optional deferred flush).

Quick Start

1. Configure the Registry

from threetears.core.collections.registry import CollectionRegistry
from threetears.core.cache.sqlite import SQLiteBackend

# Create and configure
l1 = SQLiteBackend("my_app_cache")
l1.initialize(sa_metadata)  # SQLAlchemy metadata with your table definitions

registry = CollectionRegistry()
registry.configure(
    l1_backend=l1,          # SQLiteBackend instance
    l2_client=nats_client,  # NATS client (optional, None to skip L2)
    l3_pool=postgres_pool,  # asyncpg pool
)

2. Per-Collection Pool Overrides

Different collections can use different databases:

# Default: all collections use YugabyteDB
registry.configure(l3_pool=yugabyte_pool)

# Override: geo collection uses PostGIS
registry.configure()  # keep defaults
# When creating the collection, register with override:
geo_collection = GeoCollection(registry, config, nats_client, write_buffer)
registry.register(geo_collection, l3_pool=postgis_pool)

3. Define a Collection

from threetears.core.collections.base import BaseCollection
from threetears.core.entities.base import BaseEntity

class UserEntity(BaseEntity):
    primary_key_field = "user_id"

class UsersCollection(BaseCollection[UserEntity]):
    primary_key_column = "user_id"

    @property
    def table_name(self) -> str:
        return "users"

    @property
    def entity_class(self) -> type[UserEntity]:
        return UserEntity

    async def fetch_from_store(self, entity_id):
        row = await self.l3_pool.fetchrow(
            "SELECT * FROM users WHERE user_id = $1", entity_id
        )
        return dict(row) if row else None

    async def save_to_store(self, data, original_timestamp=None):
        # INSERT or UPDATE with optimistic locking
        ...

    async def delete_from_store(self, entity_id):
        await self.l3_pool.execute(
            "DELETE FROM users WHERE user_id = $1", entity_id
        )

    def serialize(self, data):
        return json.dumps(data, default=str).encode()

    def deserialize(self, data):
        return json.loads(data)

4. Create Collection Instances

from threetears.core.collections.flush import WriteBuffer

write_buffer = WriteBuffer()
users = UsersCollection(registry, config, nats_client, write_buffer)

The config parameter must satisfy the CoreConfig protocol:

class CoreConfig(Protocol):
    collection_flush: str           # "ALWAYS", "ON_CHECKPOINT", "ON_SCHEDULE", "ON_SHUTDOWN"
    collection_flush_interval: int  # seconds between scheduled flushes
    collection_flush_tables: str    # comma-separated table names eligible for deferred flush

Access Patterns

Subscript Access (sync, transparent pull-through)

Subscript access is the primary API. On L1 miss, data is transparently pulled through L2/L3 via a background event loop. No await needed, no ensure() required:

# Read entity -- pulls through L2/L3 automatically on L1 miss
entity = users[user_id]

# Read single field
name = users[user_id, "name_display"]

# Write single field (writes to L1, tracks for flush)
users[user_id, "name_display"] = "New Name"

# Write full entity data (writes dict to L1)
users[user_id] = {"user_id": user_id, "name_display": "New Name", ...}

# Check if entity is in L1 (does NOT pull through -- L1 only)
if user_id in users:
    entity = users[user_id]

__getitem__ raises KeyError only if the entity doesn't exist in any tier. The L1 fast path is ~microseconds; an L1 miss with pull-through adds ~50-200us bridge overhead plus the actual L2/L3 I/O time.

For hot-path code where you want to avoid the sync-async bridge overhead on first access, you can pre-warm L1:

await users.ensure(user_id)  # async: pre-warms L1
entity = users[user_id]       # guaranteed L1 hit, no bridge needed

Async Operations

# Three-tier read: L1 -> L2 -> L3, promotes on miss. Returns None if not found.
entity = await users.get(user_id)

# Create a new entity (not persisted until save)
entity = users.create({"user_id": uuid7(), "name_display": "Alice", ...})

# Save through three-tier write path (L3 -> L1 -> L2)
await users.save_entity(entity)
# Or via entity directly:
await entity.save()

# Reload from L3 (discards local changes)
await entity.reload()

# Delete from all tiers
await users.delete(user_id)

# Invalidate L1 + L2 (force next read to hit L3)
await users.invalidate_cache(user_id)

Entity Attribute Access

Entities are thin cache proxies. Field data lives in L1, not in the entity object.

entity = await users.get(user_id)

# Read (checks entity._changes first, then L1 cache)
print(entity.name_display)

# Write (writes to L1 + tracks change)
entity.name_display = "Updated Name"

# Check dirty state
entity.is_dirty  # True after modification
entity.is_new    # True if created via collection.create()

# Get all changes
entity.get_changes()  # {"name_display": "Updated Name"}

# Export full entity data from L1
entity.to_dict()

# Persist
await entity.save()

Flush Strategies

Controls when deferred writes reach L3 (PostgreSQL):

Strategy Behavior
ALWAYS Every save_entity() writes to L3 immediately
ON_CHECKPOINT Writes buffer to L1 + L2; flushes to L3 on explicit flush_pending() call
ON_SCHEDULE Same as ON_CHECKPOINT but with timer-based auto-flush
ON_SHUTDOWN Writes buffer; flushes to L3 on application shutdown

Only tables listed in collection_flush_tables are eligible for deferred writes. All other tables always write immediately regardless of strategy.

Optimistic Locking

Collections use date_updated for optimistic locking. When saving an existing entity, the save_to_store implementation should check:

UPDATE users SET ... WHERE user_id = $1 AND date_updated = $2

If rows_affected == 0 for an UPDATE, BaseCollection.save_entity() raises ConcurrentModificationError.

Subclassing Guide

BaseEntity: Set primary_key_field to your PK column name. Add computed properties as needed. Do NOT store data in instance attributes. All data lives in L1.

BaseCollection: Set primary_key_column. Implement the 5 abstract methods: fetch_from_store, save_to_store, delete_from_store, serialize, deserialize. Use self.l3_pool for database access. Add domain-specific query methods (e.g., find_by_email).

Download files

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

Source Distribution

3tears-0.21.0.tar.gz (461.8 kB view details)

Uploaded Source

Built Distribution

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

3tears-0.21.0-py3-none-any.whl (310.8 kB view details)

Uploaded Python 3

File details

Details for the file 3tears-0.21.0.tar.gz.

File metadata

  • Download URL: 3tears-0.21.0.tar.gz
  • Upload date:
  • Size: 461.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for 3tears-0.21.0.tar.gz
Algorithm Hash digest
SHA256 325c645cc484d29487bc3ed58481925cca3a47abdb308bd76a27f1c3d41368ba
MD5 99670cfef4d3f3a4674cce1bd19a1b39
BLAKE2b-256 dadf5e459842813efc1d7bc6573aad59e58fd0000c6480a90ef91aac4f72a75c

See more details on using hashes here.

Provenance

The following attestation bundles were made for 3tears-0.21.0.tar.gz:

Publisher: release.yml on pacepace/3tears

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file 3tears-0.21.0-py3-none-any.whl.

File metadata

  • Download URL: 3tears-0.21.0-py3-none-any.whl
  • Upload date:
  • Size: 310.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for 3tears-0.21.0-py3-none-any.whl
Algorithm Hash digest
SHA256 88f604093592cdfa28e44e42e5ad53f8232c9116365c5b5194e3d035f02aba6a
MD5 a762482d141e6c14e5bf269f997207f7
BLAKE2b-256 ff7729c6de0b307b53130783c98555613c4dae0f0ee64a1a06af271776003e1e

See more details on using hashes here.

Provenance

The following attestation bundles were made for 3tears-0.21.0-py3-none-any.whl:

Publisher: release.yml on pacepace/3tears

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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