wirebox 0.2.0__tar.gz → 0.4.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.
Files changed (40) hide show
  1. {wirebox-0.2.0 → wirebox-0.4.0}/PKG-INFO +1 -1
  2. wirebox-0.4.0/examples/auto_reply.py +155 -0
  3. wirebox-0.4.0/examples/personal_agent_identity.py +193 -0
  4. wirebox-0.4.0/examples/quickstart.py +87 -0
  5. {wirebox-0.2.0 → wirebox-0.4.0}/pyproject.toml +1 -1
  6. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/__init__.py +19 -0
  7. wirebox-0.4.0/src/wirebox/_config.py +110 -0
  8. wirebox-0.4.0/src/wirebox/_version.py +1 -0
  9. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/async_client.py +53 -17
  10. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/client.py +51 -12
  11. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/exceptions.py +5 -0
  12. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/identity.py +134 -0
  13. wirebox-0.4.0/src/wirebox/phone.py +293 -0
  14. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/tunnels.py +95 -34
  15. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/types.py +381 -30
  16. {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_async_client.py +1 -1
  17. wirebox-0.4.0/tests/test_config.py +102 -0
  18. wirebox-0.4.0/tests/test_phone.py +254 -0
  19. {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_types.py +73 -0
  20. {wirebox-0.2.0 → wirebox-0.4.0}/uv.lock +2 -2
  21. wirebox-0.2.0/src/wirebox/_version.py +0 -1
  22. {wirebox-0.2.0 → wirebox-0.4.0}/.github/workflows/ci.yml +0 -0
  23. {wirebox-0.2.0 → wirebox-0.4.0}/.github/workflows/publish.yml +0 -0
  24. {wirebox-0.2.0 → wirebox-0.4.0}/.gitignore +0 -0
  25. {wirebox-0.2.0 → wirebox-0.4.0}/AGENTS.md +0 -0
  26. {wirebox-0.2.0 → wirebox-0.4.0}/LICENSE +0 -0
  27. {wirebox-0.2.0 → wirebox-0.4.0}/README.md +0 -0
  28. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/_http.py +0 -0
  29. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/imessage.py +0 -0
  30. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/mail.py +0 -0
  31. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/py.typed +0 -0
  32. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/verify_webhook.py +0 -0
  33. {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/webhooks.py +0 -0
  34. {wirebox-0.2.0 → wirebox-0.4.0}/tests/conftest.py +0 -0
  35. {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_client.py +0 -0
  36. {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_exceptions.py +0 -0
  37. {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_imessage.py +0 -0
  38. {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_tunnels.py +0 -0
  39. {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_verify_webhook.py +0 -0
  40. {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: wirebox
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Official Python SDK for Wirebox — Real-world identity and communication layer for AI agents
5
5
  Project-URL: Homepage, https://wirebox.sh
6
6
  Project-URL: Documentation, https://docs.wirebox.sh
@@ -0,0 +1,155 @@
1
+ """Wirebox Python SDK — Autonomous Auto-Reply Agent Example.
2
+
3
+ Demonstrates:
4
+ 1. Resolving the active AgentIdentity with zero-config credentials
5
+ 2. Ensuring an agent-scoped webhook subscription is active
6
+ 3. Connecting an in-memory WebSocket reverse-proxy tunnel
7
+ 4. Intercepting "email.received" events in process memory
8
+ 5. Automatically dispatching a threaded reply via agent.reply_email()
9
+
10
+ Usage:
11
+ python examples/auto_reply.py
12
+ python examples/auto_reply.py --once # Connects, verifies health ping, and exits cleanly
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import datetime
19
+ import json
20
+ import sys
21
+
22
+ import httpx
23
+
24
+ from wirebox import AsyncWirebox
25
+ from wirebox.exceptions import WireboxAPIError
26
+
27
+
28
+ async def main() -> None:
29
+ once_mode = "--once" in sys.argv
30
+
31
+ print("=" * 64)
32
+ print(" Wirebox Python SDK — Autonomous Auto-Reply Agent")
33
+ print("========================================================\n")
34
+
35
+ async with AsyncWirebox() as client:
36
+ agent = await client.get_identity()
37
+
38
+ print(f"Agent Handle: @{agent.agent_handle}")
39
+ print(f"Display Name: {agent.display_name or 'N/A'}")
40
+ print(f"Mailbox Address: {agent.mailbox.email_address}")
41
+ print(f"Tunnel URL: {agent.tunnel.public_url}")
42
+
43
+ target_endpoint = f"{agent.tunnel.public_url}/webhook"
44
+
45
+ # 1. Ensure webhook subscription exists
46
+ webhooks = await agent.list_webhooks()
47
+ hook = next((w for w in webhooks if w.url == target_endpoint), None)
48
+ ephemeral = False
49
+
50
+ if not hook:
51
+ print(f"\nSetting up webhook subscription for {target_endpoint}...")
52
+ hook = await agent.create_webhook(
53
+ url=target_endpoint,
54
+ events=["email.received", "test.ping"],
55
+ )
56
+ ephemeral = True
57
+ print(f"✔ Webhook registered: {hook.id}")
58
+ else:
59
+ print(f"✔ Using existing webhook subscription: {hook.id}")
60
+
61
+ # 2. In-memory event handler
62
+ async def handle_request(request: httpx.Request) -> httpx.Response:
63
+ now = datetime.datetime.now().strftime("%H:%M:%S")
64
+
65
+ try:
66
+ payload = json.loads(request.content.decode("utf-8"))
67
+ except Exception:
68
+ payload = {}
69
+
70
+ event_type = payload.get("event_type") or payload.get("type") or "unknown"
71
+
72
+ if event_type == "test.ping":
73
+ print(f"[{now}] 🏓 Ping event received. Returning 200 OK.")
74
+ return httpx.Response(200, json={"pong": True})
75
+
76
+ if event_type == "email.received":
77
+ data = payload.get("data") or {}
78
+ incoming = data.get("message") or {}
79
+ sender = incoming.get("from") or incoming.get("from_address") or "Unknown"
80
+ subject = incoming.get("subject") or "(no subject)"
81
+ snippet = incoming.get("snippet") or incoming.get("preview") or ""
82
+ msg_id = incoming.get("id")
83
+
84
+ print(f"\n[{now}] 📬 Email received!")
85
+ print(f" From: {sender}")
86
+ print(f" Subject: {subject}")
87
+ if snippet:
88
+ print(f" Snippet: {snippet[:80]}")
89
+
90
+ if msg_id:
91
+ try:
92
+ print(" ⚡ Dispatching automated reply via agent.reply_email()...")
93
+ reply = await agent.reply_email(
94
+ msg_id,
95
+ text=(
96
+ f"Hello!\n\n"
97
+ f"This is an automated response from @{agent.agent_handle} "
98
+ f"powered by the Wirebox Python SDK.\n\n"
99
+ f'I have received your email: "{subject}".\n\n'
100
+ f"All systems operational!"
101
+ ),
102
+ )
103
+ reply_id = reply.id or reply.message_id
104
+ print(f" ✔ Reply dispatched successfully! ID: {reply_id}")
105
+ except WireboxAPIError as reply_err:
106
+ print(f" ❌ Failed to reply: {reply_err}")
107
+ else:
108
+ print(f"[{now}] ℹ️ Received event: {event_type}")
109
+
110
+ return httpx.Response(200, json={"received": True})
111
+
112
+ # 3. Connect in-memory tunnel session
113
+ print("\nConnecting in-memory tunnel gateway...")
114
+ session = await agent.connect_tunnel(handler=handle_request)
115
+ print("✔ Tunnel connected and ready to receive real-world events!")
116
+ print("\n💡 Test this agent now by sending an email to:")
117
+ print(f" 👉 {agent.mailbox.email_address}")
118
+
119
+ if once_mode:
120
+ print("\n[--once flag detected] Testing tunnel ping before exit...")
121
+ # Fire an internal test ping if supported
122
+ try:
123
+ await client.webhooks.test(hook.id)
124
+ print("✔ Webhook test ping successfully delivered!")
125
+ except Exception as test_err:
126
+ print(f"ℹ️ Ping status: {test_err}")
127
+
128
+ print("\nDisconnecting tunnel session...")
129
+ await session.close()
130
+ if ephemeral and hook:
131
+ print(f"Removing ephemeral webhook {hook.id}...")
132
+ await client.webhooks.delete(hook.id)
133
+ print("[✓] Auto-reply test cycle completed successfully!")
134
+ return
135
+
136
+ print("\nWaiting for incoming events (Press Ctrl+C to exit)...\n")
137
+
138
+ try:
139
+ await session.wait_closed()
140
+ finally:
141
+ print("\nDisconnecting tunnel session...")
142
+ await session.close()
143
+ if ephemeral and hook:
144
+ print(f"Removing ephemeral webhook {hook.id}...")
145
+ await client.webhooks.delete(hook.id)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ try:
150
+ asyncio.run(main())
151
+ except KeyboardInterrupt:
152
+ print("\nShutdown complete.")
153
+ except Exception as exc:
154
+ print(f"\n[!] Error: {exc}", file=sys.stderr)
155
+ sys.exit(1)
@@ -0,0 +1,193 @@
1
+ """Wirebox Python SDK — Personal Agent Identity Layer.
2
+
3
+ Demonstrates how an AI platform or agent developer uses Wirebox as the
4
+ foundational IDENTITY and REAL-WORLD COMMUNICATION layer for autonomous agents.
5
+
6
+ Key Architectural Principle:
7
+ - Wirebox does NOT implement agent reasoning, LLM orchestration, or task planning.
8
+ - Wirebox provides the physical IDENTITY INFRASTRUCTURE that equips agents with:
9
+ 1. Identity Presence: Unique global agent handle (@handle) and tenant isolation
10
+ 2. Mailbox Infrastructure: Dedicated email address ({handle}@wireboxmail.com)
11
+ 3. Telecom & Messaging: Dedicated carrier phone numbers, SMS, and Apple iMessage
12
+ 4. Edge Gateway (Tunnel): Reverse-proxy WebSocket tunnel streaming real-world
13
+ inbound events directly into the agent's runtime process
14
+
15
+ Usage:
16
+ python examples/personal_agent_identity.py
17
+ python examples/personal_agent_identity.py --once # Self-test cycle and exit
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import os
24
+ import sys
25
+ import time
26
+
27
+ from wirebox import AsyncWirebox
28
+ from wirebox.exceptions import WireboxAPIError
29
+
30
+
31
+ async def main() -> None:
32
+ once_mode = "--once" in sys.argv
33
+
34
+ print("=" * 64)
35
+ print(" Wirebox Python SDK — Personal Agent Identity Layer")
36
+ print(" Identity & Communication Infrastructure for AI Agents")
37
+ print("=" * 64 + "\n")
38
+
39
+ async with AsyncWirebox() as client:
40
+ # ----------------------------------------------------------------------
41
+ # Phase 1: Identity Provisioning & Resolution
42
+ # ----------------------------------------------------------------------
43
+ print("[Phase 1: Identity Provisioning & Resolution]")
44
+ whoami = await client.whoami()
45
+ is_identity_scoped = bool(whoami.scoped_identity_id)
46
+
47
+ if is_identity_scoped:
48
+ print("✔ Operating in Identity-Scoped Mode (Strict Tenant Isolation)")
49
+ identity = await client.get_identity()
50
+ print(
51
+ f"✔ Loaded active agent identity: @{identity.agent_handle} "
52
+ f"({identity.display_name or 'N/A'})\n"
53
+ )
54
+ else:
55
+ print("✔ Operating in Platform Admin Mode")
56
+ # Reuse existing identity if available to conserve tenant quotas
57
+ existing_agents = await client.list_identities(limit=1)
58
+ if existing_agents:
59
+ identity = existing_agents[0]
60
+ print(
61
+ f"✔ Reusing active agent identity: @{identity.agent_handle} "
62
+ f"({identity.display_name or 'N/A'})\n"
63
+ )
64
+ else:
65
+ agent_handle = f"assistant-{int(time.time()) % 10000}"
66
+ print(f"Provisioning new agent identity container: @{agent_handle}...")
67
+ identity = await client.create_identity(
68
+ agent_handle,
69
+ display_name="Personal Travel Coordinator",
70
+ description="Dedicated real-world identity for autonomous travel coordination",
71
+ )
72
+ print(f"✔ Identity container provisioned! ID: {identity.id}\n")
73
+
74
+ # ----------------------------------------------------------------------
75
+ # Phase 2: Inspect Communication Assets Bound to this Identity
76
+ # ----------------------------------------------------------------------
77
+ print("-" * 64)
78
+ print(f" Identity Assets Bound to @{identity.agent_handle}")
79
+ print("-" * 64)
80
+ print(f" 📧 Mailbox: {identity.mailbox.email_address}")
81
+ print(f" 🌐 Edge Tunnel: {identity.tunnel.public_url}")
82
+ print(" 📱 Messaging: Configured for Carrier SMS & Phone Numbers")
83
+ print("-" * 64 + "\n")
84
+
85
+ # ----------------------------------------------------------------------
86
+ # Phase 3: Phone & Telecom Infrastructure
87
+ # ----------------------------------------------------------------------
88
+ print("[Phase 2: Phone & Telecom Infrastructure]")
89
+ try:
90
+ phone_num = await identity.get_phone_number()
91
+ print(
92
+ f"✔ Bound carrier phone number: {phone_num.phone_number} "
93
+ f"(Status: {phone_num.status})\n"
94
+ )
95
+ except Exception:
96
+ print(f"Requesting carrier phone number allocation for @{identity.agent_handle}...")
97
+ try:
98
+ new_phone = await client.phone.numbers.provision(
99
+ identity.agent_handle,
100
+ country_code="US",
101
+ type="local",
102
+ )
103
+ print(
104
+ f"✔ Carrier phone number provisioned: {new_phone.phone_number} "
105
+ f"({new_phone.type})\n"
106
+ )
107
+ except WireboxAPIError as err:
108
+ if err.status_code == 403 and "phone_provisioning_restricted" in str(err):
109
+ print(
110
+ "ℹ️ [Telecom Gate] Phone number provisioning is currently in Private Beta."
111
+ )
112
+ print(
113
+ " (Once organization is whitelisted, dedicated US/CA cellular numbers activate instantly)\n"
114
+ )
115
+ elif err.status_code == 409:
116
+ print("✔ Agent already possesses an active phone number.\n")
117
+ else:
118
+ print(f"ℹ️ [Telecom Notice] {err}\n")
119
+
120
+ # ----------------------------------------------------------------------
121
+ # Phase 4: Outbound Real-World Communication (Using Identity Mailbox)
122
+ # ----------------------------------------------------------------------
123
+ print("[Phase 3: Outbound Communication via Identity Mailbox]")
124
+ print("Dispatching travel reservation confirmation from agent's dedicated identity...")
125
+
126
+ recipient = os.environ.get("AGENT_RECIPIENT") or identity.mailbox.email_address
127
+ try:
128
+ email_result = await identity.send_email(
129
+ to=recipient,
130
+ subject="[Confirmed] Flight SFO -> HND Reservation for Alex",
131
+ text="\n".join(
132
+ [
133
+ "Hello Alex,",
134
+ "",
135
+ "Your autonomous travel assistant has finalized your booking:",
136
+ " - Flight: NH 107 (San Francisco -> Tokyo Haneda)",
137
+ " - Status: Confirmed & Ticketed",
138
+ " - Booking Ref: WB-9921B",
139
+ "",
140
+ "This notification was dispatched using the agent's dedicated Wirebox identity mailbox.",
141
+ "You can reply directly to this thread anytime to request adjustments.",
142
+ "",
143
+ f"— {identity.display_name or 'Agent'} (@{identity.agent_handle})",
144
+ ]
145
+ ),
146
+ )
147
+
148
+ print("✔ Outbound email dispatched!")
149
+ print(f" Message ID: {email_result.id or email_result.message_id}")
150
+ print(f" Recipient: {recipient}\n")
151
+ except WireboxAPIError as err:
152
+ if "not a verified address" in str(err):
153
+ print(
154
+ f"ℹ️ [Sandbox Notice] Recipient '{recipient}' is not verified in sandbox mode."
155
+ )
156
+ print(" (Set AGENT_RECIPIENT=<your-verified-email> to test external delivery)\n")
157
+ else:
158
+ raise
159
+
160
+ # ----------------------------------------------------------------------
161
+ # Phase 5: Inbound Edge Gateway (Ingesting Real-World Signals into Agent)
162
+ # ----------------------------------------------------------------------
163
+ print("[Phase 4: Inbound Edge Gateway (Mail & Phone Events)]")
164
+ target_endpoint = f"{identity.tunnel.public_url}/webhook"
165
+
166
+ hook = await identity.create_webhook(
167
+ url=target_endpoint,
168
+ events=["email.received", "test.ping", "sms.received"],
169
+ )
170
+ print(f"✔ Webhook route bound: {hook.id} -> {target_endpoint}\n")
171
+
172
+ print("----------------------------------------------------------------")
173
+ print(" Personal Agent Identity Container Ready")
174
+ print(f" Agent Handle: @{identity.agent_handle}")
175
+ print(f" Email Address: {identity.mailbox.email_address}")
176
+ print(f" Gateway Tunnel: {identity.tunnel.public_url}")
177
+ print("----------------------------------------------------------------")
178
+
179
+ if once_mode:
180
+ print("\n[--once flag detected] Test cycle complete. Exiting cleanly.")
181
+ return
182
+
183
+ print("\nAgent identity infrastructure is operational. Press Ctrl+C to stop.\n")
184
+
185
+
186
+ if __name__ == "__main__":
187
+ try:
188
+ asyncio.run(main())
189
+ except KeyboardInterrupt:
190
+ print("\nShutdown requested by user.")
191
+ except Exception as exc:
192
+ print(f"\n[!] Error: {exc}", file=sys.stderr)
193
+ sys.exit(1)
@@ -0,0 +1,87 @@
1
+ """Wirebox Python SDK — Quickstart Example.
2
+
3
+ Demonstrates zero-config initialization with Wirebox(), caller introspection,
4
+ identity discovery, sending an outbound email, and inspecting inbox messages.
5
+
6
+ Usage:
7
+ python -m examples.quickstart
8
+ # or
9
+ python examples/quickstart.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import sys
16
+
17
+ from wirebox import Wirebox
18
+
19
+
20
+ def main() -> None:
21
+ print("=" * 60)
22
+ print("Wirebox Python SDK - Quickstart")
23
+ print("=" * 60)
24
+
25
+ # 1. Zero-config client initialization
26
+ # Automatically resolves API key from ~/.wirebox/credentials or ~/.wirebox/config
27
+ client = Wirebox()
28
+
29
+ # 2. Caller introspection via /v1/me
30
+ who = client.whoami()
31
+ print(f"\n[1] Authenticated Organization: {who.organization.name} ({who.organization.id})")
32
+ print(f" Billing Plan: {who.organization.billing_plan}")
33
+ print(f" Active Actor: {who.auth.actor_id} (Type: {who.auth.type})")
34
+ print(f" Usage: {who.usage.agents_count}/{who.usage.agents_limit} agents provisioned")
35
+
36
+ # 3. Retrieve or discover active agent identity
37
+ agents = client.list_identities(limit=10)
38
+ if not agents:
39
+ print("\n[!] No active agent identities found. Creating a starter identity...")
40
+ agent = client.create_identity("assistant-bot", display_name="AI Assistant")
41
+ else:
42
+ agent = agents[0]
43
+
44
+ print(f"\n[2] Active Agent Identity: @{agent.agent_handle}")
45
+ print(f" Display Name: {agent.display_name or 'N/A'}")
46
+ print(f" Primary Mailbox: {agent.mailbox.email_address}")
47
+ print(f" Network Tunnel: {agent.tunnel.public_url}")
48
+
49
+ # 4. Dispatch a verified outbound email
50
+ recipient = os.environ.get("AGENT_RECIPIENT") or agent.mailbox.email_address
51
+ print(f"\n[3] Dispatching verified outbound message to: {recipient}...")
52
+
53
+ dispatch_result = agent.send_email(
54
+ to=recipient,
55
+ subject="[Wirebox Quickstart] Autonomous Agent Online",
56
+ text=(
57
+ f"Hello!\n\n"
58
+ f"Your personal agent @{agent.agent_handle} is fully provisioned with real-world infrastructure:\n"
59
+ f"- Email Inbox: {agent.mailbox.email_address}\n"
60
+ f"- Edge Tunnel: {agent.tunnel.public_url}\n\n"
61
+ f"Sent autonomously via the Wirebox Edge Core API."
62
+ ),
63
+ )
64
+
65
+ print(f" Message Dispatched! ID: {dispatch_result.id}")
66
+ if dispatch_result.thread_id:
67
+ print(f" Thread ID: {dispatch_result.thread_id}")
68
+
69
+ # 5. Inspect mailbox messages
70
+ print(f"\n[4] Querying recent messages for {agent.mailbox.email_address}...")
71
+ messages = agent.list_messages(limit=5)
72
+ print(f" Found {len(messages)} recent message(s):")
73
+ for msg in messages:
74
+ snippet_text = (msg.snippet or msg.preview)[:60]
75
+ print(f" - [{msg.direction.upper()}] from={msg.from_address}")
76
+ print(f" subject: {msg.subject}")
77
+ print(f" snippet: {snippet_text}...")
78
+
79
+ print("\n[✓] Wirebox Python Quickstart completed successfully!\n")
80
+
81
+
82
+ if __name__ == "__main__":
83
+ try:
84
+ main()
85
+ except Exception as exc:
86
+ print(f"\n[!] Error: {exc}", file=sys.stderr)
87
+ sys.exit(1)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wirebox"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "Official Python SDK for Wirebox — Real-world identity and communication layer for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -20,6 +20,7 @@ from wirebox.exceptions import (
20
20
  )
21
21
  from wirebox.identity import AgentIdentity, AsyncAgentIdentity
22
22
  from wirebox.imessage import AsyncIMessageClient, IMessageClient
23
+ from wirebox.phone import AsyncPhoneClient, PhoneClient
23
24
  from wirebox.tunnels import TunnelSession
24
25
  from wirebox.types import (
25
26
  EmailMessage,
@@ -29,9 +30,15 @@ from wirebox.types import (
29
30
  ImessageConversationLastMessage,
30
31
  ImessageMessage,
31
32
  ImessageRouterInfo,
33
+ ListPhoneMessagesResult,
34
+ ListPhoneNumbersResult,
32
35
  MailboxSummary,
33
36
  MessageAttachmentSummary,
34
37
  MessageSummary,
38
+ PhoneMediaItem,
39
+ PhoneMessage,
40
+ PhoneNumber,
41
+ PhoneNumberCapabilities,
35
42
  SendEmailAttachment,
36
43
  SendEmailResult,
37
44
  SendImessageResult,
@@ -44,8 +51,10 @@ from wirebox.types import (
44
51
  WebhookStatus,
45
52
  WebhookTestResult,
46
53
  WhoamiApiKey,
54
+ WhoamiAuth,
47
55
  WhoamiOrganization,
48
56
  WhoamiResult,
57
+ WhoamiUsage,
49
58
  )
50
59
  from wirebox.verify_webhook import verify_webhook
51
60
 
@@ -63,6 +72,8 @@ __all__ = [
63
72
  "AsyncAgentIdentity",
64
73
  "IMessageClient",
65
74
  "AsyncIMessageClient",
75
+ "PhoneClient",
76
+ "AsyncPhoneClient",
66
77
  "TunnelSession",
67
78
  "verify_webhook",
68
79
  # Exceptions
@@ -89,6 +100,12 @@ __all__ = [
89
100
  "ImessageConversation",
90
101
  "ImessageConversationLastMessage",
91
102
  "ImessageMessage",
103
+ "PhoneNumberCapabilities",
104
+ "PhoneNumber",
105
+ "PhoneMediaItem",
106
+ "PhoneMessage",
107
+ "ListPhoneNumbersResult",
108
+ "ListPhoneMessagesResult",
92
109
  "Tunnel",
93
110
  "TunnelClientTelemetry",
94
111
  "Webhook",
@@ -99,5 +116,7 @@ __all__ = [
99
116
  "WebhookStatus",
100
117
  "WhoamiOrganization",
101
118
  "WhoamiApiKey",
119
+ "WhoamiAuth",
120
+ "WhoamiUsage",
102
121
  "WhoamiResult",
103
122
  ]
@@ -0,0 +1,110 @@
1
+ """Wirebox Python SDK — Client Configuration and Credentials Resolver.
2
+
3
+ Resolves API key and base URL from:
4
+ 1. Explicit constructor arguments
5
+ 2. Environment variables (WIREBOX_API_KEY, WIREBOX_BASE_URL)
6
+ 3. ~/.wirebox/credentials or ~/.wirebox/config file
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+
15
+ from wirebox._http import DEFAULT_BASE_URL
16
+
17
+
18
+ def _get_config_search_paths() -> list[Path]:
19
+ custom_cred = os.environ.get("WIREBOX_CREDENTIALS_PATH")
20
+ custom_cfg = os.environ.get("WIREBOX_CONFIG_PATH")
21
+ if custom_cred or custom_cfg:
22
+ paths: list[Path] = []
23
+ if custom_cred:
24
+ paths.append(Path(custom_cred).expanduser())
25
+ if custom_cfg:
26
+ paths.append(Path(custom_cfg).expanduser())
27
+ return paths
28
+
29
+ home_dir = os.environ.get("WIREBOX_HOME")
30
+ base_dir = Path(home_dir).expanduser() / ".wirebox" if home_dir else Path.home() / ".wirebox"
31
+
32
+ return [base_dir / "credentials", base_dir / "config"]
33
+
34
+
35
+ def _read_config_file(path: Path) -> dict[str, str]:
36
+ try:
37
+ text = path.read_text(encoding="utf-8").strip()
38
+ except OSError:
39
+ return {}
40
+
41
+ if not text:
42
+ return {}
43
+
44
+ # 1. Try JSON format
45
+ if text.startswith("{"):
46
+ try:
47
+ parsed = json.loads(text)
48
+ if isinstance(parsed, dict):
49
+ return {str(k): str(v) for k, v in parsed.items() if v is not None}
50
+ except Exception:
51
+ pass
52
+
53
+ out: dict[str, str] = {}
54
+ for line in text.splitlines():
55
+ line = line.strip()
56
+ if not line or line.startswith("#"):
57
+ continue
58
+ if "=" in line:
59
+ key, _, val = line.partition("=")
60
+ out[key.strip()] = val.strip().strip("\"'")
61
+ elif ":" in line:
62
+ key, _, val = line.partition(":")
63
+ out[key.strip()] = val.strip().strip("\"'")
64
+ elif line.startswith("wb_live_") or line.startswith("wb_test_"):
65
+ out["api_key"] = line
66
+
67
+ return out
68
+
69
+
70
+ def resolve_client_settings(
71
+ *,
72
+ api_key: str | None = None,
73
+ base_url: str | None = None,
74
+ ) -> tuple[str | None, str]:
75
+ """Resolves (api_key, base_url) with fallback: explicit -> env -> file."""
76
+ # 1. Resolve API key
77
+ resolved_key = api_key.strip() if api_key and api_key.strip() else None
78
+ if not resolved_key:
79
+ env_key = os.environ.get("WIREBOX_API_KEY")
80
+ if env_key and env_key.strip():
81
+ resolved_key = env_key.strip()
82
+
83
+ # 2. Resolve Base URL
84
+ resolved_url = base_url.strip().rstrip("/") if base_url and base_url.strip() else None
85
+ if not resolved_url:
86
+ env_url = os.environ.get("WIREBOX_BASE_URL")
87
+ if env_url and env_url.strip():
88
+ resolved_url = env_url.strip().rstrip("/")
89
+
90
+ # 3. If either is still missing, search config files
91
+ if not resolved_key or not resolved_url:
92
+ for file_path in _get_config_search_paths():
93
+ cfg = _read_config_file(file_path)
94
+ if not cfg:
95
+ continue
96
+
97
+ if not resolved_key:
98
+ file_key = cfg.get("api_key") or cfg.get("apiKey") or cfg.get("token")
99
+ if file_key and file_key.strip():
100
+ resolved_key = file_key.strip()
101
+
102
+ if not resolved_url:
103
+ file_url = cfg.get("base_url") or cfg.get("baseUrl")
104
+ if file_url and file_url.strip():
105
+ resolved_url = file_url.strip().rstrip("/")
106
+
107
+ if resolved_key and resolved_url:
108
+ break
109
+
110
+ return (resolved_key, resolved_url or DEFAULT_BASE_URL)
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"