Metadata-Version: 2.4
Name: tide
Version: 0.2.0
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: MIT License
        
        Copyright (c) 2026 Gauthier Piarrette
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
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 :: MIT 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.**

`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())
```

That's it. No server-side schema, no migrations to manage, no plumbing.

---

## 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.

## What you get in v0.1

- **SQLite-backed local store** with a durable outbox of pending writes.
- **Last-writer-wins sync** over a single HTTP endpoint, ordered by a
  Lamport clock so two offline clients can converge deterministically.
- **Reactive subscriptions** (`async for change in collection.watch()`) that
  fire on both local and remote writes.
- **Offline-first**: every write hits the local DB immediately; sync runs
  in the background.
- **Pluggable transport** if you want WebSocket / gRPC / a fake for tests.
- **Reference server** (`tide.server`, optional `[server]` extra) — a
  Starlette + SQLite implementation in under 200 lines you can deploy or
  port to Postgres.

## Install

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

Python 3.10+.

## 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})

        # One-shot push/pull:
        await tide.sync_once()

        # Or run a background sync loop:
        async with tide.sync_forever(interval=1.0):
            await asyncio.sleep(10)

asyncio.run(main())
```

### Run the 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
```

### Authentication

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

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

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

### Watching changes

```python
async with Tide(...) as tide:
    notes = tide.collection("notes")
    async for change in notes.watch():
        # change.source is "local" or "remote"
        # change.kind is "put" or "delete"
        # change.doc is the new document (or None for delete)
        print(change)
```

`watch()` is cancellation-safe — break out of the loop and the subscription
unregisters automatically.

## Protocol

`tide` speaks a tiny JSON-over-HTTP protocol — one endpoint, one
deterministic conflict rule. See [PROTOCOL.md](PROTOCOL.md) for the full
spec, including idempotency guarantees and failure semantics.

## How it works

Every local write goes into two places atomically: the canonical document
row and a durable **outbox**. The outbox is what the sync loop pushes to the
server.

Each write carries a **Lamport stamp** — a `(counter, node_id)` pair. The
counter monotonically advances both on local writes and when observing
remote writes, so any two writes have a total order all nodes agree on. The
server accepts a write only if its stamp beats the stamp it already has for
that `(collection, id)`. This is plain **last-writer-wins**, but with
deterministic, network-free tie-breaking — no clock drift surprises.

A sync exchange is a single POST:

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

`cursor` is the server's monotonic sequence number; the client persists the
largest `seq` it has seen and replays from there next round.

## Non-goals (for v0.1)

- **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.
- **Browser/WASM clients.** Pure Python today.

These are explicitly deferred so v0 stays small and obvious to reason about.

## Comparison

| | `tide` | `automerge-py` | `pycrdt` / `y-py` | `sqlite-sync` |
|---|---|---|---|---|
| Turnkey sync engine | ✅ | ⚠️ low-level | ❌ | ✅ (Rust ext) |
| Pure Python client | ✅ | ✅ | ⚠️ Rust ext | ❌ |
| Conflict model | LWW + Lamport | CRDT | CRDT | CRDT (SQLite rows) |
| Reference server | ✅ Starlette | ❌ | ❌ | ❌ |
| Pluggable transport | ✅ | ⚠️ | n/a | ❌ |

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.

## Stability

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

## License

MIT
