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.
- toolplane/__init__.py +106 -0
- toolplane/common/__init__.py +93 -0
- toolplane/common/base_config.py +129 -0
- toolplane/common/base_connection_manager.py +171 -0
- toolplane/common/base_session_manager.py +321 -0
- toolplane/common/base_tool_manager.py +347 -0
- toolplane/common/constants.py +47 -0
- toolplane/common/utils.py +310 -0
- toolplane/core/__init__.py +67 -0
- toolplane/core/config.py +107 -0
- toolplane/core/connection.py +285 -0
- toolplane/core/errors.py +298 -0
- toolplane/core/machine.py +480 -0
- toolplane/core/request.py +775 -0
- toolplane/core/session.py +332 -0
- toolplane/core/session_context.py +514 -0
- toolplane/core/task.py +130 -0
- toolplane/core/tool.py +329 -0
- toolplane/http_core/__init__.py +37 -0
- toolplane/http_core/http_config.py +97 -0
- toolplane/http_core/http_connection.py +409 -0
- toolplane/http_core/http_machine.py +298 -0
- toolplane/http_core/http_request.py +748 -0
- toolplane/http_core/http_session.py +348 -0
- toolplane/http_core/http_session_context.py +491 -0
- toolplane/http_core/http_task.py +101 -0
- toolplane/http_core/http_tool.py +400 -0
- toolplane/interfaces/__init__.py +27 -0
- toolplane/interfaces/client_interface.py +122 -0
- toolplane/interfaces/connection_interface.py +193 -0
- toolplane/interfaces/event_interface.py +290 -0
- toolplane/interfaces/request_interface.py +439 -0
- toolplane/interfaces/session_interface.py +288 -0
- toolplane/interfaces/tool_interface.py +441 -0
- toolplane/proto/__init__.py +0 -0
- toolplane/proto/service_pb2.py +315 -0
- toolplane/proto/service_pb2_grpc.py +2240 -0
- toolplane/provider_cli.py +268 -0
- toolplane/provider_registry.py +77 -0
- toolplane/provider_runtime.py +302 -0
- toolplane/toolkits/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/create_directory.py +94 -0
- toolplane/toolkits/standalone_tools/create_file.py +124 -0
- toolplane/toolkits/standalone_tools/file_search.py +229 -0
- toolplane/toolkits/standalone_tools/grep_search.py +372 -0
- toolplane/toolkits/standalone_tools/launcher.py +146 -0
- toolplane/toolkits/standalone_tools/list_dir.py +395 -0
- toolplane/toolkits/standalone_tools/read_file.py +346 -0
- toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
- toolplane/toolkits/standalone_tools/run_tests.py +66 -0
- toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
- toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
- toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
- toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
- toolplane/toolkits/swe/__init__.py +35 -0
- toolplane/toolkits/swe/create_directory.py +15 -0
- toolplane/toolkits/swe/create_file.py +15 -0
- toolplane/toolkits/swe/descriptions.py +273 -0
- toolplane/toolkits/swe/execute_bash.py +93 -0
- toolplane/toolkits/swe/file_editor.py +775 -0
- toolplane/toolkits/swe/file_search.py +16 -0
- toolplane/toolkits/swe/finish.py +50 -0
- toolplane/toolkits/swe/grep_search.py +19 -0
- toolplane/toolkits/swe/list_dir.py +407 -0
- toolplane/toolkits/swe/read_file.py +18 -0
- toolplane/toolkits/swe/replace_string_in_file.py +17 -0
- toolplane/toolkits/swe/search.py +260 -0
- toolplane/toolkits/swe/semantic_search.py +20 -0
- toolplane/toolkits/swe/str_replace_editor.py +647 -0
- toolplane/toolkits/swe/submit.py +29 -0
- toolplane/toolkits/swe/swe_toolkit.py +1296 -0
- toolplane/toolplane_client.py +686 -0
- toolplane/toolplane_http_client.py +681 -0
- toolplane/utils/__init__.py +3 -0
- toolplane/utils/schema.py +146 -0
- toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
- toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
- toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
- toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
- toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
toolplane/core/tool.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Tool management for Toolplane gRPC client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
import grpc
|
|
7
|
+
|
|
8
|
+
from toolplane.proto.service_pb2 import (
|
|
9
|
+
DeleteToolRequest,
|
|
10
|
+
ExecuteToolRequest,
|
|
11
|
+
GetToolByIdRequest,
|
|
12
|
+
GetToolByNameRequest,
|
|
13
|
+
ListToolsRequest,
|
|
14
|
+
RegisterToolRequest,
|
|
15
|
+
RequestStatus,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ..common.base_tool_manager import BaseToolManager
|
|
19
|
+
from ..common.utils import parse_json_safe, timestamp_to_iso
|
|
20
|
+
from .connection import ConnectionManager
|
|
21
|
+
from .errors import ToolError, api_error_from_rpc_error
|
|
22
|
+
|
|
23
|
+
# Wire enum -> normalized status name; UNSPECIFIED maps to None so an
|
|
24
|
+
# in-flight long-poll return is distinguishable from a terminal one.
|
|
25
|
+
_REQUEST_STATUS_NAMES = {
|
|
26
|
+
RequestStatus.REQUEST_STATUS_DONE: "done",
|
|
27
|
+
RequestStatus.REQUEST_STATUS_FAILED: "failed",
|
|
28
|
+
RequestStatus.REQUEST_STATUS_CANCELLED: "cancelled",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ToolManager(BaseToolManager):
|
|
33
|
+
"""Manages tool registration and execution for gRPC client."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, connection_manager: ConnectionManager):
|
|
36
|
+
"""Initialize gRPC tool manager."""
|
|
37
|
+
super().__init__(connection_manager)
|
|
38
|
+
|
|
39
|
+
def _normalize_tool(self, tool: Any) -> Dict[str, Any]:
|
|
40
|
+
try:
|
|
41
|
+
schema = parse_json_safe(tool.schema)
|
|
42
|
+
except Exception:
|
|
43
|
+
schema = {}
|
|
44
|
+
|
|
45
|
+
if not isinstance(schema, dict):
|
|
46
|
+
schema = {}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"id": tool.id,
|
|
50
|
+
"name": tool.name,
|
|
51
|
+
"description": tool.description,
|
|
52
|
+
"schema": schema,
|
|
53
|
+
"config": dict(tool.config),
|
|
54
|
+
"created_at": timestamp_to_iso(tool.created_at),
|
|
55
|
+
"last_ping_at": timestamp_to_iso(tool.last_ping_at),
|
|
56
|
+
"session_id": tool.session_id,
|
|
57
|
+
"machine_id": tool.machine_id,
|
|
58
|
+
"tags": list(tool.tags),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def _register_tool_with_server(
|
|
62
|
+
self, session_id: str, machine_id: str, name: str, schema: Dict
|
|
63
|
+
):
|
|
64
|
+
"""Register tool with server."""
|
|
65
|
+
try:
|
|
66
|
+
self.connection_manager.ensure_connected()
|
|
67
|
+
|
|
68
|
+
request = RegisterToolRequest(
|
|
69
|
+
session_id=session_id,
|
|
70
|
+
machine_id=machine_id,
|
|
71
|
+
name=name,
|
|
72
|
+
description=schema.get("description", ""),
|
|
73
|
+
schema=json.dumps(schema.get("schema", {})),
|
|
74
|
+
tags=schema.get("tags", []),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.connection_manager.tool_stub.RegisterTool(
|
|
78
|
+
request, metadata=self.connection_manager.get_metadata()
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
except grpc.RpcError as rpc_error:
|
|
82
|
+
self._handle_rpc_error(rpc_error)
|
|
83
|
+
raise api_error_from_rpc_error(
|
|
84
|
+
rpc_error, context=f"Failed to register tool {name} with server"
|
|
85
|
+
) from rpc_error
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise ToolError(f"Failed to register tool {name} with server: {e}")
|
|
88
|
+
|
|
89
|
+
def _unregister_tool_from_server(self, session_id: str, name: str):
|
|
90
|
+
"""Unregister tool from server."""
|
|
91
|
+
try:
|
|
92
|
+
self.connection_manager.ensure_connected()
|
|
93
|
+
|
|
94
|
+
# Get tool ID first
|
|
95
|
+
tool_request = GetToolByNameRequest(session_id=session_id, tool_name=name)
|
|
96
|
+
|
|
97
|
+
tool_response = self.connection_manager.tool_stub.GetToolByName(
|
|
98
|
+
tool_request, metadata=self.connection_manager.get_metadata()
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Delete the tool. machine_id makes this a properly-attributed
|
|
102
|
+
# provide-scoped call (per-machine credential in metadata).
|
|
103
|
+
delete_request = DeleteToolRequest(
|
|
104
|
+
session_id=session_id,
|
|
105
|
+
tool_id=tool_response.tool.id,
|
|
106
|
+
machine_id=tool_response.tool.machine_id,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.connection_manager.tool_stub.DeleteTool(
|
|
110
|
+
delete_request, metadata=self.connection_manager.get_metadata()
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except grpc.RpcError as rpc_error:
|
|
114
|
+
self._handle_rpc_error(rpc_error)
|
|
115
|
+
raise api_error_from_rpc_error(
|
|
116
|
+
rpc_error, context=f"Failed to unregister tool {name} from server"
|
|
117
|
+
) from rpc_error
|
|
118
|
+
except Exception as e:
|
|
119
|
+
raise ToolError(f"Failed to unregister tool {name} from server: {e}")
|
|
120
|
+
|
|
121
|
+
def _get_available_tools_from_server(self, session_id: str) -> Dict[str, Any]:
|
|
122
|
+
"""Get available tools from server."""
|
|
123
|
+
try:
|
|
124
|
+
self.connection_manager.ensure_connected()
|
|
125
|
+
|
|
126
|
+
request = ListToolsRequest(session_id=session_id)
|
|
127
|
+
response = self.connection_manager.tool_stub.ListTools(
|
|
128
|
+
request, metadata=self.connection_manager.get_metadata()
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return {"tools": [self._normalize_tool(tool) for tool in response.tools]}
|
|
132
|
+
|
|
133
|
+
except grpc.RpcError as rpc_error:
|
|
134
|
+
self._handle_rpc_error(rpc_error)
|
|
135
|
+
raise api_error_from_rpc_error(
|
|
136
|
+
rpc_error, context="Failed to get available tools"
|
|
137
|
+
) from rpc_error
|
|
138
|
+
except Exception as e:
|
|
139
|
+
raise ToolError(f"Failed to get available tools: {e}")
|
|
140
|
+
|
|
141
|
+
def list_tools(self, session_id: str) -> List[Dict[str, Any]]:
|
|
142
|
+
"""List tools for a session."""
|
|
143
|
+
return self.get_available_tools(session_id).get("tools", [])
|
|
144
|
+
|
|
145
|
+
def get_tool_by_id(self, session_id: str, tool_id: str) -> Dict[str, Any]:
|
|
146
|
+
"""Get a tool by ID."""
|
|
147
|
+
try:
|
|
148
|
+
self.connection_manager.ensure_connected()
|
|
149
|
+
|
|
150
|
+
request = GetToolByIdRequest(session_id=session_id, tool_id=tool_id)
|
|
151
|
+
response = self.connection_manager.tool_stub.GetToolById(
|
|
152
|
+
request, metadata=self.connection_manager.get_metadata()
|
|
153
|
+
)
|
|
154
|
+
return self._normalize_tool(response.tool)
|
|
155
|
+
|
|
156
|
+
except grpc.RpcError as rpc_error:
|
|
157
|
+
self._handle_rpc_error(rpc_error)
|
|
158
|
+
raise api_error_from_rpc_error(
|
|
159
|
+
rpc_error, context=f"Failed to get tool {tool_id}"
|
|
160
|
+
) from rpc_error
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise ToolError(f"Failed to get tool {tool_id}: {e}")
|
|
163
|
+
|
|
164
|
+
def get_tool_by_name(self, session_id: str, tool_name: str) -> Dict[str, Any]:
|
|
165
|
+
"""Get a tool by name."""
|
|
166
|
+
try:
|
|
167
|
+
self.connection_manager.ensure_connected()
|
|
168
|
+
|
|
169
|
+
request = GetToolByNameRequest(session_id=session_id, tool_name=tool_name)
|
|
170
|
+
response = self.connection_manager.tool_stub.GetToolByName(
|
|
171
|
+
request, metadata=self.connection_manager.get_metadata()
|
|
172
|
+
)
|
|
173
|
+
return self._normalize_tool(response.tool)
|
|
174
|
+
|
|
175
|
+
except grpc.RpcError as rpc_error:
|
|
176
|
+
self._handle_rpc_error(rpc_error)
|
|
177
|
+
raise api_error_from_rpc_error(
|
|
178
|
+
rpc_error, context=f"Failed to get tool {tool_name}"
|
|
179
|
+
) from rpc_error
|
|
180
|
+
except Exception as e:
|
|
181
|
+
raise ToolError(f"Failed to get tool {tool_name}: {e}")
|
|
182
|
+
|
|
183
|
+
def delete_tool(self, session_id: str, tool_id: str) -> bool:
|
|
184
|
+
"""Delete a tool by ID."""
|
|
185
|
+
try:
|
|
186
|
+
self.connection_manager.ensure_connected()
|
|
187
|
+
|
|
188
|
+
tool_name = None
|
|
189
|
+
try:
|
|
190
|
+
tool = self.get_tool_by_id(session_id, tool_id)
|
|
191
|
+
tool_name = tool.get("name")
|
|
192
|
+
except Exception:
|
|
193
|
+
tool_name = None
|
|
194
|
+
|
|
195
|
+
request = DeleteToolRequest(
|
|
196
|
+
session_id=session_id,
|
|
197
|
+
tool_id=tool_id,
|
|
198
|
+
machine_id=str(tool.get("machine_id", "") or ""),
|
|
199
|
+
)
|
|
200
|
+
response = self.connection_manager.tool_stub.DeleteTool(
|
|
201
|
+
request, metadata=self.connection_manager.get_metadata()
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if response.success:
|
|
205
|
+
self.get_available_tools.cache_clear()
|
|
206
|
+
with self._lock:
|
|
207
|
+
if tool_name and session_id in self.tools:
|
|
208
|
+
self.tools[session_id].pop(tool_name, None)
|
|
209
|
+
self.tool_schemas[session_id].pop(tool_name, None)
|
|
210
|
+
self.streaming_tools[session_id].discard(tool_name)
|
|
211
|
+
|
|
212
|
+
return response.success
|
|
213
|
+
|
|
214
|
+
except grpc.RpcError as rpc_error:
|
|
215
|
+
self._handle_rpc_error(rpc_error)
|
|
216
|
+
raise api_error_from_rpc_error(
|
|
217
|
+
rpc_error, context=f"Failed to delete tool {tool_id}"
|
|
218
|
+
) from rpc_error
|
|
219
|
+
except Exception as e:
|
|
220
|
+
raise ToolError(f"Failed to delete tool {tool_id}: {e}")
|
|
221
|
+
|
|
222
|
+
def _execute_tool_on_server(
|
|
223
|
+
self,
|
|
224
|
+
session_id: str,
|
|
225
|
+
tool_name: str,
|
|
226
|
+
params: Dict,
|
|
227
|
+
idempotency_key: str = "",
|
|
228
|
+
timeout_seconds: int = 0,
|
|
229
|
+
wait_timeout_seconds: int = 0,
|
|
230
|
+
):
|
|
231
|
+
"""Execute tool on server.
|
|
232
|
+
|
|
233
|
+
Returns ``(request_id, terminal_status, result)`` where
|
|
234
|
+
``terminal_status`` is the normalized status name when the
|
|
235
|
+
server-side long-poll (a positive ``wait_timeout_seconds``) observed
|
|
236
|
+
a terminal state (``result`` carries the parsed result value for a
|
|
237
|
+
successful run), and both are ``None`` when the request is still in
|
|
238
|
+
flight.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
self.connection_manager.ensure_connected()
|
|
242
|
+
|
|
243
|
+
request = ExecuteToolRequest(
|
|
244
|
+
session_id=session_id,
|
|
245
|
+
tool_name=tool_name,
|
|
246
|
+
input=json.dumps(params),
|
|
247
|
+
idempotency_key=idempotency_key,
|
|
248
|
+
timeout_seconds=timeout_seconds,
|
|
249
|
+
wait_timeout_seconds=wait_timeout_seconds,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
response = self.connection_manager.tool_stub.InvokeTool(
|
|
253
|
+
request, metadata=self.connection_manager.get_metadata()
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Classify the terminal status BEFORE the generic error check:
|
|
257
|
+
# server-side FAILED/CANCELLED long-poll responses carry an
|
|
258
|
+
# error, and callers need the typed tuple (with the request id)
|
|
259
|
+
# rather than a generic ToolError.
|
|
260
|
+
status_name = _REQUEST_STATUS_NAMES.get(response.status)
|
|
261
|
+
if wait_timeout_seconds > 0 and status_name in (
|
|
262
|
+
"done",
|
|
263
|
+
"failed",
|
|
264
|
+
"cancelled",
|
|
265
|
+
):
|
|
266
|
+
result_value = None
|
|
267
|
+
if status_name == "done" and response.result:
|
|
268
|
+
try:
|
|
269
|
+
result_value = json.loads(response.result)
|
|
270
|
+
except (TypeError, ValueError):
|
|
271
|
+
result_value = response.result
|
|
272
|
+
return response.request_id, status_name, result_value
|
|
273
|
+
|
|
274
|
+
if response.error:
|
|
275
|
+
raise ToolError(f"Tool execution failed: {response.error}")
|
|
276
|
+
return response.request_id, None, None
|
|
277
|
+
|
|
278
|
+
except grpc.RpcError as rpc_error:
|
|
279
|
+
self._handle_rpc_error(rpc_error)
|
|
280
|
+
raise api_error_from_rpc_error(
|
|
281
|
+
rpc_error, context=f"Failed to execute tool {tool_name}"
|
|
282
|
+
) from rpc_error
|
|
283
|
+
except Exception as e:
|
|
284
|
+
raise ToolError(f"Failed to execute tool {tool_name}: {e}")
|
|
285
|
+
|
|
286
|
+
def _stream_tool_on_server(
|
|
287
|
+
self,
|
|
288
|
+
session_id: str,
|
|
289
|
+
tool_name: str,
|
|
290
|
+
params: Dict,
|
|
291
|
+
idempotency_key: str = "",
|
|
292
|
+
timeout_seconds: int = 0,
|
|
293
|
+
):
|
|
294
|
+
"""Stream tool execution on server."""
|
|
295
|
+
try:
|
|
296
|
+
self.connection_manager.ensure_connected()
|
|
297
|
+
|
|
298
|
+
request = ExecuteToolRequest(
|
|
299
|
+
session_id=session_id,
|
|
300
|
+
tool_name=tool_name,
|
|
301
|
+
input=json.dumps(params),
|
|
302
|
+
idempotency_key=idempotency_key,
|
|
303
|
+
timeout_seconds=timeout_seconds,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Use streaming endpoint
|
|
307
|
+
for chunk in self.connection_manager.tool_stub.StreamExecuteTool(
|
|
308
|
+
request, metadata=self.connection_manager.get_metadata()
|
|
309
|
+
):
|
|
310
|
+
yield chunk
|
|
311
|
+
|
|
312
|
+
except grpc.RpcError as rpc_error:
|
|
313
|
+
self._handle_rpc_error(rpc_error)
|
|
314
|
+
raise api_error_from_rpc_error(
|
|
315
|
+
rpc_error, context=f"Failed to stream tool {tool_name}"
|
|
316
|
+
) from rpc_error
|
|
317
|
+
except Exception as e:
|
|
318
|
+
raise ToolError(f"Failed to stream tool {tool_name}: {e}")
|
|
319
|
+
|
|
320
|
+
def _handle_rpc_error(self, rpc_error: grpc.RpcError):
|
|
321
|
+
"""Reset the channel when the connection itself is broken.
|
|
322
|
+
|
|
323
|
+
Only UNAVAILABLE indicates a channel-level failure. Status errors
|
|
324
|
+
like UNAUTHENTICATED (bad or revoked key) or FAILED_PRECONDITION
|
|
325
|
+
(state conflict) are deterministic per-call outcomes: recycling the
|
|
326
|
+
channel would churn reconnection and re-registration for no effect.
|
|
327
|
+
"""
|
|
328
|
+
if rpc_error.code() == grpc.StatusCode.UNAVAILABLE:
|
|
329
|
+
self.connection_manager.mark_unhealthy()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""HTTP-specific core modules for Toolplane client."""
|
|
2
|
+
|
|
3
|
+
from ..core.errors import (
|
|
4
|
+
ConnectionError,
|
|
5
|
+
MachineError,
|
|
6
|
+
RequestError,
|
|
7
|
+
SessionError,
|
|
8
|
+
TaskError,
|
|
9
|
+
ToolError,
|
|
10
|
+
ToolplaneError,
|
|
11
|
+
)
|
|
12
|
+
from .http_config import HTTPClientConfig
|
|
13
|
+
from .http_connection import HTTPConnectionManager
|
|
14
|
+
from .http_machine import HTTPMachineManager
|
|
15
|
+
from .http_request import HTTPRequestManager
|
|
16
|
+
from .http_session import HTTPSessionManager
|
|
17
|
+
from .http_session_context import HTTPSessionContext
|
|
18
|
+
from .http_task import HTTPTaskManager
|
|
19
|
+
from .http_tool import HTTPToolManager
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"HTTPClientConfig",
|
|
23
|
+
"HTTPConnectionManager",
|
|
24
|
+
"HTTPMachineManager",
|
|
25
|
+
"HTTPToolManager",
|
|
26
|
+
"HTTPRequestManager",
|
|
27
|
+
"HTTPTaskManager",
|
|
28
|
+
"HTTPSessionManager",
|
|
29
|
+
"HTTPSessionContext",
|
|
30
|
+
"ToolplaneError",
|
|
31
|
+
"ConnectionError",
|
|
32
|
+
"ToolError",
|
|
33
|
+
"SessionError",
|
|
34
|
+
"MachineError",
|
|
35
|
+
"RequestError",
|
|
36
|
+
"TaskError",
|
|
37
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""HTTP configuration management for Toolplane client."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from ..common.base_config import BaseConfig
|
|
7
|
+
from ..common.constants import (
|
|
8
|
+
DEFAULT_MAX_RETRIES,
|
|
9
|
+
DEFAULT_REQUEST_TIMEOUT,
|
|
10
|
+
DEFAULT_RETRY_BACKOFF_MS,
|
|
11
|
+
HTTP_DEFAULT_PORT,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class HTTPClientConfig(BaseConfig):
|
|
17
|
+
"""Configuration for HTTP Toolplane client."""
|
|
18
|
+
|
|
19
|
+
server_host: str = "localhost"
|
|
20
|
+
server_port: int = HTTP_DEFAULT_PORT
|
|
21
|
+
use_tls: bool = False
|
|
22
|
+
tls_cert_path: Optional[str] = None
|
|
23
|
+
tls_key_path: Optional[str] = None
|
|
24
|
+
tls_ca_cert_path: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
def normalize(self):
|
|
27
|
+
"""Normalize HTTP-specific configuration values."""
|
|
28
|
+
# Normalize server host
|
|
29
|
+
if self.server_host.startswith("http://"):
|
|
30
|
+
self.server_host = self.server_host[7:]
|
|
31
|
+
elif self.server_host.startswith("https://"):
|
|
32
|
+
self.server_host = self.server_host[8:]
|
|
33
|
+
self.use_tls = True
|
|
34
|
+
|
|
35
|
+
# Extract port from host if present
|
|
36
|
+
if ":" in self.server_host:
|
|
37
|
+
host_parts = self.server_host.split(":")
|
|
38
|
+
if len(host_parts) == 2:
|
|
39
|
+
try:
|
|
40
|
+
self.server_port = int(host_parts[1])
|
|
41
|
+
self.server_host = host_parts[0]
|
|
42
|
+
except ValueError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
# Ensure timeout and retry values are reasonable
|
|
46
|
+
if self.request_timeout <= 0:
|
|
47
|
+
self.request_timeout = DEFAULT_REQUEST_TIMEOUT
|
|
48
|
+
if self.max_retries < 0:
|
|
49
|
+
self.max_retries = DEFAULT_MAX_RETRIES
|
|
50
|
+
if self.retry_backoff_ms < 0:
|
|
51
|
+
self.retry_backoff_ms = DEFAULT_RETRY_BACKOFF_MS
|
|
52
|
+
|
|
53
|
+
def get_auth_info(self) -> Dict[str, Any]:
|
|
54
|
+
"""Get HTTP authentication information."""
|
|
55
|
+
return {
|
|
56
|
+
"headers": self.get_headers(),
|
|
57
|
+
"server_url": self.server_url,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def get_headers(self) -> Dict[str, str]:
|
|
61
|
+
"""Get HTTP headers for requests."""
|
|
62
|
+
headers = {"Content-Type": "application/json"}
|
|
63
|
+
if self.api_key:
|
|
64
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
65
|
+
# Keep legacy header for backward compatibility
|
|
66
|
+
headers["Grpc-Metadata-api_key"] = self.api_key
|
|
67
|
+
return headers
|
|
68
|
+
|
|
69
|
+
def get_server_address(self) -> str:
|
|
70
|
+
"""Get formatted server address."""
|
|
71
|
+
return f"{self.server_host}:{self.server_port}"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def server_url(self) -> str:
|
|
75
|
+
"""Get complete server URL with protocol."""
|
|
76
|
+
protocol = "https" if self.use_tls else "http"
|
|
77
|
+
return f"{protocol}://{self.server_host}:{self.server_port}"
|
|
78
|
+
|
|
79
|
+
def copy(self) -> "HTTPClientConfig":
|
|
80
|
+
"""Create a copy of the configuration."""
|
|
81
|
+
return HTTPClientConfig(**self.to_dict())
|
|
82
|
+
|
|
83
|
+
def merge(self, other: "HTTPClientConfig") -> "HTTPClientConfig":
|
|
84
|
+
"""Merge this configuration with another."""
|
|
85
|
+
merged_dict = self.to_dict()
|
|
86
|
+
merged_dict.update(other.to_dict())
|
|
87
|
+
return HTTPClientConfig(**merged_dict)
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
90
|
+
"""Convert configuration to dictionary."""
|
|
91
|
+
result = super().to_dict()
|
|
92
|
+
result.update(
|
|
93
|
+
{
|
|
94
|
+
"server_url": self.server_url,
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
return result
|