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 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"
@@ -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)