castrel-proxy 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.
@@ -0,0 +1,284 @@
1
+ """
2
+ API Client Module
3
+
4
+ Handles HTTP communication with the server
5
+ """
6
+
7
+ import asyncio
8
+ from typing import Dict
9
+
10
+ import aiohttp
11
+
12
+ from ..core.client_id import get_machine_metadata
13
+
14
+
15
+ class APIError(Exception):
16
+ """API-related errors"""
17
+
18
+ pass
19
+
20
+
21
+ class PairingError(APIError):
22
+ """Pairing verification errors"""
23
+
24
+ pass
25
+
26
+
27
+ class NetworkError(APIError):
28
+ """Network connection errors"""
29
+
30
+ pass
31
+
32
+
33
+ class APIClient:
34
+ """API client class"""
35
+
36
+ def __init__(self, timeout: float = 10.0):
37
+ """
38
+ Initialize API client
39
+
40
+ Args:
41
+ timeout: Request timeout (seconds)
42
+ """
43
+ self.timeout = aiohttp.ClientTimeout(total=timeout)
44
+
45
+ async def _verify_pairing_async(
46
+ self, server_url: str, verification_code: str, client_id: str, workspace_id: str
47
+ ) -> Dict[str, any]:
48
+ """
49
+ Asynchronously verify pairing information with server
50
+
51
+ Args:
52
+ server_url: Server URL
53
+ verification_code: Verification code
54
+ client_id: Client unique identifier
55
+ workspace_id: Workspace ID
56
+
57
+ Returns:
58
+ dict: Server response data
59
+
60
+ Raises:
61
+ NetworkError: Network connection failure
62
+ PairingError: Verification failure (invalid verification code, etc.)
63
+ APIError: Other API errors
64
+ """
65
+ # Ensure URL format is correct
66
+ if not server_url.startswith(("http://", "https://")):
67
+ server_url = f"https://{server_url}"
68
+
69
+ # Remove trailing slash
70
+ server_url = server_url.rstrip("/")
71
+
72
+ # Build pairing endpoint
73
+ endpoint = f"{server_url}/api/v1/bridge/pair/verify_code"
74
+
75
+ # Request payload
76
+ payload = {"verification_code": verification_code, "client_id": client_id, "workspace_id": workspace_id}
77
+
78
+ try:
79
+ async with aiohttp.ClientSession(timeout=self.timeout) as session:
80
+ async with session.post(endpoint, json=payload) as response:
81
+ # Get response data
82
+ try:
83
+ response_data = await response.json()
84
+ except Exception:
85
+ response_data = None
86
+
87
+ # Handle response status code
88
+ if response.status == 200:
89
+ return response_data
90
+
91
+ # Handle error responses
92
+ error_msg = "Pairing verification failed"
93
+ if response_data:
94
+ # Extract error message from response
95
+ if "message" in response_data:
96
+ error_msg = response_data["message"]
97
+ elif "error" in response_data:
98
+ error_msg = response_data["error"]
99
+ elif "data" in response_data and isinstance(response_data["data"], dict):
100
+ if "error" in response_data["data"]:
101
+ error_msg = response_data["data"]["error"]
102
+
103
+ # Raise appropriate error based on status code
104
+ if response.status == 404:
105
+ raise PairingError(f"Server pairing endpoint does not exist: {endpoint}")
106
+ elif response.status >= 500:
107
+ raise APIError(f"Server error: {error_msg}")
108
+ elif response.status in [400, 401]:
109
+ raise PairingError(error_msg)
110
+ else:
111
+ raise APIError(f"{error_msg} (HTTP {response.status})")
112
+
113
+ except aiohttp.ClientConnectorError as e:
114
+ raise NetworkError(f"Unable to connect to server: {server_url}") from e
115
+ except asyncio.TimeoutError as e:
116
+ raise NetworkError(f"Connection timeout: {server_url}") from e
117
+ except (PairingError, APIError):
118
+ # Re-raise known errors
119
+ raise
120
+ except Exception as e:
121
+ raise APIError(f"Request failed: {e}") from e
122
+
123
+ def verify_pairing(
124
+ self, server_url: str, verification_code: str, client_id: str, workspace_id: str
125
+ ) -> Dict[str, any]:
126
+ """
127
+ Verify pairing information with server (synchronous wrapper)
128
+
129
+ Args:
130
+ server_url: Server URL
131
+ verification_code: Verification code
132
+ client_id: Client unique identifier
133
+ workspace_id: Workspace ID
134
+
135
+ Returns:
136
+ dict: Server response data
137
+
138
+ Raises:
139
+ NetworkError: Network connection failure
140
+ PairingError: Verification failure (invalid verification code, etc.)
141
+ APIError: Other API errors
142
+ """
143
+ return asyncio.run(self._verify_pairing_async(server_url, verification_code, client_id, workspace_id))
144
+
145
+ async def _test_connection_async(self, server_url: str) -> bool:
146
+ """
147
+ Asynchronously test connection to server
148
+
149
+ Args:
150
+ server_url: Server URL
151
+
152
+ Returns:
153
+ bool: True if connection successful, False otherwise
154
+ """
155
+ # Ensure URL format is correct
156
+ if not server_url.startswith(("http://", "https://")):
157
+ server_url = f"https://{server_url}"
158
+
159
+ server_url = server_url.rstrip("/")
160
+
161
+ try:
162
+ async with aiohttp.ClientSession(timeout=self.timeout) as session:
163
+ async with session.get(f"{server_url}/api/v1/bridge/health") as response:
164
+ return response.status == 200
165
+ except Exception:
166
+ return False
167
+
168
+ def test_connection(self, server_url: str) -> bool:
169
+ """
170
+ Test connection to server (synchronous wrapper)
171
+
172
+ Args:
173
+ server_url: Server URL
174
+
175
+ Returns:
176
+ bool: True if connection successful, False otherwise
177
+ """
178
+ return asyncio.run(self._test_connection_async(server_url))
179
+
180
+ async def _send_client_info(
181
+ self, server_url: str, client_id: str, verification_code: str, workspace_id: str, tools: Dict[str, list]
182
+ ) -> bool:
183
+ """
184
+ Asynchronously send MCP tools information to server
185
+
186
+ Args:
187
+ server_url: Server URL
188
+ client_id: Client unique identifier
189
+ verification_code: Verification code
190
+ workspace_id: Workspace ID
191
+ tools: MCP tools list, format: {server_name: [tool_name, ...]}
192
+
193
+ Returns:
194
+ bool: True if send successful
195
+
196
+ Raises:
197
+ NetworkError: Network connection failure
198
+ APIError: API error
199
+ """
200
+ # Ensure URL format is correct
201
+ if not server_url.startswith(("http://", "https://")):
202
+ server_url = f"https://{server_url}"
203
+
204
+ server_url = server_url.rstrip("/")
205
+ endpoint = f"{server_url}/api/v1/bridge/pair/client_info"
206
+
207
+ # Request payload
208
+ payload = {
209
+ "client_id": client_id,
210
+ "verification_code": verification_code,
211
+ "workspace_id": workspace_id,
212
+ "mcp_tools": tools,
213
+ "metadata": get_machine_metadata(),
214
+ }
215
+
216
+ try:
217
+ async with aiohttp.ClientSession(timeout=self.timeout) as session:
218
+ async with session.post(endpoint, json=payload) as response:
219
+ # Get response data
220
+ try:
221
+ response_data = await response.json()
222
+ except Exception:
223
+ response_data = None
224
+
225
+ # Handle response status code
226
+ if response.status == 200:
227
+ return True
228
+
229
+ # Handle error responses
230
+ error_msg = "Failed to send MCP tools"
231
+ if response_data:
232
+ # Extract error message from response
233
+ if "message" in response_data:
234
+ error_msg = response_data["message"]
235
+ elif "error" in response_data:
236
+ error_msg = response_data["error"]
237
+ elif "data" in response_data and isinstance(response_data["data"], dict):
238
+ if "error" in response_data["data"]:
239
+ error_msg = response_data["data"]["error"]
240
+
241
+ # Raise appropriate error based on status code
242
+ if response.status == 404:
243
+ raise APIError(f"MCP tools endpoint does not exist: {endpoint}")
244
+ elif response.status >= 500:
245
+ raise APIError(f"Server error: {error_msg}")
246
+ else:
247
+ raise APIError(f"{error_msg} (HTTP {response.status})")
248
+
249
+ except aiohttp.ClientConnectorError as e:
250
+ raise NetworkError(f"Unable to connect to server: {server_url}") from e
251
+ except asyncio.TimeoutError as e:
252
+ raise NetworkError(f"Connection timeout: {server_url}") from e
253
+ except (APIError, NetworkError):
254
+ raise
255
+ except Exception as e:
256
+ raise APIError(f"Request failed: {e}") from e
257
+
258
+ def send_mcp_tools(self, server_url: str, client_id: str, verification_code: str, tools: list) -> bool:
259
+ """
260
+ Send MCP tools information to server (synchronous wrapper)
261
+
262
+ Args:
263
+ server_url: Server URL
264
+ client_id: Client unique identifier
265
+ verification_code: Verification code
266
+ tools: MCP tools list
267
+
268
+ Returns:
269
+ bool: True if send successful
270
+
271
+ Raises:
272
+ NetworkError: Network connection failure
273
+ APIError: API error
274
+ """
275
+ return asyncio.run(self._send_mcp_tools_async(server_url, client_id, verification_code, tools))
276
+
277
+
278
+ # Global API client instance
279
+ _api_client = APIClient()
280
+
281
+
282
+ def get_api_client() -> APIClient:
283
+ """Get global API client instance"""
284
+ return _api_client