Metadata-Version: 2.4
Name: memydev-base-sdk
Version: 0.1.0
Summary: Official MemyBase client SDK for Python
License: Proprietary and unlicensed pending approved service terms.
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: editables==0.5; extra == 'dev'
Requires-Dist: hatchling==1.27.0; extra == 'dev'
Requires-Dist: pytest-asyncio==0.24.0; extra == 'dev'
Requires-Dist: pytest==8.3.5; extra == 'dev'
Requires-Dist: respx==0.22.0; extra == 'dev'
Requires-Dist: unasync==0.6.0; extra == 'dev'
Description-Content-Type: text/markdown

# memydev-base-sdk — Official MemyBase Python SDK

Async-first (with sync wrapper) client for the [MemyBase](https://base.memy.dev) hosted data service.
Mirrors the JS `@memydev/base-sdk` selector→handle API: `mb.collection(slug, project=…, database=…)`.

## Install

Install a released package from the customer-accessible Python registry configured for the hosted
MemyBase subscription. Keep the release version explicit in application configuration:

```bash
MEMYBASE_SDK_VERSION="0.1.0"
MEMYBASE_PYTHON_INDEX_URL="<customer Python index URL>"
python -m pip install --index-url "$MEMYBASE_PYTHON_INDEX_URL" "memydev-base-sdk==$MEMYBASE_SDK_VERSION"
```

Do not install this SDK from a workspace path, local wheel, or `file://` dependency in subscriber
applications. A hosted subscription provides the MemyBase service and SDK integration surface; it does
not deliver runtime/source/Devtron/MongoDB components.

Release validation produces both the wheel and source distribution through Hatchling, verifies both with
`twine check`, and installs the wheel into a clean venv from the configured subscriber Python index. The
package includes `py.typed` and both async `memybase.MemyBase` and sync `memybase._sync.MemyBase` surfaces.

## Usage

```python
from memybase import MemyBase

mb = MemyBase("https://base.memy.dev", api_key="…")           # or token=… / user_token=…
users = mb.collection("users", project="acme", database="prod")

rec  = await users.create({"name": "Ada"})                    # omit None keys (reserved-field safe)
page = await users.list(filter={"active": True}, page=1, page_size=50)
one  = await users.get(rec["_id"])
await users.update(rec["_id"], {"name": "Ada L."})
await users.soft_delete(rec["_id"], reason="gdpr")
```

Sync facade: `from memybase.sync import MemyBase` (mechanically generated from the async core via
`scripts/gen_sync.py` — single source, no drift; CRUD + customer management only).

Retry policy mirrors the JS SDK: `retry={"max_retries": 2, "base_delay": 0.5,
"retryable_statuses": []}` by default. Extra retryable statuses apply only to reads; writes stay limited
to 429/503. The legacy constructor shortcut `max_retries=` is still accepted as an alias for
`retry["max_retries"]`.

### The `MemyBaseClient` facade convention (recommended)

Do **not** spray `MemyBase(...)` construction and `except NotFoundError/…` handling across your codebase.
The memy convention — shared by **every** MemyBase consumer — is a **single-seam `MemyBaseClient` facade**:
ONE module wraps ONE `MemyBase` instance, and the rest of the app depends on that facade (never on
`@memydev/base-sdk` / `memybase` directly). This keeps the SDK swappable, centralizes error translation, and
makes call sites read **identically across languages**:

```python
# your_app/data/memybase/client.py — the ONLY module that imports `memybase`
from memybase import MemyBase

class MemyBaseClient:                       # ← the canonical name (same in JS + memybase itself)
    def __init__(self, base_url: str, api_key: str, project: str, db: str) -> None:
        self._mb = MemyBase(base_url, api_key=api_key)
        self._project, self._db = project, db

    def collection(self, slug: str):
        return self._mb.collection(slug, project=self._project, database=self._db)
    # + None-on-404 reads, bool-returning deletes, a `normalize_filter` chokepoint, SDK→app error
    #   translation — whatever adaptations the SDK does not provide.

client = MemyBaseClient(base_url, api_key, project="acme", db="prod")   # usage reads the same everywhere
```

The **same decision** is implemented in every project: `class MemyBaseClient` in memyswarm
(`data/memybase/client.py`) and this SDK's Python peer; `class MemyBaseClient` in memyui
(`apps/server/src/memybase/client.ts`); and `class MemyBaseClient` in memybase's own stdio-MCP consumer
(`src/mcp/memybase-client.ts`). Reference implementation: memyswarm's `MemyBaseClient`.

> A **producer/engine** (the tier holding the DB connection) is the ONE exception: in-process code binds
> the data services directly and must NOT loop back through the SDK. `MemyBaseClient` is for **consumers**
> — anything reaching the engine over the wire (apps, workers, MCP-over-stdio, a separate frontend).

### Realtime (SSE)

Subscribe to live change events over Server-Sent Events (async only — SSE is inherently async; the sync
facade does not expose it). The callback fires for `ready` / `change` / `heartbeat` / `resync` events;
the client auto-reconnects with backoff and resumes via `Last-Event-ID`.

```python
async with MemyBase("https://base.memy.dev", api_key="…") as mb:
    def on_event(e):        # e: StreamEvent(event, data, id)
        if e.event == "change":
            print(e.data["type"], e.data["entity"], e.data["id"])

    sub = await mb.collection("users", project="acme", database="prod").subscribe(on_event)
    # … or all entities: sub = await mb.stream("acme", "prod", on_event)
    await sub.wait()        # run until closed / terminal
    await sub.close()       # stop + drain
```

## Surface

- **Data plane** — `collection(slug, project=, database=)` → list/get/create/update/soft_delete/restore/versions/audit/hard_delete
- **Realtime** — `collection(…).subscribe(on_event)` / `mb.stream(project, database, on_event)` → SSE (async only): `RealtimeClient`, `StreamEvent`, `SubscribeOptions`, `Subscription`
- **Customer management** — `SelfService` for projects, databases, schema, credentials, settings, plans, usage, and lifecycle
- **GraphQL** — `mb.graphql(project=, database=)`
- **Filters** — `FilterBuilder`, `OPERATORS`, `serialize_filter`
- **Errors** — typed hierarchy (`MemyBaseError` + `Conflict`/`NotFound`/`Validation`/`RateLimited`/… `decode_error`)
- **Helpers** — `with_conflict_retry`, `guard_reserved_fields`, `omit_none`, `RESERVED_FIELDS`

Reserved fields (`_id`, `createdAt`, `updatedAt`, …) are never sent on write; string fields are non-nullable — omit a key rather than sending `null`.

## Versioning

`memybase.__version__` tracks `SDK_VERSION`. Vendored artifacts are regenerated by `memybase/scripts/sdk-pack.sh` (packs this wheel + the JS tgz into their consumer repos).
