bee-sdk 0.1.1__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.
bee_sdk/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """Bee Python SDK — official client for the Bee Intelligence Engine API.
2
+
3
+ pip install bee-sdk
4
+
5
+ from bee_sdk import Bee
6
+
7
+ bee = Bee() # reads BEE_API_URL + BEE_API_KEY from env
8
+ print(bee.chat("Explain quantum error correction", domain="quantum"))
9
+
10
+ # Streaming
11
+ for chunk in bee.chat_stream("Write a fibonacci function", domain="programming"):
12
+ print(chunk, end="", flush=True)
13
+
14
+ # Async
15
+ import asyncio
16
+ async def main():
17
+ client = Bee.async_client()
18
+ result = await client.chat("Audit this code for SQL injection", domain="cybersecurity")
19
+ print(result)
20
+ asyncio.run(main())
21
+
22
+ The SDK calls the Bee FastAPI surface (see bee/server.py for the full
23
+ endpoint catalogue). For MCP-tool integration (Claude Desktop, Cursor,
24
+ etc.) see bee/mcp_server.py — the SDK and the MCP server are independent
25
+ surfaces over the same model.
26
+ """
27
+ from .client import Bee, AsyncBee, BeeError, RateLimitError, BeeAPIError
28
+ from .types import ChatMessage, ChatResponse, Domain, ModelTier
29
+
30
+ __version__ = "0.1.1"
31
+ __all__ = [
32
+ "Bee",
33
+ "AsyncBee",
34
+ "BeeError",
35
+ "RateLimitError",
36
+ "BeeAPIError",
37
+ "ChatMessage",
38
+ "ChatResponse",
39
+ "Domain",
40
+ "ModelTier",
41
+ "__version__",
42
+ ]
bee_sdk/client.py ADDED
@@ -0,0 +1,329 @@
1
+ """Bee SDK client.
2
+
3
+ Two flavours:
4
+ - `Bee` synchronous, uses urllib stdlib only (zero deps)
5
+ - `AsyncBee` async, uses httpx (optional dep — `pip install bee-sdk[async]`)
6
+
7
+ Both expose the same surface:
8
+
9
+ .chat(message, domain=..., max_tokens=...) → str
10
+ .chat_messages(messages, ...) → ChatResponse
11
+ .chat_stream(message, ...) → Iterator[str]
12
+ .feedback(interaction_id, rating) → None
13
+ .domains() → list[str]
14
+ .next_domain() → dict (admin)
15
+ .training_runs(limit=20) → list[dict]
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import time
22
+ import urllib.error
23
+ import urllib.request
24
+ from typing import Any, AsyncIterator, Iterator, Optional
25
+
26
+ from .types import ChatMessage, ChatResponse, Domain
27
+
28
+
29
+ # Public OpenAI-compatible proxy. MUST be the bee.cuilabs.io/bee gateway,
30
+ # never the raw Modal app URL — the gateway is where API-key auth, plan /
31
+ # per-tier allowance enforcement and usage metering happen. Pointing at
32
+ # Modal directly bypasses billing entirely and a bee_sk_ key 401s there
33
+ # (the backend only knows Supabase JWTs / the static BEE_API_KEYS env).
34
+ DEFAULT_BASE_URL = "https://bee.cuilabs.io/bee"
35
+ DEFAULT_TIMEOUT = 60
36
+ DEFAULT_RETRIES = 3
37
+ RETRYABLE_STATUS = {429, 500, 502, 503, 504}
38
+
39
+
40
+ class BeeError(Exception):
41
+ """Base error class for the SDK."""
42
+
43
+
44
+ class BeeAPIError(BeeError):
45
+ def __init__(self, status: int, body: str) -> None:
46
+ super().__init__(f"HTTP {status}: {body[:300]}")
47
+ self.status = status
48
+ self.body = body
49
+
50
+
51
+ class RateLimitError(BeeAPIError):
52
+ """Raised on persistent 429 after retries exhausted."""
53
+
54
+
55
+ class _BaseClient:
56
+ def __init__(
57
+ self,
58
+ base_url: Optional[str] = None,
59
+ api_key: Optional[str] = None,
60
+ timeout: int = DEFAULT_TIMEOUT,
61
+ retries: int = DEFAULT_RETRIES,
62
+ ) -> None:
63
+ self.base_url = (base_url or os.environ.get("BEE_API_URL") or DEFAULT_BASE_URL).rstrip("/")
64
+ self.api_key = api_key or os.environ.get("BEE_API_KEY") or os.environ.get("BEE_PORTAL_API_KEY")
65
+ self.timeout = timeout
66
+ self.retries = retries
67
+
68
+ def _headers(self, extra: Optional[dict] = None) -> dict:
69
+ h = {
70
+ "Content-Type": "application/json",
71
+ "Accept": "application/json",
72
+ "User-Agent": "bee-sdk/0.1.1",
73
+ }
74
+ if self.api_key:
75
+ h["Authorization"] = f"Bearer {self.api_key}"
76
+ if extra:
77
+ h.update(extra)
78
+ return h
79
+
80
+
81
+ class Bee(_BaseClient):
82
+ """Synchronous client. Zero non-stdlib dependencies."""
83
+
84
+ @classmethod
85
+ def async_client(cls, **kwargs) -> "AsyncBee":
86
+ """Convenience factory for the async client."""
87
+ return AsyncBee(**kwargs)
88
+
89
+ # ── HTTP plumbing ───────────────────────────────────────────────────
90
+ def _request(self, method: str, path: str, body: Optional[dict] = None) -> dict:
91
+ url = f"{self.base_url}{path}"
92
+ data = json.dumps(body).encode() if body is not None else None
93
+ last_err: Optional[Exception] = None
94
+ for attempt in range(self.retries + 1):
95
+ req = urllib.request.Request(url, data=data, headers=self._headers(), method=method)
96
+ try:
97
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
98
+ raw = resp.read().decode("utf-8")
99
+ return json.loads(raw) if raw else {}
100
+ except urllib.error.HTTPError as e:
101
+ body_text = e.read().decode(errors="replace")
102
+ if e.code in RETRYABLE_STATUS and attempt < self.retries:
103
+ last_err = e
104
+ time.sleep(min(2 ** attempt, 8))
105
+ continue
106
+ if e.code == 429:
107
+ raise RateLimitError(e.code, body_text)
108
+ raise BeeAPIError(e.code, body_text)
109
+ except (urllib.error.URLError, TimeoutError, ConnectionError) as e:
110
+ last_err = e
111
+ if attempt < self.retries:
112
+ time.sleep(min(2 ** attempt, 8))
113
+ continue
114
+ raise BeeError(f"network error after {self.retries + 1} attempts: {e}") from e
115
+ raise BeeError(f"unreachable: {last_err}")
116
+
117
+ # ── Public API ──────────────────────────────────────────────────────
118
+ def chat(
119
+ self,
120
+ message: str,
121
+ domain: Domain = "general",
122
+ max_tokens: int = 512,
123
+ temperature: float = 0.3,
124
+ system: Optional[str] = None,
125
+ ) -> str:
126
+ """Single-turn chat. Returns the assistant text only.
127
+
128
+ For multi-turn or detailed response metadata, use `chat_messages`.
129
+ """
130
+ msgs: list[ChatMessage] = []
131
+ if system:
132
+ msgs.append(ChatMessage(role="system", content=system))
133
+ msgs.append(ChatMessage(role="user", content=message))
134
+ return self.chat_messages(msgs, domain=domain, max_tokens=max_tokens, temperature=temperature).content
135
+
136
+ def chat_messages(
137
+ self,
138
+ messages: list[ChatMessage],
139
+ domain: Domain = "general",
140
+ max_tokens: int = 512,
141
+ temperature: float = 0.3,
142
+ ) -> ChatResponse:
143
+ # Switch domain first (the Bee API has explicit /domain/switch).
144
+ try:
145
+ self._request("POST", "/domain/switch", {"domain": domain})
146
+ except BeeAPIError:
147
+ # Domain switch is best-effort — if the server hasn't loaded
148
+ # that domain's adapter, fall through and serve from current.
149
+ pass
150
+ body = {
151
+ "model": "bee-cell",
152
+ "messages": [{"role": m.role, "content": m.content} for m in messages],
153
+ "max_tokens": max_tokens,
154
+ "temperature": temperature,
155
+ }
156
+ out = self._request("POST", "/chat/completions", body)
157
+ choice = (out.get("choices") or [{}])[0]
158
+ msg = choice.get("message", {})
159
+ return ChatResponse(
160
+ id=out.get("id", ""),
161
+ model=out.get("model", ""),
162
+ content=msg.get("content", ""),
163
+ role=msg.get("role", "assistant"),
164
+ finish_reason=choice.get("finish_reason"),
165
+ usage=out.get("usage", {}),
166
+ interaction_id=out.get("interaction_id"),
167
+ raw=out,
168
+ )
169
+
170
+ def chat_stream(
171
+ self,
172
+ message: str,
173
+ domain: Domain = "general",
174
+ max_tokens: int = 512,
175
+ temperature: float = 0.3,
176
+ system: Optional[str] = None,
177
+ ) -> Iterator[str]:
178
+ """Stream tokens as they're generated.
179
+
180
+ Falls back to a single-shot response if the server doesn't yet
181
+ emit SSE chunks (current /chat/completions doesn't stream;
182
+ this method preserves the API for when streaming lands).
183
+ """
184
+ try:
185
+ self._request("POST", "/domain/switch", {"domain": domain})
186
+ except BeeAPIError:
187
+ pass
188
+ msgs = []
189
+ if system:
190
+ msgs.append({"role": "system", "content": system})
191
+ msgs.append({"role": "user", "content": message})
192
+ body = {
193
+ "model": "bee-cell",
194
+ "messages": msgs,
195
+ "max_tokens": max_tokens,
196
+ "temperature": temperature,
197
+ "stream": True,
198
+ }
199
+ # Try SSE first; if the server returns plain JSON, yield it whole.
200
+ url = f"{self.base_url}/chat/completions"
201
+ req = urllib.request.Request(
202
+ url, data=json.dumps(body).encode(), headers=self._headers(), method="POST",
203
+ )
204
+ try:
205
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
206
+ ct = resp.headers.get("content-type", "")
207
+ if "text/event-stream" in ct:
208
+ for line in resp:
209
+ s = line.decode().strip()
210
+ if not s or not s.startswith("data:"):
211
+ continue
212
+ payload = s[5:].strip()
213
+ if payload == "[DONE]":
214
+ return
215
+ try:
216
+ d = json.loads(payload)
217
+ delta = (d.get("choices") or [{}])[0].get("delta", {})
218
+ if "content" in delta:
219
+ yield delta["content"]
220
+ except json.JSONDecodeError:
221
+ continue
222
+ else:
223
+ # Server didn't honour stream=True — yield full response.
224
+ out = json.loads(resp.read().decode())
225
+ content = (out.get("choices") or [{}])[0].get("message", {}).get("content", "")
226
+ if content:
227
+ yield content
228
+ except urllib.error.HTTPError as e:
229
+ raise BeeAPIError(e.code, e.read().decode(errors="replace"))
230
+
231
+ def feedback(self, interaction_id: str, rating: str, comment: Optional[str] = None) -> None:
232
+ """Submit thumbs-up/down feedback. rating: 'up' | 'down'."""
233
+ self._request("POST", "/feedback", {
234
+ "interaction_id": interaction_id,
235
+ "rating": rating,
236
+ "comment": comment,
237
+ })
238
+
239
+ def domains(self) -> list[str]:
240
+ """List the domains Bee knows about."""
241
+ return self._request("GET", "/models").get("domains", [])
242
+
243
+ def health(self) -> dict:
244
+ return self._request("GET", "/health")
245
+
246
+ def adapters(self) -> dict:
247
+ """Currently-loaded LoRA adapters per domain (added 2026-04-28)."""
248
+ return self._request("GET", "/adapters")
249
+
250
+
251
+ class AsyncBee(_BaseClient):
252
+ """Async client — requires httpx. `pip install bee-sdk[async]`."""
253
+
254
+ def __init__(self, *args, **kwargs):
255
+ super().__init__(*args, **kwargs)
256
+ try:
257
+ import httpx # noqa: F401
258
+ except ImportError as e:
259
+ raise ImportError(
260
+ "AsyncBee requires httpx. Install with `pip install bee-sdk[async]`."
261
+ ) from e
262
+
263
+ async def chat(
264
+ self,
265
+ message: str,
266
+ domain: Domain = "general",
267
+ max_tokens: int = 512,
268
+ temperature: float = 0.3,
269
+ system: Optional[str] = None,
270
+ ) -> str:
271
+ import httpx
272
+ msgs: list[dict] = []
273
+ if system:
274
+ msgs.append({"role": "system", "content": system})
275
+ msgs.append({"role": "user", "content": message})
276
+ async with httpx.AsyncClient(timeout=self.timeout, headers=self._headers()) as cl:
277
+ try:
278
+ await cl.post(f"{self.base_url}/domain/switch", json={"domain": domain})
279
+ except httpx.HTTPError:
280
+ pass
281
+ r = await cl.post(f"{self.base_url}/chat/completions", json={
282
+ "model": "bee-cell",
283
+ "messages": msgs,
284
+ "max_tokens": max_tokens,
285
+ "temperature": temperature,
286
+ })
287
+ r.raise_for_status()
288
+ out = r.json()
289
+ return (out.get("choices") or [{}])[0].get("message", {}).get("content", "")
290
+
291
+ async def chat_stream(
292
+ self,
293
+ message: str,
294
+ domain: Domain = "general",
295
+ max_tokens: int = 512,
296
+ temperature: float = 0.3,
297
+ system: Optional[str] = None,
298
+ ) -> AsyncIterator[str]:
299
+ import httpx
300
+ msgs: list[dict] = []
301
+ if system:
302
+ msgs.append({"role": "system", "content": system})
303
+ msgs.append({"role": "user", "content": message})
304
+ async with httpx.AsyncClient(timeout=self.timeout, headers=self._headers()) as cl:
305
+ try:
306
+ await cl.post(f"{self.base_url}/domain/switch", json={"domain": domain})
307
+ except httpx.HTTPError:
308
+ pass
309
+ async with cl.stream("POST", f"{self.base_url}/chat/completions", json={
310
+ "model": "bee-cell",
311
+ "messages": msgs,
312
+ "max_tokens": max_tokens,
313
+ "temperature": temperature,
314
+ "stream": True,
315
+ }) as resp:
316
+ async for line in resp.aiter_lines():
317
+ s = line.strip()
318
+ if not s or not s.startswith("data:"):
319
+ continue
320
+ payload = s[5:].strip()
321
+ if payload == "[DONE]":
322
+ return
323
+ try:
324
+ d = json.loads(payload)
325
+ delta = (d.get("choices") or [{}])[0].get("delta", {})
326
+ if "content" in delta:
327
+ yield delta["content"]
328
+ except json.JSONDecodeError:
329
+ continue
bee_sdk/types.py ADDED
@@ -0,0 +1,51 @@
1
+ """Public types for the Bee SDK.
2
+
3
+ Mirrors the JSON shape of the Bee FastAPI surface (`bee/server.py`).
4
+ Kept as plain dataclasses — no pydantic dependency on the SDK so it's
5
+ lightweight to install.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Literal, Optional
11
+
12
+ # Tier-1 domains, mirror of bee/domains.py.
13
+ Domain = Literal[
14
+ "general",
15
+ "programming",
16
+ "ai",
17
+ "cybersecurity",
18
+ "quantum",
19
+ "fintech",
20
+ "blockchain",
21
+ "infrastructure",
22
+ "research",
23
+ "business",
24
+ ]
25
+
26
+ # Trainable tier names, mirror of bee/tiers.py.
27
+ ModelTier = Literal["cell", "brood", "comb", "buzz", "hive", "swarm", "enclave", "ignite"]
28
+
29
+
30
+ @dataclass
31
+ class ChatMessage:
32
+ role: Literal["system", "user", "assistant"]
33
+ content: str
34
+
35
+
36
+ @dataclass
37
+ class ChatResponse:
38
+ """Response shape from POST /chat/completions.
39
+
40
+ Mirrors the OpenAI ChatCompletion shape because that's what the
41
+ Bee API emits — preserves drop-in compatibility for callers that
42
+ already know the OpenAI SDK.
43
+ """
44
+ id: str
45
+ model: str
46
+ content: str
47
+ role: str = "assistant"
48
+ finish_reason: Optional[str] = None
49
+ usage: dict = field(default_factory=dict)
50
+ interaction_id: Optional[str] = None
51
+ raw: dict = field(default_factory=dict)
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: bee-sdk
3
+ Version: 0.1.1
4
+ Summary: Official Python client for the Bee Intelligence Engine — domain-specialized LoRA-routed LLM by CUI Labs.
5
+ Project-URL: Homepage, https://bee.cuilabs.io
6
+ Project-URL: Documentation, https://bee.cuilabs.io/docs/sdks
7
+ Project-URL: Repository, https://github.com/cuilabs/bee
8
+ Project-URL: Issues, https://github.com/cuilabs/bee-community/issues
9
+ Project-URL: Changelog, https://bee.cuilabs.io/changelog
10
+ Project-URL: Hugging Face, https://huggingface.co/cuilabs
11
+ Author-email: "CUI Labs (Pte.) Ltd." <engineering@cuilabs.io>
12
+ License: Apache-2.0
13
+ Keywords: ai,bee,blockchain,cybersecurity,domain-experts,huggingface,llm,lora,mcp,quantum
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Provides-Extra: async
29
+ Requires-Dist: httpx>=0.27; extra == 'async'
30
+ Provides-Extra: dev
31
+ Requires-Dist: httpx>=0.27; extra == 'dev'
32
+ Requires-Dist: mypy>=1.10; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
34
+ Requires-Dist: pytest>=8; extra == 'dev'
35
+ Requires-Dist: ruff>=0.5; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # bee-sdk
39
+
40
+ Official Python client for the **Bee Intelligence Engine** — a domain-specialized LLM by [CUI Labs](https://bee.cuilabs.io) routing per-domain LoRA adapters over a verified open-weight base.
41
+
42
+ SDK version: `0.1.1` (pre-release).
43
+
44
+ > **Status:** functional sync + async client (stdlib + optional `httpx`).
45
+ > The SDK targets the Bee `/chat/completions` API contract on
46
+ > production via the public gateway `https://bee.cuilabs.io/bee` — this
47
+ > is the default and it is where API-key auth, plan / per-tier allowance
48
+ > enforcement and usage metering happen. Do **not** point `BEE_API_URL`
49
+ > at the raw Modal app URL: that bypasses billing and a `bee_sk_` key
50
+ > is rejected there (the backend only trusts Supabase JWTs / the
51
+ > static `BEE_API_KEYS` env, not customer-issued keys). Override
52
+ > `BEE_API_URL` only for a self-hosted Bee Enclave or staging.
53
+
54
+ ## Install
55
+
56
+ The PyPI `cuilabs` organisation is currently pending approval; until it
57
+ lands, install directly from GitHub:
58
+
59
+ ```bash
60
+ # From GitHub (recommended while PyPI approval is pending)
61
+ pip install "git+https://github.com/cuilabs/bee.git#subdirectory=sdks/python"
62
+
63
+ # With the optional async client (adds httpx)
64
+ pip install "git+https://github.com/cuilabs/bee.git#subdirectory=sdks/python" httpx
65
+ ```
66
+
67
+ Once PyPI approval lands, the canonical install is:
68
+
69
+ ```bash
70
+ pip install bee-sdk # sync client (stdlib only — zero deps)
71
+ pip install bee-sdk[async] # async client (adds httpx)
72
+ ```
73
+
74
+ Install + quickstart on the marketing site:
75
+ [bee.cuilabs.io/docs/sdks](https://bee.cuilabs.io/docs/sdks).
76
+
77
+ ## Quick start
78
+
79
+ ```python
80
+ from bee_sdk import Bee
81
+
82
+ bee = Bee() # reads BEE_API_URL + BEE_API_KEY from env
83
+ print(bee.chat("Explain Shor's algorithm at NISQ depth", domain="quantum"))
84
+ ```
85
+
86
+ ### Streaming
87
+
88
+ ```python
89
+ for chunk in bee.chat_stream("Write a Rust fibonacci function", domain="programming"):
90
+ print(chunk, end="", flush=True)
91
+ ```
92
+
93
+ ### Async
94
+
95
+ ```python
96
+ import asyncio
97
+ from bee_sdk import AsyncBee
98
+
99
+ async def main():
100
+ client = AsyncBee()
101
+ text = await client.chat("Audit this contract for re-entrancy", domain="blockchain")
102
+ print(text)
103
+
104
+ asyncio.run(main())
105
+ ```
106
+
107
+ ### Multi-turn
108
+
109
+ ```python
110
+ from bee_sdk import Bee, ChatMessage
111
+
112
+ bee = Bee()
113
+ resp = bee.chat_messages(
114
+ [
115
+ ChatMessage(role="system", content="You are a senior security auditor."),
116
+ ChatMessage(role="user", content="Review this nginx config for hardening gaps:\n\n..."),
117
+ ],
118
+ domain="cybersecurity",
119
+ max_tokens=1024,
120
+ )
121
+ print(resp.content)
122
+ print(resp.usage, resp.interaction_id)
123
+ ```
124
+
125
+ ### Feedback loop
126
+
127
+ ```python
128
+ resp = bee.chat_messages([...], domain="ai")
129
+ if user_likes_answer:
130
+ bee.feedback(resp.interaction_id, rating="up")
131
+ ```
132
+
133
+ ## Domains
134
+
135
+ The `domain=` parameter selects which LoRA adapter Bee routes through. Tier-1 domains:
136
+
137
+ | domain | what it's tuned for |
138
+ |---|---|
139
+ | `general` | balanced, no specialization |
140
+ | `programming` | code generation, refactoring, debugging |
141
+ | `ai` | ML/AI papers, training, evaluation |
142
+ | `cybersecurity` | threat modelling, audits, defensive analysis |
143
+ | `quantum` | NISQ-aware quantum computing, Qiskit |
144
+ | `fintech` | payments, risk, compliance |
145
+ | `blockchain` | smart contract audits, protocol design |
146
+ | `infrastructure` | systems, networking, devops |
147
+ | `research` | literature review, paper critique |
148
+ | `business` | strategy, GTM, ops |
149
+
150
+ Adapters live at [`cuilabs/bee-cell`](https://huggingface.co/cuilabs/bee-cell) on branches `<domain>-<UTC-timestamp>`.
151
+
152
+ ## Environment variables
153
+
154
+ | var | purpose |
155
+ |---|---|
156
+ | `BEE_API_URL` | Endpoint override. Defaults to the public gateway `https://bee.cuilabs.io/bee` (where auth + billing + metering run). Set this **only** for a self-hosted Bee Enclave or a staging environment — never the raw Modal app URL (that bypasses billing and rejects `bee_sk_` keys). |
157
+ | `BEE_API_KEY` | Bearer token (also accepts `BEE_PORTAL_API_KEY`) |
158
+
159
+ ## Errors
160
+
161
+ ```python
162
+ from bee_sdk import BeeAPIError, RateLimitError, BeeError
163
+
164
+ try:
165
+ bee.chat("...", domain="quantum")
166
+ except RateLimitError as e: # 429 after retries
167
+ ...
168
+ except BeeAPIError as e: # other HTTP errors
169
+ print(e.status, e.body)
170
+ except BeeError: # network / timeout
171
+ ...
172
+ ```
173
+
174
+ The sync client retries 429/5xx with exponential backoff (max 4 attempts).
175
+
176
+ ## Versioning
177
+
178
+ `bee-sdk` follows the Bee API surface in `bee/server.py`. Breaking API changes bump the **minor** version pre-1.0; the SDK is currently **0.1.1** and the API is `v1`.
179
+
180
+ ## License
181
+
182
+ Apache-2.0 © 2026 CUI Labs (Pte.) Ltd.
@@ -0,0 +1,6 @@
1
+ bee_sdk/__init__.py,sha256=QMDcS1Gz1SwF8v3ZYu21dHCLOMUCLtEwcojv2bQNW5s,1238
2
+ bee_sdk/client.py,sha256=x56s82E4DzDsh44IPDPM1-5BqEwbTOIIimVFUN_WjfI,12976
3
+ bee_sdk/types.py,sha256=09qs8Uyx1f10kL9E7NyuCV9H340aT31637s9LebpWYY,1295
4
+ bee_sdk-0.1.1.dist-info/METADATA,sha256=Z-il7GsmUtlYAkDmihoFPJuzBtXLqKE1-xQlCqdTbHk,6281
5
+ bee_sdk-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ bee_sdk-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any