memydev-base-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- memydev_base_sdk-0.1.0/.gitignore +5 -0
- memydev_base_sdk-0.1.0/PKG-INFO +130 -0
- memydev_base_sdk-0.1.0/README.md +114 -0
- memydev_base_sdk-0.1.0/pyproject.toml +33 -0
- memydev_base_sdk-0.1.0/requirements-release.in +12 -0
- memydev_base_sdk-0.1.0/requirements-release.lock +634 -0
- memydev_base_sdk-0.1.0/scripts/gen_sync.py +123 -0
- memydev_base_sdk-0.1.0/src/memybase/__init__.py +88 -0
- memydev_base_sdk-0.1.0/src/memybase/_client.py +233 -0
- memydev_base_sdk-0.1.0/src/memybase/_collection.py +411 -0
- memydev_base_sdk-0.1.0/src/memybase/_errors.py +232 -0
- memydev_base_sdk-0.1.0/src/memybase/_filter.py +86 -0
- memydev_base_sdk-0.1.0/src/memybase/_graphql.py +49 -0
- memydev_base_sdk-0.1.0/src/memybase/_helpers.py +35 -0
- memydev_base_sdk-0.1.0/src/memybase/_mapping.py +55 -0
- memydev_base_sdk-0.1.0/src/memybase/_realtime.py +295 -0
- memydev_base_sdk-0.1.0/src/memybase/_self.py +137 -0
- memydev_base_sdk-0.1.0/src/memybase/_sync/__init__.py +3 -0
- memydev_base_sdk-0.1.0/src/memybase/_sync/_client.py +219 -0
- memydev_base_sdk-0.1.0/src/memybase/_sync/_collection.py +383 -0
- memydev_base_sdk-0.1.0/src/memybase/_sync/_graphql.py +51 -0
- memydev_base_sdk-0.1.0/src/memybase/_sync/_helpers.py +37 -0
- memydev_base_sdk-0.1.0/src/memybase/_sync/_self.py +139 -0
- memydev_base_sdk-0.1.0/src/memybase/_sync/_transport.py +223 -0
- memydev_base_sdk-0.1.0/src/memybase/_transport.py +257 -0
- memydev_base_sdk-0.1.0/src/memybase/_types.py +91 -0
- memydev_base_sdk-0.1.0/src/memybase/py.typed +0 -0
- memydev_base_sdk-0.1.0/src/memybase/sync/__init__.py +41 -0
- memydev_base_sdk-0.1.0/tests/test_client.py +102 -0
- memydev_base_sdk-0.1.0/tests/test_collection.py +251 -0
- memydev_base_sdk-0.1.0/tests/test_contract_parity.py +101 -0
- memydev_base_sdk-0.1.0/tests/test_errors.py +77 -0
- memydev_base_sdk-0.1.0/tests/test_filter.py +64 -0
- memydev_base_sdk-0.1.0/tests/test_graphql.py +29 -0
- memydev_base_sdk-0.1.0/tests/test_helpers.py +47 -0
- memydev_base_sdk-0.1.0/tests/test_mapping.py +45 -0
- memydev_base_sdk-0.1.0/tests/test_realtime.py +386 -0
- memydev_base_sdk-0.1.0/tests/test_self.py +71 -0
- memydev_base_sdk-0.1.0/tests/test_sync_functional.py +44 -0
- memydev_base_sdk-0.1.0/tests/test_sync_parity.py +71 -0
- memydev_base_sdk-0.1.0/tests/test_transport.py +206 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memydev-base-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official MemyBase client SDK for Python
|
|
5
|
+
License: Proprietary and unlicensed pending approved service terms.
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx<1,>=0.27
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: editables==0.5; extra == 'dev'
|
|
10
|
+
Requires-Dist: hatchling==1.27.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest-asyncio==0.24.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest==8.3.5; extra == 'dev'
|
|
13
|
+
Requires-Dist: respx==0.22.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: unasync==0.6.0; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# memydev-base-sdk — Official MemyBase Python SDK
|
|
18
|
+
|
|
19
|
+
Async-first (with sync wrapper) client for the [MemyBase](https://base.memy.dev) hosted data service.
|
|
20
|
+
Mirrors the JS `@memydev/base-sdk` selector→handle API: `mb.collection(slug, project=…, database=…)`.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
Install a released package from the customer-accessible Python registry configured for the hosted
|
|
25
|
+
MemyBase subscription. Keep the release version explicit in application configuration:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
MEMYBASE_SDK_VERSION="0.1.0"
|
|
29
|
+
MEMYBASE_PYTHON_INDEX_URL="<customer Python index URL>"
|
|
30
|
+
python -m pip install --index-url "$MEMYBASE_PYTHON_INDEX_URL" "memydev-base-sdk==$MEMYBASE_SDK_VERSION"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Do not install this SDK from a workspace path, local wheel, or `file://` dependency in subscriber
|
|
34
|
+
applications. A hosted subscription provides the MemyBase service and SDK integration surface; it does
|
|
35
|
+
not deliver runtime/source/Devtron/MongoDB components.
|
|
36
|
+
|
|
37
|
+
Release validation produces both the wheel and source distribution through Hatchling, verifies both with
|
|
38
|
+
`twine check`, and installs the wheel into a clean venv from the configured subscriber Python index. The
|
|
39
|
+
package includes `py.typed` and both async `memybase.MemyBase` and sync `memybase._sync.MemyBase` surfaces.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from memybase import MemyBase
|
|
45
|
+
|
|
46
|
+
mb = MemyBase("https://base.memy.dev", api_key="…") # or token=… / user_token=…
|
|
47
|
+
users = mb.collection("users", project="acme", database="prod")
|
|
48
|
+
|
|
49
|
+
rec = await users.create({"name": "Ada"}) # omit None keys (reserved-field safe)
|
|
50
|
+
page = await users.list(filter={"active": True}, page=1, page_size=50)
|
|
51
|
+
one = await users.get(rec["_id"])
|
|
52
|
+
await users.update(rec["_id"], {"name": "Ada L."})
|
|
53
|
+
await users.soft_delete(rec["_id"], reason="gdpr")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Sync facade: `from memybase.sync import MemyBase` (mechanically generated from the async core via
|
|
57
|
+
`scripts/gen_sync.py` — single source, no drift; CRUD + customer management only).
|
|
58
|
+
|
|
59
|
+
Retry policy mirrors the JS SDK: `retry={"max_retries": 2, "base_delay": 0.5,
|
|
60
|
+
"retryable_statuses": []}` by default. Extra retryable statuses apply only to reads; writes stay limited
|
|
61
|
+
to 429/503. The legacy constructor shortcut `max_retries=` is still accepted as an alias for
|
|
62
|
+
`retry["max_retries"]`.
|
|
63
|
+
|
|
64
|
+
### The `MemyBaseClient` facade convention (recommended)
|
|
65
|
+
|
|
66
|
+
Do **not** spray `MemyBase(...)` construction and `except NotFoundError/…` handling across your codebase.
|
|
67
|
+
The memy convention — shared by **every** MemyBase consumer — is a **single-seam `MemyBaseClient` facade**:
|
|
68
|
+
ONE module wraps ONE `MemyBase` instance, and the rest of the app depends on that facade (never on
|
|
69
|
+
`@memydev/base-sdk` / `memybase` directly). This keeps the SDK swappable, centralizes error translation, and
|
|
70
|
+
makes call sites read **identically across languages**:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# your_app/data/memybase/client.py — the ONLY module that imports `memybase`
|
|
74
|
+
from memybase import MemyBase
|
|
75
|
+
|
|
76
|
+
class MemyBaseClient: # ← the canonical name (same in JS + memybase itself)
|
|
77
|
+
def __init__(self, base_url: str, api_key: str, project: str, db: str) -> None:
|
|
78
|
+
self._mb = MemyBase(base_url, api_key=api_key)
|
|
79
|
+
self._project, self._db = project, db
|
|
80
|
+
|
|
81
|
+
def collection(self, slug: str):
|
|
82
|
+
return self._mb.collection(slug, project=self._project, database=self._db)
|
|
83
|
+
# + None-on-404 reads, bool-returning deletes, a `normalize_filter` chokepoint, SDK→app error
|
|
84
|
+
# translation — whatever adaptations the SDK does not provide.
|
|
85
|
+
|
|
86
|
+
client = MemyBaseClient(base_url, api_key, project="acme", db="prod") # usage reads the same everywhere
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The **same decision** is implemented in every project: `class MemyBaseClient` in memyswarm
|
|
90
|
+
(`data/memybase/client.py`) and this SDK's Python peer; `class MemyBaseClient` in memyui
|
|
91
|
+
(`apps/server/src/memybase/client.ts`); and `class MemyBaseClient` in memybase's own stdio-MCP consumer
|
|
92
|
+
(`src/mcp/memybase-client.ts`). Reference implementation: memyswarm's `MemyBaseClient`.
|
|
93
|
+
|
|
94
|
+
> A **producer/engine** (the tier holding the DB connection) is the ONE exception: in-process code binds
|
|
95
|
+
> the data services directly and must NOT loop back through the SDK. `MemyBaseClient` is for **consumers**
|
|
96
|
+
> — anything reaching the engine over the wire (apps, workers, MCP-over-stdio, a separate frontend).
|
|
97
|
+
|
|
98
|
+
### Realtime (SSE)
|
|
99
|
+
|
|
100
|
+
Subscribe to live change events over Server-Sent Events (async only — SSE is inherently async; the sync
|
|
101
|
+
facade does not expose it). The callback fires for `ready` / `change` / `heartbeat` / `resync` events;
|
|
102
|
+
the client auto-reconnects with backoff and resumes via `Last-Event-ID`.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
async with MemyBase("https://base.memy.dev", api_key="…") as mb:
|
|
106
|
+
def on_event(e): # e: StreamEvent(event, data, id)
|
|
107
|
+
if e.event == "change":
|
|
108
|
+
print(e.data["type"], e.data["entity"], e.data["id"])
|
|
109
|
+
|
|
110
|
+
sub = await mb.collection("users", project="acme", database="prod").subscribe(on_event)
|
|
111
|
+
# … or all entities: sub = await mb.stream("acme", "prod", on_event)
|
|
112
|
+
await sub.wait() # run until closed / terminal
|
|
113
|
+
await sub.close() # stop + drain
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Surface
|
|
117
|
+
|
|
118
|
+
- **Data plane** — `collection(slug, project=, database=)` → list/get/create/update/soft_delete/restore/versions/audit/hard_delete
|
|
119
|
+
- **Realtime** — `collection(…).subscribe(on_event)` / `mb.stream(project, database, on_event)` → SSE (async only): `RealtimeClient`, `StreamEvent`, `SubscribeOptions`, `Subscription`
|
|
120
|
+
- **Customer management** — `SelfService` for projects, databases, schema, credentials, settings, plans, usage, and lifecycle
|
|
121
|
+
- **GraphQL** — `mb.graphql(project=, database=)`
|
|
122
|
+
- **Filters** — `FilterBuilder`, `OPERATORS`, `serialize_filter`
|
|
123
|
+
- **Errors** — typed hierarchy (`MemyBaseError` + `Conflict`/`NotFound`/`Validation`/`RateLimited`/… `decode_error`)
|
|
124
|
+
- **Helpers** — `with_conflict_retry`, `guard_reserved_fields`, `omit_none`, `RESERVED_FIELDS`
|
|
125
|
+
|
|
126
|
+
Reserved fields (`_id`, `createdAt`, `updatedAt`, …) are never sent on write; string fields are non-nullable — omit a key rather than sending `null`.
|
|
127
|
+
|
|
128
|
+
## Versioning
|
|
129
|
+
|
|
130
|
+
`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).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# memydev-base-sdk — Official MemyBase Python SDK
|
|
2
|
+
|
|
3
|
+
Async-first (with sync wrapper) client for the [MemyBase](https://base.memy.dev) hosted data service.
|
|
4
|
+
Mirrors the JS `@memydev/base-sdk` selector→handle API: `mb.collection(slug, project=…, database=…)`.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
Install a released package from the customer-accessible Python registry configured for the hosted
|
|
9
|
+
MemyBase subscription. Keep the release version explicit in application configuration:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
MEMYBASE_SDK_VERSION="0.1.0"
|
|
13
|
+
MEMYBASE_PYTHON_INDEX_URL="<customer Python index URL>"
|
|
14
|
+
python -m pip install --index-url "$MEMYBASE_PYTHON_INDEX_URL" "memydev-base-sdk==$MEMYBASE_SDK_VERSION"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Do not install this SDK from a workspace path, local wheel, or `file://` dependency in subscriber
|
|
18
|
+
applications. A hosted subscription provides the MemyBase service and SDK integration surface; it does
|
|
19
|
+
not deliver runtime/source/Devtron/MongoDB components.
|
|
20
|
+
|
|
21
|
+
Release validation produces both the wheel and source distribution through Hatchling, verifies both with
|
|
22
|
+
`twine check`, and installs the wheel into a clean venv from the configured subscriber Python index. The
|
|
23
|
+
package includes `py.typed` and both async `memybase.MemyBase` and sync `memybase._sync.MemyBase` surfaces.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from memybase import MemyBase
|
|
29
|
+
|
|
30
|
+
mb = MemyBase("https://base.memy.dev", api_key="…") # or token=… / user_token=…
|
|
31
|
+
users = mb.collection("users", project="acme", database="prod")
|
|
32
|
+
|
|
33
|
+
rec = await users.create({"name": "Ada"}) # omit None keys (reserved-field safe)
|
|
34
|
+
page = await users.list(filter={"active": True}, page=1, page_size=50)
|
|
35
|
+
one = await users.get(rec["_id"])
|
|
36
|
+
await users.update(rec["_id"], {"name": "Ada L."})
|
|
37
|
+
await users.soft_delete(rec["_id"], reason="gdpr")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Sync facade: `from memybase.sync import MemyBase` (mechanically generated from the async core via
|
|
41
|
+
`scripts/gen_sync.py` — single source, no drift; CRUD + customer management only).
|
|
42
|
+
|
|
43
|
+
Retry policy mirrors the JS SDK: `retry={"max_retries": 2, "base_delay": 0.5,
|
|
44
|
+
"retryable_statuses": []}` by default. Extra retryable statuses apply only to reads; writes stay limited
|
|
45
|
+
to 429/503. The legacy constructor shortcut `max_retries=` is still accepted as an alias for
|
|
46
|
+
`retry["max_retries"]`.
|
|
47
|
+
|
|
48
|
+
### The `MemyBaseClient` facade convention (recommended)
|
|
49
|
+
|
|
50
|
+
Do **not** spray `MemyBase(...)` construction and `except NotFoundError/…` handling across your codebase.
|
|
51
|
+
The memy convention — shared by **every** MemyBase consumer — is a **single-seam `MemyBaseClient` facade**:
|
|
52
|
+
ONE module wraps ONE `MemyBase` instance, and the rest of the app depends on that facade (never on
|
|
53
|
+
`@memydev/base-sdk` / `memybase` directly). This keeps the SDK swappable, centralizes error translation, and
|
|
54
|
+
makes call sites read **identically across languages**:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# your_app/data/memybase/client.py — the ONLY module that imports `memybase`
|
|
58
|
+
from memybase import MemyBase
|
|
59
|
+
|
|
60
|
+
class MemyBaseClient: # ← the canonical name (same in JS + memybase itself)
|
|
61
|
+
def __init__(self, base_url: str, api_key: str, project: str, db: str) -> None:
|
|
62
|
+
self._mb = MemyBase(base_url, api_key=api_key)
|
|
63
|
+
self._project, self._db = project, db
|
|
64
|
+
|
|
65
|
+
def collection(self, slug: str):
|
|
66
|
+
return self._mb.collection(slug, project=self._project, database=self._db)
|
|
67
|
+
# + None-on-404 reads, bool-returning deletes, a `normalize_filter` chokepoint, SDK→app error
|
|
68
|
+
# translation — whatever adaptations the SDK does not provide.
|
|
69
|
+
|
|
70
|
+
client = MemyBaseClient(base_url, api_key, project="acme", db="prod") # usage reads the same everywhere
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The **same decision** is implemented in every project: `class MemyBaseClient` in memyswarm
|
|
74
|
+
(`data/memybase/client.py`) and this SDK's Python peer; `class MemyBaseClient` in memyui
|
|
75
|
+
(`apps/server/src/memybase/client.ts`); and `class MemyBaseClient` in memybase's own stdio-MCP consumer
|
|
76
|
+
(`src/mcp/memybase-client.ts`). Reference implementation: memyswarm's `MemyBaseClient`.
|
|
77
|
+
|
|
78
|
+
> A **producer/engine** (the tier holding the DB connection) is the ONE exception: in-process code binds
|
|
79
|
+
> the data services directly and must NOT loop back through the SDK. `MemyBaseClient` is for **consumers**
|
|
80
|
+
> — anything reaching the engine over the wire (apps, workers, MCP-over-stdio, a separate frontend).
|
|
81
|
+
|
|
82
|
+
### Realtime (SSE)
|
|
83
|
+
|
|
84
|
+
Subscribe to live change events over Server-Sent Events (async only — SSE is inherently async; the sync
|
|
85
|
+
facade does not expose it). The callback fires for `ready` / `change` / `heartbeat` / `resync` events;
|
|
86
|
+
the client auto-reconnects with backoff and resumes via `Last-Event-ID`.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
async with MemyBase("https://base.memy.dev", api_key="…") as mb:
|
|
90
|
+
def on_event(e): # e: StreamEvent(event, data, id)
|
|
91
|
+
if e.event == "change":
|
|
92
|
+
print(e.data["type"], e.data["entity"], e.data["id"])
|
|
93
|
+
|
|
94
|
+
sub = await mb.collection("users", project="acme", database="prod").subscribe(on_event)
|
|
95
|
+
# … or all entities: sub = await mb.stream("acme", "prod", on_event)
|
|
96
|
+
await sub.wait() # run until closed / terminal
|
|
97
|
+
await sub.close() # stop + drain
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Surface
|
|
101
|
+
|
|
102
|
+
- **Data plane** — `collection(slug, project=, database=)` → list/get/create/update/soft_delete/restore/versions/audit/hard_delete
|
|
103
|
+
- **Realtime** — `collection(…).subscribe(on_event)` / `mb.stream(project, database, on_event)` → SSE (async only): `RealtimeClient`, `StreamEvent`, `SubscribeOptions`, `Subscription`
|
|
104
|
+
- **Customer management** — `SelfService` for projects, databases, schema, credentials, settings, plans, usage, and lifecycle
|
|
105
|
+
- **GraphQL** — `mb.graphql(project=, database=)`
|
|
106
|
+
- **Filters** — `FilterBuilder`, `OPERATORS`, `serialize_filter`
|
|
107
|
+
- **Errors** — typed hierarchy (`MemyBaseError` + `Conflict`/`NotFound`/`Validation`/`RateLimited`/… `decode_error`)
|
|
108
|
+
- **Helpers** — `with_conflict_retry`, `guard_reserved_fields`, `omit_none`, `RESERVED_FIELDS`
|
|
109
|
+
|
|
110
|
+
Reserved fields (`_id`, `createdAt`, `updatedAt`, …) are never sent on write; string fields are non-nullable — omit a key rather than sending `null`.
|
|
111
|
+
|
|
112
|
+
## Versioning
|
|
113
|
+
|
|
114
|
+
`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).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "memydev-base-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official MemyBase client SDK for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "Proprietary and unlicensed pending approved service terms."}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27,<1",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"editables==0.5",
|
|
19
|
+
"hatchling==1.27.0",
|
|
20
|
+
"pytest==8.3.5",
|
|
21
|
+
"pytest-asyncio==0.24.0",
|
|
22
|
+
"respx==0.22.0",
|
|
23
|
+
"unasync==0.6.0", # generates the sync twins in _sync/ (scripts/gen_sync.py); dev-only — the
|
|
24
|
+
# generated files are committed. Pulls tokenize-rt transitively.
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/memybase"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
asyncio_mode = "auto"
|
|
32
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
33
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Exact top-level inputs for the Python SDK release and subscriber-install gate.
|
|
2
|
+
# Compile with hashes into requirements-release.lock; the protected-tag workflow installs only that lock.
|
|
3
|
+
build==1.2.2.post1
|
|
4
|
+
editables==0.5
|
|
5
|
+
hatchling==1.27.0
|
|
6
|
+
httpx==0.27.2
|
|
7
|
+
pip-audit==2.8.0
|
|
8
|
+
pytest==8.3.5
|
|
9
|
+
pytest-asyncio==0.24.0
|
|
10
|
+
respx==0.22.0
|
|
11
|
+
twine==6.1.0
|
|
12
|
+
unasync==0.6.0
|