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