toolplane-python-client 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.
Files changed (81) hide show
  1. toolplane/__init__.py +106 -0
  2. toolplane/common/__init__.py +93 -0
  3. toolplane/common/base_config.py +129 -0
  4. toolplane/common/base_connection_manager.py +171 -0
  5. toolplane/common/base_session_manager.py +321 -0
  6. toolplane/common/base_tool_manager.py +347 -0
  7. toolplane/common/constants.py +47 -0
  8. toolplane/common/utils.py +310 -0
  9. toolplane/core/__init__.py +67 -0
  10. toolplane/core/config.py +107 -0
  11. toolplane/core/connection.py +285 -0
  12. toolplane/core/errors.py +298 -0
  13. toolplane/core/machine.py +480 -0
  14. toolplane/core/request.py +775 -0
  15. toolplane/core/session.py +332 -0
  16. toolplane/core/session_context.py +514 -0
  17. toolplane/core/task.py +130 -0
  18. toolplane/core/tool.py +329 -0
  19. toolplane/http_core/__init__.py +37 -0
  20. toolplane/http_core/http_config.py +97 -0
  21. toolplane/http_core/http_connection.py +409 -0
  22. toolplane/http_core/http_machine.py +298 -0
  23. toolplane/http_core/http_request.py +748 -0
  24. toolplane/http_core/http_session.py +348 -0
  25. toolplane/http_core/http_session_context.py +491 -0
  26. toolplane/http_core/http_task.py +101 -0
  27. toolplane/http_core/http_tool.py +400 -0
  28. toolplane/interfaces/__init__.py +27 -0
  29. toolplane/interfaces/client_interface.py +122 -0
  30. toolplane/interfaces/connection_interface.py +193 -0
  31. toolplane/interfaces/event_interface.py +290 -0
  32. toolplane/interfaces/request_interface.py +439 -0
  33. toolplane/interfaces/session_interface.py +288 -0
  34. toolplane/interfaces/tool_interface.py +441 -0
  35. toolplane/proto/__init__.py +0 -0
  36. toolplane/proto/service_pb2.py +315 -0
  37. toolplane/proto/service_pb2_grpc.py +2240 -0
  38. toolplane/provider_cli.py +268 -0
  39. toolplane/provider_registry.py +77 -0
  40. toolplane/provider_runtime.py +302 -0
  41. toolplane/toolkits/__init__.py +0 -0
  42. toolplane/toolkits/standalone_tools/__init__.py +0 -0
  43. toolplane/toolkits/standalone_tools/create_directory.py +94 -0
  44. toolplane/toolkits/standalone_tools/create_file.py +124 -0
  45. toolplane/toolkits/standalone_tools/file_search.py +229 -0
  46. toolplane/toolkits/standalone_tools/grep_search.py +372 -0
  47. toolplane/toolkits/standalone_tools/launcher.py +146 -0
  48. toolplane/toolkits/standalone_tools/list_dir.py +395 -0
  49. toolplane/toolkits/standalone_tools/read_file.py +346 -0
  50. toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
  51. toolplane/toolkits/standalone_tools/run_tests.py +66 -0
  52. toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
  53. toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
  54. toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
  55. toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
  56. toolplane/toolkits/swe/__init__.py +35 -0
  57. toolplane/toolkits/swe/create_directory.py +15 -0
  58. toolplane/toolkits/swe/create_file.py +15 -0
  59. toolplane/toolkits/swe/descriptions.py +273 -0
  60. toolplane/toolkits/swe/execute_bash.py +93 -0
  61. toolplane/toolkits/swe/file_editor.py +775 -0
  62. toolplane/toolkits/swe/file_search.py +16 -0
  63. toolplane/toolkits/swe/finish.py +50 -0
  64. toolplane/toolkits/swe/grep_search.py +19 -0
  65. toolplane/toolkits/swe/list_dir.py +407 -0
  66. toolplane/toolkits/swe/read_file.py +18 -0
  67. toolplane/toolkits/swe/replace_string_in_file.py +17 -0
  68. toolplane/toolkits/swe/search.py +260 -0
  69. toolplane/toolkits/swe/semantic_search.py +20 -0
  70. toolplane/toolkits/swe/str_replace_editor.py +647 -0
  71. toolplane/toolkits/swe/submit.py +29 -0
  72. toolplane/toolkits/swe/swe_toolkit.py +1296 -0
  73. toolplane/toolplane_client.py +686 -0
  74. toolplane/toolplane_http_client.py +681 -0
  75. toolplane/utils/__init__.py +3 -0
  76. toolplane/utils/schema.py +146 -0
  77. toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
  78. toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
  79. toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
  80. toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
  81. toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,409 @@
1
+ """HTTP connection management for Toolplane client."""
2
+
3
+ import time
4
+ from typing import Any, Dict, Optional
5
+
6
+ import requests
7
+
8
+ from ..common.constants import (
9
+ DEFAULT_MAX_RETRIES,
10
+ DEFAULT_RETRY_BACKOFF_MS,
11
+ ERROR_CONNECTION_FAILED,
12
+ )
13
+ from ..common.utils import format_error_message, with_retry
14
+ from ..core.errors import (
15
+ ConnectionError,
16
+ ToolplaneAPIError,
17
+ ToolplaneResourceExhaustedError,
18
+ ToolplaneUnavailableError,
19
+ api_error_from_http_response,
20
+ )
21
+ from .http_config import HTTPClientConfig
22
+
23
+
24
+ class HTTPConnectionManager:
25
+ """Manages HTTP connections and request handling."""
26
+
27
+ def __init__(self, config: HTTPClientConfig):
28
+ """Initialize HTTP connection manager with configuration."""
29
+ self.config = config
30
+ self.session = requests.Session()
31
+ self.connected = False
32
+ self.current_buffer_size = 0
33
+ # Per-machine credential for provide-scoped RPCs; set by the machine
34
+ # registration that minted it.
35
+ self.machine_token: Optional[str] = None
36
+
37
+ def set_machine_token(self, token: str):
38
+ """Store the per-machine credential minted at registration."""
39
+ self.machine_token = token or None
40
+
41
+ def _request_headers(self) -> Dict:
42
+ headers = self.config.get_headers()
43
+ if self.machine_token:
44
+ headers["X-Toolplane-Machine-Token"] = self.machine_token
45
+ return headers
46
+
47
+ def connect(self) -> bool:
48
+ """Test connection to HTTP server."""
49
+ try:
50
+ self._post("api.v1/HealthCheck")
51
+ self.connected = True
52
+ return True
53
+ except Exception as e:
54
+ error_msg = format_error_message(e, ERROR_CONNECTION_FAILED)
55
+ raise ConnectionError(error_msg)
56
+
57
+ def disconnect(self):
58
+ """Close HTTP session."""
59
+ if self.session:
60
+ self.session.close()
61
+ self.connected = False
62
+
63
+ def ensure_connected(self):
64
+ """Ensure connection is established."""
65
+ if not self.connected:
66
+ if not self.connect():
67
+ raise ConnectionError("Failed to establish HTTP connection")
68
+
69
+ def _error_from_response(self, response) -> ToolplaneAPIError:
70
+ """Translate a non-2xx response into a typed error.
71
+
72
+ Honors Retry-After on 429/503 (the backpressure statuses) before
73
+ raising; the resulting error is retryable, so the with_retry wrapper
74
+ sleeps the server-directed interval and tries again.
75
+ """
76
+ if response.status_code in (429, 503):
77
+ retry_after = response.headers.get("Retry-After")
78
+ if retry_after:
79
+ time.sleep(float(retry_after))
80
+ return api_error_from_http_response(
81
+ response.status_code, response.text, context=f"POST {response.url}"
82
+ )
83
+
84
+ @with_retry(
85
+ max_retries=DEFAULT_MAX_RETRIES,
86
+ backoff_ms=DEFAULT_RETRY_BACKOFF_MS,
87
+ exceptions=(
88
+ ConnectionError,
89
+ ToolplaneUnavailableError,
90
+ ToolplaneResourceExhaustedError,
91
+ ),
92
+ )
93
+ def _post(self, path: str, payload: Optional[Dict] = None) -> Any:
94
+ """Make a POST request, retrying only transport and capacity errors.
95
+
96
+ Deterministic failures (bad credentials, invalid input, state
97
+ conflicts, missing entities) surface as typed ToolplaneAPIError
98
+ subclasses and are not retried: the same request would fail again.
99
+ """
100
+ url = self.config.server_url.rstrip("/") + "/" + path
101
+ headers = self._request_headers()
102
+
103
+ try:
104
+ response = self.session.post(
105
+ url,
106
+ json=payload or {},
107
+ headers=headers,
108
+ timeout=self.config.request_timeout,
109
+ )
110
+
111
+ if not response.ok:
112
+ raise self._error_from_response(response)
113
+
114
+ return response.json()
115
+
116
+ except requests.exceptions.Timeout:
117
+ raise ConnectionError(
118
+ f"Request timed out after {self.config.request_timeout}s"
119
+ )
120
+
121
+ except requests.exceptions.RequestException as e:
122
+ raise ConnectionError(f"HTTP request failed: {str(e)}")
123
+
124
+ @with_retry(
125
+ max_retries=DEFAULT_MAX_RETRIES,
126
+ backoff_ms=DEFAULT_RETRY_BACKOFF_MS,
127
+ exceptions=(
128
+ ConnectionError,
129
+ ToolplaneUnavailableError,
130
+ ToolplaneResourceExhaustedError,
131
+ ),
132
+ )
133
+ def stream_post(self, path: str, payload: Optional[Dict] = None):
134
+ """Make a streaming POST request.
135
+
136
+ Retries the same failure classes as _post: transport errors and
137
+ capacity/backpressure (retryable typed errors)."""
138
+ url = self.config.server_url.rstrip("/") + "/" + path
139
+ headers = self._request_headers()
140
+
141
+ try:
142
+ response = self.session.post(
143
+ url,
144
+ json=payload or {},
145
+ headers=headers,
146
+ stream=True,
147
+ timeout=self.config.request_timeout,
148
+ )
149
+
150
+ if response.status_code != 200:
151
+ raise self._error_from_response(response)
152
+
153
+ return response
154
+
155
+ except requests.exceptions.Timeout:
156
+ raise ConnectionError(
157
+ f"Stream request timed out after {self.config.request_timeout}s"
158
+ )
159
+
160
+ except requests.exceptions.RequestException as e:
161
+ raise ConnectionError(f"Stream request failed: {str(e)}")
162
+
163
+ # Health check
164
+ def health_check(self):
165
+ """Check server health."""
166
+ return self._post("api.v1/HealthCheck")
167
+
168
+ # Session endpoints
169
+ def create_session(self, payload: Dict):
170
+ """Create a new session."""
171
+ return self._post("api.v1/CreateSession", payload)
172
+
173
+ def get_session(self, session_id: str):
174
+ """Get session by ID."""
175
+ return self._post(f"api.v1/sessions/{session_id}", {"sessionId": session_id})
176
+
177
+ def list_sessions(self, user_id: str):
178
+ """List sessions for user."""
179
+ return self._post("api.v1/sessions", {"userId": user_id})
180
+
181
+ def update_session(self, payload: Dict):
182
+ """Update session."""
183
+ return self._post("api.v1/UpdateSession", payload)
184
+
185
+ def delete_session(self, session_id: str):
186
+ """Delete session."""
187
+ return self._post("api.v1/DeleteSession", {"sessionId": session_id})
188
+
189
+ # New endpoints for user session management
190
+ def list_user_sessions(self, payload: Dict):
191
+ """List user sessions with pagination and filtering."""
192
+ user_id = payload.get("userId", "")
193
+ return self._post(f"api.v1/users/{user_id}/sessions", payload)
194
+
195
+ def bulk_delete_sessions(self, payload: Dict):
196
+ """Bulk delete sessions."""
197
+ return self._post("api.v1/BulkDeleteSessions", payload)
198
+
199
+ def get_session_stats(self, payload: Dict):
200
+ """Get session statistics."""
201
+ user_id = payload.get("userId", "")
202
+ return self._post(f"api.v1/users/{user_id}/session-stats", payload)
203
+
204
+ def invalidate_session(self, payload: Dict):
205
+ """Invalidate session."""
206
+ return self._post("api.v1/InvalidateSession", payload)
207
+
208
+ def create_api_key(self, payload: Dict):
209
+ """Create an API key for a session."""
210
+ return self._post("api.v1/CreateApiKey", payload)
211
+
212
+ def list_api_keys(self, payload: Dict):
213
+ """List API keys for a session."""
214
+ session_id = payload.get("sessionId", "")
215
+ return self._post(f"api.v1/sessions/{session_id}/api-keys", payload)
216
+
217
+ def revoke_api_key(self, payload: Dict):
218
+ """Revoke an API key for a session."""
219
+ return self._post("api.v1/RevokeApiKey", payload)
220
+
221
+ # Tool endpoints
222
+ def register_tool(self, payload: Dict):
223
+ """Register a tool."""
224
+ return self._post("api.v1/RegisterTool", payload)
225
+
226
+ def list_tools(self, session_id: str):
227
+ """List tools for session."""
228
+ return self._post("api.v1/ListTools", {"sessionId": session_id})
229
+
230
+ def get_tool_by_id(self, session_id: str, tool_id: str):
231
+ """Get tool by ID."""
232
+ return self._post(
233
+ "api.v1/GetTool", {"sessionId": session_id, "toolId": tool_id}
234
+ )
235
+
236
+ def get_tool_by_name(self, session_id: str, tool_name: str):
237
+ """Get tool by name."""
238
+ return self._post(
239
+ "api.v1/GetTool", {"sessionId": session_id, "toolName": tool_name}
240
+ )
241
+
242
+ def delete_tool(self, session_id: str, tool_id: str):
243
+ """Delete tool."""
244
+ return self._post(
245
+ "api.v1/DeleteTool", {"sessionId": session_id, "toolId": tool_id}
246
+ )
247
+
248
+ # Machine endpoints
249
+ def register_machine(self, payload: Dict):
250
+ """Register a machine."""
251
+ return self._post("api.v1/RegisterMachine", payload)
252
+
253
+ def update_machine_ping(self, session_id: str, machine_id: str):
254
+ """Update machine ping."""
255
+ return self._post(
256
+ "api.v1/UpdateMachinePing",
257
+ {"sessionId": session_id, "machineId": machine_id},
258
+ )
259
+
260
+ def list_machines(self, session_id: str):
261
+ """List machines for a session."""
262
+ return self._post(
263
+ f"api.v1/sessions/{session_id}/machines", {"sessionId": session_id}
264
+ )
265
+
266
+ def get_machine(self, session_id: str, machine_id: str):
267
+ """Get a machine by ID."""
268
+ return self._post(
269
+ f"api.v1/sessions/{session_id}/machines/{machine_id}",
270
+ {"sessionId": session_id, "machineId": machine_id},
271
+ )
272
+
273
+ def unregister_machine(self, session_id: str, machine_id: str):
274
+ """Unregister machine."""
275
+ return self._post(
276
+ "api.v1/UnregisterMachine",
277
+ {"sessionId": session_id, "machineId": machine_id},
278
+ )
279
+
280
+ def drain_machine(self, session_id: str, machine_id: str):
281
+ """Drain machine."""
282
+ return self._post(
283
+ "api.v1/DrainMachine", {"sessionId": session_id, "machineId": machine_id}
284
+ )
285
+
286
+ # Request endpoints
287
+ def create_request(self, payload: Dict):
288
+ """Create a request."""
289
+ return self._post("api.v1/CreateRequest", payload)
290
+
291
+ def get_request(self, session_id: str, request_id: str):
292
+ """Get request by ID."""
293
+ return self._post(
294
+ f"api.v1/sessions/{session_id}/requests/{request_id}",
295
+ {"sessionId": session_id, "requestId": request_id},
296
+ )
297
+
298
+ def list_requests(self, payload: Dict):
299
+ """List requests."""
300
+ session_id = payload.get("sessionId", "")
301
+ return self._post(f"api.v1/sessions/{session_id}/requests", payload)
302
+
303
+ def update_request(self, payload: Dict):
304
+ """Update request."""
305
+ return self._post("api.v1/UpdateRequest", payload)
306
+
307
+ def claim_request(self, session_id: str, request_id: str, machine_id: str):
308
+ """Claim request."""
309
+ return self._post(
310
+ "api.v1/ClaimRequest",
311
+ {"sessionId": session_id, "requestId": request_id, "machineId": machine_id},
312
+ )
313
+
314
+ def claim_next_request(self, payload: Dict):
315
+ """Atomically lease the oldest claimable pending request (provider poll)."""
316
+ return self._post("api.v1/ClaimNextRequest", payload)
317
+
318
+ def cancel_request(self, session_id: str, request_id: str):
319
+ """Cancel request."""
320
+ return self._post(
321
+ "api.v1/CancelRequest",
322
+ {"sessionId": session_id, "requestId": request_id},
323
+ )
324
+
325
+ def submit_request_result(self, payload: Dict):
326
+ """Submit request result."""
327
+ return self._post("api.v1/SubmitRequestResult", payload)
328
+
329
+ def append_request_chunks(self, payload: Dict):
330
+ """Append request chunks."""
331
+ return self._post("api.v1/AppendRequestChunks", payload)
332
+
333
+ def get_request_chunks(self, session_id: str, request_id: str):
334
+ """Get request chunks."""
335
+ return self._post(
336
+ f"api.v1/sessions/{session_id}/requests/{request_id}/chunks",
337
+ {"sessionId": session_id, "requestId": request_id},
338
+ )
339
+
340
+ def renew_request_lease(
341
+ self, session_id: str, request_id: str, machine_id: str, lease_epoch: int
342
+ ):
343
+ """Renew the execution lease for a claimed/running request."""
344
+ return self._post(
345
+ "api.v1/RenewRequestLease",
346
+ {
347
+ "sessionId": session_id,
348
+ "requestId": request_id,
349
+ "machineId": machine_id,
350
+ "leaseEpoch": lease_epoch,
351
+ },
352
+ )
353
+
354
+ # Task endpoints
355
+ def create_task(self, payload: Dict):
356
+ """Create a task."""
357
+ return self._post("api.v1/CreateTask", payload)
358
+
359
+ def get_task(self, session_id: str, task_id: str):
360
+ """Get task by ID."""
361
+ return self._post(
362
+ f"api.v1/sessions/{session_id}/tasks/{task_id}",
363
+ {"sessionId": session_id, "taskId": task_id},
364
+ )
365
+
366
+ def list_tasks(self, session_id: str):
367
+ """List tasks for a session."""
368
+ return self._post(
369
+ f"api.v1/sessions/{session_id}/tasks", {"sessionId": session_id}
370
+ )
371
+
372
+ def cancel_task(self, session_id: str, task_id: str):
373
+ """Cancel task by ID."""
374
+ return self._post(
375
+ "api.v1/CancelTask", {"sessionId": session_id, "taskId": task_id}
376
+ )
377
+
378
+ # Execution endpoints
379
+ def execute_tool(
380
+ self,
381
+ session_id: str,
382
+ tool_name: str,
383
+ input_data: str,
384
+ idempotency_key: str = "",
385
+ timeout_seconds: int = 0,
386
+ wait_timeout_seconds: int = 0,
387
+ ):
388
+ """Execute tool."""
389
+ payload = {"sessionId": session_id, "toolName": tool_name, "input": input_data}
390
+ if idempotency_key:
391
+ payload["idempotencyKey"] = idempotency_key
392
+ if timeout_seconds > 0:
393
+ payload["timeoutSeconds"] = timeout_seconds
394
+ if wait_timeout_seconds > 0:
395
+ payload["waitTimeoutSeconds"] = wait_timeout_seconds
396
+ return self._post("api.v1/InvokeTool", payload)
397
+
398
+ def stream_execute_tool(
399
+ self,
400
+ session_id: str,
401
+ tool_name: str,
402
+ input_data: str,
403
+ idempotency_key: str = "",
404
+ ):
405
+ """Stream execute tool."""
406
+ payload = {"sessionId": session_id, "toolName": tool_name, "input": input_data}
407
+ if idempotency_key:
408
+ payload["idempotencyKey"] = idempotency_key
409
+ return self.stream_post("api.v1/StreamExecuteTool", payload)