wirebox 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.
- wirebox/__init__.py +90 -0
- wirebox/_http.py +204 -0
- wirebox/_version.py +1 -0
- wirebox/async_client.py +138 -0
- wirebox/client.py +117 -0
- wirebox/exceptions.py +118 -0
- wirebox/identity.py +490 -0
- wirebox/mail.py +276 -0
- wirebox/py.typed +1 -0
- wirebox/tunnels.py +342 -0
- wirebox/types.py +512 -0
- wirebox/verify_webhook.py +109 -0
- wirebox/webhooks.py +235 -0
- wirebox-0.1.0.dist-info/METADATA +223 -0
- wirebox-0.1.0.dist-info/RECORD +17 -0
- wirebox-0.1.0.dist-info/WHEEL +4 -0
- wirebox-0.1.0.dist-info/licenses/LICENSE +21 -0
wirebox/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Wirebox — Real-world identity, communication, and context execution layer for AI agents.
|
|
2
|
+
|
|
3
|
+
Official Python SDK providing synchronous and asynchronous interfaces to equip autonomous
|
|
4
|
+
agents with dedicated phone numbers, email inboxes, network tunnels, and webhooks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from wirebox._version import __version__
|
|
8
|
+
from wirebox.async_client import AsyncWirebox
|
|
9
|
+
from wirebox.client import Wirebox
|
|
10
|
+
from wirebox.exceptions import (
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
FreeTierLimitExceededError,
|
|
13
|
+
HandleAlreadyTakenError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
WireboxAPIError,
|
|
18
|
+
WireboxConnectionError,
|
|
19
|
+
WireboxError,
|
|
20
|
+
)
|
|
21
|
+
from wirebox.identity import AgentIdentity, AsyncAgentIdentity
|
|
22
|
+
from wirebox.tunnels import TunnelSession
|
|
23
|
+
from wirebox.types import (
|
|
24
|
+
EmailMessage,
|
|
25
|
+
IdentityData,
|
|
26
|
+
IdentityTunnelSummary,
|
|
27
|
+
MailboxSummary,
|
|
28
|
+
MessageAttachmentSummary,
|
|
29
|
+
MessageSummary,
|
|
30
|
+
SendEmailAttachment,
|
|
31
|
+
SendEmailResult,
|
|
32
|
+
Tunnel,
|
|
33
|
+
TunnelClientTelemetry,
|
|
34
|
+
Webhook,
|
|
35
|
+
WebhookCreateResult,
|
|
36
|
+
WebhookEventType,
|
|
37
|
+
WebhookRotateSecretResult,
|
|
38
|
+
WebhookStatus,
|
|
39
|
+
WebhookTestResult,
|
|
40
|
+
WhoamiApiKey,
|
|
41
|
+
WhoamiOrganization,
|
|
42
|
+
WhoamiResult,
|
|
43
|
+
)
|
|
44
|
+
from wirebox.verify_webhook import verify_webhook
|
|
45
|
+
|
|
46
|
+
# Canonical aliases for cross-SDK naming consistency
|
|
47
|
+
WireboxClient = Wirebox
|
|
48
|
+
AsyncWireboxClient = AsyncWirebox
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"__version__",
|
|
52
|
+
"Wirebox",
|
|
53
|
+
"WireboxClient",
|
|
54
|
+
"AsyncWirebox",
|
|
55
|
+
"AsyncWireboxClient",
|
|
56
|
+
"AgentIdentity",
|
|
57
|
+
"AsyncAgentIdentity",
|
|
58
|
+
"TunnelSession",
|
|
59
|
+
"verify_webhook",
|
|
60
|
+
# Exceptions
|
|
61
|
+
"WireboxError",
|
|
62
|
+
"WireboxAPIError",
|
|
63
|
+
"AuthenticationError",
|
|
64
|
+
"NotFoundError",
|
|
65
|
+
"RateLimitError",
|
|
66
|
+
"HandleAlreadyTakenError",
|
|
67
|
+
"FreeTierLimitExceededError",
|
|
68
|
+
"WireboxConnectionError",
|
|
69
|
+
"ValidationError",
|
|
70
|
+
# Types
|
|
71
|
+
"IdentityData",
|
|
72
|
+
"IdentityTunnelSummary",
|
|
73
|
+
"MailboxSummary",
|
|
74
|
+
"SendEmailAttachment",
|
|
75
|
+
"SendEmailResult",
|
|
76
|
+
"MessageAttachmentSummary",
|
|
77
|
+
"MessageSummary",
|
|
78
|
+
"EmailMessage",
|
|
79
|
+
"Tunnel",
|
|
80
|
+
"TunnelClientTelemetry",
|
|
81
|
+
"Webhook",
|
|
82
|
+
"WebhookCreateResult",
|
|
83
|
+
"WebhookTestResult",
|
|
84
|
+
"WebhookRotateSecretResult",
|
|
85
|
+
"WebhookEventType",
|
|
86
|
+
"WebhookStatus",
|
|
87
|
+
"WhoamiOrganization",
|
|
88
|
+
"WhoamiApiKey",
|
|
89
|
+
"WhoamiResult",
|
|
90
|
+
]
|
wirebox/_http.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Wirebox Python SDK — HTTP Transport Layer.
|
|
2
|
+
|
|
3
|
+
Provides unified synchronous and asynchronous HTTP transports built on httpx,
|
|
4
|
+
handling authentication, connection pooling, timeouts, and error normalization.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from wirebox._version import __version__
|
|
15
|
+
from wirebox.exceptions import WireboxConnectionError, parse_api_error
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE_URL = "https://api.wirebox.sh"
|
|
18
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _build_headers(
|
|
22
|
+
api_key: str | None, custom_headers: Mapping[str, str] | None = None
|
|
23
|
+
) -> dict[str, str]:
|
|
24
|
+
headers = {
|
|
25
|
+
"Accept": "application/json",
|
|
26
|
+
"User-Agent": f"wirebox-python/{__version__}",
|
|
27
|
+
}
|
|
28
|
+
if api_key:
|
|
29
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
30
|
+
if custom_headers:
|
|
31
|
+
headers.update(custom_headers)
|
|
32
|
+
return headers
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _clean_params(params: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
|
36
|
+
if not params:
|
|
37
|
+
return None
|
|
38
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _handle_response(resp: httpx.Response) -> Any:
|
|
42
|
+
request_id = resp.headers.get("x-wirebox-request-id") or resp.headers.get("x-request-id")
|
|
43
|
+
|
|
44
|
+
if resp.status_code == 204:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
content_type = resp.headers.get("content-type", "")
|
|
48
|
+
data: Any
|
|
49
|
+
if "application/json" in content_type:
|
|
50
|
+
try:
|
|
51
|
+
data = resp.json()
|
|
52
|
+
except Exception:
|
|
53
|
+
data = resp.text
|
|
54
|
+
else:
|
|
55
|
+
data = resp.text
|
|
56
|
+
|
|
57
|
+
if not resp.is_success:
|
|
58
|
+
raise parse_api_error(resp.status_code, data, request_id)
|
|
59
|
+
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SyncHttpTransport:
|
|
64
|
+
"""Synchronous HTTP transport for Wirebox API communication."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
api_key: str | None = None,
|
|
69
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
70
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
71
|
+
client: httpx.Client | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
self.base_url = base_url.rstrip("/")
|
|
74
|
+
self.api_key = api_key
|
|
75
|
+
self.timeout = timeout
|
|
76
|
+
self._external_client = client is not None
|
|
77
|
+
self._client = client or httpx.Client(
|
|
78
|
+
base_url=self.base_url,
|
|
79
|
+
headers=_build_headers(api_key),
|
|
80
|
+
timeout=timeout,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def request(
|
|
84
|
+
self,
|
|
85
|
+
method: str,
|
|
86
|
+
path: str,
|
|
87
|
+
*,
|
|
88
|
+
params: Mapping[str, Any] | None = None,
|
|
89
|
+
json: Any = None,
|
|
90
|
+
headers: Mapping[str, str] | None = None,
|
|
91
|
+
timeout: float | None = None,
|
|
92
|
+
) -> Any:
|
|
93
|
+
url = path if path.startswith("/") else f"/{path}"
|
|
94
|
+
try:
|
|
95
|
+
resp = self._client.request(
|
|
96
|
+
method,
|
|
97
|
+
url,
|
|
98
|
+
params=_clean_params(params),
|
|
99
|
+
json=json,
|
|
100
|
+
headers=dict(headers) if headers else None,
|
|
101
|
+
timeout=timeout or self.timeout,
|
|
102
|
+
)
|
|
103
|
+
return _handle_response(resp)
|
|
104
|
+
except httpx.TimeoutException as exc:
|
|
105
|
+
raise WireboxConnectionError(
|
|
106
|
+
f"Request timed out after {timeout or self.timeout}s: {exc}", exc
|
|
107
|
+
) from exc
|
|
108
|
+
except httpx.NetworkError as exc:
|
|
109
|
+
raise WireboxConnectionError(f"Network connection failed: {exc}", exc) from exc
|
|
110
|
+
|
|
111
|
+
def get(self, path: str, *, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
|
|
112
|
+
return self.request("GET", path, params=params, **kwargs)
|
|
113
|
+
|
|
114
|
+
def post(self, path: str, *, json: Any = None, **kwargs: Any) -> Any:
|
|
115
|
+
return self.request("POST", path, json=json, **kwargs)
|
|
116
|
+
|
|
117
|
+
def patch(self, path: str, *, json: Any = None, **kwargs: Any) -> Any:
|
|
118
|
+
return self.request("PATCH", path, json=json, **kwargs)
|
|
119
|
+
|
|
120
|
+
def delete(self, path: str, **kwargs: Any) -> Any:
|
|
121
|
+
return self.request("DELETE", path, **kwargs)
|
|
122
|
+
|
|
123
|
+
def close(self) -> None:
|
|
124
|
+
if not self._external_client:
|
|
125
|
+
self._client.close()
|
|
126
|
+
|
|
127
|
+
def __enter__(self) -> SyncHttpTransport:
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def __exit__(self, *args: Any) -> None:
|
|
131
|
+
self.close()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AsyncHttpTransport:
|
|
135
|
+
"""Asynchronous HTTP transport for Wirebox API communication."""
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
api_key: str | None = None,
|
|
140
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
141
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
142
|
+
client: httpx.AsyncClient | None = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
self.base_url = base_url.rstrip("/")
|
|
145
|
+
self.api_key = api_key
|
|
146
|
+
self.timeout = timeout
|
|
147
|
+
self._external_client = client is not None
|
|
148
|
+
self._client = client or httpx.AsyncClient(
|
|
149
|
+
base_url=self.base_url,
|
|
150
|
+
headers=_build_headers(api_key),
|
|
151
|
+
timeout=timeout,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def request(
|
|
155
|
+
self,
|
|
156
|
+
method: str,
|
|
157
|
+
path: str,
|
|
158
|
+
*,
|
|
159
|
+
params: Mapping[str, Any] | None = None,
|
|
160
|
+
json: Any = None,
|
|
161
|
+
headers: Mapping[str, str] | None = None,
|
|
162
|
+
timeout: float | None = None,
|
|
163
|
+
) -> Any:
|
|
164
|
+
url = path if path.startswith("/") else f"/{path}"
|
|
165
|
+
try:
|
|
166
|
+
resp = await self._client.request(
|
|
167
|
+
method,
|
|
168
|
+
url,
|
|
169
|
+
params=_clean_params(params),
|
|
170
|
+
json=json,
|
|
171
|
+
headers=dict(headers) if headers else None,
|
|
172
|
+
timeout=timeout or self.timeout,
|
|
173
|
+
)
|
|
174
|
+
return _handle_response(resp)
|
|
175
|
+
except httpx.TimeoutException as exc:
|
|
176
|
+
raise WireboxConnectionError(
|
|
177
|
+
f"Request timed out after {timeout or self.timeout}s: {exc}", exc
|
|
178
|
+
) from exc
|
|
179
|
+
except httpx.NetworkError as exc:
|
|
180
|
+
raise WireboxConnectionError(f"Network connection failed: {exc}", exc) from exc
|
|
181
|
+
|
|
182
|
+
async def get(
|
|
183
|
+
self, path: str, *, params: Mapping[str, Any] | None = None, **kwargs: Any
|
|
184
|
+
) -> Any:
|
|
185
|
+
return await self.request("GET", path, params=params, **kwargs)
|
|
186
|
+
|
|
187
|
+
async def post(self, path: str, *, json: Any = None, **kwargs: Any) -> Any:
|
|
188
|
+
return await self.request("POST", path, json=json, **kwargs)
|
|
189
|
+
|
|
190
|
+
async def patch(self, path: str, *, json: Any = None, **kwargs: Any) -> Any:
|
|
191
|
+
return await self.request("PATCH", path, json=json, **kwargs)
|
|
192
|
+
|
|
193
|
+
async def delete(self, path: str, **kwargs: Any) -> Any:
|
|
194
|
+
return await self.request("DELETE", path, **kwargs)
|
|
195
|
+
|
|
196
|
+
async def aclose(self) -> None:
|
|
197
|
+
if not self._external_client:
|
|
198
|
+
await self._client.aclose()
|
|
199
|
+
|
|
200
|
+
async def __aenter__(self) -> AsyncHttpTransport:
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
204
|
+
await self.aclose()
|
wirebox/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
wirebox/async_client.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Wirebox Python SDK — Asynchronous Client.
|
|
2
|
+
|
|
3
|
+
Main asynchronous entry point for interacting with the Wirebox Edge Core API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from wirebox._http import DEFAULT_BASE_URL, DEFAULT_TIMEOUT_SECONDS, AsyncHttpTransport
|
|
15
|
+
from wirebox.identity import AsyncAgentIdentity
|
|
16
|
+
from wirebox.mail import AsyncMailClient
|
|
17
|
+
from wirebox.tunnels import AsyncTunnelsClient
|
|
18
|
+
from wirebox.types import IdentityData, WhoamiResult
|
|
19
|
+
from wirebox.webhooks import AsyncWebhooksClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsyncWirebox:
|
|
23
|
+
"""Asynchronous client for the Wirebox API.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> from wirebox import AsyncWirebox
|
|
27
|
+
>>> async with AsyncWirebox(api_key="wb_live_...") as client:
|
|
28
|
+
... agent = await client.create_identity("sales-bot", display_name="Sales Bot")
|
|
29
|
+
... await agent.send_email(to="customer@example.com", subject="Hi", text="Hello!")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
api_key: str | None = None,
|
|
35
|
+
*,
|
|
36
|
+
base_url: str | None = None,
|
|
37
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
38
|
+
http_client: httpx.AsyncClient | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
resolved_key = api_key or os.environ.get("WIREBOX_API_KEY")
|
|
41
|
+
resolved_url = base_url or os.environ.get("WIREBOX_BASE_URL") or DEFAULT_BASE_URL
|
|
42
|
+
|
|
43
|
+
self._api_key = resolved_key
|
|
44
|
+
self._base_url = resolved_url
|
|
45
|
+
|
|
46
|
+
self._transport = AsyncHttpTransport(
|
|
47
|
+
api_key=resolved_key,
|
|
48
|
+
base_url=resolved_url,
|
|
49
|
+
timeout=timeout,
|
|
50
|
+
client=http_client,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.mail = AsyncMailClient(self._transport)
|
|
54
|
+
self.tunnels = AsyncTunnelsClient(self._transport, resolved_key, resolved_url)
|
|
55
|
+
self.webhooks = AsyncWebhooksClient(self._transport)
|
|
56
|
+
|
|
57
|
+
async def create_identity(
|
|
58
|
+
self,
|
|
59
|
+
agent_handle: str,
|
|
60
|
+
*,
|
|
61
|
+
display_name: str | None = None,
|
|
62
|
+
description: str | None = None,
|
|
63
|
+
) -> AsyncAgentIdentity:
|
|
64
|
+
"""Provisions a new agent identity with an atomic dedicated mailbox and network tunnel."""
|
|
65
|
+
payload: dict[str, Any] = {
|
|
66
|
+
"agent_handle": agent_handle.strip().lstrip("@").lower(),
|
|
67
|
+
}
|
|
68
|
+
if display_name is not None:
|
|
69
|
+
payload["display_name"] = display_name
|
|
70
|
+
if description is not None:
|
|
71
|
+
payload["description"] = description
|
|
72
|
+
|
|
73
|
+
data = await self._transport.post("/v1/identities", json=payload)
|
|
74
|
+
return AsyncAgentIdentity(
|
|
75
|
+
IdentityData.from_dict(data),
|
|
76
|
+
self._transport,
|
|
77
|
+
self._api_key,
|
|
78
|
+
self._base_url,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def get_identity(self, handle_or_id: str) -> AsyncAgentIdentity:
|
|
82
|
+
"""Retrieves an existing agent identity by handle or ID."""
|
|
83
|
+
clean_handle = handle_or_id.strip().lstrip("@").lower()
|
|
84
|
+
data = await self._transport.get(f"/v1/identities/{quote(clean_handle)}")
|
|
85
|
+
return AsyncAgentIdentity(
|
|
86
|
+
IdentityData.from_dict(data),
|
|
87
|
+
self._transport,
|
|
88
|
+
self._api_key,
|
|
89
|
+
self._base_url,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def list_identities(
|
|
93
|
+
self,
|
|
94
|
+
*,
|
|
95
|
+
limit: int | None = None,
|
|
96
|
+
offset: int | None = None,
|
|
97
|
+
status: Literal["active", "archived", "deleted"] | None = None,
|
|
98
|
+
) -> list[AsyncAgentIdentity]:
|
|
99
|
+
"""Lists agent identities in the organization."""
|
|
100
|
+
params: dict[str, Any] = {}
|
|
101
|
+
if limit is not None:
|
|
102
|
+
params["limit"] = limit
|
|
103
|
+
if offset is not None:
|
|
104
|
+
params["offset"] = offset
|
|
105
|
+
if status is not None:
|
|
106
|
+
params["status"] = status
|
|
107
|
+
|
|
108
|
+
data = await self._transport.get("/v1/identities", params=params)
|
|
109
|
+
if isinstance(data, list):
|
|
110
|
+
raw_items = data
|
|
111
|
+
elif isinstance(data, dict):
|
|
112
|
+
raw_items = data.get("identities", [])
|
|
113
|
+
else:
|
|
114
|
+
raw_items = []
|
|
115
|
+
return [
|
|
116
|
+
AsyncAgentIdentity(
|
|
117
|
+
IdentityData.from_dict(item),
|
|
118
|
+
self._transport,
|
|
119
|
+
self._api_key,
|
|
120
|
+
self._base_url,
|
|
121
|
+
)
|
|
122
|
+
for item in raw_items
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
async def whoami(self) -> WhoamiResult:
|
|
126
|
+
"""Inspects the active API key and organization authentication context."""
|
|
127
|
+
data = await self._transport.get("/v1/whoami")
|
|
128
|
+
return WhoamiResult.from_dict(data)
|
|
129
|
+
|
|
130
|
+
async def aclose(self) -> None:
|
|
131
|
+
"""Closes underlying HTTP connections."""
|
|
132
|
+
await self._transport.aclose()
|
|
133
|
+
|
|
134
|
+
async def __aenter__(self) -> AsyncWirebox:
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
138
|
+
await self.aclose()
|
wirebox/client.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Wirebox Python SDK — Synchronous Client.
|
|
2
|
+
|
|
3
|
+
Main synchronous entry point for interacting with the Wirebox Edge Core API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from wirebox._http import DEFAULT_BASE_URL, DEFAULT_TIMEOUT_SECONDS, SyncHttpTransport
|
|
15
|
+
from wirebox.identity import AgentIdentity
|
|
16
|
+
from wirebox.mail import MailClient
|
|
17
|
+
from wirebox.tunnels import TunnelsClient
|
|
18
|
+
from wirebox.types import IdentityData, WhoamiResult
|
|
19
|
+
from wirebox.webhooks import WebhooksClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Wirebox:
|
|
23
|
+
"""Synchronous client for the Wirebox API.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> from wirebox import Wirebox
|
|
27
|
+
>>> client = Wirebox(api_key="wb_live_...")
|
|
28
|
+
>>> agent = client.create_identity("sales-bot", display_name="Sales Bot")
|
|
29
|
+
>>> agent.send_email(to="customer@example.com", subject="Hi", text="Hello!")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
api_key: str | None = None,
|
|
35
|
+
*,
|
|
36
|
+
base_url: str | None = None,
|
|
37
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
38
|
+
http_client: httpx.Client | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
resolved_key = api_key or os.environ.get("WIREBOX_API_KEY")
|
|
41
|
+
resolved_url = base_url or os.environ.get("WIREBOX_BASE_URL") or DEFAULT_BASE_URL
|
|
42
|
+
|
|
43
|
+
self._transport = SyncHttpTransport(
|
|
44
|
+
api_key=resolved_key,
|
|
45
|
+
base_url=resolved_url,
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
client=http_client,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.mail = MailClient(self._transport)
|
|
51
|
+
self.tunnels = TunnelsClient(self._transport)
|
|
52
|
+
self.webhooks = WebhooksClient(self._transport)
|
|
53
|
+
|
|
54
|
+
def create_identity(
|
|
55
|
+
self,
|
|
56
|
+
agent_handle: str,
|
|
57
|
+
*,
|
|
58
|
+
display_name: str | None = None,
|
|
59
|
+
description: str | None = None,
|
|
60
|
+
) -> AgentIdentity:
|
|
61
|
+
"""Provisions a new agent identity with an atomic dedicated mailbox and network tunnel."""
|
|
62
|
+
payload: dict[str, Any] = {
|
|
63
|
+
"agent_handle": agent_handle.strip().lstrip("@").lower(),
|
|
64
|
+
}
|
|
65
|
+
if display_name is not None:
|
|
66
|
+
payload["display_name"] = display_name
|
|
67
|
+
if description is not None:
|
|
68
|
+
payload["description"] = description
|
|
69
|
+
|
|
70
|
+
data = self._transport.post("/v1/identities", json=payload)
|
|
71
|
+
return AgentIdentity(IdentityData.from_dict(data), self._transport)
|
|
72
|
+
|
|
73
|
+
def get_identity(self, handle_or_id: str) -> AgentIdentity:
|
|
74
|
+
"""Retrieves an existing agent identity by handle or ID."""
|
|
75
|
+
clean_handle = handle_or_id.strip().lstrip("@").lower()
|
|
76
|
+
data = self._transport.get(f"/v1/identities/{quote(clean_handle)}")
|
|
77
|
+
return AgentIdentity(IdentityData.from_dict(data), self._transport)
|
|
78
|
+
|
|
79
|
+
def list_identities(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
limit: int | None = None,
|
|
83
|
+
offset: int | None = None,
|
|
84
|
+
status: Literal["active", "archived", "deleted"] | None = None,
|
|
85
|
+
) -> list[AgentIdentity]:
|
|
86
|
+
"""Lists agent identities in the organization."""
|
|
87
|
+
params: dict[str, Any] = {}
|
|
88
|
+
if limit is not None:
|
|
89
|
+
params["limit"] = limit
|
|
90
|
+
if offset is not None:
|
|
91
|
+
params["offset"] = offset
|
|
92
|
+
if status is not None:
|
|
93
|
+
params["status"] = status
|
|
94
|
+
|
|
95
|
+
data = self._transport.get("/v1/identities", params=params)
|
|
96
|
+
if isinstance(data, list):
|
|
97
|
+
raw_items = data
|
|
98
|
+
elif isinstance(data, dict):
|
|
99
|
+
raw_items = data.get("identities", [])
|
|
100
|
+
else:
|
|
101
|
+
raw_items = []
|
|
102
|
+
return [AgentIdentity(IdentityData.from_dict(item), self._transport) for item in raw_items]
|
|
103
|
+
|
|
104
|
+
def whoami(self) -> WhoamiResult:
|
|
105
|
+
"""Inspects the active API key and organization authentication context."""
|
|
106
|
+
data = self._transport.get("/v1/whoami")
|
|
107
|
+
return WhoamiResult.from_dict(data)
|
|
108
|
+
|
|
109
|
+
def close(self) -> None:
|
|
110
|
+
"""Closes underlying HTTP connections."""
|
|
111
|
+
self._transport.close()
|
|
112
|
+
|
|
113
|
+
def __enter__(self) -> Wirebox:
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def __exit__(self, *args: Any) -> None:
|
|
117
|
+
self.close()
|
wirebox/exceptions.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Wirebox Python SDK — Exception Hierarchy.
|
|
2
|
+
|
|
3
|
+
Provides structured, typed exceptions for network, authentication, validation,
|
|
4
|
+
and API error conditions with full request ID tracing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WireboxError(Exception):
|
|
13
|
+
"""Base exception for all Wirebox SDK errors."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WireboxConnectionError(WireboxError):
|
|
17
|
+
"""Raised when an HTTP or WebSocket connection fails or times out."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str, original_error: Exception | None = None) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.original_error = original_error
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationError(WireboxError):
|
|
25
|
+
"""Raised when client-side parameter validation fails before sending a request."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WireboxAPIError(WireboxError):
|
|
29
|
+
"""Raised when the Wirebox Edge Core API returns a non-2xx response.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
status: HTTP response status code (e.g. 400, 401, 404, 429, 500).
|
|
33
|
+
code: Machine-readable error code (e.g. 'handle_already_taken', 'unauthorized').
|
|
34
|
+
message: Human-readable error message explaining the failure.
|
|
35
|
+
request_id: Optional Wirebox request ID (x-wirebox-request-id) for tracing.
|
|
36
|
+
details: Optional structured details dictionary from the server.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
status: int,
|
|
42
|
+
code: str,
|
|
43
|
+
message: str,
|
|
44
|
+
request_id: str | None = None,
|
|
45
|
+
details: Any = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
super().__init__(
|
|
48
|
+
f"[{status} {code}] {message}"
|
|
49
|
+
if request_id is None
|
|
50
|
+
else f"[{status} {code}] {message} (request_id: {request_id})"
|
|
51
|
+
)
|
|
52
|
+
self.status = status
|
|
53
|
+
self.code = code
|
|
54
|
+
self.message = message
|
|
55
|
+
self.request_id = request_id
|
|
56
|
+
self.details = details
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AuthenticationError(WireboxAPIError):
|
|
60
|
+
"""Raised on HTTP 401 or 403 when the provided API key is missing, invalid, or lacks permissions."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class NotFoundError(WireboxAPIError):
|
|
64
|
+
"""Raised on HTTP 404 when the requested identity, mailbox, message, tunnel, or webhook does not exist."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RateLimitError(WireboxAPIError):
|
|
68
|
+
"""Raised on HTTP 429 when API rate limits are exceeded."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class HandleAlreadyTakenError(WireboxAPIError):
|
|
72
|
+
"""Raised on HTTP 409 when attempting to create an identity with an agent_handle that is already claimed."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FreeTierLimitExceededError(WireboxAPIError):
|
|
76
|
+
"""Raised when an organization attempts to provision resources beyond their free tier allowance."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_api_error(status: int, data: Any, request_id: str | None = None) -> WireboxAPIError:
|
|
80
|
+
"""Parses a response status and body into the appropriate WireboxAPIError subclass."""
|
|
81
|
+
code = "unknown_error"
|
|
82
|
+
message = f"Request failed with HTTP status {status}"
|
|
83
|
+
details: Any = None
|
|
84
|
+
|
|
85
|
+
if isinstance(data, dict):
|
|
86
|
+
if "error" in data:
|
|
87
|
+
err = data["error"]
|
|
88
|
+
if isinstance(err, dict):
|
|
89
|
+
code = str(err.get("code", code))
|
|
90
|
+
message = str(err.get("message", message))
|
|
91
|
+
details = err.get("details", data)
|
|
92
|
+
elif isinstance(err, str):
|
|
93
|
+
message = err
|
|
94
|
+
elif "message" in data:
|
|
95
|
+
message = str(data["message"])
|
|
96
|
+
if "code" in data:
|
|
97
|
+
code = str(data["code"])
|
|
98
|
+
details = data
|
|
99
|
+
|
|
100
|
+
clean_code = code.lower()
|
|
101
|
+
|
|
102
|
+
if status in (401, 403):
|
|
103
|
+
if "free_tier" in clean_code or "quota" in clean_code:
|
|
104
|
+
return FreeTierLimitExceededError(status, code, message, request_id, details)
|
|
105
|
+
return AuthenticationError(status, code, message, request_id, details)
|
|
106
|
+
|
|
107
|
+
if status == 404:
|
|
108
|
+
return NotFoundError(status, code, message, request_id, details)
|
|
109
|
+
|
|
110
|
+
if status == 409 or clean_code in ("handle_already_taken", "conflict"):
|
|
111
|
+
return HandleAlreadyTakenError(status, code, message, request_id, details)
|
|
112
|
+
|
|
113
|
+
if status == 429:
|
|
114
|
+
if "free_tier" in clean_code:
|
|
115
|
+
return FreeTierLimitExceededError(status, code, message, request_id, details)
|
|
116
|
+
return RateLimitError(status, code, message, request_id, details)
|
|
117
|
+
|
|
118
|
+
return WireboxAPIError(status, code, message, request_id, details)
|