rafikidb 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.
@@ -0,0 +1,10 @@
1
+ .env
2
+ .env.*
3
+ !.env.example
4
+ node_modules/
5
+ frontend/node_modules/
6
+ landing/node_modules/
7
+ sdks/js/node_modules/
8
+ backend/tmp/
9
+ vendor/
10
+ sdks/php/vendor/
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: rafikidb
3
+ Version: 0.1.0
4
+ Summary: Typed Python client for RafikiDB - data, auth, realtime, storage, env vars, secrets, webhooks, edge functions and payments.
5
+ Author: RafikiDB
6
+ License: MIT
7
+ Keywords: baas,backend,database,rafikidb,realtime
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
11
+ Requires-Python: >=3.10
12
+ Provides-Extra: realtime
13
+ Requires-Dist: websockets>=12.0; extra == 'realtime'
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=8.0; extra == 'test'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # rafikidb
19
+
20
+ Typed Python client for [RafikiDB](https://rafikidb.com) - data, auth,
21
+ realtime, storage, environment variables, secrets, webhooks, edge functions
22
+ and payments.
23
+
24
+ - **Zero required dependencies** (stdlib HTTP)
25
+ - Optional `websockets` for the WebSocket realtime transport
26
+ - Same envelope and error model as the REST API
27
+ - Python 3.10+
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install rafikidb # core
33
+ pip install rafikidb[realtime] # with WebSocket realtime
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from rafikidb import create_client
40
+
41
+ db = create_client(
42
+ project_id="5c73dab0-...",
43
+ api_key="raf_live_...",
44
+ base_url="https://api.rafikidb.com/api/v1", # optional
45
+ )
46
+ ```
47
+
48
+ Every call returns an `Envelope`:
49
+
50
+ ```python
51
+ rows = db.from_("profiles").select("id, nationality").eq("nationality", "Tanzania").limit(25).execute()
52
+ rows.success # True
53
+ rows.data # list of dicts
54
+ rows.message # "Request was successful"
55
+ ```
56
+
57
+ Failures raise `RafikiDBError` with `message`, `status`, `code` and `errors`.
58
+
59
+ ## Data
60
+
61
+ ```python
62
+ # Filters: eq, neq, gt, gte, lt, lte, like, ilike, is_null, is_not_null, in_
63
+ db.from_("profiles").eq("nationality", "Tanzania").is_not_null("user_id")
64
+
65
+ # Ordering and pagination
66
+ db.from_("profiles").order("created_at", ascending=False).limit(50).offset(100)
67
+
68
+ # Keyset pagination (O(1) per page on large tables)
69
+ db.from_("logs").select("id").limit(100).cursor(last_id)
70
+
71
+ # Joins (column-based, no SQL)
72
+ from rafikidb import JoinSpec
73
+ rows = (
74
+ db.from_("messages")
75
+ .select("id as message_id, message, users.full_name as full_name")
76
+ .join(JoinSpec(table="users", from_column="user_id", to_column="id"))
77
+ .limit(20)
78
+ ).execute()
79
+ # rows.data[i]["full_name"]
80
+
81
+ # Row operations
82
+ db.from_("profiles").get(record_id)
83
+ db.from_("profiles").single()
84
+ db.from_("profiles").head() # {"count": n}
85
+ db.from_("profiles").insert({...}) # single or list
86
+ db.from_("profiles").update({...}).eq("id", record_id)
87
+ db.from_("profiles").delete().eq("id", record_id)
88
+ ```
89
+
90
+ ## Auth
91
+
92
+ ```python
93
+ session = db.auth.signup(
94
+ email="user@example.com", password="strong-pass1",
95
+ full_name="Asha Mwinyi", phone="+255712345678",
96
+ )
97
+ session = db.auth.login(email="user@example.com", password="strong-pass1")
98
+
99
+ # The session is stored on the client and sent with every request,
100
+ # so RLS sees the logged-in user.
101
+ db.client.access_token # the project-user JWT
102
+
103
+ db.auth.sign_out()
104
+
105
+ # OTP
106
+ db.auth.otp_request("+255712345678")
107
+ db.auth.otp_verify(phone="+255712345678", code="123456", full_name="Asha Mwinyi")
108
+
109
+ # Password reset
110
+ db.auth.reset_password("user@example.com")
111
+ db.auth.confirm_reset_password(email="user@example.com", code="123456", new_password="brand-new-pass1")
112
+ ```
113
+
114
+ ## Realtime
115
+
116
+ ```python
117
+ def on_event(event):
118
+ print(event.type, event.table, event.record)
119
+
120
+ sub = db.realtime.subscribe("messages", on_event, events=["INSERT", "DELETE"])
121
+
122
+ # Stop listening (or sub.wait() to block)
123
+ sub.close()
124
+ ```
125
+
126
+ Default transport: WebSocket when `websockets` is installed, SSE otherwise.
127
+
128
+ ```python
129
+ db.realtime.subscribe("messages", on_event, transport="sse")
130
+ db.realtime.subscribe("messages", on_event, transport="websocket")
131
+ ```
132
+
133
+ ## Storage / env / secrets / webhooks / functions / payments
134
+
135
+ ```python
136
+ db.storage.create_bucket(name="Avatars", slug="avatars", is_public=True)
137
+ db.storage.signed_upload_url(bucket_id=..., object_name="user-1.png")
138
+
139
+ db.env.set("STRIPE_KEY", "sk_test_123", environment="production")
140
+ db.env.bulk_set("development", {"DEBUG": "true"})
141
+
142
+ db.secrets.set(key="API_SECRET", value="s3cr3t")
143
+ db.secrets.reveal(secret_id)
144
+
145
+ db.webhooks.create(name="order.created", url="https://myapp.com/hooks/orders", events=["order.created"])
146
+ db.webhooks.list_deliveries(webhook_id)
147
+
148
+ db.functions.create(name="hello", runtime="javascript", code="...")
149
+ db.functions.deploy(fn_id)
150
+ db.functions.invoke(fn_id, body='{"name":"Asha"}')
151
+
152
+ db.payments.stk_push(phone="+255712345678", amount=5000, description="Order #123")
153
+ db.payments.snippe_status(reference)
154
+ db.payments.list_transactions()
155
+ ```
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ pip install -e ".[test]"
161
+ python -m unittest discover -s tests -q
162
+ ```
@@ -0,0 +1,145 @@
1
+ # rafikidb
2
+
3
+ Typed Python client for [RafikiDB](https://rafikidb.com) - data, auth,
4
+ realtime, storage, environment variables, secrets, webhooks, edge functions
5
+ and payments.
6
+
7
+ - **Zero required dependencies** (stdlib HTTP)
8
+ - Optional `websockets` for the WebSocket realtime transport
9
+ - Same envelope and error model as the REST API
10
+ - Python 3.10+
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install rafikidb # core
16
+ pip install rafikidb[realtime] # with WebSocket realtime
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```python
22
+ from rafikidb import create_client
23
+
24
+ db = create_client(
25
+ project_id="5c73dab0-...",
26
+ api_key="raf_live_...",
27
+ base_url="https://api.rafikidb.com/api/v1", # optional
28
+ )
29
+ ```
30
+
31
+ Every call returns an `Envelope`:
32
+
33
+ ```python
34
+ rows = db.from_("profiles").select("id, nationality").eq("nationality", "Tanzania").limit(25).execute()
35
+ rows.success # True
36
+ rows.data # list of dicts
37
+ rows.message # "Request was successful"
38
+ ```
39
+
40
+ Failures raise `RafikiDBError` with `message`, `status`, `code` and `errors`.
41
+
42
+ ## Data
43
+
44
+ ```python
45
+ # Filters: eq, neq, gt, gte, lt, lte, like, ilike, is_null, is_not_null, in_
46
+ db.from_("profiles").eq("nationality", "Tanzania").is_not_null("user_id")
47
+
48
+ # Ordering and pagination
49
+ db.from_("profiles").order("created_at", ascending=False).limit(50).offset(100)
50
+
51
+ # Keyset pagination (O(1) per page on large tables)
52
+ db.from_("logs").select("id").limit(100).cursor(last_id)
53
+
54
+ # Joins (column-based, no SQL)
55
+ from rafikidb import JoinSpec
56
+ rows = (
57
+ db.from_("messages")
58
+ .select("id as message_id, message, users.full_name as full_name")
59
+ .join(JoinSpec(table="users", from_column="user_id", to_column="id"))
60
+ .limit(20)
61
+ ).execute()
62
+ # rows.data[i]["full_name"]
63
+
64
+ # Row operations
65
+ db.from_("profiles").get(record_id)
66
+ db.from_("profiles").single()
67
+ db.from_("profiles").head() # {"count": n}
68
+ db.from_("profiles").insert({...}) # single or list
69
+ db.from_("profiles").update({...}).eq("id", record_id)
70
+ db.from_("profiles").delete().eq("id", record_id)
71
+ ```
72
+
73
+ ## Auth
74
+
75
+ ```python
76
+ session = db.auth.signup(
77
+ email="user@example.com", password="strong-pass1",
78
+ full_name="Asha Mwinyi", phone="+255712345678",
79
+ )
80
+ session = db.auth.login(email="user@example.com", password="strong-pass1")
81
+
82
+ # The session is stored on the client and sent with every request,
83
+ # so RLS sees the logged-in user.
84
+ db.client.access_token # the project-user JWT
85
+
86
+ db.auth.sign_out()
87
+
88
+ # OTP
89
+ db.auth.otp_request("+255712345678")
90
+ db.auth.otp_verify(phone="+255712345678", code="123456", full_name="Asha Mwinyi")
91
+
92
+ # Password reset
93
+ db.auth.reset_password("user@example.com")
94
+ db.auth.confirm_reset_password(email="user@example.com", code="123456", new_password="brand-new-pass1")
95
+ ```
96
+
97
+ ## Realtime
98
+
99
+ ```python
100
+ def on_event(event):
101
+ print(event.type, event.table, event.record)
102
+
103
+ sub = db.realtime.subscribe("messages", on_event, events=["INSERT", "DELETE"])
104
+
105
+ # Stop listening (or sub.wait() to block)
106
+ sub.close()
107
+ ```
108
+
109
+ Default transport: WebSocket when `websockets` is installed, SSE otherwise.
110
+
111
+ ```python
112
+ db.realtime.subscribe("messages", on_event, transport="sse")
113
+ db.realtime.subscribe("messages", on_event, transport="websocket")
114
+ ```
115
+
116
+ ## Storage / env / secrets / webhooks / functions / payments
117
+
118
+ ```python
119
+ db.storage.create_bucket(name="Avatars", slug="avatars", is_public=True)
120
+ db.storage.signed_upload_url(bucket_id=..., object_name="user-1.png")
121
+
122
+ db.env.set("STRIPE_KEY", "sk_test_123", environment="production")
123
+ db.env.bulk_set("development", {"DEBUG": "true"})
124
+
125
+ db.secrets.set(key="API_SECRET", value="s3cr3t")
126
+ db.secrets.reveal(secret_id)
127
+
128
+ db.webhooks.create(name="order.created", url="https://myapp.com/hooks/orders", events=["order.created"])
129
+ db.webhooks.list_deliveries(webhook_id)
130
+
131
+ db.functions.create(name="hello", runtime="javascript", code="...")
132
+ db.functions.deploy(fn_id)
133
+ db.functions.invoke(fn_id, body='{"name":"Asha"}')
134
+
135
+ db.payments.stk_push(phone="+255712345678", amount=5000, description="Order #123")
136
+ db.payments.snippe_status(reference)
137
+ db.payments.list_transactions()
138
+ ```
139
+
140
+ ## Development
141
+
142
+ ```bash
143
+ pip install -e ".[test]"
144
+ python -m unittest discover -s tests -q
145
+ ```
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "rafikidb"
7
+ version = "0.1.0"
8
+ description = "Typed Python client for RafikiDB - data, auth, realtime, storage, env vars, secrets, webhooks, edge functions and payments."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "RafikiDB" }]
13
+ keywords = ["rafikidb", "baas", "backend", "database", "realtime"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ realtime = ["websockets>=12.0"]
22
+ test = ["pytest>=8.0"]
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["rafikidb"]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
@@ -0,0 +1,78 @@
1
+ """RafikiDB Python SDK - typed client for data, auth, realtime, storage, env
2
+ vars, secrets, webhooks, edge functions and payments."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, Optional, TypeVar
7
+
8
+ from .client import DEFAULT_BASE, RafikiDBClient
9
+ from .types import (
10
+ AuthSession,
11
+ Envelope,
12
+ JoinSpec,
13
+ ProjectTokens,
14
+ ProjectUser,
15
+ RafikiDBError,
16
+ RealtimeEvent,
17
+ Record,
18
+ )
19
+ from .data import DataBuilder
20
+ from .auth import Auth
21
+ from .realtime import Realtime, Subscription
22
+ from .modules import Storage, Env, Secrets, Webhooks, Functions, Payments
23
+
24
+ __all__ = [
25
+ "create_client",
26
+ "RafikiDBError",
27
+ "Envelope",
28
+ "AuthSession",
29
+ "ProjectUser",
30
+ "ProjectTokens",
31
+ "RealtimeEvent",
32
+ "Record",
33
+ "JoinSpec",
34
+ "DEFAULT_BASE",
35
+ ]
36
+
37
+ R = TypeVar("R", bound=Record)
38
+
39
+
40
+ class RafikiDB:
41
+ """Facade returned by create_client()."""
42
+
43
+ def __init__(self, client: RafikiDBClient):
44
+ self.client = client
45
+ self.auth = Auth(client)
46
+ self.realtime = Realtime(client)
47
+ self.storage = Storage(client)
48
+ self.env = Env(client)
49
+ self.secrets = Secrets(client)
50
+ self.webhooks = Webhooks(client)
51
+ self.functions = Functions(client)
52
+ self.payments = Payments(client)
53
+
54
+ def from_(self, table: str) -> DataBuilder:
55
+ """Typed data builder: `db.from_("messages").select(...).eq(...)`"""
56
+ return DataBuilder(self.client, table)
57
+
58
+ def request(self, path: str, method: str = "GET", body: Optional[Dict[str, Any]] = None) -> Envelope[Any]:
59
+ return self.client.request(path, method=method, body=body)
60
+
61
+
62
+ def create_client(
63
+ project_id: str,
64
+ api_key: str,
65
+ base_url: Optional[str] = None,
66
+ session: Optional[Dict[str, Any]] = None,
67
+ timeout: float = 30.0,
68
+ ) -> RafikiDB:
69
+ """Create a typed RafikiDB client.
70
+
71
+ Example:
72
+ db = create_client(
73
+ project_id="5c73dab0-...",
74
+ api_key="raf_live_...",
75
+ base_url="https://api.rafikidb.com/api/v1",
76
+ )
77
+ """
78
+ return RafikiDB(RafikiDBClient(project_id, api_key, base_url, session, timeout))
@@ -0,0 +1,107 @@
1
+ """Auth flows for the RafikiDB Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ from .client import RafikiDBClient
8
+ from .types import AuthSession, Envelope, ProjectTokens
9
+
10
+
11
+ class Auth:
12
+ """Project auth: signup, login, OTP, password reset. Sessions are stored
13
+ on the client automatically and sent with every request."""
14
+
15
+ def __init__(self, client: RafikiDBClient):
16
+ self._client = client
17
+
18
+ def signup(
19
+ self,
20
+ email: str,
21
+ password: str,
22
+ full_name: str,
23
+ phone: str,
24
+ metadata: Optional[Dict[str, Any]] = None,
25
+ ) -> Envelope[AuthSession]:
26
+ env = self._client.post(
27
+ f"/projects/{self._client.project_id}/auth/signup",
28
+ body={
29
+ "email": email,
30
+ "password": password,
31
+ "full_name": full_name,
32
+ "phone": phone,
33
+ "metadata": metadata,
34
+ },
35
+ )
36
+ session_env = self._to_session(env)
37
+ self._store_session(session_env)
38
+ return session_env
39
+
40
+ def login(self, email: str, password: str) -> Envelope[AuthSession]:
41
+ """Sign an end-user of THIS project in (POST /auth/signin)."""
42
+ env = self._client.post(
43
+ f"/projects/{self._client.project_id}/auth/signin",
44
+ body={"email": email, "password": password},
45
+ )
46
+ session_env = self._to_session(env)
47
+ self._store_session(session_env)
48
+ return session_env
49
+
50
+ signin = login
51
+
52
+ def otp_request(self, phone: str) -> Envelope[Dict[str, Any]]:
53
+ return self._client.post(
54
+ f"/projects/{self._client.project_id}/auth/otp/request",
55
+ body={"phone": phone},
56
+ )
57
+
58
+ def otp_verify(self, phone: str, code: str, full_name: str, metadata: Optional[Dict[str, Any]] = None) -> Envelope[AuthSession]:
59
+ env = self._client.post(
60
+ f"/projects/{self._client.project_id}/auth/otp/verify",
61
+ body={"phone": phone, "code": code, "full_name": full_name, "metadata": metadata},
62
+ )
63
+ session_env = self._to_session(env)
64
+ self._store_session(session_env)
65
+ return session_env
66
+
67
+ def reset_password(self, email: str) -> Envelope[Dict[str, Any]]:
68
+ return self._client.post(
69
+ f"/projects/{self._client.project_id}/auth/reset-password",
70
+ body={"email": email},
71
+ )
72
+
73
+ def confirm_reset_password(self, email: str, code: str, new_password: str) -> Envelope[Dict[str, Any]]:
74
+ return self._client.post(
75
+ f"/projects/{self._client.project_id}/auth/reset-password/confirm",
76
+ body={"email": email, "code": code, "new_password": new_password},
77
+ )
78
+
79
+ def sign_out(self) -> None:
80
+ """Clear the stored project-user session."""
81
+ self._client.set_session(None)
82
+
83
+ def refresh(self, refresh_token: str) -> Envelope[ProjectTokens]:
84
+ return self._client.post("/auth/refresh", body={"refresh_token": refresh_token})
85
+
86
+ def logout(self, refresh_token: str) -> Envelope[None]:
87
+ return self._client.post("/auth/logout", body={"refresh_token": refresh_token})
88
+
89
+ # -- internals ----------------------------------------------------------
90
+
91
+ def _store_session(self, env: Envelope[AuthSession]) -> None:
92
+ if env.data and env.data.tokens.access_token:
93
+ self._client.set_session(
94
+ {
95
+ "user": {
96
+ k: v for k, v in env.data.user.__dict__.items()
97
+ if not k.startswith("_")
98
+ },
99
+ "tokens": env.data.tokens.__dict__,
100
+ }
101
+ )
102
+
103
+ def _to_session(self, env: Envelope[Any]) -> Envelope[AuthSession]:
104
+ if env.data and isinstance(env.data, dict):
105
+ session = AuthSession.from_payload(env.data)
106
+ return Envelope(env.success, env.message, session, env.errors)
107
+ return Envelope(env.success, env.message, env.data, env.errors)
@@ -0,0 +1,115 @@
1
+ """HTTP client for the RafikiDB Python SDK (stdlib only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.parse
8
+ import urllib.request
9
+ from typing import Any, Dict, Optional
10
+
11
+ from .types import Envelope, RafikiDBError
12
+
13
+ DEFAULT_BASE = "http://localhost:8080/api/v1"
14
+
15
+
16
+ class RafikiDBClient:
17
+ """Low-level HTTP client. Sends the API key and optional session token."""
18
+
19
+ def __init__(
20
+ self,
21
+ project_id: str,
22
+ api_key: str,
23
+ base_url: Optional[str] = None,
24
+ session: Optional[Dict[str, Any]] = None,
25
+ timeout: float = 30.0,
26
+ ) -> None:
27
+ self.project_id = project_id
28
+ self.api_key = api_key
29
+ self.base_url = (base_url or DEFAULT_BASE).rstrip("/")
30
+ self.timeout = timeout
31
+ self._session: Optional[Dict[str, Any]] = session
32
+
33
+ # -- session ------------------------------------------------------------
34
+
35
+ @property
36
+ def session(self) -> Optional[Dict[str, Any]]:
37
+ return self._session
38
+
39
+ def set_session(self, session: Optional[Dict[str, Any]]) -> None:
40
+ self._session = session
41
+
42
+ @property
43
+ def access_token(self) -> Optional[str]:
44
+ if self._session:
45
+ return self._session.get("tokens", {}).get("access_token")
46
+ return None
47
+
48
+ # -- request ------------------------------------------------------------
49
+
50
+ def request(
51
+ self,
52
+ path: str,
53
+ method: str = "GET",
54
+ body: Optional[Dict[str, Any]] = None,
55
+ query: Optional[Dict[str, str]] = None,
56
+ ) -> Envelope[Any]:
57
+ url = self.base_url + path
58
+ if query:
59
+ url += "?" + urllib.parse.urlencode(query)
60
+
61
+ headers = {
62
+ "X-AFRIBASE-API-Key": self.api_key,
63
+ "Content-Type": "application/json",
64
+ "Accept": "application/json",
65
+ }
66
+ token = self.access_token
67
+ if token:
68
+ headers["Authorization"] = f"Bearer {token}"
69
+
70
+ data = json.dumps(body).encode() if body is not None else None
71
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
72
+
73
+ status: Optional[int] = None
74
+ payload: Dict[str, Any] = {}
75
+ try:
76
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
77
+ status = resp.status
78
+ raw = resp.read()
79
+ if raw:
80
+ payload = json.loads(raw)
81
+ except urllib.error.HTTPError as err:
82
+ status = err.code
83
+ raw = err.read()
84
+ try:
85
+ payload = json.loads(raw)
86
+ except json.JSONDecodeError:
87
+ payload = {}
88
+ except urllib.error.URLError as err:
89
+ raise RafikiDBError(f"Connection error: {err.reason}", 0) from err
90
+
91
+ envelope = Envelope.from_payload(payload)
92
+ if not envelope.success or (status is not None and status >= 400):
93
+ raise RafikiDBError(
94
+ envelope.message or f"Request failed ({status})",
95
+ status or 500,
96
+ envelope.errors,
97
+ )
98
+ return envelope
99
+
100
+ # -- typed convenience ----------------------------------------------
101
+
102
+ def get(self, path: str, query: Optional[Dict[str, str]] = None) -> Envelope[Any]:
103
+ return self.request(path, "GET", query=query)
104
+
105
+ def post(self, path: str, body: Optional[Dict[str, Any]] = None) -> Envelope[Any]:
106
+ return self.request(path, "POST", body=body)
107
+
108
+ def patch(self, path: str, body: Optional[Dict[str, Any]] = None) -> Envelope[Any]:
109
+ return self.request(path, "PATCH", body=body)
110
+
111
+ def put(self, path: str, body: Optional[Dict[str, Any]] = None) -> Envelope[Any]:
112
+ return self.request(path, "PUT", body=body)
113
+
114
+ def delete(self, path: str) -> Envelope[Any]:
115
+ return self.request(path, "DELETE")