Metadata-Version: 2.4
Name: tide
Version: 0.2.1
Summary: Local-first SQLite sync for Python apps.
Project-URL: Homepage, https://github.com/gauthierpiarrette/tide
Project-URL: Repository, https://github.com/gauthierpiarrette/tide
Project-URL: Issues, https://github.com/gauthierpiarrette/tide/issues
Project-URL: Changelog, https://github.com/gauthierpiarrette/tide/blob/main/CHANGELOG.md
Author: Gauthier Piarrette
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: async,crdt,local-first,offline,replication,sqlite,sync
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pyright>=1.1; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: starlette>=0.37; extra == 'dev'
Provides-Extra: server
Requires-Dist: starlette>=0.37; extra == 'server'
Description-Content-Type: text/markdown

# tide 🌊

**Local-first SQLite sync for Python apps.**

[![PyPI version](https://img.shields.io/pypi/v/tide.svg)](https://pypi.org/project/tide/)
[![Python versions](https://img.shields.io/pypi/pyversions/tide.svg)](https://pypi.org/project/tide/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![CI](https://github.com/gauthierpiarrette/tide/actions/workflows/ci.yml/badge.svg)](https://github.com/gauthierpiarrette/tide/actions/workflows/ci.yml)

`tide` gives a Python app a local SQLite store, a durable outbox of pending
writes, and bidirectional sync against an HTTP server — so your data is
available offline and converges automatically when the network comes back.

```python
import asyncio
from tide import Tide

async def main():
    async with Tide(
        db_path="app.db",
        endpoint="https://example.com/sync",
    ) as tide:
        notes = tide.collection("notes")
        await notes.put("n1", {"title": "Hello", "body": "World"})
        print(await notes.get("n1"))

        async with tide.sync_forever(interval=2.0):
            async for change in notes.watch():
                print(change.source, change.id, change.doc)

asyncio.run(main())
```

No server-side schema, no migrations to manage, no plumbing.

## Install

```bash
pip install tide
# with the optional reference server:
pip install "tide[server]"
```

Python 3.10+.

## Why tide

JavaScript has a thick local-first stack — Yjs, Automerge, Replicache,
ElectricSQL, PowerSync, RxDB, Triplit. Python's local-first story is mostly
low-level CRDT bindings with no integrated engine on top.

If you build with Streamlit, Marimo, Gradio, Textual, FastHTML, NiceGUI,
BeeWare/Briefcase, or any AI agent that needs durable local state that
reconciles with a cloud store, `tide` is the missing piece.

## Features

- **SQLite-backed local store** with WAL mode and a durable outbox of
  pending writes.
- **Last-writer-wins sync** over a single HTTP endpoint, ordered by
  Lamport stamp — two offline clients converge deterministically.
- **Reactive subscriptions** — `async for change in collection.watch()` —
  fire on both local and remote writes.
- **Offline-first**: every write hits the local DB immediately; sync runs
  in the background with automatic retry.
- **Pluggable transport** for WebSocket, gRPC, or fake transports for
  tests.
- **Reference server** (`tide.server`, `[server]` extra) — a Starlette +
  SQLite implementation under 200 lines you can deploy or port to Postgres.
- **Typed, tested, async-first**: `py.typed`, ships with pyright-clean
  source, 36+ tests including crash/retry/replay coverage.

## Quickstart

### Client

```python
import asyncio
from tide import Tide

async def main():
    async with Tide(db_path="app.db", endpoint="http://localhost:8000/sync") as tide:
        todos = tide.collection("todos")
        await todos.put("t1", {"text": "buy bread", "done": False})

        await tide.sync_once()          # one-shot push/pull
        async with tide.sync_forever(interval=1.0):  # background loop
            await asyncio.sleep(10)

asyncio.run(main())
```

### Reference server

```python
# server.py
import uvicorn
from tide.server import create_app

app = create_app("server.db")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

```bash
pip install "tide[server]" uvicorn
python server.py
```

### Auth

`headers` accepts a mapping or a callable (sync or async) — useful for
short-lived tokens:

```python
async def auth_headers():
    return {"Authorization": f"Bearer {await get_token()}"}

Tide(db_path="app.db", endpoint="...", headers=auth_headers)
```

## How it works

Every local write goes into two places atomically: the canonical document
row and a durable outbox. Each write carries a **Lamport stamp** —
`(counter, node_id)` — that totally orders writes across nodes without
relying on wall-clock time. The server accepts a write only if its stamp
beats the stamp it already has for that `(collection, id)`. Plain
last-writer-wins, with deterministic, network-free tie-breaking.

A sync exchange is a single POST:

```
POST /sync
{ "node_id": "...", "cursor": "...", "ops": [...] }
→ { "cursor": "...", "changes": [...] }
```

See [PROTOCOL.md](PROTOCOL.md) for the full wire spec, idempotency
guarantees, and failure semantics.

## Comparison

| | `tide` | `automerge-py` | `pycrdt`¹ | `sqlite-sync` |
|---|---|---|---|---|
| Turnkey sync engine | ✅ | ❌ (low-level primitives) | ❌ | ✅ (Rust extension) |
| Pure Python client | ✅ | ✅ | ❌ (Rust extension) | ❌ |
| Conflict model | LWW + Lamport | CRDT | CRDT | CRDT (SQLite rows) |
| Reference server | ✅ Starlette | ❌ | ❌ | ❌ |
| Pluggable transport | ✅ | ❌ | — | ❌ |

¹ Successor to `y-py`, which is no longer actively maintained.

Use `automerge-py` or `pycrdt` directly when you need true collaborative
editing (concurrent text/JSON merges). Use `tide` when you want
offline-first row sync without writing the plumbing yourself.

## Non-goals (for v0.x)

- **CRDT merges.** v0 is row-level LWW. A separate `tide-crdt` package
  will add Automerge / Yjs-backed merge semantics for documents that need
  it.
- **Schemas and validation.** Documents are plain JSON-serializable dicts.
  Layer Pydantic or msgspec on top.
- **Multi-master writes without a server.** v0 needs a sync endpoint;
  P2P is a future addition.
- **Encryption at rest** and **end-to-end encryption.** Out of scope;
  bring your own.

## Status

**Alpha.** The Python API and wire protocol may change in
backward-incompatible ways until `1.0`. The store schema is versioned and
migrated forward.

## License

[Apache 2.0](LICENSE).
