robosystems-client 0.1.12__py3-none-any.whl → 0.1.14__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.
- robosystems_client/api/backup/create_backup.py +1 -1
- robosystems_client/api/backup/export_backup.py +14 -19
- robosystems_client/api/backup/get_backup_download_url.py +1 -1
- robosystems_client/api/backup/get_backup_stats.py +19 -1
- robosystems_client/api/backup/list_backups.py +19 -1
- robosystems_client/api/backup/restore_backup.py +1 -1
- robosystems_client/api/copy/copy_data_to_graph.py +479 -0
- robosystems_client/api/{credits_ → graph_credits}/check_credit_balance.py +42 -20
- robosystems_client/api/graph_health/__init__.py +1 -0
- robosystems_client/api/{graph_status → graph_health}/get_database_health.py +1 -1
- robosystems_client/api/graph_info/__init__.py +1 -0
- robosystems_client/api/{graph_status → graph_info}/get_database_info.py +1 -1
- robosystems_client/api/graph_limits/__init__.py +1 -0
- robosystems_client/api/graph_limits/get_graph_limits.py +259 -0
- robosystems_client/api/subgraphs/create_subgraph.py +63 -173
- robosystems_client/api/subgraphs/delete_subgraph.py +14 -14
- robosystems_client/api/subgraphs/get_subgraph_info.py +14 -14
- robosystems_client/api/subgraphs/get_subgraph_quota.py +8 -4
- robosystems_client/api/subgraphs/list_subgraphs.py +39 -75
- robosystems_client/api/user/get_all_credit_summaries.py +1 -1
- robosystems_client/extensions/README.md +211 -0
- robosystems_client/extensions/__init__.py +29 -0
- robosystems_client/extensions/copy_client.py +469 -0
- robosystems_client/extensions/extensions.py +17 -0
- robosystems_client/models/__init__.py +26 -10
- robosystems_client/models/copy_response.py +275 -0
- robosystems_client/models/{kuzu_backup_health_response_kuzubackuphealth.py → copy_response_error_details_type_0.py} +5 -5
- robosystems_client/models/copy_response_status.py +11 -0
- robosystems_client/models/custom_schema_definition.py +2 -2
- robosystems_client/models/data_frame_copy_request.py +125 -0
- robosystems_client/models/data_frame_copy_request_format.py +10 -0
- robosystems_client/models/get_graph_limits_response_getgraphlimits.py +44 -0
- robosystems_client/models/s3_copy_request.py +378 -0
- robosystems_client/models/s3_copy_request_file_format.py +12 -0
- robosystems_client/models/s3_copy_request_s3_url_style_type_0.py +9 -0
- robosystems_client/models/url_copy_request.py +157 -0
- robosystems_client/models/url_copy_request_file_format.py +10 -0
- robosystems_client/models/url_copy_request_headers_type_0.py +44 -0
- {robosystems_client-0.1.12.dist-info → robosystems_client-0.1.14.dist-info}/METADATA +1 -1
- {robosystems_client-0.1.12.dist-info → robosystems_client-0.1.14.dist-info}/RECORD +52 -44
- robosystems_client/api/backup/kuzu_backup_health.py +0 -202
- robosystems_client/api/billing/get_available_subscription_plans_v1_graph_id_billing_available_plans_get.py +0 -198
- robosystems_client/api/billing/get_credit_billing_info_v1_graph_id_billing_credits_get.py +0 -210
- robosystems_client/api/billing/get_graph_pricing_info_v1_graph_id_billing_pricing_get.py +0 -198
- robosystems_client/api/billing/get_graph_subscription_v1_graph_id_billing_subscription_get.py +0 -198
- robosystems_client/api/billing/upgrade_graph_subscription_v1_graph_id_billing_subscription_upgrade_post.py +0 -216
- robosystems_client/models/backup_export_request.py +0 -72
- robosystems_client/models/credit_check_request.py +0 -82
- robosystems_client/models/upgrade_subscription_request.py +0 -82
- /robosystems_client/api/{billing → copy}/__init__.py +0 -0
- /robosystems_client/api/{credits_ → graph_billing}/__init__.py +0 -0
- /robosystems_client/api/{billing → graph_billing}/get_current_graph_bill.py +0 -0
- /robosystems_client/api/{billing → graph_billing}/get_graph_billing_history.py +0 -0
- /robosystems_client/api/{billing → graph_billing}/get_graph_monthly_bill.py +0 -0
- /robosystems_client/api/{billing → graph_billing}/get_graph_usage_details.py +0 -0
- /robosystems_client/api/{graph_status → graph_credits}/__init__.py +0 -0
- /robosystems_client/api/{credits_ → graph_credits}/check_storage_limits.py +0 -0
- /robosystems_client/api/{credits_ → graph_credits}/get_credit_summary.py +0 -0
- /robosystems_client/api/{credits_ → graph_credits}/get_storage_usage.py +0 -0
- /robosystems_client/api/{credits_ → graph_credits}/list_credit_transactions.py +0 -0
- {robosystems_client-0.1.12.dist-info → robosystems_client-0.1.14.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,469 @@
|
|
|
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.sse_client: Optional[SSEClient] = None
|
|
78
|
+
|
|
79
|
+
# Get client authentication if provided
|
|
80
|
+
self.auth_token = config.get("auth_token")
|
|
81
|
+
self.api_key = config.get("api_key")
|
|
82
|
+
|
|
83
|
+
def copy_from_s3(
|
|
84
|
+
self, graph_id: str, request: S3CopyRequest, options: Optional[CopyOptions] = None
|
|
85
|
+
) -> CopyResult:
|
|
86
|
+
"""Copy data from S3 to graph database"""
|
|
87
|
+
return self._execute_copy(graph_id, request, CopySourceType.S3, options)
|
|
88
|
+
|
|
89
|
+
def copy_from_url(
|
|
90
|
+
self, graph_id: str, request: URLCopyRequest, options: Optional[CopyOptions] = None
|
|
91
|
+
) -> CopyResult:
|
|
92
|
+
"""Copy data from URL to graph database (when available)"""
|
|
93
|
+
return self._execute_copy(graph_id, request, CopySourceType.URL, options)
|
|
94
|
+
|
|
95
|
+
def copy_from_dataframe(
|
|
96
|
+
self,
|
|
97
|
+
graph_id: str,
|
|
98
|
+
request: DataFrameCopyRequest,
|
|
99
|
+
options: Optional[CopyOptions] = None,
|
|
100
|
+
) -> CopyResult:
|
|
101
|
+
"""Copy data from DataFrame to graph database (when available)"""
|
|
102
|
+
return self._execute_copy(graph_id, request, CopySourceType.DATAFRAME, options)
|
|
103
|
+
|
|
104
|
+
def _execute_copy(
|
|
105
|
+
self,
|
|
106
|
+
graph_id: str,
|
|
107
|
+
request: Union[S3CopyRequest, URLCopyRequest, DataFrameCopyRequest],
|
|
108
|
+
source_type: CopySourceType,
|
|
109
|
+
options: Optional[CopyOptions] = None,
|
|
110
|
+
) -> CopyResult:
|
|
111
|
+
"""Execute copy operation with automatic SSE monitoring for long-running operations"""
|
|
112
|
+
if options is None:
|
|
113
|
+
options = CopyOptions()
|
|
114
|
+
|
|
115
|
+
start_time = time.time()
|
|
116
|
+
|
|
117
|
+
# Import client here to avoid circular imports
|
|
118
|
+
from ..client import AuthenticatedClient
|
|
119
|
+
|
|
120
|
+
# Create authenticated client
|
|
121
|
+
client = AuthenticatedClient(
|
|
122
|
+
base_url=self.base_url,
|
|
123
|
+
token=self.auth_token,
|
|
124
|
+
headers={"X-API-Key": self.api_key} if self.api_key else None,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Execute the copy request
|
|
129
|
+
response = copy_data_to_graph(graph_id=graph_id, client=client, body=request)
|
|
130
|
+
|
|
131
|
+
if response.parsed:
|
|
132
|
+
response_data: CopyResponse = response.parsed
|
|
133
|
+
|
|
134
|
+
# Check if this is an accepted (async) operation
|
|
135
|
+
if (
|
|
136
|
+
response_data.status == CopyResponseStatus.ACCEPTED
|
|
137
|
+
and response_data.operation_id
|
|
138
|
+
):
|
|
139
|
+
# This is a long-running operation with SSE monitoring
|
|
140
|
+
if options.on_progress:
|
|
141
|
+
options.on_progress("Copy operation started. Monitoring progress...", None)
|
|
142
|
+
|
|
143
|
+
# If SSE URL is provided, use it for monitoring
|
|
144
|
+
if response_data.sse_url:
|
|
145
|
+
return self._monitor_copy_operation(
|
|
146
|
+
response_data.operation_id, options, start_time
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Otherwise return the accepted response
|
|
150
|
+
return CopyResult(
|
|
151
|
+
status="accepted",
|
|
152
|
+
operation_id=response_data.operation_id,
|
|
153
|
+
sse_url=response_data.sse_url,
|
|
154
|
+
message=response_data.message,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# This is a synchronous response - operation completed immediately
|
|
158
|
+
return self._build_copy_result(response_data, time.time() - start_time)
|
|
159
|
+
else:
|
|
160
|
+
return CopyResult(
|
|
161
|
+
status="failed",
|
|
162
|
+
error="No response data received",
|
|
163
|
+
execution_time_ms=(time.time() - start_time) * 1000,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return CopyResult(
|
|
168
|
+
status="failed",
|
|
169
|
+
error=str(e),
|
|
170
|
+
execution_time_ms=(time.time() - start_time) * 1000,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _monitor_copy_operation(
|
|
174
|
+
self, operation_id: str, options: CopyOptions, start_time: float
|
|
175
|
+
) -> CopyResult:
|
|
176
|
+
"""Monitor a copy operation using SSE"""
|
|
177
|
+
timeout_ms = options.timeout or 3600000 # Default 1 hour for copy operations
|
|
178
|
+
timeout_time = time.time() + (timeout_ms / 1000)
|
|
179
|
+
|
|
180
|
+
result = CopyResult(status="failed")
|
|
181
|
+
warnings: List[str] = []
|
|
182
|
+
|
|
183
|
+
# Set up SSE connection
|
|
184
|
+
sse_config = SSEConfig(base_url=self.base_url, timeout=timeout_ms // 1000)
|
|
185
|
+
sse_client = SSEClient(sse_config)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
sse_client.connect(operation_id)
|
|
189
|
+
|
|
190
|
+
# Set up event handlers
|
|
191
|
+
def on_queue_update(data):
|
|
192
|
+
if options.on_queue_update:
|
|
193
|
+
position = data.get("position", data.get("queue_position", 0))
|
|
194
|
+
wait_time = data.get("estimated_wait_seconds", 0)
|
|
195
|
+
options.on_queue_update(position, wait_time)
|
|
196
|
+
|
|
197
|
+
def on_progress(data):
|
|
198
|
+
if options.on_progress:
|
|
199
|
+
message = data.get("message", data.get("status", "Processing..."))
|
|
200
|
+
progress_percent = data.get("progress_percent", data.get("progress"))
|
|
201
|
+
options.on_progress(message, progress_percent)
|
|
202
|
+
|
|
203
|
+
# Check for warnings in progress updates
|
|
204
|
+
if "warnings" in data and data["warnings"]:
|
|
205
|
+
warnings.extend(data["warnings"])
|
|
206
|
+
if options.on_warning:
|
|
207
|
+
for warning in data["warnings"]:
|
|
208
|
+
options.on_warning(warning)
|
|
209
|
+
|
|
210
|
+
def on_completed(data):
|
|
211
|
+
nonlocal result
|
|
212
|
+
completion_data = data.get("result", data)
|
|
213
|
+
result = CopyResult(
|
|
214
|
+
status=completion_data.get("status", "completed"),
|
|
215
|
+
rows_imported=completion_data.get("rows_imported"),
|
|
216
|
+
rows_skipped=completion_data.get("rows_skipped"),
|
|
217
|
+
bytes_processed=completion_data.get("bytes_processed"),
|
|
218
|
+
execution_time_ms=(time.time() - start_time) * 1000,
|
|
219
|
+
warnings=warnings if warnings else completion_data.get("warnings"),
|
|
220
|
+
message=completion_data.get("message"),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def on_error(data):
|
|
224
|
+
nonlocal result
|
|
225
|
+
result = CopyResult(
|
|
226
|
+
status="failed",
|
|
227
|
+
error=data.get("message", data.get("error", "Copy operation failed")),
|
|
228
|
+
execution_time_ms=(time.time() - start_time) * 1000,
|
|
229
|
+
warnings=warnings if warnings else None,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def on_cancelled(data):
|
|
233
|
+
nonlocal result
|
|
234
|
+
result = CopyResult(
|
|
235
|
+
status="failed",
|
|
236
|
+
error="Copy operation cancelled",
|
|
237
|
+
execution_time_ms=(time.time() - start_time) * 1000,
|
|
238
|
+
warnings=warnings if warnings else None,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Register event handlers
|
|
242
|
+
sse_client.on(EventType.QUEUE_UPDATE.value, on_queue_update)
|
|
243
|
+
sse_client.on(EventType.OPERATION_PROGRESS.value, on_progress)
|
|
244
|
+
sse_client.on(EventType.OPERATION_COMPLETED.value, on_completed)
|
|
245
|
+
sse_client.on(EventType.OPERATION_ERROR.value, on_error)
|
|
246
|
+
sse_client.on(EventType.OPERATION_CANCELLED.value, on_cancelled)
|
|
247
|
+
|
|
248
|
+
# Listen for events until completion or timeout
|
|
249
|
+
while time.time() < timeout_time:
|
|
250
|
+
sse_client.listen(timeout=1) # Process events for 1 second
|
|
251
|
+
|
|
252
|
+
# Check if operation is complete
|
|
253
|
+
if result.status in ["completed", "failed", "partial"]:
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
if time.time() >= timeout_time:
|
|
257
|
+
result = CopyResult(
|
|
258
|
+
status="failed",
|
|
259
|
+
error=f"Copy operation timeout after {timeout_ms}ms",
|
|
260
|
+
execution_time_ms=(time.time() - start_time) * 1000,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
finally:
|
|
264
|
+
sse_client.close()
|
|
265
|
+
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
def _build_copy_result(
|
|
269
|
+
self, response_data: CopyResponse, execution_time: float
|
|
270
|
+
) -> CopyResult:
|
|
271
|
+
"""Build copy result from response data"""
|
|
272
|
+
return CopyResult(
|
|
273
|
+
status=response_data.status.value,
|
|
274
|
+
rows_imported=response_data.rows_imported,
|
|
275
|
+
rows_skipped=response_data.rows_skipped,
|
|
276
|
+
bytes_processed=response_data.bytes_processed,
|
|
277
|
+
execution_time_ms=response_data.execution_time_ms or (execution_time * 1000),
|
|
278
|
+
warnings=response_data.warnings,
|
|
279
|
+
message=response_data.message,
|
|
280
|
+
error=str(response_data.error_details) if response_data.error_details else None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def calculate_statistics(self, result: CopyResult) -> Optional[CopyStatistics]:
|
|
284
|
+
"""Calculate copy statistics from result"""
|
|
285
|
+
if result.status == "failed" or not result.rows_imported:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
total_rows = (result.rows_imported or 0) + (result.rows_skipped or 0)
|
|
289
|
+
duration = (result.execution_time_ms or 0) / 1000 # Convert to seconds
|
|
290
|
+
throughput = (result.rows_imported or 0) / duration if duration > 0 else 0
|
|
291
|
+
|
|
292
|
+
return CopyStatistics(
|
|
293
|
+
total_rows=total_rows,
|
|
294
|
+
imported_rows=result.rows_imported or 0,
|
|
295
|
+
skipped_rows=result.rows_skipped or 0,
|
|
296
|
+
bytes_processed=result.bytes_processed or 0,
|
|
297
|
+
duration=duration,
|
|
298
|
+
throughput=throughput,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def copy_s3(
|
|
302
|
+
self,
|
|
303
|
+
graph_id: str,
|
|
304
|
+
table_name: str,
|
|
305
|
+
s3_path: str,
|
|
306
|
+
access_key_id: str,
|
|
307
|
+
secret_access_key: str,
|
|
308
|
+
region: str = "us-east-1",
|
|
309
|
+
file_format: Optional[str] = None,
|
|
310
|
+
ignore_errors: bool = False,
|
|
311
|
+
) -> CopyResult:
|
|
312
|
+
"""Convenience method for simple S3 copy with default options"""
|
|
313
|
+
|
|
314
|
+
# Map string format to enum
|
|
315
|
+
format_enum = S3CopyRequestFileFormat.PARQUET
|
|
316
|
+
if file_format:
|
|
317
|
+
format_map = {
|
|
318
|
+
"csv": S3CopyRequestFileFormat.CSV,
|
|
319
|
+
"parquet": S3CopyRequestFileFormat.PARQUET,
|
|
320
|
+
"json": S3CopyRequestFileFormat.JSON,
|
|
321
|
+
"delta": S3CopyRequestFileFormat.DELTA,
|
|
322
|
+
"iceberg": S3CopyRequestFileFormat.ICEBERG,
|
|
323
|
+
}
|
|
324
|
+
format_enum = format_map.get(file_format.lower(), S3CopyRequestFileFormat.PARQUET)
|
|
325
|
+
|
|
326
|
+
request = S3CopyRequest(
|
|
327
|
+
table_name=table_name,
|
|
328
|
+
s3_path=s3_path,
|
|
329
|
+
s3_access_key_id=access_key_id,
|
|
330
|
+
s3_secret_access_key=secret_access_key,
|
|
331
|
+
s3_region=region,
|
|
332
|
+
file_format=format_enum,
|
|
333
|
+
ignore_errors=ignore_errors,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return self.copy_from_s3(graph_id, request)
|
|
337
|
+
|
|
338
|
+
def monitor_multiple_copies(
|
|
339
|
+
self, operation_ids: List[str], options: Optional[CopyOptions] = None
|
|
340
|
+
) -> Dict[str, CopyResult]:
|
|
341
|
+
"""Monitor multiple copy operations concurrently"""
|
|
342
|
+
results = {}
|
|
343
|
+
for operation_id in operation_ids:
|
|
344
|
+
result = self._monitor_copy_operation(
|
|
345
|
+
operation_id, options or CopyOptions(), time.time()
|
|
346
|
+
)
|
|
347
|
+
results[operation_id] = result
|
|
348
|
+
return results
|
|
349
|
+
|
|
350
|
+
def batch_copy_from_s3(
|
|
351
|
+
self, graph_id: str, copies: List[Dict[str, Any]]
|
|
352
|
+
) -> List[CopyResult]:
|
|
353
|
+
"""Batch copy multiple tables from S3"""
|
|
354
|
+
results = []
|
|
355
|
+
for copy_config in copies:
|
|
356
|
+
request = copy_config.get("request")
|
|
357
|
+
options = copy_config.get("options")
|
|
358
|
+
if request:
|
|
359
|
+
result = self.copy_from_s3(graph_id, request, options)
|
|
360
|
+
results.append(result)
|
|
361
|
+
return results
|
|
362
|
+
|
|
363
|
+
def copy_with_retry(
|
|
364
|
+
self,
|
|
365
|
+
graph_id: str,
|
|
366
|
+
request: Union[S3CopyRequest, URLCopyRequest, DataFrameCopyRequest],
|
|
367
|
+
source_type: CopySourceType,
|
|
368
|
+
max_retries: int = 3,
|
|
369
|
+
options: Optional[CopyOptions] = None,
|
|
370
|
+
) -> CopyResult:
|
|
371
|
+
"""Copy with retry logic for transient failures"""
|
|
372
|
+
if options is None:
|
|
373
|
+
options = CopyOptions()
|
|
374
|
+
|
|
375
|
+
last_error: Optional[Exception] = None
|
|
376
|
+
attempt = 0
|
|
377
|
+
|
|
378
|
+
while attempt < max_retries:
|
|
379
|
+
attempt += 1
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
result = self._execute_copy(graph_id, request, source_type, options)
|
|
383
|
+
|
|
384
|
+
# If successful or partially successful, return
|
|
385
|
+
if result.status in ["completed", "partial"]:
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
# If failed, check if it's retryable
|
|
389
|
+
if result.status == "failed":
|
|
390
|
+
is_retryable = self._is_retryable_error(result.error)
|
|
391
|
+
if not is_retryable or attempt == max_retries:
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
# Wait before retry with exponential backoff
|
|
395
|
+
wait_time = min(1000 * (2 ** (attempt - 1)), 30000) / 1000
|
|
396
|
+
if options.on_progress:
|
|
397
|
+
options.on_progress(
|
|
398
|
+
f"Retrying copy operation (attempt {attempt}/{max_retries}) in {wait_time}s...",
|
|
399
|
+
None,
|
|
400
|
+
)
|
|
401
|
+
time.sleep(wait_time)
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
last_error = e
|
|
405
|
+
|
|
406
|
+
if attempt == max_retries:
|
|
407
|
+
raise last_error
|
|
408
|
+
|
|
409
|
+
# Wait before retry
|
|
410
|
+
wait_time = min(1000 * (2 ** (attempt - 1)), 30000) / 1000
|
|
411
|
+
if options.on_progress:
|
|
412
|
+
options.on_progress(
|
|
413
|
+
f"Retrying after error (attempt {attempt}/{max_retries}) in {wait_time}s...",
|
|
414
|
+
None,
|
|
415
|
+
)
|
|
416
|
+
time.sleep(wait_time)
|
|
417
|
+
|
|
418
|
+
raise last_error or Exception("Copy operation failed after all retries")
|
|
419
|
+
|
|
420
|
+
def _is_retryable_error(self, error: Optional[str]) -> bool:
|
|
421
|
+
"""Check if an error is retryable"""
|
|
422
|
+
if not error:
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
retryable_patterns = [
|
|
426
|
+
"timeout",
|
|
427
|
+
"network",
|
|
428
|
+
"connection",
|
|
429
|
+
"temporary",
|
|
430
|
+
"unavailable",
|
|
431
|
+
"rate limit",
|
|
432
|
+
"throttl",
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
lower_error = error.lower()
|
|
436
|
+
return any(pattern in lower_error for pattern in retryable_patterns)
|
|
437
|
+
|
|
438
|
+
def close(self):
|
|
439
|
+
"""Cancel any active SSE connections"""
|
|
440
|
+
if self.sse_client:
|
|
441
|
+
self.sse_client.close()
|
|
442
|
+
self.sse_client = None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class AsyncCopyClient:
|
|
446
|
+
"""Async version of CopyClient for async/await usage"""
|
|
447
|
+
|
|
448
|
+
def __init__(self, config: Dict[str, Any]):
|
|
449
|
+
self.config = config
|
|
450
|
+
self.base_url = config["base_url"]
|
|
451
|
+
self.sse_client: Optional[AsyncSSEClient] = None
|
|
452
|
+
|
|
453
|
+
# Get client authentication if provided
|
|
454
|
+
self.auth_token = config.get("auth_token")
|
|
455
|
+
self.api_key = config.get("api_key")
|
|
456
|
+
|
|
457
|
+
async def copy_from_s3(
|
|
458
|
+
self, graph_id: str, request: S3CopyRequest, options: Optional[CopyOptions] = None
|
|
459
|
+
) -> CopyResult:
|
|
460
|
+
"""Copy data from S3 to graph database asynchronously"""
|
|
461
|
+
# Async implementation would go here
|
|
462
|
+
# For now, this is a placeholder
|
|
463
|
+
raise NotImplementedError("Async copy client not yet implemented")
|
|
464
|
+
|
|
465
|
+
async def close(self):
|
|
466
|
+
"""Close any active connections"""
|
|
467
|
+
if self.sse_client:
|
|
468
|
+
await self.sse_client.close()
|
|
469
|
+
self.sse_client = None
|
|
@@ -8,6 +8,7 @@ from typing import Dict, Any, Optional, Callable
|
|
|
8
8
|
|
|
9
9
|
from .query_client import QueryClient
|
|
10
10
|
from .operation_client import OperationClient
|
|
11
|
+
from .copy_client import CopyClient
|
|
11
12
|
from .sse_client import SSEClient
|
|
12
13
|
|
|
13
14
|
|
|
@@ -39,6 +40,7 @@ class RoboSystemsExtensions:
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
# Initialize clients
|
|
43
|
+
self.copy = CopyClient(self.config)
|
|
42
44
|
self.query = QueryClient(self.config)
|
|
43
45
|
self.operations = OperationClient(self.config)
|
|
44
46
|
|
|
@@ -67,6 +69,7 @@ class RoboSystemsExtensions:
|
|
|
67
69
|
|
|
68
70
|
def close(self):
|
|
69
71
|
"""Clean up all active connections"""
|
|
72
|
+
self.copy.close()
|
|
70
73
|
self.query.close()
|
|
71
74
|
self.operations.close_all()
|
|
72
75
|
|
|
@@ -93,6 +96,20 @@ class RoboSystemsExtensions:
|
|
|
93
96
|
"""Cancel an operation using the operation client"""
|
|
94
97
|
return self.operations.cancel_operation(operation_id)
|
|
95
98
|
|
|
99
|
+
def copy_from_s3(
|
|
100
|
+
self,
|
|
101
|
+
graph_id: str,
|
|
102
|
+
table_name: str,
|
|
103
|
+
s3_path: str,
|
|
104
|
+
access_key_id: str,
|
|
105
|
+
secret_access_key: str,
|
|
106
|
+
**kwargs,
|
|
107
|
+
):
|
|
108
|
+
"""Copy data from S3 using the copy client"""
|
|
109
|
+
return self.copy.copy_s3(
|
|
110
|
+
graph_id, table_name, s3_path, access_key_id, secret_access_key, **kwargs
|
|
111
|
+
)
|
|
112
|
+
|
|
96
113
|
|
|
97
114
|
class AsyncRoboSystemsExtensions:
|
|
98
115
|
"""Async version of the extensions class"""
|
|
@@ -14,7 +14,6 @@ from .auth_response_user import AuthResponseUser
|
|
|
14
14
|
from .available_extension import AvailableExtension
|
|
15
15
|
from .available_extensions_response import AvailableExtensionsResponse
|
|
16
16
|
from .backup_create_request import BackupCreateRequest
|
|
17
|
-
from .backup_export_request import BackupExportRequest
|
|
18
17
|
from .backup_list_response import BackupListResponse
|
|
19
18
|
from .backup_response import BackupResponse
|
|
20
19
|
from .backup_restore_request import BackupRestoreRequest
|
|
@@ -34,6 +33,9 @@ from .connection_provider_info_provider import ConnectionProviderInfoProvider
|
|
|
34
33
|
from .connection_response import ConnectionResponse
|
|
35
34
|
from .connection_response_metadata import ConnectionResponseMetadata
|
|
36
35
|
from .connection_response_provider import ConnectionResponseProvider
|
|
36
|
+
from .copy_response import CopyResponse
|
|
37
|
+
from .copy_response_error_details_type_0 import CopyResponseErrorDetailsType0
|
|
38
|
+
from .copy_response_status import CopyResponseStatus
|
|
37
39
|
from .create_api_key_request import CreateAPIKeyRequest
|
|
38
40
|
from .create_api_key_response import CreateAPIKeyResponse
|
|
39
41
|
from .create_connection_request import CreateConnectionRequest
|
|
@@ -41,7 +43,6 @@ from .create_connection_request_provider import CreateConnectionRequestProvider
|
|
|
41
43
|
from .create_graph_request import CreateGraphRequest
|
|
42
44
|
from .create_subgraph_request import CreateSubgraphRequest
|
|
43
45
|
from .create_subgraph_request_metadata_type_0 import CreateSubgraphRequestMetadataType0
|
|
44
|
-
from .credit_check_request import CreditCheckRequest
|
|
45
46
|
from .credit_summary import CreditSummary
|
|
46
47
|
from .credit_summary_response import CreditSummaryResponse
|
|
47
48
|
from .credits_summary_response import CreditsSummaryResponse
|
|
@@ -56,6 +57,8 @@ from .custom_schema_definition_relationships_item import (
|
|
|
56
57
|
)
|
|
57
58
|
from .cypher_query_request import CypherQueryRequest
|
|
58
59
|
from .cypher_query_request_parameters_type_0 import CypherQueryRequestParametersType0
|
|
60
|
+
from .data_frame_copy_request import DataFrameCopyRequest
|
|
61
|
+
from .data_frame_copy_request_format import DataFrameCopyRequestFormat
|
|
59
62
|
from .database_health_response import DatabaseHealthResponse
|
|
60
63
|
from .database_info_response import DatabaseInfoResponse
|
|
61
64
|
from .delete_subgraph_request import DeleteSubgraphRequest
|
|
@@ -90,6 +93,9 @@ from .get_current_graph_bill_response_getcurrentgraphbill import (
|
|
|
90
93
|
from .get_graph_billing_history_response_getgraphbillinghistory import (
|
|
91
94
|
GetGraphBillingHistoryResponseGetgraphbillinghistory,
|
|
92
95
|
)
|
|
96
|
+
from .get_graph_limits_response_getgraphlimits import (
|
|
97
|
+
GetGraphLimitsResponseGetgraphlimits,
|
|
98
|
+
)
|
|
93
99
|
from .get_graph_monthly_bill_response_getgraphmonthlybill import (
|
|
94
100
|
GetGraphMonthlyBillResponseGetgraphmonthlybill,
|
|
95
101
|
)
|
|
@@ -125,9 +131,6 @@ from .health_status import HealthStatus
|
|
|
125
131
|
from .health_status_details_type_0 import HealthStatusDetailsType0
|
|
126
132
|
from .http_validation_error import HTTPValidationError
|
|
127
133
|
from .initial_entity_data import InitialEntityData
|
|
128
|
-
from .kuzu_backup_health_response_kuzubackuphealth import (
|
|
129
|
-
KuzuBackupHealthResponseKuzubackuphealth,
|
|
130
|
-
)
|
|
131
134
|
from .link_token_request import LinkTokenRequest
|
|
132
135
|
from .link_token_request_options_type_0 import LinkTokenRequestOptionsType0
|
|
133
136
|
from .link_token_request_provider_type_0 import LinkTokenRequestProviderType0
|
|
@@ -166,6 +169,9 @@ from .repository_credits_response import RepositoryCreditsResponse
|
|
|
166
169
|
from .repository_plan import RepositoryPlan
|
|
167
170
|
from .repository_type import RepositoryType
|
|
168
171
|
from .response_mode import ResponseMode
|
|
172
|
+
from .s3_copy_request import S3CopyRequest
|
|
173
|
+
from .s3_copy_request_file_format import S3CopyRequestFileFormat
|
|
174
|
+
from .s3_copy_request_s3_url_style_type_0 import S3CopyRequestS3UrlStyleType0
|
|
169
175
|
from .schema_export_response import SchemaExportResponse
|
|
170
176
|
from .schema_export_response_data_stats_type_0 import SchemaExportResponseDataStatsType0
|
|
171
177
|
from .schema_export_response_schema_definition_type_0 import (
|
|
@@ -210,7 +216,9 @@ from .transaction_summary_response import TransactionSummaryResponse
|
|
|
210
216
|
from .update_api_key_request import UpdateAPIKeyRequest
|
|
211
217
|
from .update_password_request import UpdatePasswordRequest
|
|
212
218
|
from .update_user_request import UpdateUserRequest
|
|
213
|
-
from .
|
|
219
|
+
from .url_copy_request import URLCopyRequest
|
|
220
|
+
from .url_copy_request_file_format import URLCopyRequestFileFormat
|
|
221
|
+
from .url_copy_request_headers_type_0 import URLCopyRequestHeadersType0
|
|
214
222
|
from .user_analytics_response import UserAnalyticsResponse
|
|
215
223
|
from .user_analytics_response_api_usage import UserAnalyticsResponseApiUsage
|
|
216
224
|
from .user_analytics_response_graph_usage import UserAnalyticsResponseGraphUsage
|
|
@@ -247,7 +255,6 @@ __all__ = (
|
|
|
247
255
|
"AvailableExtension",
|
|
248
256
|
"AvailableExtensionsResponse",
|
|
249
257
|
"BackupCreateRequest",
|
|
250
|
-
"BackupExportRequest",
|
|
251
258
|
"BackupListResponse",
|
|
252
259
|
"BackupResponse",
|
|
253
260
|
"BackupRestoreRequest",
|
|
@@ -263,6 +270,9 @@ __all__ = (
|
|
|
263
270
|
"ConnectionResponse",
|
|
264
271
|
"ConnectionResponseMetadata",
|
|
265
272
|
"ConnectionResponseProvider",
|
|
273
|
+
"CopyResponse",
|
|
274
|
+
"CopyResponseErrorDetailsType0",
|
|
275
|
+
"CopyResponseStatus",
|
|
266
276
|
"CreateAPIKeyRequest",
|
|
267
277
|
"CreateAPIKeyResponse",
|
|
268
278
|
"CreateConnectionRequest",
|
|
@@ -270,7 +280,6 @@ __all__ = (
|
|
|
270
280
|
"CreateGraphRequest",
|
|
271
281
|
"CreateSubgraphRequest",
|
|
272
282
|
"CreateSubgraphRequestMetadataType0",
|
|
273
|
-
"CreditCheckRequest",
|
|
274
283
|
"CreditsSummaryResponse",
|
|
275
284
|
"CreditsSummaryResponseCreditsByAddonItem",
|
|
276
285
|
"CreditSummary",
|
|
@@ -283,6 +292,8 @@ __all__ = (
|
|
|
283
292
|
"CypherQueryRequestParametersType0",
|
|
284
293
|
"DatabaseHealthResponse",
|
|
285
294
|
"DatabaseInfoResponse",
|
|
295
|
+
"DataFrameCopyRequest",
|
|
296
|
+
"DataFrameCopyRequestFormat",
|
|
286
297
|
"DeleteSubgraphRequest",
|
|
287
298
|
"DeleteSubgraphResponse",
|
|
288
299
|
"DetailedTransactionsResponse",
|
|
@@ -299,6 +310,7 @@ __all__ = (
|
|
|
299
310
|
"GetCurrentAuthUserResponseGetcurrentauthuser",
|
|
300
311
|
"GetCurrentGraphBillResponseGetcurrentgraphbill",
|
|
301
312
|
"GetGraphBillingHistoryResponseGetgraphbillinghistory",
|
|
313
|
+
"GetGraphLimitsResponseGetgraphlimits",
|
|
302
314
|
"GetGraphMonthlyBillResponseGetgraphmonthlybill",
|
|
303
315
|
"GetGraphSchemaInfoResponseGetgraphschemainfo",
|
|
304
316
|
"GetGraphUsageDetailsResponseGetgraphusagedetails",
|
|
@@ -320,7 +332,6 @@ __all__ = (
|
|
|
320
332
|
"HealthStatusDetailsType0",
|
|
321
333
|
"HTTPValidationError",
|
|
322
334
|
"InitialEntityData",
|
|
323
|
-
"KuzuBackupHealthResponseKuzubackuphealth",
|
|
324
335
|
"LinkTokenRequest",
|
|
325
336
|
"LinkTokenRequestOptionsType0",
|
|
326
337
|
"LinkTokenRequestProviderType0",
|
|
@@ -351,6 +362,9 @@ __all__ = (
|
|
|
351
362
|
"RepositoryPlan",
|
|
352
363
|
"RepositoryType",
|
|
353
364
|
"ResponseMode",
|
|
365
|
+
"S3CopyRequest",
|
|
366
|
+
"S3CopyRequestFileFormat",
|
|
367
|
+
"S3CopyRequestS3UrlStyleType0",
|
|
354
368
|
"SchemaExportResponse",
|
|
355
369
|
"SchemaExportResponseDataStatsType0",
|
|
356
370
|
"SchemaExportResponseSchemaDefinitionType0",
|
|
@@ -385,7 +399,9 @@ __all__ = (
|
|
|
385
399
|
"UpdateAPIKeyRequest",
|
|
386
400
|
"UpdatePasswordRequest",
|
|
387
401
|
"UpdateUserRequest",
|
|
388
|
-
"
|
|
402
|
+
"URLCopyRequest",
|
|
403
|
+
"URLCopyRequestFileFormat",
|
|
404
|
+
"URLCopyRequestHeadersType0",
|
|
389
405
|
"UserAnalyticsResponse",
|
|
390
406
|
"UserAnalyticsResponseApiUsage",
|
|
391
407
|
"UserAnalyticsResponseGraphUsage",
|