robosystems-client 0.2.16__py3-none-any.whl → 0.2.18__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.

@@ -28,12 +28,22 @@ from .operation_client import (
28
28
  OperationProgress,
29
29
  OperationResult,
30
30
  )
31
- from .table_ingest_client import (
32
- TableIngestClient,
33
- UploadOptions,
34
- IngestOptions,
35
- UploadResult,
31
+ from .file_client import (
32
+ FileClient,
33
+ FileUploadOptions,
34
+ FileUploadResult,
35
+ FileInfo,
36
+ )
37
+ from .materialization_client import (
38
+ MaterializationClient,
39
+ MaterializationOptions,
40
+ MaterializationResult,
41
+ MaterializationStatus,
42
+ )
43
+ from .table_client import (
44
+ TableClient,
36
45
  TableInfo,
46
+ QueryResult as TableQueryResult,
37
47
  )
38
48
  from .graph_client import (
39
49
  GraphClient,
@@ -177,12 +187,20 @@ __all__ = [
177
187
  "OperationStatus",
178
188
  "OperationProgress",
179
189
  "OperationResult",
180
- # Table Ingest Client
181
- "TableIngestClient",
182
- "UploadOptions",
183
- "IngestOptions",
184
- "UploadResult",
190
+ # File Client
191
+ "FileClient",
192
+ "FileUploadOptions",
193
+ "FileUploadResult",
194
+ "FileInfo",
195
+ # Materialization Client
196
+ "MaterializationClient",
197
+ "MaterializationOptions",
198
+ "MaterializationResult",
199
+ "MaterializationStatus",
200
+ # Table Client
201
+ "TableClient",
185
202
  "TableInfo",
203
+ "TableQueryResult",
186
204
  # Graph Client
187
205
  "GraphClient",
188
206
  "GraphMetadata",
@@ -9,7 +9,9 @@ from typing import Dict, Any, Optional, Callable
9
9
  from .query_client import QueryClient
10
10
  from .agent_client import AgentClient
11
11
  from .operation_client import OperationClient
12
- from .table_ingest_client import TableIngestClient
12
+ from .file_client import FileClient
13
+ from .materialization_client import MaterializationClient
14
+ from .table_client import TableClient
13
15
  from .graph_client import GraphClient
14
16
  from .sse_client import SSEClient
15
17
 
@@ -61,7 +63,9 @@ class RoboSystemsExtensions:
61
63
  self.query = QueryClient(self.config)
62
64
  self.agent = AgentClient(self.config)
63
65
  self.operations = OperationClient(self.config)
64
- self.tables = TableIngestClient(self.config)
66
+ self.files = FileClient(self.config)
67
+ self.materialization = MaterializationClient(self.config)
68
+ self.tables = TableClient(self.config)
65
69
  self.graphs = GraphClient(self.config)
66
70
 
67
71
  def monitor_operation(
@@ -92,7 +96,12 @@ class RoboSystemsExtensions:
92
96
  self.query.close()
93
97
  self.agent.close()
94
98
  self.operations.close_all()
95
- self.tables.close()
99
+ if hasattr(self.files, "close"):
100
+ self.files.close()
101
+ if hasattr(self.materialization, "close"):
102
+ self.materialization.close()
103
+ if hasattr(self.tables, "close"):
104
+ self.tables.close()
96
105
  self.graphs.close()
97
106
 
98
107
  # Convenience methods that delegate to the appropriate clients
@@ -0,0 +1,432 @@
1
+ """File Client for RoboSystems API
2
+
3
+ Manages file operations as first-class resources with multi-layer status tracking.
4
+ Files are independent entities with their own lifecycle (S3 → DuckDB → Graph).
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from io import BytesIO
9
+ from pathlib import Path
10
+ from typing import Dict, Any, Optional, Callable, Union, BinaryIO
11
+ import logging
12
+ import httpx
13
+
14
+ from ..api.files.create_file_upload import (
15
+ sync_detailed as create_file_upload,
16
+ )
17
+ from ..api.files.update_file import (
18
+ sync_detailed as update_file,
19
+ )
20
+ from ..api.files.list_files import (
21
+ sync_detailed as list_files,
22
+ )
23
+ from ..api.files.get_file import (
24
+ sync_detailed as get_file,
25
+ )
26
+ from ..api.files.delete_file import (
27
+ sync_detailed as delete_file,
28
+ )
29
+ from ..models.file_upload_request import FileUploadRequest
30
+ from ..models.file_status_update import FileStatusUpdate
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ @dataclass
36
+ class FileUploadOptions:
37
+ """Options for file upload operations"""
38
+
39
+ on_progress: Optional[Callable[[str], None]] = None
40
+ fix_localstack_url: bool = True
41
+ ingest_to_graph: bool = False
42
+
43
+
44
+ @dataclass
45
+ class FileUploadResult:
46
+ """Result from file upload operation"""
47
+
48
+ file_id: str
49
+ file_size: int
50
+ row_count: int
51
+ table_name: str
52
+ file_name: str
53
+ success: bool = True
54
+ error: Optional[str] = None
55
+
56
+
57
+ @dataclass
58
+ class FileInfo:
59
+ """Information about a file"""
60
+
61
+ file_id: str
62
+ file_name: str
63
+ file_format: str
64
+ size_bytes: int
65
+ row_count: Optional[int]
66
+ upload_status: str
67
+ table_name: str
68
+ created_at: Optional[str]
69
+ uploaded_at: Optional[str]
70
+ layers: Optional[Dict[str, Any]] = None
71
+
72
+
73
+ class FileClient:
74
+ """Client for managing files as first-class resources"""
75
+
76
+ def __init__(self, config: Dict[str, Any]):
77
+ self.config = config
78
+ self.base_url = config["base_url"]
79
+ self.headers = config.get("headers", {})
80
+ self.token = config.get("token")
81
+ self._http_client = httpx.Client(timeout=120.0)
82
+
83
+ def upload(
84
+ self,
85
+ graph_id: str,
86
+ table_name: str,
87
+ file_or_buffer: Union[Path, str, BytesIO, BinaryIO],
88
+ options: Optional[FileUploadOptions] = None,
89
+ ) -> FileUploadResult:
90
+ """
91
+ Upload a file to a table.
92
+
93
+ This handles the complete 3-step upload process:
94
+ 1. Get presigned upload URL
95
+ 2. Upload file to S3
96
+ 3. Mark file as 'uploaded' (triggers DuckDB staging)
97
+
98
+ Args:
99
+ graph_id: Graph database identifier
100
+ table_name: Table to associate file with
101
+ file_or_buffer: File path, Path object, BytesIO, or file-like object
102
+ options: Upload options (progress callback, LocalStack URL fix, auto-ingest)
103
+
104
+ Returns:
105
+ FileUploadResult with file metadata and status
106
+ """
107
+ options = options or FileUploadOptions()
108
+
109
+ try:
110
+ # Determine file name and read content
111
+ if isinstance(file_or_buffer, (str, Path)):
112
+ file_path = Path(file_or_buffer)
113
+ file_name = file_path.name
114
+ with open(file_path, "rb") as f:
115
+ file_content = f.read()
116
+ elif isinstance(file_or_buffer, BytesIO):
117
+ file_name = "data.parquet"
118
+ file_content = file_or_buffer.getvalue()
119
+ elif hasattr(file_or_buffer, "read"):
120
+ file_name = getattr(file_or_buffer, "name", "data.parquet")
121
+ file_content = file_or_buffer.read()
122
+ else:
123
+ raise ValueError(f"Unsupported file type: {type(file_or_buffer)}")
124
+
125
+ # Step 1: Get presigned upload URL
126
+ if options.on_progress:
127
+ options.on_progress(
128
+ f"Getting upload URL for {file_name} → table '{table_name}'..."
129
+ )
130
+
131
+ upload_request = FileUploadRequest(
132
+ file_name=file_name,
133
+ content_type="application/x-parquet",
134
+ table_name=table_name,
135
+ )
136
+
137
+ from ..client import AuthenticatedClient
138
+
139
+ if not self.token:
140
+ raise Exception("No API key provided. Set X-API-Key in headers.")
141
+
142
+ client = AuthenticatedClient(
143
+ base_url=self.base_url,
144
+ token=self.token,
145
+ prefix="",
146
+ auth_header_name="X-API-Key",
147
+ headers=self.headers,
148
+ )
149
+
150
+ kwargs = {
151
+ "graph_id": graph_id,
152
+ "client": client,
153
+ "body": upload_request,
154
+ }
155
+
156
+ response = create_file_upload(**kwargs)
157
+
158
+ if response.status_code != 200 or not response.parsed:
159
+ error_msg = f"Failed to get upload URL: {response.status_code}"
160
+ return FileUploadResult(
161
+ file_id="",
162
+ file_size=0,
163
+ row_count=0,
164
+ table_name=table_name,
165
+ file_name=file_name,
166
+ success=False,
167
+ error=error_msg,
168
+ )
169
+
170
+ upload_data = response.parsed
171
+ upload_url = upload_data.upload_url
172
+ file_id = upload_data.file_id
173
+
174
+ # Fix LocalStack URL if needed
175
+ if options.fix_localstack_url and "localstack:4566" in upload_url:
176
+ upload_url = upload_url.replace("localstack:4566", "localhost:4566")
177
+
178
+ # Step 2: Upload file to S3
179
+ if options.on_progress:
180
+ options.on_progress(f"Uploading {file_name} to S3...")
181
+
182
+ s3_response = self._http_client.put(
183
+ upload_url,
184
+ content=file_content,
185
+ headers={"Content-Type": "application/x-parquet"},
186
+ )
187
+
188
+ if s3_response.status_code not in [200, 204]:
189
+ return FileUploadResult(
190
+ file_id=file_id,
191
+ file_size=len(file_content),
192
+ row_count=0,
193
+ table_name=table_name,
194
+ file_name=file_name,
195
+ success=False,
196
+ error=f"S3 upload failed: {s3_response.status_code}",
197
+ )
198
+
199
+ # Step 3: Mark file as uploaded
200
+ if options.on_progress:
201
+ options.on_progress(f"Marking {file_name} as uploaded...")
202
+
203
+ status_update = FileStatusUpdate(
204
+ status="uploaded",
205
+ ingest_to_graph=options.ingest_to_graph,
206
+ )
207
+
208
+ update_kwargs = {
209
+ "graph_id": graph_id,
210
+ "file_id": file_id,
211
+ "client": client,
212
+ "body": status_update,
213
+ }
214
+
215
+ update_response = update_file(**update_kwargs)
216
+
217
+ if update_response.status_code != 200 or not update_response.parsed:
218
+ return FileUploadResult(
219
+ file_id=file_id,
220
+ file_size=len(file_content),
221
+ row_count=0,
222
+ table_name=table_name,
223
+ file_name=file_name,
224
+ success=False,
225
+ error="Failed to complete file upload",
226
+ )
227
+
228
+ # Extract metadata from response
229
+ response_data = update_response.parsed
230
+ actual_file_size = getattr(response_data, "file_size_bytes", len(file_content))
231
+ actual_row_count = getattr(response_data, "row_count", 0)
232
+
233
+ if options.on_progress:
234
+ options.on_progress(
235
+ f"✅ Uploaded {file_name} ({actual_file_size:,} bytes, {actual_row_count:,} rows)"
236
+ )
237
+
238
+ return FileUploadResult(
239
+ file_id=file_id,
240
+ file_size=actual_file_size,
241
+ row_count=actual_row_count,
242
+ table_name=table_name,
243
+ file_name=file_name,
244
+ success=True,
245
+ )
246
+
247
+ except Exception as e:
248
+ logger.error(f"File upload failed: {e}")
249
+ return FileUploadResult(
250
+ file_id="",
251
+ file_size=0,
252
+ row_count=0,
253
+ table_name=table_name,
254
+ file_name=getattr(file_or_buffer, "name", "unknown"),
255
+ success=False,
256
+ error=str(e),
257
+ )
258
+
259
+ def list(
260
+ self,
261
+ graph_id: str,
262
+ table_name: Optional[str] = None,
263
+ status: Optional[str] = None,
264
+ ) -> list[FileInfo]:
265
+ """
266
+ List files in a graph with optional filtering.
267
+
268
+ Args:
269
+ graph_id: Graph database identifier
270
+ table_name: Optional table name filter
271
+ status: Optional upload status filter (uploaded, pending, etc.)
272
+
273
+ Returns:
274
+ List of FileInfo objects
275
+ """
276
+ try:
277
+ from ..client import AuthenticatedClient
278
+
279
+ if not self.token:
280
+ raise Exception("No API key provided. Set X-API-Key in headers.")
281
+
282
+ client = AuthenticatedClient(
283
+ base_url=self.base_url,
284
+ token=self.token,
285
+ prefix="",
286
+ auth_header_name="X-API-Key",
287
+ headers=self.headers,
288
+ )
289
+
290
+ kwargs = {
291
+ "graph_id": graph_id,
292
+ "client": client,
293
+ }
294
+
295
+ if table_name:
296
+ kwargs["table_name"] = table_name
297
+ if status:
298
+ kwargs["status"] = status
299
+
300
+ response = list_files(**kwargs)
301
+
302
+ if response.status_code != 200 or not response.parsed:
303
+ logger.error(f"Failed to list files: {response.status_code}")
304
+ return []
305
+
306
+ files_data = response.parsed
307
+ files = getattr(files_data, "files", [])
308
+
309
+ return [
310
+ FileInfo(
311
+ file_id=f.file_id,
312
+ file_name=f.file_name,
313
+ file_format=f.file_format,
314
+ size_bytes=f.size_bytes or 0,
315
+ row_count=f.row_count,
316
+ upload_status=f.upload_status,
317
+ table_name=getattr(f, "table_name", ""),
318
+ created_at=f.created_at,
319
+ uploaded_at=f.uploaded_at,
320
+ )
321
+ for f in files
322
+ ]
323
+
324
+ except Exception as e:
325
+ logger.error(f"Failed to list files: {e}")
326
+ return []
327
+
328
+ def get(self, graph_id: str, file_id: str) -> Optional[FileInfo]:
329
+ """
330
+ Get detailed information about a specific file.
331
+
332
+ Args:
333
+ graph_id: Graph database identifier
334
+ file_id: File ID
335
+
336
+ Returns:
337
+ FileInfo with multi-layer status tracking, or None if not found
338
+ """
339
+ try:
340
+ from ..client import AuthenticatedClient
341
+
342
+ if not self.token:
343
+ raise Exception("No API key provided. Set X-API-Key in headers.")
344
+
345
+ client = AuthenticatedClient(
346
+ base_url=self.base_url,
347
+ token=self.token,
348
+ prefix="",
349
+ auth_header_name="X-API-Key",
350
+ headers=self.headers,
351
+ )
352
+
353
+ kwargs = {
354
+ "graph_id": graph_id,
355
+ "file_id": file_id,
356
+ "client": client,
357
+ }
358
+
359
+ response = get_file(**kwargs)
360
+
361
+ if response.status_code != 200 or not response.parsed:
362
+ logger.error(f"Failed to get file {file_id}: {response.status_code}")
363
+ return None
364
+
365
+ file_data = response.parsed
366
+
367
+ return FileInfo(
368
+ file_id=file_data.file_id,
369
+ file_name=file_data.file_name,
370
+ file_format=file_data.file_format,
371
+ size_bytes=file_data.size_bytes or 0,
372
+ row_count=file_data.row_count,
373
+ upload_status=file_data.upload_status,
374
+ table_name=file_data.table_name or "",
375
+ created_at=file_data.created_at,
376
+ uploaded_at=file_data.uploaded_at,
377
+ layers=getattr(file_data, "layers", None),
378
+ )
379
+
380
+ except Exception as e:
381
+ logger.error(f"Failed to get file {file_id}: {e}")
382
+ return None
383
+
384
+ def delete(self, graph_id: str, file_id: str, cascade: bool = False) -> bool:
385
+ """
386
+ Delete a file from all layers.
387
+
388
+ Args:
389
+ graph_id: Graph database identifier
390
+ file_id: File ID to delete
391
+ cascade: If True, delete from all layers including DuckDB and graph
392
+
393
+ Returns:
394
+ True if deletion succeeded, False otherwise
395
+ """
396
+ try:
397
+ from ..client import AuthenticatedClient
398
+
399
+ if not self.token:
400
+ raise Exception("No API key provided. Set X-API-Key in headers.")
401
+
402
+ client = AuthenticatedClient(
403
+ base_url=self.base_url,
404
+ token=self.token,
405
+ prefix="",
406
+ auth_header_name="X-API-Key",
407
+ headers=self.headers,
408
+ )
409
+
410
+ kwargs = {
411
+ "graph_id": graph_id,
412
+ "file_id": file_id,
413
+ "client": client,
414
+ "cascade": cascade,
415
+ }
416
+
417
+ response = delete_file(**kwargs)
418
+
419
+ if response.status_code not in [200, 204]:
420
+ logger.error(f"Failed to delete file {file_id}: {response.status_code}")
421
+ return False
422
+
423
+ return True
424
+
425
+ except Exception as e:
426
+ logger.error(f"Failed to delete file {file_id}: {e}")
427
+ return False
428
+
429
+ def __del__(self):
430
+ """Cleanup HTTP client on deletion"""
431
+ if hasattr(self, "_http_client"):
432
+ self._http_client.close()