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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Constants used across Toolplane client implementations."""
|
|
2
|
+
|
|
3
|
+
# Server-side ceiling for wait_timeout_seconds on the synchronous execution
|
|
4
|
+
# entrypoints (the same ceiling as timeout_seconds). The server rejects
|
|
5
|
+
# values above it with OUT_OF_RANGE / TIMEOUT_ABOVE_MAX.
|
|
6
|
+
WAIT_TIMEOUT_MAX_SECONDS = 3600
|
|
7
|
+
|
|
8
|
+
# gRPC message-size ladder, sized from the server's chunk limits: replay
|
|
9
|
+
# window reads (8 MiB) and chunk batch writes (16 MiB) plus envelope
|
|
10
|
+
# headroom. The gRPC defaults (4 MiB receive) reject a full window read at
|
|
11
|
+
# the client before it reaches application code.
|
|
12
|
+
GRPC_MAX_RECEIVE_MESSAGE_LENGTH = 9 * 1024 * 1024
|
|
13
|
+
GRPC_MAX_SEND_MESSAGE_LENGTH = 17 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
# Default configuration values
|
|
16
|
+
DEFAULT_HEARTBEAT_INTERVAL = 60
|
|
17
|
+
DEFAULT_MAX_WORKERS = 10
|
|
18
|
+
DEFAULT_POLL_INTERVAL = 0.5
|
|
19
|
+
DEFAULT_REQUEST_TIMEOUT = 60
|
|
20
|
+
DEFAULT_MAX_RETRIES = 3
|
|
21
|
+
DEFAULT_BUFFER_SIZE = 4 * 1024 * 1024 # 4MB
|
|
22
|
+
DEFAULT_RETRY_BACKOFF_MS = 250
|
|
23
|
+
|
|
24
|
+
# Cache settings
|
|
25
|
+
CACHE_TTL_SECONDS = 0.5
|
|
26
|
+
|
|
27
|
+
# Protocol-specific defaults
|
|
28
|
+
GRPC_DEFAULT_PORT = 9001
|
|
29
|
+
HTTP_DEFAULT_PORT = 8080
|
|
30
|
+
|
|
31
|
+
# Session settings
|
|
32
|
+
MAX_SESSION_NAME_LENGTH = 100
|
|
33
|
+
MAX_TOOL_NAME_LENGTH = 50
|
|
34
|
+
MAX_DESCRIPTION_LENGTH = 500
|
|
35
|
+
|
|
36
|
+
# Tool execution settings
|
|
37
|
+
MAX_TOOL_EXECUTION_TIME = 300 # 5 minutes
|
|
38
|
+
STREAM_CHUNK_SIZE = 1024
|
|
39
|
+
BACKPRESSURE_THRESHOLD = 0.8 # 80% of buffer size
|
|
40
|
+
|
|
41
|
+
# Error messages
|
|
42
|
+
ERROR_CONNECTION_FAILED = "Failed to connect to server"
|
|
43
|
+
ERROR_TOOL_NOT_FOUND = "Tool not found"
|
|
44
|
+
ERROR_SESSION_NOT_FOUND = "Session not found"
|
|
45
|
+
ERROR_INVALID_PARAMETERS = "Invalid parameters"
|
|
46
|
+
ERROR_TIMEOUT = "Operation timed out"
|
|
47
|
+
ERROR_STREAM_INTERRUPTED = "Stream was interrupted"
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Utility functions shared across Toolplane client implementations."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from threading import RLock
|
|
9
|
+
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
10
|
+
|
|
11
|
+
from .constants import (
|
|
12
|
+
CACHE_TTL_SECONDS,
|
|
13
|
+
MAX_DESCRIPTION_LENGTH,
|
|
14
|
+
MAX_SESSION_NAME_LENGTH,
|
|
15
|
+
MAX_TOOL_NAME_LENGTH,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_session_id() -> str:
|
|
22
|
+
"""Generate a unique session ID."""
|
|
23
|
+
return str(uuid.uuid4())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_tool_name(name: str) -> bool:
|
|
27
|
+
"""Validate tool name format."""
|
|
28
|
+
if not name or len(name) > MAX_TOOL_NAME_LENGTH:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
# Tool names should be alphanumeric with underscores/hyphens
|
|
32
|
+
return re.match(r"^[a-zA-Z0-9_-]+$", name) is not None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_session_name(name: str) -> bool:
|
|
36
|
+
"""Validate session name format."""
|
|
37
|
+
if not name or len(name) > MAX_SESSION_NAME_LENGTH:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
# Session names allow more characters
|
|
41
|
+
return re.match(r"^[a-zA-Z0-9_\-\s\.]+$", name) is not None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_description(description: str) -> bool:
|
|
45
|
+
"""Validate description length."""
|
|
46
|
+
return len(description) <= MAX_DESCRIPTION_LENGTH
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_json_safe(json_str: str, default: Any = None) -> Any:
|
|
50
|
+
"""Safely parse JSON string."""
|
|
51
|
+
try:
|
|
52
|
+
return json.loads(json_str)
|
|
53
|
+
except (json.JSONDecodeError, TypeError):
|
|
54
|
+
return default
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def proto_enum_name(value: Any, enum_type: Any) -> str:
|
|
58
|
+
"""Return the symbolic name for a v1 enum value.
|
|
59
|
+
|
|
60
|
+
The gRPC transport delivers proto3 enums as plain ints while the HTTP
|
|
61
|
+
transport delivers the symbolic name; normalize both to the name so
|
|
62
|
+
callers can map it through normalize_status_name.
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
return value
|
|
66
|
+
try:
|
|
67
|
+
return enum_type.Name(value)
|
|
68
|
+
except (ValueError, AttributeError, TypeError):
|
|
69
|
+
return ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def timestamp_to_iso(value: Any) -> str:
|
|
73
|
+
"""Render a v1 Timestamp message as an RFC3339 string.
|
|
74
|
+
|
|
75
|
+
Tolerates strings (HTTP transport), None, and unset (epoch-zero)
|
|
76
|
+
timestamps so callers always get a plain str.
|
|
77
|
+
"""
|
|
78
|
+
if value is None:
|
|
79
|
+
return ""
|
|
80
|
+
to_json = getattr(value, "ToJsonString", None)
|
|
81
|
+
if callable(to_json):
|
|
82
|
+
try:
|
|
83
|
+
if not getattr(value, "seconds", 1) and not getattr(value, "nanos", 1):
|
|
84
|
+
return ""
|
|
85
|
+
return to_json()
|
|
86
|
+
except Exception:
|
|
87
|
+
return ""
|
|
88
|
+
return value if isinstance(value, str) else str(value)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def format_error_message(error: Exception, context: str = "") -> str:
|
|
92
|
+
"""Format error message with context."""
|
|
93
|
+
if context:
|
|
94
|
+
return f"{context}: {str(error)}"
|
|
95
|
+
return str(error)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def with_retry(
|
|
99
|
+
max_retries: int = 3, backoff_ms: int = 250, exceptions: tuple = (Exception,)
|
|
100
|
+
) -> Callable:
|
|
101
|
+
"""Decorator for retrying operations."""
|
|
102
|
+
|
|
103
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
104
|
+
@functools.wraps(func)
|
|
105
|
+
def wrapper(*args, **kwargs) -> T:
|
|
106
|
+
last_exception = None
|
|
107
|
+
|
|
108
|
+
for attempt in range(max_retries + 1):
|
|
109
|
+
try:
|
|
110
|
+
return func(*args, **kwargs)
|
|
111
|
+
except exceptions as e:
|
|
112
|
+
last_exception = e
|
|
113
|
+
if attempt < max_retries:
|
|
114
|
+
# Exponential backoff
|
|
115
|
+
sleep_time = (backoff_ms * (2**attempt)) / 1000
|
|
116
|
+
time.sleep(sleep_time)
|
|
117
|
+
else:
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
# If all retries failed, raise the last exception
|
|
121
|
+
raise last_exception
|
|
122
|
+
|
|
123
|
+
return wrapper
|
|
124
|
+
|
|
125
|
+
return decorator
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TTLCache:
|
|
129
|
+
"""Thread-safe cache with TTL (Time To Live)."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, ttl_seconds: int = CACHE_TTL_SECONDS):
|
|
132
|
+
self.ttl_seconds = ttl_seconds
|
|
133
|
+
self.cache: Dict[str, tuple] = {} # key -> (timestamp, value)
|
|
134
|
+
self.lock = RLock()
|
|
135
|
+
|
|
136
|
+
def get(self, key: str) -> Optional[Any]:
|
|
137
|
+
"""Get value from cache if not expired."""
|
|
138
|
+
with self.lock:
|
|
139
|
+
if key in self.cache:
|
|
140
|
+
timestamp, value = self.cache[key]
|
|
141
|
+
if time.time() - timestamp < self.ttl_seconds:
|
|
142
|
+
return value
|
|
143
|
+
else:
|
|
144
|
+
# Remove expired entry
|
|
145
|
+
del self.cache[key]
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def set(self, key: str, value: Any) -> None:
|
|
149
|
+
"""Set value in cache with current timestamp."""
|
|
150
|
+
with self.lock:
|
|
151
|
+
self.cache[key] = (time.time(), value)
|
|
152
|
+
|
|
153
|
+
def clear(self) -> None:
|
|
154
|
+
"""Clear all cache entries."""
|
|
155
|
+
with self.lock:
|
|
156
|
+
self.cache.clear()
|
|
157
|
+
|
|
158
|
+
def cleanup_expired(self) -> None:
|
|
159
|
+
"""Remove expired entries from cache."""
|
|
160
|
+
now = time.time()
|
|
161
|
+
with self.lock:
|
|
162
|
+
expired_keys = [
|
|
163
|
+
key
|
|
164
|
+
for key, (timestamp, _) in self.cache.items()
|
|
165
|
+
if now - timestamp >= self.ttl_seconds
|
|
166
|
+
]
|
|
167
|
+
for key in expired_keys:
|
|
168
|
+
del self.cache[key]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cache_with_ttl(ttl_seconds: int = CACHE_TTL_SECONDS) -> Callable:
|
|
172
|
+
"""Decorator for caching function results with TTL."""
|
|
173
|
+
|
|
174
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
175
|
+
cache = TTLCache(ttl_seconds)
|
|
176
|
+
|
|
177
|
+
@functools.wraps(func)
|
|
178
|
+
def wrapper(*args, **kwargs) -> T:
|
|
179
|
+
# Create cache key from function name and arguments
|
|
180
|
+
key = f"{func.__name__}:{hash((args, tuple(sorted(kwargs.items()))))}"
|
|
181
|
+
|
|
182
|
+
# Try to get from cache first
|
|
183
|
+
cached_result = cache.get(key)
|
|
184
|
+
if cached_result is not None:
|
|
185
|
+
return cached_result
|
|
186
|
+
|
|
187
|
+
# If not in cache, execute function and cache result
|
|
188
|
+
result = func(*args, **kwargs)
|
|
189
|
+
cache.set(key, result)
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
# Add cache management methods
|
|
193
|
+
wrapper.cache_clear = cache.clear
|
|
194
|
+
wrapper.cache_cleanup = cache.cleanup_expired
|
|
195
|
+
|
|
196
|
+
return wrapper
|
|
197
|
+
|
|
198
|
+
return decorator
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def normalize_url(url: str, default_protocol: str = "http") -> str:
|
|
202
|
+
"""Normalize URL by adding protocol if missing."""
|
|
203
|
+
if not url.startswith(("http://", "https://")):
|
|
204
|
+
url = f"{default_protocol}://{url}"
|
|
205
|
+
return url.rstrip("/")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def extract_host_port(url: str) -> tuple[str, int]:
|
|
209
|
+
"""Extract host and port from URL."""
|
|
210
|
+
# Remove protocol if present
|
|
211
|
+
if "://" in url:
|
|
212
|
+
url = url.split("://")[1]
|
|
213
|
+
|
|
214
|
+
# Remove path if present
|
|
215
|
+
if "/" in url:
|
|
216
|
+
url = url.split("/")[0]
|
|
217
|
+
|
|
218
|
+
# Split host and port
|
|
219
|
+
if ":" in url:
|
|
220
|
+
host, port_str = url.rsplit(":", 1)
|
|
221
|
+
try:
|
|
222
|
+
port = int(port_str)
|
|
223
|
+
except ValueError:
|
|
224
|
+
port = 80 if "http" in url else 443
|
|
225
|
+
else:
|
|
226
|
+
host = url
|
|
227
|
+
port = 80 if "http" in url else 443
|
|
228
|
+
|
|
229
|
+
return host, port
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def merge_dicts(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
233
|
+
"""Merge two dictionaries, with override taking precedence."""
|
|
234
|
+
result = base.copy()
|
|
235
|
+
result.update(override)
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def sanitize_input(value: str, max_length: int = 1000) -> str:
|
|
240
|
+
"""Sanitize input string by limiting length and removing control characters."""
|
|
241
|
+
if not isinstance(value, str):
|
|
242
|
+
value = str(value)
|
|
243
|
+
|
|
244
|
+
# Remove control characters except newlines and tabs
|
|
245
|
+
sanitized = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", value)
|
|
246
|
+
|
|
247
|
+
# Limit length
|
|
248
|
+
if len(sanitized) > max_length:
|
|
249
|
+
sanitized = sanitized[:max_length]
|
|
250
|
+
|
|
251
|
+
return sanitized
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def is_valid_json(json_str: str) -> bool:
|
|
255
|
+
"""Check if string is valid JSON."""
|
|
256
|
+
try:
|
|
257
|
+
json.loads(json_str)
|
|
258
|
+
return True
|
|
259
|
+
except (json.JSONDecodeError, TypeError):
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def deep_merge(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
|
|
264
|
+
"""Deep merge two dictionaries."""
|
|
265
|
+
result = dict1.copy()
|
|
266
|
+
|
|
267
|
+
for key, value in dict2.items():
|
|
268
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
269
|
+
result[key] = deep_merge(result[key], value)
|
|
270
|
+
else:
|
|
271
|
+
result[key] = value
|
|
272
|
+
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def timeout_wrapper(timeout_seconds: int) -> Callable:
|
|
277
|
+
"""Decorator to add timeout to function execution."""
|
|
278
|
+
|
|
279
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
280
|
+
@functools.wraps(func)
|
|
281
|
+
def wrapper(*args, **kwargs) -> T:
|
|
282
|
+
import threading
|
|
283
|
+
|
|
284
|
+
result = [None]
|
|
285
|
+
exception = [None]
|
|
286
|
+
|
|
287
|
+
def target():
|
|
288
|
+
try:
|
|
289
|
+
result[0] = func(*args, **kwargs)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
exception[0] = e
|
|
292
|
+
|
|
293
|
+
thread = threading.Thread(target=target)
|
|
294
|
+
thread.daemon = True
|
|
295
|
+
thread.start()
|
|
296
|
+
thread.join(timeout_seconds)
|
|
297
|
+
|
|
298
|
+
if thread.is_alive():
|
|
299
|
+
raise TimeoutError(
|
|
300
|
+
f"Function {func.__name__} timed out after {timeout_seconds} seconds"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if exception[0]:
|
|
304
|
+
raise exception[0]
|
|
305
|
+
|
|
306
|
+
return result[0]
|
|
307
|
+
|
|
308
|
+
return wrapper
|
|
309
|
+
|
|
310
|
+
return decorator
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Core modules for Toolplane client."""
|
|
2
|
+
|
|
3
|
+
from .config import ClientConfig
|
|
4
|
+
from .connection import ConnectionManager
|
|
5
|
+
from .errors import (
|
|
6
|
+
ConnectionError,
|
|
7
|
+
MachineError,
|
|
8
|
+
RequestError,
|
|
9
|
+
SessionError,
|
|
10
|
+
TaskError,
|
|
11
|
+
ToolError,
|
|
12
|
+
ToolplaneAlreadyExistsError,
|
|
13
|
+
ToolplaneAPIError,
|
|
14
|
+
ToolplaneCancelledError,
|
|
15
|
+
ToolplaneConnectionError,
|
|
16
|
+
ToolplaneError,
|
|
17
|
+
ToolplaneFailedPreconditionError,
|
|
18
|
+
ToolplaneInternalError,
|
|
19
|
+
ToolplaneInvalidArgumentError,
|
|
20
|
+
ToolplaneNotFoundError,
|
|
21
|
+
ToolplanePermissionDeniedError,
|
|
22
|
+
ToolplaneResourceExhaustedError,
|
|
23
|
+
ToolplaneTimeoutError,
|
|
24
|
+
ToolplaneUnauthenticatedError,
|
|
25
|
+
ToolplaneUnavailableError,
|
|
26
|
+
api_error_from_http_response,
|
|
27
|
+
api_error_from_rpc_error,
|
|
28
|
+
)
|
|
29
|
+
from .machine import MachineManager
|
|
30
|
+
from .request import RequestManager
|
|
31
|
+
from .session import SessionManager
|
|
32
|
+
from .session_context import SessionContext
|
|
33
|
+
from .task import TaskManager
|
|
34
|
+
from .tool import ToolManager
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ConnectionManager",
|
|
38
|
+
"MachineManager",
|
|
39
|
+
"ToolManager",
|
|
40
|
+
"RequestManager",
|
|
41
|
+
"TaskManager",
|
|
42
|
+
"SessionManager",
|
|
43
|
+
"SessionContext",
|
|
44
|
+
"ToolplaneError",
|
|
45
|
+
"ConnectionError",
|
|
46
|
+
"ToolplaneConnectionError",
|
|
47
|
+
"ToolError",
|
|
48
|
+
"SessionError",
|
|
49
|
+
"MachineError",
|
|
50
|
+
"RequestError",
|
|
51
|
+
"TaskError",
|
|
52
|
+
"ClientConfig",
|
|
53
|
+
"ToolplaneAPIError",
|
|
54
|
+
"ToolplaneNotFoundError",
|
|
55
|
+
"ToolplaneInvalidArgumentError",
|
|
56
|
+
"ToolplaneFailedPreconditionError",
|
|
57
|
+
"ToolplaneResourceExhaustedError",
|
|
58
|
+
"ToolplaneUnauthenticatedError",
|
|
59
|
+
"ToolplanePermissionDeniedError",
|
|
60
|
+
"ToolplaneAlreadyExistsError",
|
|
61
|
+
"ToolplaneTimeoutError",
|
|
62
|
+
"ToolplaneUnavailableError",
|
|
63
|
+
"ToolplaneCancelledError",
|
|
64
|
+
"ToolplaneInternalError",
|
|
65
|
+
"api_error_from_rpc_error",
|
|
66
|
+
"api_error_from_http_response",
|
|
67
|
+
]
|
toolplane/core/config.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Configuration management for Toolplane gRPC client."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from ..common.base_config import BaseConfig
|
|
7
|
+
from ..common.constants import GRPC_DEFAULT_PORT
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ClientConfig(BaseConfig):
|
|
12
|
+
"""Configuration for Toolplane gRPC client."""
|
|
13
|
+
|
|
14
|
+
server_host: str = "localhost"
|
|
15
|
+
server_port: int = GRPC_DEFAULT_PORT
|
|
16
|
+
use_tls: bool = False
|
|
17
|
+
tls_cert_path: Optional[str] = None
|
|
18
|
+
tls_key_path: Optional[str] = None
|
|
19
|
+
tls_ca_cert_path: Optional[str] = None
|
|
20
|
+
tls_server_name: Optional[str] = None
|
|
21
|
+
# Retry configuration parameters
|
|
22
|
+
max_retries: int = 3
|
|
23
|
+
retry_base_delay: float = 1.0
|
|
24
|
+
retry_max_delay: float = 60.0
|
|
25
|
+
retry_backoff_factor: float = 2.0
|
|
26
|
+
|
|
27
|
+
def normalize(self):
|
|
28
|
+
"""Normalize gRPC-specific configuration values."""
|
|
29
|
+
# Normalize server host
|
|
30
|
+
if self.server_host.startswith("http://"):
|
|
31
|
+
self.server_host = self.server_host[7:]
|
|
32
|
+
elif self.server_host.startswith("https://"):
|
|
33
|
+
self.server_host = self.server_host[8:]
|
|
34
|
+
self.use_tls = True
|
|
35
|
+
|
|
36
|
+
# Extract port from host if present
|
|
37
|
+
if ":" in self.server_host:
|
|
38
|
+
host_parts = self.server_host.split(":")
|
|
39
|
+
if len(host_parts) == 2:
|
|
40
|
+
try:
|
|
41
|
+
self.server_port = int(host_parts[1])
|
|
42
|
+
self.server_host = host_parts[0]
|
|
43
|
+
except ValueError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
# Ensure retry values are reasonable
|
|
47
|
+
if self.max_retries < 0:
|
|
48
|
+
self.max_retries = 3
|
|
49
|
+
if self.retry_base_delay <= 0:
|
|
50
|
+
self.retry_base_delay = 1.0
|
|
51
|
+
if self.retry_max_delay <= 0:
|
|
52
|
+
self.retry_max_delay = 60.0
|
|
53
|
+
if self.retry_backoff_factor <= 1.0:
|
|
54
|
+
self.retry_backoff_factor = 2.0
|
|
55
|
+
|
|
56
|
+
def get_auth_info(self) -> Dict[str, Any]:
|
|
57
|
+
"""Get gRPC authentication information."""
|
|
58
|
+
return {
|
|
59
|
+
"metadata": self.get_metadata(),
|
|
60
|
+
"use_tls": self.use_tls,
|
|
61
|
+
"tls_cert_path": self.tls_cert_path,
|
|
62
|
+
"tls_key_path": self.tls_key_path,
|
|
63
|
+
"tls_ca_cert_path": self.tls_ca_cert_path,
|
|
64
|
+
"tls_server_name": self.tls_server_name,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def get_metadata(self) -> List[tuple]:
|
|
68
|
+
"""Get gRPC metadata headers."""
|
|
69
|
+
metadata = []
|
|
70
|
+
if self.api_key:
|
|
71
|
+
metadata.append(("authorization", f"Bearer {self.api_key}"))
|
|
72
|
+
metadata.append(("api_key", self.api_key))
|
|
73
|
+
return metadata
|
|
74
|
+
|
|
75
|
+
def get_server_address(self) -> str:
|
|
76
|
+
"""Get formatted server address."""
|
|
77
|
+
return f"{self.server_host}:{self.server_port}"
|
|
78
|
+
|
|
79
|
+
def copy(self) -> "ClientConfig":
|
|
80
|
+
"""Create a copy of the configuration."""
|
|
81
|
+
return ClientConfig(**self.to_dict())
|
|
82
|
+
|
|
83
|
+
def merge(self, other: "ClientConfig") -> "ClientConfig":
|
|
84
|
+
"""Merge this configuration with another."""
|
|
85
|
+
merged_dict = self.to_dict()
|
|
86
|
+
merged_dict.update(other.to_dict())
|
|
87
|
+
return ClientConfig(**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_host": self.server_host,
|
|
95
|
+
"server_port": self.server_port,
|
|
96
|
+
"use_tls": self.use_tls,
|
|
97
|
+
"tls_cert_path": self.tls_cert_path,
|
|
98
|
+
"tls_key_path": self.tls_key_path,
|
|
99
|
+
"tls_ca_cert_path": self.tls_ca_cert_path,
|
|
100
|
+
"tls_server_name": self.tls_server_name,
|
|
101
|
+
"max_retries": self.max_retries,
|
|
102
|
+
"retry_base_delay": self.retry_base_delay,
|
|
103
|
+
"retry_max_delay": self.retry_max_delay,
|
|
104
|
+
"retry_backoff_factor": self.retry_backoff_factor,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return result
|