robosystems-client 0.1.18__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of robosystems-client might be problematic. Click here for more details.
- robosystems_client/api/agent/auto_select_agent.py +10 -4
- robosystems_client/api/agent/batch_process_queries.py +9 -4
- robosystems_client/api/agent/execute_specific_agent.py +11 -4
- robosystems_client/api/agent/get_agent_metadata.py +4 -1
- robosystems_client/api/agent/list_agents.py +4 -1
- robosystems_client/api/agent/recommend_agent.py +4 -1
- robosystems_client/api/auth/check_password_strength.py +2 -0
- robosystems_client/api/auth/complete_sso_auth.py +3 -0
- robosystems_client/api/auth/forgot_password.py +6 -3
- robosystems_client/api/auth/generate_sso_token.py +3 -0
- robosystems_client/api/auth/get_captcha_config.py +1 -0
- robosystems_client/api/auth/get_current_auth_user.py +3 -0
- robosystems_client/api/auth/get_password_policy.py +1 -0
- robosystems_client/api/auth/login_user.py +7 -3
- robosystems_client/api/auth/logout_user.py +2 -0
- robosystems_client/api/auth/refresh_auth_session.py +3 -0
- robosystems_client/api/auth/register_user.py +11 -6
- robosystems_client/api/auth/resend_verification_email.py +8 -3
- robosystems_client/api/auth/reset_password.py +3 -0
- robosystems_client/api/auth/sso_token_exchange.py +7 -3
- robosystems_client/api/auth/validate_reset_token.py +2 -0
- robosystems_client/api/auth/verify_email.py +3 -0
- robosystems_client/api/backup/create_backup.py +14 -8
- robosystems_client/api/backup/get_backup_download_url.py +9 -4
- robosystems_client/api/backup/get_backup_stats.py +3 -1
- robosystems_client/api/backup/list_backups.py +7 -5
- robosystems_client/api/backup/restore_backup.py +27 -8
- robosystems_client/api/connections/create_connection.py +14 -8
- robosystems_client/api/connections/create_link_token.py +9 -4
- robosystems_client/api/connections/delete_connection.py +13 -8
- robosystems_client/api/connections/exchange_link_token.py +9 -4
- robosystems_client/api/connections/get_connection.py +9 -4
- robosystems_client/api/connections/get_connection_options.py +8 -4
- robosystems_client/api/connections/init_o_auth.py +3 -1
- robosystems_client/api/connections/list_connections.py +8 -4
- robosystems_client/api/connections/oauth_callback.py +10 -4
- robosystems_client/api/connections/sync_connection.py +13 -8
- robosystems_client/api/graph_analytics/get_graph_metrics.py +13 -8
- robosystems_client/api/graph_analytics/get_graph_usage_stats.py +12 -8
- robosystems_client/api/graph_billing/get_current_graph_bill.py +9 -4
- robosystems_client/api/graph_billing/get_graph_billing_history.py +9 -4
- robosystems_client/api/graph_billing/get_graph_monthly_bill.py +10 -4
- robosystems_client/api/graph_billing/get_graph_usage_details.py +10 -4
- robosystems_client/api/graph_credits/check_credit_balance.py +9 -4
- robosystems_client/api/graph_credits/check_storage_limits.py +9 -4
- robosystems_client/api/graph_credits/get_credit_summary.py +9 -4
- robosystems_client/api/graph_credits/get_storage_usage.py +8 -4
- robosystems_client/api/graph_credits/list_credit_transactions.py +9 -4
- robosystems_client/api/graph_health/get_database_health.py +9 -4
- robosystems_client/api/graph_info/get_database_info.py +9 -4
- robosystems_client/api/graph_limits/get_graph_limits.py +9 -4
- robosystems_client/api/{create → graphs}/create_graph.py +7 -5
- robosystems_client/api/{create → graphs}/get_available_extensions.py +2 -1
- robosystems_client/api/{user/get_user_graphs.py → graphs/get_graphs.py} +3 -1
- robosystems_client/api/{user/select_user_graph.py → graphs/select_graph.py} +13 -8
- robosystems_client/api/mcp/call_mcp_tool.py +18 -8
- robosystems_client/api/mcp/list_mcp_tools.py +12 -8
- robosystems_client/api/operations/cancel_operation.py +9 -3
- robosystems_client/api/operations/get_operation_status.py +8 -3
- robosystems_client/api/operations/stream_operation_events.py +8 -3
- robosystems_client/api/query/execute_cypher_query.py +49 -16
- robosystems_client/api/schema/export_graph_schema.py +3 -1
- robosystems_client/api/schema/{get_graph_schema_info.py → get_graph_schema.py} +37 -47
- robosystems_client/api/schema/validate_schema.py +10 -5
- robosystems_client/api/service_offerings/get_service_offerings.py +2 -0
- robosystems_client/api/status/get_service_status.py +1 -0
- robosystems_client/api/subgraphs/create_subgraph.py +3 -1
- robosystems_client/api/subgraphs/delete_subgraph.py +15 -7
- robosystems_client/api/subgraphs/get_subgraph_info.py +14 -7
- robosystems_client/api/subgraphs/get_subgraph_quota.py +10 -4
- robosystems_client/api/subgraphs/list_subgraphs.py +3 -1
- robosystems_client/api/tables/delete_file_v1_graphs_graph_id_tables_files_file_id_delete.py +287 -0
- robosystems_client/api/tables/get_file_info_v1_graphs_graph_id_tables_files_file_id_get.py +283 -0
- robosystems_client/api/tables/get_upload_url_v1_graphs_graph_id_tables_table_name_files_post.py +260 -0
- robosystems_client/api/tables/ingest_tables_v1_graphs_graph_id_tables_ingest_post.py +251 -0
- robosystems_client/api/tables/list_table_files_v1_graphs_graph_id_tables_table_name_files_get.py +283 -0
- robosystems_client/api/{backup/export_backup.py → tables/list_tables_v1_graphs_graph_id_tables_get.py} +36 -36
- robosystems_client/api/{schema/list_schema_extensions.py → tables/query_tables_v1_graphs_graph_id_tables_query_post.py} +67 -43
- robosystems_client/api/tables/update_file_v1_graphs_graph_id_tables_files_file_id_patch.py +306 -0
- robosystems_client/api/user/create_user_api_key.py +2 -0
- robosystems_client/api/user/get_all_credit_summaries.py +6 -3
- robosystems_client/api/user/get_current_user.py +2 -0
- robosystems_client/api/user/list_user_api_keys.py +2 -0
- robosystems_client/api/user/revoke_user_api_key.py +7 -3
- robosystems_client/api/user/update_user.py +2 -0
- robosystems_client/api/user/update_user_api_key.py +2 -0
- robosystems_client/api/user/update_user_password.py +8 -3
- robosystems_client/api/user_analytics/get_detailed_user_analytics.py +2 -0
- robosystems_client/api/user_analytics/get_user_usage_overview.py +2 -0
- robosystems_client/api/user_limits/get_all_shared_repository_limits.py +2 -0
- robosystems_client/api/user_limits/get_shared_repository_limits.py +6 -4
- robosystems_client/api/user_limits/get_user_limits.py +3 -0
- robosystems_client/api/user_limits/get_user_usage.py +2 -0
- robosystems_client/api/user_subscriptions/cancel_shared_repository_subscription.py +11 -6
- robosystems_client/api/user_subscriptions/get_repository_credits.py +7 -3
- robosystems_client/api/user_subscriptions/get_shared_repository_credits.py +7 -3
- robosystems_client/api/user_subscriptions/get_user_shared_subscriptions.py +7 -3
- robosystems_client/api/user_subscriptions/subscribe_to_shared_repository.py +8 -3
- robosystems_client/api/user_subscriptions/upgrade_shared_repository_subscription.py +12 -6
- robosystems_client/extensions/README.md +1 -212
- robosystems_client/extensions/__init__.py +12 -28
- robosystems_client/extensions/extensions.py +3 -17
- robosystems_client/extensions/operation_client.py +12 -4
- robosystems_client/extensions/query_client.py +38 -24
- robosystems_client/extensions/sse_client.py +11 -0
- robosystems_client/extensions/table_ingest_client.py +466 -0
- robosystems_client/models/__init__.py +39 -31
- robosystems_client/models/api_key_info.py +20 -0
- robosystems_client/models/backup_restore_request.py +1 -12
- robosystems_client/models/bulk_ingest_request.py +50 -0
- robosystems_client/models/bulk_ingest_response.py +137 -0
- robosystems_client/models/create_api_key_request.py +20 -0
- robosystems_client/models/create_graph_request.py +4 -3
- robosystems_client/models/{sso_login_request.py → delete_file_v1_graphs_graph_id_tables_files_file_id_delete_response_delete_file_v1_graphs_graph_id_tables_files_file_id_delete.py} +9 -22
- robosystems_client/models/file_update_request.py +62 -0
- robosystems_client/models/file_upload_request.py +51 -0
- robosystems_client/models/file_upload_response.py +83 -0
- robosystems_client/models/{get_graph_schema_info_response_getgraphschemainfo.py → get_file_info_v1_graphs_graph_id_tables_files_file_id_get_response_get_file_info_v1_graphs_graph_id_tables_files_file_id_get.py} +8 -5
- robosystems_client/models/{copy_response_error_details_type_0.py → get_graph_schema_response_getgraphschema.py} +5 -5
- robosystems_client/models/list_table_files_v1_graphs_graph_id_tables_table_name_files_get_response_list_table_files_v1_graphs_graph_id_tables_table_name_files_get.py +47 -0
- robosystems_client/models/table_info.py +107 -0
- robosystems_client/models/table_ingest_result.py +107 -0
- robosystems_client/models/table_list_response.py +81 -0
- robosystems_client/models/table_query_request.py +40 -0
- robosystems_client/models/table_query_response.py +92 -0
- robosystems_client/models/{list_schema_extensions_response_listschemaextensions.py → update_file_v1_graphs_graph_id_tables_files_file_id_patch_response_update_file_v1_graphs_graph_id_tables_files_file_id_patch.py} +8 -5
- {robosystems_client-0.1.18.dist-info → robosystems_client-0.2.0.dist-info}/METADATA +15 -3
- {robosystems_client-0.1.18.dist-info → robosystems_client-0.2.0.dist-info}/RECORD +132 -127
- robosystems_client/api/auth/sso_login.py +0 -177
- robosystems_client/api/copy/copy_data_to_graph.py +0 -486
- robosystems_client/extensions/copy_client.py +0 -479
- robosystems_client/models/copy_response.py +0 -275
- robosystems_client/models/copy_response_status.py +0 -11
- robosystems_client/models/data_frame_copy_request.py +0 -125
- robosystems_client/models/data_frame_copy_request_format.py +0 -10
- robosystems_client/models/s3_copy_request.py +0 -378
- robosystems_client/models/s3_copy_request_file_format.py +0 -12
- robosystems_client/models/s3_copy_request_s3_url_style_type_0.py +0 -9
- robosystems_client/models/url_copy_request.py +0 -157
- robosystems_client/models/url_copy_request_file_format.py +0 -10
- robosystems_client/models/url_copy_request_headers_type_0.py +0 -44
- /robosystems_client/api/{copy → graphs}/__init__.py +0 -0
- /robosystems_client/api/{create → tables}/__init__.py +0 -0
- {robosystems_client-0.1.18.dist-info → robosystems_client-0.2.0.dist-info}/WHEEL +0 -0
- {robosystems_client-0.1.18.dist-info → robosystems_client-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,479 +0,0 @@
|
|
|
1
|
-
"""Enhanced Copy Client with SSE support
|
|
2
|
-
|
|
3
|
-
Provides intelligent data copy operations with progress monitoring.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from typing import Dict, Any, Optional, Callable, Union, List
|
|
8
|
-
from enum import Enum
|
|
9
|
-
import time
|
|
10
|
-
import logging
|
|
11
|
-
|
|
12
|
-
from ..api.copy.copy_data_to_graph import sync_detailed as copy_data_to_graph
|
|
13
|
-
from ..models.s3_copy_request import S3CopyRequest
|
|
14
|
-
from ..models.url_copy_request import URLCopyRequest
|
|
15
|
-
from ..models.data_frame_copy_request import DataFrameCopyRequest
|
|
16
|
-
from ..models.copy_response import CopyResponse
|
|
17
|
-
from ..models.copy_response_status import CopyResponseStatus
|
|
18
|
-
from ..models.s3_copy_request_file_format import S3CopyRequestFileFormat
|
|
19
|
-
from .sse_client import SSEClient, AsyncSSEClient, SSEConfig, EventType
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class CopySourceType(Enum):
|
|
25
|
-
"""Types of copy sources"""
|
|
26
|
-
|
|
27
|
-
S3 = "s3"
|
|
28
|
-
URL = "url"
|
|
29
|
-
DATAFRAME = "dataframe"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class CopyOptions:
|
|
34
|
-
"""Options for copy operations"""
|
|
35
|
-
|
|
36
|
-
on_progress: Optional[Callable[[str, Optional[float]], None]] = None
|
|
37
|
-
on_queue_update: Optional[Callable[[int, int], None]] = None
|
|
38
|
-
on_warning: Optional[Callable[[str], None]] = None
|
|
39
|
-
timeout: Optional[int] = None
|
|
40
|
-
test_mode: Optional[bool] = None
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@dataclass
|
|
44
|
-
class CopyResult:
|
|
45
|
-
"""Result from copy operation"""
|
|
46
|
-
|
|
47
|
-
status: str # 'completed', 'failed', 'partial', 'accepted'
|
|
48
|
-
rows_imported: Optional[int] = None
|
|
49
|
-
rows_skipped: Optional[int] = None
|
|
50
|
-
bytes_processed: Optional[int] = None
|
|
51
|
-
execution_time_ms: Optional[float] = None
|
|
52
|
-
warnings: Optional[List[str]] = None
|
|
53
|
-
error: Optional[str] = None
|
|
54
|
-
operation_id: Optional[str] = None
|
|
55
|
-
sse_url: Optional[str] = None
|
|
56
|
-
message: Optional[str] = None
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@dataclass
|
|
60
|
-
class CopyStatistics:
|
|
61
|
-
"""Statistics from copy operation"""
|
|
62
|
-
|
|
63
|
-
total_rows: int
|
|
64
|
-
imported_rows: int
|
|
65
|
-
skipped_rows: int
|
|
66
|
-
bytes_processed: int
|
|
67
|
-
duration: float # seconds
|
|
68
|
-
throughput: float # rows per second
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class CopyClient:
|
|
72
|
-
"""Enhanced copy client with SSE streaming support"""
|
|
73
|
-
|
|
74
|
-
def __init__(self, config: Dict[str, Any]):
|
|
75
|
-
self.config = config
|
|
76
|
-
self.base_url = config["base_url"]
|
|
77
|
-
self.headers = config.get("headers", {})
|
|
78
|
-
# Get token from config if passed by parent
|
|
79
|
-
self.token = config.get("token")
|
|
80
|
-
self.sse_client: Optional[SSEClient] = None
|
|
81
|
-
|
|
82
|
-
def copy_from_s3(
|
|
83
|
-
self, graph_id: str, request: S3CopyRequest, options: Optional[CopyOptions] = None
|
|
84
|
-
) -> CopyResult:
|
|
85
|
-
"""Copy data from S3 to graph database"""
|
|
86
|
-
return self._execute_copy(graph_id, request, CopySourceType.S3, options)
|
|
87
|
-
|
|
88
|
-
def copy_from_url(
|
|
89
|
-
self, graph_id: str, request: URLCopyRequest, options: Optional[CopyOptions] = None
|
|
90
|
-
) -> CopyResult:
|
|
91
|
-
"""Copy data from URL to graph database (when available)"""
|
|
92
|
-
return self._execute_copy(graph_id, request, CopySourceType.URL, options)
|
|
93
|
-
|
|
94
|
-
def copy_from_dataframe(
|
|
95
|
-
self,
|
|
96
|
-
graph_id: str,
|
|
97
|
-
request: DataFrameCopyRequest,
|
|
98
|
-
options: Optional[CopyOptions] = None,
|
|
99
|
-
) -> CopyResult:
|
|
100
|
-
"""Copy data from DataFrame to graph database (when available)"""
|
|
101
|
-
return self._execute_copy(graph_id, request, CopySourceType.DATAFRAME, options)
|
|
102
|
-
|
|
103
|
-
def _execute_copy(
|
|
104
|
-
self,
|
|
105
|
-
graph_id: str,
|
|
106
|
-
request: Union[S3CopyRequest, URLCopyRequest, DataFrameCopyRequest],
|
|
107
|
-
source_type: CopySourceType,
|
|
108
|
-
options: Optional[CopyOptions] = None,
|
|
109
|
-
) -> CopyResult:
|
|
110
|
-
"""Execute copy operation with automatic SSE monitoring for long-running operations"""
|
|
111
|
-
if options is None:
|
|
112
|
-
options = CopyOptions()
|
|
113
|
-
|
|
114
|
-
start_time = time.time()
|
|
115
|
-
|
|
116
|
-
# Import client here to avoid circular imports
|
|
117
|
-
from ..client import Client
|
|
118
|
-
|
|
119
|
-
# Create client with headers
|
|
120
|
-
client = Client(base_url=self.base_url, headers=self.headers)
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
# Execute the copy request with token if available
|
|
124
|
-
kwargs = {"graph_id": graph_id, "client": client, "body": request}
|
|
125
|
-
# Only add token if it's a valid string
|
|
126
|
-
if self.token and isinstance(self.token, str) and self.token.strip():
|
|
127
|
-
kwargs["token"] = self.token
|
|
128
|
-
response = copy_data_to_graph(**kwargs)
|
|
129
|
-
|
|
130
|
-
if response.parsed:
|
|
131
|
-
response_data: CopyResponse = response.parsed
|
|
132
|
-
|
|
133
|
-
# Check if this is an accepted (async) operation
|
|
134
|
-
if (
|
|
135
|
-
response_data.status == CopyResponseStatus.ACCEPTED
|
|
136
|
-
and response_data.operation_id
|
|
137
|
-
):
|
|
138
|
-
# This is a long-running operation with SSE monitoring
|
|
139
|
-
if options.on_progress:
|
|
140
|
-
options.on_progress("Copy operation started. Monitoring progress...", None)
|
|
141
|
-
|
|
142
|
-
# If SSE URL is provided, use it for monitoring
|
|
143
|
-
if response_data.sse_url:
|
|
144
|
-
return self._monitor_copy_operation(
|
|
145
|
-
response_data.operation_id, options, start_time
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
# Otherwise return the accepted response
|
|
149
|
-
return CopyResult(
|
|
150
|
-
status="accepted",
|
|
151
|
-
operation_id=response_data.operation_id,
|
|
152
|
-
sse_url=response_data.sse_url,
|
|
153
|
-
message=response_data.message,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
# This is a synchronous response - operation completed immediately
|
|
157
|
-
return self._build_copy_result(response_data, time.time() - start_time)
|
|
158
|
-
else:
|
|
159
|
-
return CopyResult(
|
|
160
|
-
status="failed",
|
|
161
|
-
error="No response data received",
|
|
162
|
-
execution_time_ms=(time.time() - start_time) * 1000,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
except Exception as e:
|
|
166
|
-
error_msg = str(e)
|
|
167
|
-
# Check for authentication errors
|
|
168
|
-
if (
|
|
169
|
-
"401" in error_msg or "403" in error_msg or "unauthorized" in error_msg.lower()
|
|
170
|
-
):
|
|
171
|
-
logger.error(f"Authentication failed during copy operation: {e}")
|
|
172
|
-
return CopyResult(
|
|
173
|
-
status="failed",
|
|
174
|
-
error=f"Authentication failed: {error_msg}",
|
|
175
|
-
execution_time_ms=(time.time() - start_time) * 1000,
|
|
176
|
-
)
|
|
177
|
-
else:
|
|
178
|
-
logger.error(f"Copy operation failed: {e}")
|
|
179
|
-
return CopyResult(
|
|
180
|
-
status="failed",
|
|
181
|
-
error=error_msg,
|
|
182
|
-
execution_time_ms=(time.time() - start_time) * 1000,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
def _monitor_copy_operation(
|
|
186
|
-
self, operation_id: str, options: CopyOptions, start_time: float
|
|
187
|
-
) -> CopyResult:
|
|
188
|
-
"""Monitor a copy operation using SSE"""
|
|
189
|
-
timeout_ms = options.timeout or 3600000 # Default 1 hour for copy operations
|
|
190
|
-
timeout_time = time.time() + (timeout_ms / 1000)
|
|
191
|
-
|
|
192
|
-
result = CopyResult(status="failed")
|
|
193
|
-
warnings: List[str] = []
|
|
194
|
-
|
|
195
|
-
# Set up SSE connection
|
|
196
|
-
sse_config = SSEConfig(base_url=self.base_url, timeout=timeout_ms // 1000)
|
|
197
|
-
sse_client = SSEClient(sse_config)
|
|
198
|
-
|
|
199
|
-
try:
|
|
200
|
-
sse_client.connect(operation_id)
|
|
201
|
-
|
|
202
|
-
# Set up event handlers
|
|
203
|
-
def on_queue_update(data):
|
|
204
|
-
if options.on_queue_update:
|
|
205
|
-
position = data.get("position", data.get("queue_position", 0))
|
|
206
|
-
wait_time = data.get("estimated_wait_seconds", 0)
|
|
207
|
-
options.on_queue_update(position, wait_time)
|
|
208
|
-
|
|
209
|
-
def on_progress(data):
|
|
210
|
-
if options.on_progress:
|
|
211
|
-
message = data.get("message", data.get("status", "Processing..."))
|
|
212
|
-
progress_percent = data.get("progress_percent", data.get("progress"))
|
|
213
|
-
options.on_progress(message, progress_percent)
|
|
214
|
-
|
|
215
|
-
# Check for warnings in progress updates
|
|
216
|
-
if "warnings" in data and data["warnings"]:
|
|
217
|
-
warnings.extend(data["warnings"])
|
|
218
|
-
if options.on_warning:
|
|
219
|
-
for warning in data["warnings"]:
|
|
220
|
-
options.on_warning(warning)
|
|
221
|
-
|
|
222
|
-
def on_completed(data):
|
|
223
|
-
nonlocal result
|
|
224
|
-
completion_data = data.get("result", data)
|
|
225
|
-
result = CopyResult(
|
|
226
|
-
status=completion_data.get("status", "completed"),
|
|
227
|
-
rows_imported=completion_data.get("rows_imported"),
|
|
228
|
-
rows_skipped=completion_data.get("rows_skipped"),
|
|
229
|
-
bytes_processed=completion_data.get("bytes_processed"),
|
|
230
|
-
execution_time_ms=(time.time() - start_time) * 1000,
|
|
231
|
-
warnings=warnings if warnings else completion_data.get("warnings"),
|
|
232
|
-
message=completion_data.get("message"),
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
def on_error(data):
|
|
236
|
-
nonlocal result
|
|
237
|
-
result = CopyResult(
|
|
238
|
-
status="failed",
|
|
239
|
-
error=data.get("message", data.get("error", "Copy operation failed")),
|
|
240
|
-
execution_time_ms=(time.time() - start_time) * 1000,
|
|
241
|
-
warnings=warnings if warnings else None,
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
def on_cancelled(data):
|
|
245
|
-
nonlocal result
|
|
246
|
-
result = CopyResult(
|
|
247
|
-
status="failed",
|
|
248
|
-
error="Copy operation cancelled",
|
|
249
|
-
execution_time_ms=(time.time() - start_time) * 1000,
|
|
250
|
-
warnings=warnings if warnings else None,
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
# Register event handlers
|
|
254
|
-
sse_client.on(EventType.QUEUE_UPDATE.value, on_queue_update)
|
|
255
|
-
sse_client.on(EventType.OPERATION_PROGRESS.value, on_progress)
|
|
256
|
-
sse_client.on(EventType.OPERATION_COMPLETED.value, on_completed)
|
|
257
|
-
sse_client.on(EventType.OPERATION_ERROR.value, on_error)
|
|
258
|
-
sse_client.on(EventType.OPERATION_CANCELLED.value, on_cancelled)
|
|
259
|
-
|
|
260
|
-
# Listen for events until completion or timeout
|
|
261
|
-
while time.time() < timeout_time:
|
|
262
|
-
sse_client.listen(timeout=1) # Process events for 1 second
|
|
263
|
-
|
|
264
|
-
# Check if operation is complete
|
|
265
|
-
if result.status in ["completed", "failed", "partial"]:
|
|
266
|
-
break
|
|
267
|
-
|
|
268
|
-
if time.time() >= timeout_time:
|
|
269
|
-
result = CopyResult(
|
|
270
|
-
status="failed",
|
|
271
|
-
error=f"Copy operation timeout after {timeout_ms}ms",
|
|
272
|
-
execution_time_ms=(time.time() - start_time) * 1000,
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
finally:
|
|
276
|
-
sse_client.close()
|
|
277
|
-
|
|
278
|
-
return result
|
|
279
|
-
|
|
280
|
-
def _build_copy_result(
|
|
281
|
-
self, response_data: CopyResponse, execution_time: float
|
|
282
|
-
) -> CopyResult:
|
|
283
|
-
"""Build copy result from response data"""
|
|
284
|
-
return CopyResult(
|
|
285
|
-
status=response_data.status.value,
|
|
286
|
-
rows_imported=response_data.rows_imported,
|
|
287
|
-
rows_skipped=response_data.rows_skipped,
|
|
288
|
-
bytes_processed=response_data.bytes_processed,
|
|
289
|
-
execution_time_ms=response_data.execution_time_ms or (execution_time * 1000),
|
|
290
|
-
warnings=response_data.warnings,
|
|
291
|
-
message=response_data.message,
|
|
292
|
-
error=str(response_data.error_details) if response_data.error_details else None,
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
def calculate_statistics(self, result: CopyResult) -> Optional[CopyStatistics]:
|
|
296
|
-
"""Calculate copy statistics from result"""
|
|
297
|
-
if result.status == "failed" or not result.rows_imported:
|
|
298
|
-
return None
|
|
299
|
-
|
|
300
|
-
total_rows = (result.rows_imported or 0) + (result.rows_skipped or 0)
|
|
301
|
-
duration = (result.execution_time_ms or 0) / 1000 # Convert to seconds
|
|
302
|
-
throughput = (result.rows_imported or 0) / duration if duration > 0 else 0
|
|
303
|
-
|
|
304
|
-
return CopyStatistics(
|
|
305
|
-
total_rows=total_rows,
|
|
306
|
-
imported_rows=result.rows_imported or 0,
|
|
307
|
-
skipped_rows=result.rows_skipped or 0,
|
|
308
|
-
bytes_processed=result.bytes_processed or 0,
|
|
309
|
-
duration=duration,
|
|
310
|
-
throughput=throughput,
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
def copy_s3(
|
|
314
|
-
self,
|
|
315
|
-
graph_id: str,
|
|
316
|
-
table_name: str,
|
|
317
|
-
s3_path: str,
|
|
318
|
-
access_key_id: str,
|
|
319
|
-
secret_access_key: str,
|
|
320
|
-
region: str = "us-east-1",
|
|
321
|
-
file_format: Optional[str] = None,
|
|
322
|
-
ignore_errors: bool = False,
|
|
323
|
-
) -> CopyResult:
|
|
324
|
-
"""Convenience method for simple S3 copy with default options"""
|
|
325
|
-
|
|
326
|
-
# Map string format to enum
|
|
327
|
-
format_enum = S3CopyRequestFileFormat.PARQUET
|
|
328
|
-
if file_format:
|
|
329
|
-
format_map = {
|
|
330
|
-
"csv": S3CopyRequestFileFormat.CSV,
|
|
331
|
-
"parquet": S3CopyRequestFileFormat.PARQUET,
|
|
332
|
-
"json": S3CopyRequestFileFormat.JSON,
|
|
333
|
-
"delta": S3CopyRequestFileFormat.DELTA,
|
|
334
|
-
"iceberg": S3CopyRequestFileFormat.ICEBERG,
|
|
335
|
-
}
|
|
336
|
-
format_enum = format_map.get(file_format.lower(), S3CopyRequestFileFormat.PARQUET)
|
|
337
|
-
|
|
338
|
-
request = S3CopyRequest(
|
|
339
|
-
table_name=table_name,
|
|
340
|
-
s3_path=s3_path,
|
|
341
|
-
s3_access_key_id=access_key_id,
|
|
342
|
-
s3_secret_access_key=secret_access_key,
|
|
343
|
-
s3_region=region,
|
|
344
|
-
file_format=format_enum,
|
|
345
|
-
ignore_errors=ignore_errors,
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
return self.copy_from_s3(graph_id, request)
|
|
349
|
-
|
|
350
|
-
def monitor_multiple_copies(
|
|
351
|
-
self, operation_ids: List[str], options: Optional[CopyOptions] = None
|
|
352
|
-
) -> Dict[str, CopyResult]:
|
|
353
|
-
"""Monitor multiple copy operations concurrently"""
|
|
354
|
-
results = {}
|
|
355
|
-
for operation_id in operation_ids:
|
|
356
|
-
result = self._monitor_copy_operation(
|
|
357
|
-
operation_id, options or CopyOptions(), time.time()
|
|
358
|
-
)
|
|
359
|
-
results[operation_id] = result
|
|
360
|
-
return results
|
|
361
|
-
|
|
362
|
-
def batch_copy_from_s3(
|
|
363
|
-
self, graph_id: str, copies: List[Dict[str, Any]]
|
|
364
|
-
) -> List[CopyResult]:
|
|
365
|
-
"""Batch copy multiple tables from S3"""
|
|
366
|
-
results = []
|
|
367
|
-
for copy_config in copies:
|
|
368
|
-
request = copy_config.get("request")
|
|
369
|
-
options = copy_config.get("options")
|
|
370
|
-
if request:
|
|
371
|
-
result = self.copy_from_s3(graph_id, request, options)
|
|
372
|
-
results.append(result)
|
|
373
|
-
return results
|
|
374
|
-
|
|
375
|
-
def copy_with_retry(
|
|
376
|
-
self,
|
|
377
|
-
graph_id: str,
|
|
378
|
-
request: Union[S3CopyRequest, URLCopyRequest, DataFrameCopyRequest],
|
|
379
|
-
source_type: CopySourceType,
|
|
380
|
-
max_retries: int = 3,
|
|
381
|
-
options: Optional[CopyOptions] = None,
|
|
382
|
-
) -> CopyResult:
|
|
383
|
-
"""Copy with retry logic for transient failures"""
|
|
384
|
-
if options is None:
|
|
385
|
-
options = CopyOptions()
|
|
386
|
-
|
|
387
|
-
last_error: Optional[Exception] = None
|
|
388
|
-
attempt = 0
|
|
389
|
-
|
|
390
|
-
while attempt < max_retries:
|
|
391
|
-
attempt += 1
|
|
392
|
-
|
|
393
|
-
try:
|
|
394
|
-
result = self._execute_copy(graph_id, request, source_type, options)
|
|
395
|
-
|
|
396
|
-
# If successful or partially successful, return
|
|
397
|
-
if result.status in ["completed", "partial"]:
|
|
398
|
-
return result
|
|
399
|
-
|
|
400
|
-
# If failed, check if it's retryable
|
|
401
|
-
if result.status == "failed":
|
|
402
|
-
is_retryable = self._is_retryable_error(result.error)
|
|
403
|
-
if not is_retryable or attempt == max_retries:
|
|
404
|
-
return result
|
|
405
|
-
|
|
406
|
-
# Wait before retry with exponential backoff
|
|
407
|
-
wait_time = min(1000 * (2 ** (attempt - 1)), 30000) / 1000
|
|
408
|
-
if options.on_progress:
|
|
409
|
-
options.on_progress(
|
|
410
|
-
f"Retrying copy operation (attempt {attempt}/{max_retries}) in {wait_time}s...",
|
|
411
|
-
None,
|
|
412
|
-
)
|
|
413
|
-
time.sleep(wait_time)
|
|
414
|
-
|
|
415
|
-
except Exception as e:
|
|
416
|
-
last_error = e
|
|
417
|
-
|
|
418
|
-
if attempt == max_retries:
|
|
419
|
-
raise last_error
|
|
420
|
-
|
|
421
|
-
# Wait before retry
|
|
422
|
-
wait_time = min(1000 * (2 ** (attempt - 1)), 30000) / 1000
|
|
423
|
-
if options.on_progress:
|
|
424
|
-
options.on_progress(
|
|
425
|
-
f"Retrying after error (attempt {attempt}/{max_retries}) in {wait_time}s...",
|
|
426
|
-
None,
|
|
427
|
-
)
|
|
428
|
-
time.sleep(wait_time)
|
|
429
|
-
|
|
430
|
-
raise last_error or Exception("Copy operation failed after all retries")
|
|
431
|
-
|
|
432
|
-
def _is_retryable_error(self, error: Optional[str]) -> bool:
|
|
433
|
-
"""Check if an error is retryable"""
|
|
434
|
-
if not error:
|
|
435
|
-
return False
|
|
436
|
-
|
|
437
|
-
retryable_patterns = [
|
|
438
|
-
"timeout",
|
|
439
|
-
"network",
|
|
440
|
-
"connection",
|
|
441
|
-
"temporary",
|
|
442
|
-
"unavailable",
|
|
443
|
-
"rate limit",
|
|
444
|
-
"throttl",
|
|
445
|
-
]
|
|
446
|
-
|
|
447
|
-
lower_error = error.lower()
|
|
448
|
-
return any(pattern in lower_error for pattern in retryable_patterns)
|
|
449
|
-
|
|
450
|
-
def close(self):
|
|
451
|
-
"""Cancel any active SSE connections"""
|
|
452
|
-
if self.sse_client:
|
|
453
|
-
self.sse_client.close()
|
|
454
|
-
self.sse_client = None
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
class AsyncCopyClient:
|
|
458
|
-
"""Async version of CopyClient for async/await usage"""
|
|
459
|
-
|
|
460
|
-
def __init__(self, config: Dict[str, Any]):
|
|
461
|
-
self.config = config
|
|
462
|
-
self.base_url = config["base_url"]
|
|
463
|
-
self.sse_client: Optional[AsyncSSEClient] = None
|
|
464
|
-
# Get token from config if passed by parent
|
|
465
|
-
self.token = config.get("token")
|
|
466
|
-
|
|
467
|
-
async def copy_from_s3(
|
|
468
|
-
self, graph_id: str, request: S3CopyRequest, options: Optional[CopyOptions] = None
|
|
469
|
-
) -> CopyResult:
|
|
470
|
-
"""Copy data from S3 to graph database asynchronously"""
|
|
471
|
-
# Async implementation would go here
|
|
472
|
-
# For now, this is a placeholder
|
|
473
|
-
raise NotImplementedError("Async copy client not yet implemented")
|
|
474
|
-
|
|
475
|
-
async def close(self):
|
|
476
|
-
"""Close any active connections"""
|
|
477
|
-
if self.sse_client:
|
|
478
|
-
await self.sse_client.close()
|
|
479
|
-
self.sse_client = None
|