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,298 @@
1
+ """HTTP machine management for Toolplane client."""
2
+
3
+ import threading
4
+ import time
5
+ import uuid
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from toolplane.interfaces.event_interface import Event, EventType
9
+
10
+ from ..common.constants import DEFAULT_HEARTBEAT_INTERVAL
11
+ from ..common.utils import format_error_message
12
+ from ..core.errors import MachineError
13
+ from .http_connection import HTTPConnectionManager
14
+
15
+
16
+ class HTTPMachineManager:
17
+ """Manages machine registration and heartbeats for HTTP client."""
18
+
19
+ def __init__(
20
+ self,
21
+ connection_manager: HTTPConnectionManager,
22
+ event_emitter: Optional[Any] = None,
23
+ ):
24
+ """Initialize HTTP machine manager."""
25
+ self.connection_manager = connection_manager
26
+ self.machines: Dict[str, str] = {} # session_id -> machine_id
27
+ self.machines_lock = threading.RLock()
28
+ self._last_heartbeat: Dict[str, float] = {}
29
+ self._heartbeat_thread: Optional[threading.Thread] = None
30
+ self._running = False
31
+ self._event_emitter = event_emitter
32
+
33
+ def _emit_event(self, event_type: EventType, data: Dict[str, Any]) -> None:
34
+ if not self._event_emitter:
35
+ return
36
+ try:
37
+ self._event_emitter.emit(
38
+ Event(type=event_type, source="http_machine_manager", data=data)
39
+ )
40
+ except Exception:
41
+ pass
42
+
43
+ def _normalize_machine(self, machine: Dict[str, Any]) -> Dict[str, Any]:
44
+ return {
45
+ "id": machine.get("id", ""),
46
+ "session_id": machine.get("sessionId", machine.get("session_id", "")),
47
+ "sdk_version": machine.get("sdkVersion", machine.get("sdk_version", "")),
48
+ "sdk_language": machine.get("sdkLanguage", machine.get("sdk_language", "")),
49
+ "ip": machine.get("ip", ""),
50
+ "created_at": machine.get("createdAt", machine.get("created_at", "")),
51
+ "last_ping_at": machine.get("lastPingAt", machine.get("last_ping_at", "")),
52
+ }
53
+
54
+ def _clear_local_machine(self, session_id: str, machine_id: str) -> None:
55
+ with self.machines_lock:
56
+ if self.machines.get(session_id) == machine_id:
57
+ self.machines.pop(session_id, None)
58
+ self._last_heartbeat.pop(session_id, None)
59
+
60
+ def list_machines(self, session_id: str) -> List[Dict[str, Any]]:
61
+ """List machines for a session."""
62
+ try:
63
+ self.connection_manager.ensure_connected()
64
+ response = self.connection_manager.list_machines(session_id)
65
+ machines = response.get("machines", [])
66
+ return [self._normalize_machine(machine) for machine in machines]
67
+ except Exception as e:
68
+ error_msg = format_error_message(
69
+ e, f"Failed to list machines for session {session_id}"
70
+ )
71
+ raise MachineError(error_msg)
72
+
73
+ def get_machine(self, session_id: str, machine_id: str) -> Dict[str, Any]:
74
+ """Get a machine by ID."""
75
+ try:
76
+ self.connection_manager.ensure_connected()
77
+ response = self.connection_manager.get_machine(session_id, machine_id)
78
+ machine = response.get("machine", response)
79
+ if not isinstance(machine, dict):
80
+ raise MachineError(
81
+ f"Unexpected machine payload for session {session_id}: {machine}"
82
+ )
83
+ return self._normalize_machine(machine)
84
+ except Exception as e:
85
+ error_msg = format_error_message(
86
+ e, f"Failed to get machine {machine_id} for session {session_id}"
87
+ )
88
+ raise MachineError(error_msg)
89
+
90
+ def register_machine(self, session_id: str) -> str:
91
+ """Register a machine for a session."""
92
+ self.connection_manager.ensure_connected()
93
+
94
+ self._emit_event(
95
+ EventType.MACHINE_REGISTERED,
96
+ {
97
+ "session_id": session_id,
98
+ "phase": "start",
99
+ },
100
+ )
101
+
102
+ try:
103
+ machine_id = str(uuid.uuid4())
104
+
105
+ payload = {
106
+ "sessionId": session_id,
107
+ "machineId": machine_id,
108
+ "sdkVersion": "1.0.0",
109
+ "sdkLanguage": "python-http",
110
+ "tools": [],
111
+ }
112
+
113
+ response = self.connection_manager.register_machine(payload)
114
+ actual_machine_id = response.get("id", machine_id)
115
+
116
+ # The registration response carries the per-machine credential
117
+ # exactly once; store it so subsequent provide-scoped calls
118
+ # present it (see HTTPConnectionManager._request_headers).
119
+ machine_token = response.get("machineToken", "")
120
+ if machine_token:
121
+ self.connection_manager.set_machine_token(machine_token)
122
+
123
+ with self.machines_lock:
124
+ self.machines[session_id] = actual_machine_id
125
+ self._last_heartbeat[session_id] = time.time()
126
+
127
+ self._emit_event(
128
+ EventType.MACHINE_REGISTERED,
129
+ {
130
+ "session_id": session_id,
131
+ "machine_id": actual_machine_id,
132
+ "phase": "success",
133
+ },
134
+ )
135
+
136
+ return actual_machine_id
137
+
138
+ except Exception as e:
139
+ error_msg = format_error_message(
140
+ e, f"Failed to register machine for session {session_id}"
141
+ )
142
+ self._emit_event(
143
+ EventType.MACHINE_REGISTERED,
144
+ {
145
+ "session_id": session_id,
146
+ "phase": "error",
147
+ "error": error_msg,
148
+ },
149
+ )
150
+ raise MachineError(error_msg)
151
+
152
+ def unregister_machine(
153
+ self,
154
+ session_id: str,
155
+ machine_id: Optional[str] = None,
156
+ reason: str = "explicit",
157
+ ) -> bool:
158
+ """Unregister a machine for a session."""
159
+ machine_id = machine_id or self.get_machine_id(session_id)
160
+ if not machine_id:
161
+ return True
162
+
163
+ try:
164
+ self._emit_event(
165
+ EventType.MACHINE_UNREGISTERED,
166
+ {
167
+ "session_id": session_id,
168
+ "machine_id": machine_id,
169
+ "phase": "start",
170
+ "reason": reason,
171
+ },
172
+ )
173
+
174
+ # Drain machine first
175
+ self.drain_machine(session_id, machine_id)
176
+
177
+ # Unregister machine
178
+ self.connection_manager.unregister_machine(session_id, machine_id)
179
+
180
+ self._clear_local_machine(session_id, machine_id)
181
+
182
+ self._emit_event(
183
+ EventType.MACHINE_UNREGISTERED,
184
+ {
185
+ "session_id": session_id,
186
+ "machine_id": machine_id,
187
+ "phase": "success",
188
+ "reason": reason,
189
+ },
190
+ )
191
+
192
+ return True
193
+
194
+ except Exception as e:
195
+ error_msg = format_error_message(
196
+ e, f"Failed to unregister machine for session {session_id}"
197
+ )
198
+ self._emit_event(
199
+ EventType.MACHINE_UNREGISTERED,
200
+ {
201
+ "session_id": session_id,
202
+ "machine_id": machine_id,
203
+ "phase": "error",
204
+ "reason": reason,
205
+ "error": error_msg,
206
+ },
207
+ )
208
+ raise MachineError(error_msg)
209
+
210
+ def drain_machine(self, session_id: str, machine_id: Optional[str] = None) -> bool:
211
+ """Drain a machine for a session."""
212
+ machine_id = machine_id or self.get_machine_id(session_id)
213
+ if not machine_id:
214
+ return True
215
+
216
+ try:
217
+ response = self.connection_manager.drain_machine(session_id, machine_id)
218
+ if response.get("drained", False):
219
+ self._clear_local_machine(session_id, machine_id)
220
+ return response.get("drained", False)
221
+
222
+ except Exception as e:
223
+ error_msg = format_error_message(
224
+ e, f"Failed to drain machine for session {session_id}"
225
+ )
226
+ raise MachineError(error_msg)
227
+
228
+ def get_machine_id(self, session_id: str) -> Optional[str]:
229
+ """Get machine ID for a session."""
230
+ with self.machines_lock:
231
+ return self.machines.get(session_id)
232
+
233
+ def start_heartbeat(self, heartbeat_interval: int = DEFAULT_HEARTBEAT_INTERVAL):
234
+ """Start heartbeat thread."""
235
+ if self._running:
236
+ return
237
+
238
+ self._running = True
239
+ self._heartbeat_thread = threading.Thread(
240
+ target=self._heartbeat_loop, args=(heartbeat_interval,), daemon=True
241
+ )
242
+ self._heartbeat_thread.start()
243
+
244
+ def stop_heartbeat(self):
245
+ """Stop heartbeat thread."""
246
+ self._running = False
247
+ if self._heartbeat_thread:
248
+ self._heartbeat_thread.join(timeout=1)
249
+
250
+ def _heartbeat_loop(self, interval: int):
251
+ """Heartbeat loop for all machines."""
252
+ while self._running:
253
+ try:
254
+ now = time.time()
255
+
256
+ with self.machines_lock:
257
+ for session_id, machine_id in self.machines.items():
258
+ if now - self._last_heartbeat.get(session_id, 0) < interval:
259
+ continue
260
+
261
+ try:
262
+ self.connection_manager.update_machine_ping(
263
+ session_id, machine_id
264
+ )
265
+ self._last_heartbeat[session_id] = now
266
+
267
+ except Exception:
268
+ self._emit_event(
269
+ EventType.MACHINE_HEARTBEAT_FAILED,
270
+ {
271
+ "session_id": session_id,
272
+ "machine_id": machine_id,
273
+ "error": "heartbeat_failed",
274
+ },
275
+ )
276
+
277
+ except Exception:
278
+ self._emit_event(
279
+ EventType.MACHINE_HEARTBEAT_FAILED,
280
+ {
281
+ "session_id": "*",
282
+ "error": "heartbeat_loop_error",
283
+ },
284
+ )
285
+
286
+ time.sleep(5) # Check frequently, but send only per interval
287
+
288
+ def cleanup_all(self):
289
+ """Cleanup all machines."""
290
+ self.stop_heartbeat()
291
+
292
+ with self.machines_lock:
293
+ for session_id in list(self.machines.keys()):
294
+ try:
295
+ self.unregister_machine(session_id, reason="cleanup_all")
296
+ except Exception:
297
+ # Ignore cleanup errors
298
+ pass