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/__init__.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Modular Toolplane client package."""
|
|
2
|
+
|
|
3
|
+
from .common import (
|
|
4
|
+
CACHE_TTL_SECONDS,
|
|
5
|
+
DEFAULT_BUFFER_SIZE,
|
|
6
|
+
DEFAULT_HEARTBEAT_INTERVAL,
|
|
7
|
+
DEFAULT_MAX_RETRIES,
|
|
8
|
+
DEFAULT_MAX_WORKERS,
|
|
9
|
+
DEFAULT_POLL_INTERVAL,
|
|
10
|
+
DEFAULT_REQUEST_TIMEOUT,
|
|
11
|
+
GRPC_DEFAULT_PORT,
|
|
12
|
+
HTTP_DEFAULT_PORT,
|
|
13
|
+
BaseConfig,
|
|
14
|
+
BaseConnectionManager,
|
|
15
|
+
BaseSessionManager,
|
|
16
|
+
BaseToolManager,
|
|
17
|
+
TTLCache,
|
|
18
|
+
cache_with_ttl,
|
|
19
|
+
format_error_message,
|
|
20
|
+
generate_session_id,
|
|
21
|
+
normalize_url,
|
|
22
|
+
parse_json_safe,
|
|
23
|
+
validate_session_name,
|
|
24
|
+
validate_tool_name,
|
|
25
|
+
with_retry,
|
|
26
|
+
)
|
|
27
|
+
from .core import (
|
|
28
|
+
ClientConfig,
|
|
29
|
+
ConnectionError,
|
|
30
|
+
MachineError,
|
|
31
|
+
RequestError,
|
|
32
|
+
SessionContext,
|
|
33
|
+
SessionError,
|
|
34
|
+
TaskError,
|
|
35
|
+
ToolError,
|
|
36
|
+
ToolplaneAlreadyExistsError,
|
|
37
|
+
ToolplaneAPIError,
|
|
38
|
+
ToolplaneCancelledError,
|
|
39
|
+
ToolplaneConnectionError,
|
|
40
|
+
ToolplaneError,
|
|
41
|
+
ToolplaneFailedPreconditionError,
|
|
42
|
+
ToolplaneInternalError,
|
|
43
|
+
ToolplaneInvalidArgumentError,
|
|
44
|
+
ToolplaneNotFoundError,
|
|
45
|
+
ToolplanePermissionDeniedError,
|
|
46
|
+
ToolplaneResourceExhaustedError,
|
|
47
|
+
ToolplaneTimeoutError,
|
|
48
|
+
ToolplaneUnauthenticatedError,
|
|
49
|
+
ToolplaneUnavailableError,
|
|
50
|
+
)
|
|
51
|
+
from .http_core import HTTPClientConfig, HTTPSessionContext
|
|
52
|
+
from .provider_runtime import ProviderRuntime
|
|
53
|
+
from .toolplane_client import Toolplane
|
|
54
|
+
from .toolplane_http_client import ToolplaneHTTP
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"Toolplane",
|
|
58
|
+
"ProviderRuntime",
|
|
59
|
+
"SessionContext",
|
|
60
|
+
"ClientConfig",
|
|
61
|
+
"ToolplaneError",
|
|
62
|
+
"ConnectionError",
|
|
63
|
+
"ToolplaneConnectionError",
|
|
64
|
+
"ToolError",
|
|
65
|
+
"SessionError",
|
|
66
|
+
"MachineError",
|
|
67
|
+
"RequestError",
|
|
68
|
+
"TaskError",
|
|
69
|
+
"ToolplaneAPIError",
|
|
70
|
+
"ToolplaneNotFoundError",
|
|
71
|
+
"ToolplaneInvalidArgumentError",
|
|
72
|
+
"ToolplaneFailedPreconditionError",
|
|
73
|
+
"ToolplaneResourceExhaustedError",
|
|
74
|
+
"ToolplaneUnauthenticatedError",
|
|
75
|
+
"ToolplanePermissionDeniedError",
|
|
76
|
+
"ToolplaneAlreadyExistsError",
|
|
77
|
+
"ToolplaneTimeoutError",
|
|
78
|
+
"ToolplaneUnavailableError",
|
|
79
|
+
"ToolplaneCancelledError",
|
|
80
|
+
"ToolplaneInternalError",
|
|
81
|
+
"ToolplaneHTTP",
|
|
82
|
+
"HTTPSessionContext",
|
|
83
|
+
"HTTPClientConfig",
|
|
84
|
+
"BaseConfig",
|
|
85
|
+
"BaseToolManager",
|
|
86
|
+
"BaseSessionManager",
|
|
87
|
+
"BaseConnectionManager",
|
|
88
|
+
"generate_session_id",
|
|
89
|
+
"validate_tool_name",
|
|
90
|
+
"validate_session_name",
|
|
91
|
+
"parse_json_safe",
|
|
92
|
+
"format_error_message",
|
|
93
|
+
"with_retry",
|
|
94
|
+
"cache_with_ttl",
|
|
95
|
+
"TTLCache",
|
|
96
|
+
"normalize_url",
|
|
97
|
+
"DEFAULT_HEARTBEAT_INTERVAL",
|
|
98
|
+
"DEFAULT_MAX_WORKERS",
|
|
99
|
+
"DEFAULT_POLL_INTERVAL",
|
|
100
|
+
"DEFAULT_REQUEST_TIMEOUT",
|
|
101
|
+
"DEFAULT_MAX_RETRIES",
|
|
102
|
+
"DEFAULT_BUFFER_SIZE",
|
|
103
|
+
"CACHE_TTL_SECONDS",
|
|
104
|
+
"GRPC_DEFAULT_PORT",
|
|
105
|
+
"HTTP_DEFAULT_PORT",
|
|
106
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Common functionality shared between gRPC and HTTP Toolplane clients."""
|
|
2
|
+
|
|
3
|
+
from .base_config import BaseConfig
|
|
4
|
+
from .base_connection_manager import BaseConnectionManager
|
|
5
|
+
from .base_session_manager import BaseSessionManager
|
|
6
|
+
from .base_tool_manager import BaseToolManager
|
|
7
|
+
from .constants import (
|
|
8
|
+
BACKPRESSURE_THRESHOLD,
|
|
9
|
+
CACHE_TTL_SECONDS,
|
|
10
|
+
DEFAULT_BUFFER_SIZE,
|
|
11
|
+
DEFAULT_HEARTBEAT_INTERVAL,
|
|
12
|
+
DEFAULT_MAX_RETRIES,
|
|
13
|
+
DEFAULT_MAX_WORKERS,
|
|
14
|
+
DEFAULT_POLL_INTERVAL,
|
|
15
|
+
DEFAULT_REQUEST_TIMEOUT,
|
|
16
|
+
DEFAULT_RETRY_BACKOFF_MS,
|
|
17
|
+
ERROR_CONNECTION_FAILED,
|
|
18
|
+
ERROR_INVALID_PARAMETERS,
|
|
19
|
+
ERROR_SESSION_NOT_FOUND,
|
|
20
|
+
ERROR_STREAM_INTERRUPTED,
|
|
21
|
+
ERROR_TIMEOUT,
|
|
22
|
+
ERROR_TOOL_NOT_FOUND,
|
|
23
|
+
GRPC_DEFAULT_PORT,
|
|
24
|
+
HTTP_DEFAULT_PORT,
|
|
25
|
+
MAX_DESCRIPTION_LENGTH,
|
|
26
|
+
MAX_SESSION_NAME_LENGTH,
|
|
27
|
+
MAX_TOOL_EXECUTION_TIME,
|
|
28
|
+
MAX_TOOL_NAME_LENGTH,
|
|
29
|
+
STREAM_CHUNK_SIZE,
|
|
30
|
+
)
|
|
31
|
+
from .utils import (
|
|
32
|
+
TTLCache,
|
|
33
|
+
cache_with_ttl,
|
|
34
|
+
deep_merge,
|
|
35
|
+
extract_host_port,
|
|
36
|
+
format_error_message,
|
|
37
|
+
generate_session_id,
|
|
38
|
+
is_valid_json,
|
|
39
|
+
merge_dicts,
|
|
40
|
+
normalize_url,
|
|
41
|
+
parse_json_safe,
|
|
42
|
+
sanitize_input,
|
|
43
|
+
timeout_wrapper,
|
|
44
|
+
validate_description,
|
|
45
|
+
validate_session_name,
|
|
46
|
+
validate_tool_name,
|
|
47
|
+
with_retry,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"BaseConfig",
|
|
52
|
+
"BaseToolManager",
|
|
53
|
+
"BaseSessionManager",
|
|
54
|
+
"BaseConnectionManager",
|
|
55
|
+
"generate_session_id",
|
|
56
|
+
"validate_tool_name",
|
|
57
|
+
"validate_session_name",
|
|
58
|
+
"validate_description",
|
|
59
|
+
"parse_json_safe",
|
|
60
|
+
"format_error_message",
|
|
61
|
+
"with_retry",
|
|
62
|
+
"cache_with_ttl",
|
|
63
|
+
"TTLCache",
|
|
64
|
+
"normalize_url",
|
|
65
|
+
"extract_host_port",
|
|
66
|
+
"merge_dicts",
|
|
67
|
+
"sanitize_input",
|
|
68
|
+
"is_valid_json",
|
|
69
|
+
"deep_merge",
|
|
70
|
+
"timeout_wrapper",
|
|
71
|
+
"DEFAULT_HEARTBEAT_INTERVAL",
|
|
72
|
+
"DEFAULT_MAX_WORKERS",
|
|
73
|
+
"DEFAULT_POLL_INTERVAL",
|
|
74
|
+
"DEFAULT_REQUEST_TIMEOUT",
|
|
75
|
+
"DEFAULT_MAX_RETRIES",
|
|
76
|
+
"DEFAULT_BUFFER_SIZE",
|
|
77
|
+
"DEFAULT_RETRY_BACKOFF_MS",
|
|
78
|
+
"CACHE_TTL_SECONDS",
|
|
79
|
+
"GRPC_DEFAULT_PORT",
|
|
80
|
+
"HTTP_DEFAULT_PORT",
|
|
81
|
+
"MAX_SESSION_NAME_LENGTH",
|
|
82
|
+
"MAX_TOOL_NAME_LENGTH",
|
|
83
|
+
"MAX_DESCRIPTION_LENGTH",
|
|
84
|
+
"MAX_TOOL_EXECUTION_TIME",
|
|
85
|
+
"STREAM_CHUNK_SIZE",
|
|
86
|
+
"BACKPRESSURE_THRESHOLD",
|
|
87
|
+
"ERROR_CONNECTION_FAILED",
|
|
88
|
+
"ERROR_TOOL_NOT_FOUND",
|
|
89
|
+
"ERROR_SESSION_NOT_FOUND",
|
|
90
|
+
"ERROR_INVALID_PARAMETERS",
|
|
91
|
+
"ERROR_TIMEOUT",
|
|
92
|
+
"ERROR_STREAM_INTERRUPTED",
|
|
93
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Base configuration class shared between gRPC and HTTP clients."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from .constants import (
|
|
8
|
+
DEFAULT_BUFFER_SIZE,
|
|
9
|
+
DEFAULT_HEARTBEAT_INTERVAL,
|
|
10
|
+
DEFAULT_MAX_RETRIES,
|
|
11
|
+
DEFAULT_MAX_WORKERS,
|
|
12
|
+
DEFAULT_POLL_INTERVAL,
|
|
13
|
+
DEFAULT_REQUEST_TIMEOUT,
|
|
14
|
+
DEFAULT_RETRY_BACKOFF_MS,
|
|
15
|
+
)
|
|
16
|
+
from .utils import validate_description, validate_session_name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class BaseConfig(ABC):
|
|
21
|
+
"""Base configuration for Toolplane clients."""
|
|
22
|
+
|
|
23
|
+
# Common configuration fields
|
|
24
|
+
api_key: Optional[str] = None
|
|
25
|
+
user_id: Optional[str] = None
|
|
26
|
+
session_name: Optional[str] = None
|
|
27
|
+
session_description: Optional[str] = None
|
|
28
|
+
session_namespace: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
# Performance settings
|
|
31
|
+
heartbeat_interval: int = DEFAULT_HEARTBEAT_INTERVAL
|
|
32
|
+
max_workers: int = DEFAULT_MAX_WORKERS
|
|
33
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL
|
|
34
|
+
request_timeout: int = DEFAULT_REQUEST_TIMEOUT
|
|
35
|
+
max_retries: int = DEFAULT_MAX_RETRIES
|
|
36
|
+
max_buffer_size: int = DEFAULT_BUFFER_SIZE
|
|
37
|
+
retry_backoff_ms: int = DEFAULT_RETRY_BACKOFF_MS
|
|
38
|
+
|
|
39
|
+
# Additional configuration
|
|
40
|
+
extra_config: Dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
def __post_init__(self):
|
|
43
|
+
"""Validate configuration after initialization."""
|
|
44
|
+
self.validate()
|
|
45
|
+
self.normalize()
|
|
46
|
+
|
|
47
|
+
def validate(self):
|
|
48
|
+
"""Validate configuration parameters."""
|
|
49
|
+
if self.session_name and not validate_session_name(self.session_name):
|
|
50
|
+
raise ValueError(f"Invalid session name: {self.session_name}")
|
|
51
|
+
|
|
52
|
+
if self.session_description and not validate_description(
|
|
53
|
+
self.session_description
|
|
54
|
+
):
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Session description too long: {len(self.session_description)} characters"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if self.heartbeat_interval <= 0:
|
|
60
|
+
raise ValueError("Heartbeat interval must be positive")
|
|
61
|
+
|
|
62
|
+
if self.max_workers <= 0:
|
|
63
|
+
raise ValueError("Max workers must be positive")
|
|
64
|
+
|
|
65
|
+
if self.poll_interval <= 0:
|
|
66
|
+
raise ValueError("Poll interval must be positive")
|
|
67
|
+
|
|
68
|
+
if self.request_timeout <= 0:
|
|
69
|
+
raise ValueError("Request timeout must be positive")
|
|
70
|
+
|
|
71
|
+
if self.max_retries < 0:
|
|
72
|
+
raise ValueError("Max retries must be non-negative")
|
|
73
|
+
|
|
74
|
+
if self.max_buffer_size <= 0:
|
|
75
|
+
raise ValueError("Max buffer size must be positive")
|
|
76
|
+
|
|
77
|
+
if self.retry_backoff_ms < 0:
|
|
78
|
+
raise ValueError("Retry backoff must be non-negative")
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def normalize(self):
|
|
82
|
+
"""Normalize configuration values (protocol-specific)."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def get_auth_info(self) -> Dict[str, Any]:
|
|
87
|
+
"""Get authentication information (protocol-specific)."""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def get_common_fields(self) -> Dict[str, Any]:
|
|
91
|
+
"""Get common configuration fields."""
|
|
92
|
+
return {
|
|
93
|
+
"api_key": self.api_key,
|
|
94
|
+
"user_id": self.user_id,
|
|
95
|
+
"session_name": self.session_name,
|
|
96
|
+
"session_description": self.session_description,
|
|
97
|
+
"session_namespace": self.session_namespace,
|
|
98
|
+
"heartbeat_interval": self.heartbeat_interval,
|
|
99
|
+
"max_workers": self.max_workers,
|
|
100
|
+
"poll_interval": self.poll_interval,
|
|
101
|
+
"request_timeout": self.request_timeout,
|
|
102
|
+
"max_retries": self.max_retries,
|
|
103
|
+
"max_buffer_size": self.max_buffer_size,
|
|
104
|
+
"retry_backoff_ms": self.retry_backoff_ms,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def update_from_dict(self, config_dict: Dict[str, Any]):
|
|
108
|
+
"""Update configuration from dictionary."""
|
|
109
|
+
for key, value in config_dict.items():
|
|
110
|
+
if hasattr(self, key):
|
|
111
|
+
setattr(self, key, value)
|
|
112
|
+
else:
|
|
113
|
+
self.extra_config[key] = value
|
|
114
|
+
|
|
115
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
116
|
+
"""Convert configuration to dictionary."""
|
|
117
|
+
result = self.get_common_fields()
|
|
118
|
+
result.update(self.extra_config)
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
def copy(self) -> "BaseConfig":
|
|
122
|
+
"""Create a copy of the configuration."""
|
|
123
|
+
# This will be implemented by subclasses
|
|
124
|
+
raise NotImplementedError("Subclasses must implement copy method")
|
|
125
|
+
|
|
126
|
+
def merge(self, other: "BaseConfig") -> "BaseConfig":
|
|
127
|
+
"""Merge this configuration with another."""
|
|
128
|
+
# This will be implemented by subclasses
|
|
129
|
+
raise NotImplementedError("Subclasses must implement merge method")
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Base connection manager class shared between gRPC and HTTP clients."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from ..core.errors import ConnectionError
|
|
10
|
+
except ImportError:
|
|
11
|
+
# Fallback for standalone testing
|
|
12
|
+
class ConnectionError(Exception):
|
|
13
|
+
"""Connection error for standalone testing."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .constants import (
|
|
19
|
+
DEFAULT_MAX_RETRIES,
|
|
20
|
+
DEFAULT_RETRY_BACKOFF_MS,
|
|
21
|
+
)
|
|
22
|
+
from .utils import with_retry
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseConnectionManager(ABC):
|
|
26
|
+
"""Base class for connection management."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, config):
|
|
29
|
+
"""Initialize base connection manager."""
|
|
30
|
+
self.config = config
|
|
31
|
+
self.connected = False
|
|
32
|
+
self.connection_lock = threading.RLock()
|
|
33
|
+
self.last_heartbeat = 0
|
|
34
|
+
self.connection_stats = {
|
|
35
|
+
"connect_time": 0,
|
|
36
|
+
"last_activity": 0,
|
|
37
|
+
"total_requests": 0,
|
|
38
|
+
"failed_requests": 0,
|
|
39
|
+
"reconnect_count": 0,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def connect(self):
|
|
44
|
+
"""Establish connection (protocol-specific)."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def disconnect(self):
|
|
49
|
+
"""Close connection (protocol-specific)."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def is_connected(self) -> bool:
|
|
54
|
+
"""Check if connection is active (protocol-specific)."""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def ensure_connected(self):
|
|
58
|
+
"""Ensure connection is established."""
|
|
59
|
+
if not self.is_connected():
|
|
60
|
+
self.connect()
|
|
61
|
+
|
|
62
|
+
@with_retry(max_retries=DEFAULT_MAX_RETRIES, backoff_ms=DEFAULT_RETRY_BACKOFF_MS)
|
|
63
|
+
def reconnect(self):
|
|
64
|
+
"""Reconnect with retry logic."""
|
|
65
|
+
try:
|
|
66
|
+
self.disconnect()
|
|
67
|
+
time.sleep(0.1) # Brief pause before reconnecting
|
|
68
|
+
self.connect()
|
|
69
|
+
|
|
70
|
+
with self.connection_lock:
|
|
71
|
+
self.connection_stats["reconnect_count"] += 1
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
raise ConnectionError(f"Failed to reconnect: {e}")
|
|
75
|
+
|
|
76
|
+
def health_check(self) -> bool:
|
|
77
|
+
"""Perform health check."""
|
|
78
|
+
try:
|
|
79
|
+
return self._perform_health_check()
|
|
80
|
+
except Exception:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def _perform_health_check(self) -> bool:
|
|
85
|
+
"""Perform protocol-specific health check."""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def heartbeat(self):
|
|
89
|
+
"""Send heartbeat to server."""
|
|
90
|
+
try:
|
|
91
|
+
if self.is_connected():
|
|
92
|
+
current_time = time.time()
|
|
93
|
+
if current_time - self.last_heartbeat >= self.config.heartbeat_interval:
|
|
94
|
+
self._send_heartbeat()
|
|
95
|
+
self.last_heartbeat = current_time
|
|
96
|
+
except Exception:
|
|
97
|
+
# Heartbeat failures are not critical
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def _send_heartbeat(self):
|
|
102
|
+
"""Send protocol-specific heartbeat."""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def get_connection_info(self) -> Dict[str, Any]:
|
|
106
|
+
"""Get connection information."""
|
|
107
|
+
with self.connection_lock:
|
|
108
|
+
return {
|
|
109
|
+
"connected": self.is_connected(),
|
|
110
|
+
"config": self.config.to_dict(),
|
|
111
|
+
"stats": self.connection_stats.copy(),
|
|
112
|
+
"last_heartbeat": self.last_heartbeat,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def reset_stats(self):
|
|
116
|
+
"""Reset connection statistics."""
|
|
117
|
+
with self.connection_lock:
|
|
118
|
+
self.connection_stats = {
|
|
119
|
+
"connect_time": time.time() if self.is_connected() else 0,
|
|
120
|
+
"last_activity": time.time(),
|
|
121
|
+
"total_requests": 0,
|
|
122
|
+
"failed_requests": 0,
|
|
123
|
+
"reconnect_count": 0,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def _record_request(self, success: bool = True):
|
|
127
|
+
"""Record request statistics."""
|
|
128
|
+
with self.connection_lock:
|
|
129
|
+
self.connection_stats["total_requests"] += 1
|
|
130
|
+
self.connection_stats["last_activity"] = time.time()
|
|
131
|
+
if not success:
|
|
132
|
+
self.connection_stats["failed_requests"] += 1
|
|
133
|
+
|
|
134
|
+
def get_request_stats(self) -> Dict[str, Any]:
|
|
135
|
+
"""Get request statistics."""
|
|
136
|
+
with self.connection_lock:
|
|
137
|
+
stats = self.connection_stats.copy()
|
|
138
|
+
|
|
139
|
+
# Calculate success rate
|
|
140
|
+
total = stats["total_requests"]
|
|
141
|
+
failed = stats["failed_requests"]
|
|
142
|
+
success_rate = ((total - failed) / total * 100) if total > 0 else 0
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"total_requests": total,
|
|
146
|
+
"failed_requests": failed,
|
|
147
|
+
"success_rate": success_rate,
|
|
148
|
+
"reconnect_count": stats["reconnect_count"],
|
|
149
|
+
"uptime": (
|
|
150
|
+
time.time() - stats["connect_time"]
|
|
151
|
+
if stats["connect_time"] > 0
|
|
152
|
+
else 0
|
|
153
|
+
),
|
|
154
|
+
"last_activity": stats["last_activity"],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
def __enter__(self):
|
|
158
|
+
"""Context manager entry."""
|
|
159
|
+
self.connect()
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
163
|
+
"""Context manager exit."""
|
|
164
|
+
self.disconnect()
|
|
165
|
+
|
|
166
|
+
def __del__(self):
|
|
167
|
+
"""Destructor to ensure connection is closed."""
|
|
168
|
+
try:
|
|
169
|
+
self.disconnect()
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|