memydev-base-sdk 0.1.0__py3-none-any.whl
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.
- memybase/__init__.py +88 -0
- memybase/_client.py +233 -0
- memybase/_collection.py +411 -0
- memybase/_errors.py +232 -0
- memybase/_filter.py +86 -0
- memybase/_graphql.py +49 -0
- memybase/_helpers.py +35 -0
- memybase/_mapping.py +55 -0
- memybase/_realtime.py +295 -0
- memybase/_self.py +137 -0
- memybase/_sync/__init__.py +3 -0
- memybase/_sync/_client.py +219 -0
- memybase/_sync/_collection.py +383 -0
- memybase/_sync/_graphql.py +51 -0
- memybase/_sync/_helpers.py +37 -0
- memybase/_sync/_self.py +139 -0
- memybase/_sync/_transport.py +223 -0
- memybase/_transport.py +257 -0
- memybase/_types.py +91 -0
- memybase/py.typed +0 -0
- memybase/sync/__init__.py +41 -0
- memydev_base_sdk-0.1.0.dist-info/METADATA +130 -0
- memydev_base_sdk-0.1.0.dist-info/RECORD +24 -0
- memydev_base_sdk-0.1.0.dist-info/WHEEL +4 -0
memybase/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@fileoverview Official MemyBase Python SDK — async-first with sync wrapper.
|
|
3
|
+
@module memybase
|
|
4
|
+
@description Public entrypoint. Async: `memybase.MemyBase`. Sync: `memybase.sync.MemyBase`.
|
|
5
|
+
@created 2026-07-04
|
|
6
|
+
"""
|
|
7
|
+
from ._client import MemyBase, SDK_VERSION
|
|
8
|
+
from ._collection import Collection
|
|
9
|
+
from ._errors import (
|
|
10
|
+
MemyBaseError,
|
|
11
|
+
ConflictError,
|
|
12
|
+
ForbiddenError,
|
|
13
|
+
NetworkError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
QuotaExceededError,
|
|
16
|
+
QueryTimeoutError,
|
|
17
|
+
RateLimitedError,
|
|
18
|
+
ReservedFieldError,
|
|
19
|
+
SchemaError,
|
|
20
|
+
ServiceUnavailableError,
|
|
21
|
+
TimeoutError,
|
|
22
|
+
UnauthorizedError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
decode_error,
|
|
25
|
+
)
|
|
26
|
+
from ._filter import FilterBuilder, OPERATORS, serialize_filter
|
|
27
|
+
from ._graphql import GraphQLClient
|
|
28
|
+
from ._helpers import with_conflict_retry
|
|
29
|
+
from ._mapping import (
|
|
30
|
+
CUSTOMER_WRITABLE_RESERVED_FIELDS,
|
|
31
|
+
RESERVED_FIELDS,
|
|
32
|
+
SCHEMA_RESERVED_FIELDS,
|
|
33
|
+
guard_reserved_fields,
|
|
34
|
+
omit_none,
|
|
35
|
+
)
|
|
36
|
+
from ._realtime import RealtimeClient, StreamEvent, SubscribeOptions, Subscription
|
|
37
|
+
from ._self import SelfService
|
|
38
|
+
from ._transport import RetryConfig
|
|
39
|
+
from ._types import (
|
|
40
|
+
ExpandMeta,
|
|
41
|
+
ExpandRelationMeta,
|
|
42
|
+
SchemaActivationJobState,
|
|
43
|
+
SchemaActivationJobSummary,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__version__ = SDK_VERSION
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"MemyBase",
|
|
50
|
+
"Collection",
|
|
51
|
+
"FilterBuilder",
|
|
52
|
+
"GraphQLClient",
|
|
53
|
+
"SelfService",
|
|
54
|
+
"RealtimeClient",
|
|
55
|
+
"StreamEvent",
|
|
56
|
+
"SubscribeOptions",
|
|
57
|
+
"Subscription",
|
|
58
|
+
"RetryConfig",
|
|
59
|
+
"MemyBaseError",
|
|
60
|
+
"ConflictError",
|
|
61
|
+
"ForbiddenError",
|
|
62
|
+
"NetworkError",
|
|
63
|
+
"NotFoundError",
|
|
64
|
+
"QuotaExceededError",
|
|
65
|
+
"QueryTimeoutError",
|
|
66
|
+
"RateLimitedError",
|
|
67
|
+
"ReservedFieldError",
|
|
68
|
+
"SchemaError",
|
|
69
|
+
"ServiceUnavailableError",
|
|
70
|
+
"TimeoutError",
|
|
71
|
+
"UnauthorizedError",
|
|
72
|
+
"ValidationError",
|
|
73
|
+
"decode_error",
|
|
74
|
+
"with_conflict_retry",
|
|
75
|
+
"guard_reserved_fields",
|
|
76
|
+
"omit_none",
|
|
77
|
+
"OPERATORS",
|
|
78
|
+
"RESERVED_FIELDS",
|
|
79
|
+
"SCHEMA_RESERVED_FIELDS",
|
|
80
|
+
"CUSTOMER_WRITABLE_RESERVED_FIELDS",
|
|
81
|
+
"serialize_filter",
|
|
82
|
+
"SDK_VERSION",
|
|
83
|
+
"__version__",
|
|
84
|
+
"ExpandMeta",
|
|
85
|
+
"ExpandRelationMeta",
|
|
86
|
+
"SchemaActivationJobState",
|
|
87
|
+
"SchemaActivationJobSummary",
|
|
88
|
+
]
|
memybase/_client.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@fileoverview Top-level MemyBase async client.
|
|
3
|
+
@module memybase._client
|
|
4
|
+
@description Entry point: MemyBase(base_url, api_key=...) → .collection("slug", project=...) →
|
|
5
|
+
CRUD. Exposes customer management via .self, and GraphQL via .graphql().
|
|
6
|
+
Includes key-gated discovery helpers for descriptors and OpenAPI.
|
|
7
|
+
@created 2026-07-04
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Callable, Optional
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
from ._collection import Collection
|
|
15
|
+
from ._graphql import GraphQLClient
|
|
16
|
+
from ._self import SelfService
|
|
17
|
+
from ._transport import Transport, RetryConfig
|
|
18
|
+
|
|
19
|
+
__all__ = ["MemyBase"]
|
|
20
|
+
|
|
21
|
+
SDK_VERSION = "0.1.0"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _idempotency_headers(idempotency_key: Optional[str] = None) -> Optional[dict[str, str]]:
|
|
25
|
+
return {"Idempotency-Key": idempotency_key} if idempotency_key is not None else None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MemyBase:
|
|
29
|
+
"""Async MemyBase client.
|
|
30
|
+
|
|
31
|
+
Usage::
|
|
32
|
+
|
|
33
|
+
async with MemyBase("https://memybase.example.com", api_key="pbk_...") as mb:
|
|
34
|
+
customers = mb.collection("customers", project="crm", database="analytics")
|
|
35
|
+
page = await customers.list(page_size=10)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
base_url: str,
|
|
41
|
+
*,
|
|
42
|
+
api_key: Optional[str] = None,
|
|
43
|
+
token: Optional[str] = None,
|
|
44
|
+
user_token: Optional[str] = None,
|
|
45
|
+
cookie: Optional[str] = None,
|
|
46
|
+
headers: Optional[dict[str, str]] = None,
|
|
47
|
+
timeout: float = 30.0,
|
|
48
|
+
max_retries: Optional[int] = None,
|
|
49
|
+
retry: Optional[RetryConfig] = None,
|
|
50
|
+
transport: Optional[Any] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._transport = Transport(
|
|
53
|
+
base_url,
|
|
54
|
+
api_key=api_key,
|
|
55
|
+
token=token,
|
|
56
|
+
user_token=user_token,
|
|
57
|
+
cookie=cookie,
|
|
58
|
+
headers=headers,
|
|
59
|
+
timeout=timeout,
|
|
60
|
+
max_retries=max_retries,
|
|
61
|
+
retry=retry,
|
|
62
|
+
transport=transport,
|
|
63
|
+
)
|
|
64
|
+
self._self = SelfService(self._transport)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _data_prefix(project: str, database: Optional[str]) -> str:
|
|
68
|
+
"""The data-plane path prefix for a project/database — multi-db (/p/{p}/d/{db}) or legacy
|
|
69
|
+
(/projects/{p}). The `_batch`, `admin/schema`, and `openapi.json` routes hang off it."""
|
|
70
|
+
if database:
|
|
71
|
+
return f"/api/v1/p/{quote(project, safe='')}/d/{quote(database, safe='')}"
|
|
72
|
+
return f"/api/v1/projects/{quote(project, safe='')}"
|
|
73
|
+
|
|
74
|
+
def collection(
|
|
75
|
+
self,
|
|
76
|
+
slug: str,
|
|
77
|
+
*,
|
|
78
|
+
project: str,
|
|
79
|
+
database: Optional[str] = None,
|
|
80
|
+
) -> Collection:
|
|
81
|
+
"""Get a handle for a data-plane entity collection."""
|
|
82
|
+
return Collection(
|
|
83
|
+
self._transport,
|
|
84
|
+
slug,
|
|
85
|
+
project=project,
|
|
86
|
+
database=database,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def graphql(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
project: str,
|
|
93
|
+
database: Optional[str] = None,
|
|
94
|
+
) -> GraphQLClient:
|
|
95
|
+
"""Get a GraphQL client for a specific project."""
|
|
96
|
+
return GraphQLClient(
|
|
97
|
+
self._transport,
|
|
98
|
+
project=project,
|
|
99
|
+
database=database,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def batch(
|
|
103
|
+
self,
|
|
104
|
+
project: str,
|
|
105
|
+
database: Optional[str],
|
|
106
|
+
operations: list[dict[str, Any]],
|
|
107
|
+
*,
|
|
108
|
+
idempotency_key: Optional[str] = None,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
"""Cross-entity atomic batch write → POST {prefix}/_batch.
|
|
111
|
+
|
|
112
|
+
``operations`` is an ordered list of ``{op, entity, id?, data?, reason?}`` (each op names its own
|
|
113
|
+
entity), applied all-or-nothing in ONE placement transaction; the first failing op aborts the
|
|
114
|
+
batch (its error naming the op index). Use ``collection.bulk`` for the single-entity variant.
|
|
115
|
+
"""
|
|
116
|
+
path = f"{self._data_prefix(project, database)}/_batch"
|
|
117
|
+
return await self._transport.request(
|
|
118
|
+
"POST", path, json_body={"operations": operations}, headers=_idempotency_headers(idempotency_key)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def prepare_ai_proposal(
|
|
122
|
+
self,
|
|
123
|
+
project: str,
|
|
124
|
+
database: Optional[str],
|
|
125
|
+
input: dict[str, Any],
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
"""Prepare a project/database-scoped AI mutation for approval.
|
|
128
|
+
|
|
129
|
+
Non-AI credentials return ``{"status": "not_applicable"}``; AI credentials return the durable
|
|
130
|
+
approval-required proposal envelope and never execute the mutation here.
|
|
131
|
+
"""
|
|
132
|
+
return await self._transport.request(
|
|
133
|
+
"POST", f"{self._data_prefix(project, database)}/ai-proposals/prepare", json_body=input
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def get_ai_proposal(self, project: str, database: Optional[str], proposal_id: str) -> dict[str, Any]:
|
|
137
|
+
"""Read one proposal scoped by the addressed project and database."""
|
|
138
|
+
path = f"{self._data_prefix(project, database)}/ai-proposals/{quote(proposal_id, safe='')}"
|
|
139
|
+
return await self._transport.request("GET", path)
|
|
140
|
+
|
|
141
|
+
async def approve_ai_proposal(
|
|
142
|
+
self,
|
|
143
|
+
project: str,
|
|
144
|
+
database: Optional[str],
|
|
145
|
+
proposal_id: str,
|
|
146
|
+
*,
|
|
147
|
+
reason: Optional[str] = None,
|
|
148
|
+
policy_decision_reason: Optional[str] = None,
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
"""Approve and execute once; the server enforces non-AI approval policy."""
|
|
151
|
+
body = {
|
|
152
|
+
key: value
|
|
153
|
+
for key, value in {
|
|
154
|
+
"reason": reason,
|
|
155
|
+
"policyDecisionReason": policy_decision_reason,
|
|
156
|
+
}.items()
|
|
157
|
+
if value is not None
|
|
158
|
+
}
|
|
159
|
+
path = f"{self._data_prefix(project, database)}/ai-proposals/{quote(proposal_id, safe='')}/approve"
|
|
160
|
+
return await self._transport.request("POST", path, json_body=body)
|
|
161
|
+
|
|
162
|
+
async def reject_ai_proposal(
|
|
163
|
+
self,
|
|
164
|
+
project: str,
|
|
165
|
+
database: Optional[str],
|
|
166
|
+
proposal_id: str,
|
|
167
|
+
*,
|
|
168
|
+
reason: Optional[str] = None,
|
|
169
|
+
policy_decision_reason: Optional[str] = None,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
"""Reject a pending proposal; the server enforces non-AI approval policy."""
|
|
172
|
+
body = {
|
|
173
|
+
key: value
|
|
174
|
+
for key, value in {
|
|
175
|
+
"reason": reason,
|
|
176
|
+
"policyDecisionReason": policy_decision_reason,
|
|
177
|
+
}.items()
|
|
178
|
+
if value is not None
|
|
179
|
+
}
|
|
180
|
+
path = f"{self._data_prefix(project, database)}/ai-proposals/{quote(proposal_id, safe='')}/reject"
|
|
181
|
+
return await self._transport.request("POST", path, json_body=body)
|
|
182
|
+
|
|
183
|
+
async def describe(self, project: str, database: Optional[str] = None) -> dict[str, Any]:
|
|
184
|
+
"""GET {prefix}/admin/schema — the key-gated data-plane app descriptor (entities/fields metadata,
|
|
185
|
+
structure only). Lets a key-only consumer introspect the database it holds a key for."""
|
|
186
|
+
return await self._transport.request("GET", f"{self._data_prefix(project, database)}/admin/schema")
|
|
187
|
+
|
|
188
|
+
async def describe_entity(self, project: str, database: Optional[str], slug: str) -> dict[str, Any]:
|
|
189
|
+
"""GET {prefix}/admin/entities/{slug} — the key-gated descriptor for one entity."""
|
|
190
|
+
path = f"{self._data_prefix(project, database)}/admin/entities/{quote(slug, safe='')}"
|
|
191
|
+
return await self._transport.request("GET", path)
|
|
192
|
+
|
|
193
|
+
async def openapi(self, project: str, database: Optional[str] = None) -> dict[str, Any]:
|
|
194
|
+
"""GET {prefix}/openapi.json — the key-gated data-plane OpenAPI 3.1 contract generated from
|
|
195
|
+
the active compiled schema."""
|
|
196
|
+
return await self._transport.request("GET", f"{self._data_prefix(project, database)}/openapi.json")
|
|
197
|
+
|
|
198
|
+
async def health(self, timeout: Optional[float] = None) -> bool:
|
|
199
|
+
"""Unauthenticated liveness probe → GET /api/health (process-up; no Mongo, schema, or key required).
|
|
200
|
+
True on a 2xx, False on any non-OK / network error / timeout — a cheap, schema-independent
|
|
201
|
+
reachability signal for readiness gates, safe to call BEFORE a schema is provisioned."""
|
|
202
|
+
return await self._transport.health(timeout)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def self_(self) -> SelfService:
|
|
206
|
+
"""Authenticated customer management (/api/self/*)."""
|
|
207
|
+
return self._self
|
|
208
|
+
|
|
209
|
+
# <sync:exclude> realtime is async-only — see Collection.subscribe / memybase._realtime.
|
|
210
|
+
# test_sync_parity.py documents this asymmetry and gen_sync.py strips this block from _sync/.
|
|
211
|
+
async def stream(
|
|
212
|
+
self,
|
|
213
|
+
project: str,
|
|
214
|
+
database: Optional[str],
|
|
215
|
+
on_event: Callable[[Any], None],
|
|
216
|
+
opts: Optional["SubscribeOptions"] = None,
|
|
217
|
+
) -> "Subscription":
|
|
218
|
+
"""Open a realtime SSE subscription for a project/database (all entities unless opts.entity is
|
|
219
|
+
set). Returns a live Subscription; ``await subscription.close()`` stops it. Mirrors JS
|
|
220
|
+
MemyBase.stream()."""
|
|
221
|
+
from ._realtime import RealtimeClient
|
|
222
|
+
|
|
223
|
+
return await RealtimeClient(self._transport).subscribe(project, database, on_event, opts)
|
|
224
|
+
# </sync:exclude>
|
|
225
|
+
|
|
226
|
+
async def close(self) -> None:
|
|
227
|
+
await self._transport.close()
|
|
228
|
+
|
|
229
|
+
async def __aenter__(self) -> "MemyBase":
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
233
|
+
await self.close()
|