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

@@ -134,7 +134,7 @@ def sync_detailed(
134
134
  **Execution Strategies (automatic):**
135
135
  - Fast operations (<5s): Immediate synchronous response
136
136
  - Medium operations (5-30s): SSE streaming with progress updates
137
- - Long operations (>30s): Async Celery worker with operation tracking
137
+ - Long operations (>30s): Background queue with operation tracking
138
138
 
139
139
  **Response Mode Override:**
140
140
  Use query parameter `?mode=sync|async` to override automatic strategy selection.
@@ -228,7 +228,7 @@ def sync(
228
228
  **Execution Strategies (automatic):**
229
229
  - Fast operations (<5s): Immediate synchronous response
230
230
  - Medium operations (5-30s): SSE streaming with progress updates
231
- - Long operations (>30s): Async Celery worker with operation tracking
231
+ - Long operations (>30s): Background queue with operation tracking
232
232
 
233
233
  **Response Mode Override:**
234
234
  Use query parameter `?mode=sync|async` to override automatic strategy selection.
@@ -317,7 +317,7 @@ async def asyncio_detailed(
317
317
  **Execution Strategies (automatic):**
318
318
  - Fast operations (<5s): Immediate synchronous response
319
319
  - Medium operations (5-30s): SSE streaming with progress updates
320
- - Long operations (>30s): Async Celery worker with operation tracking
320
+ - Long operations (>30s): Background queue with operation tracking
321
321
 
322
322
  **Response Mode Override:**
323
323
  Use query parameter `?mode=sync|async` to override automatic strategy selection.
@@ -409,7 +409,7 @@ async def asyncio(
409
409
  **Execution Strategies (automatic):**
410
410
  - Fast operations (<5s): Immediate synchronous response
411
411
  - Medium operations (5-30s): SSE streaming with progress updates
412
- - Long operations (>30s): Async Celery worker with operation tracking
412
+ - Long operations (>30s): Background queue with operation tracking
413
413
 
414
414
  **Response Mode Override:**
415
415
  Use query parameter `?mode=sync|async` to override automatic strategy selection.
@@ -124,7 +124,7 @@ def sync_detailed(
124
124
  **Execution Strategies (automatic):**
125
125
  - Fast operations (<5s): Immediate synchronous response
126
126
  - Medium operations (5-30s): SSE streaming with progress updates
127
- - Long operations (>30s): Async Celery worker with operation tracking
127
+ - Long operations (>30s): Background queue with operation tracking
128
128
 
129
129
  **Response Mode Override:**
130
130
  Use query parameter `?mode=sync|async` to override automatic strategy selection.
@@ -179,7 +179,7 @@ def sync(
179
179
  **Execution Strategies (automatic):**
180
180
  - Fast operations (<5s): Immediate synchronous response
181
181
  - Medium operations (5-30s): SSE streaming with progress updates
182
- - Long operations (>30s): Async Celery worker with operation tracking
182
+ - Long operations (>30s): Background queue with operation tracking
183
183
 
184
184
  **Response Mode Override:**
185
185
  Use query parameter `?mode=sync|async` to override automatic strategy selection.
@@ -229,7 +229,7 @@ async def asyncio_detailed(
229
229
  **Execution Strategies (automatic):**
230
230
  - Fast operations (<5s): Immediate synchronous response
231
231
  - Medium operations (5-30s): SSE streaming with progress updates
232
- - Long operations (>30s): Async Celery worker with operation tracking
232
+ - Long operations (>30s): Background queue with operation tracking
233
233
 
234
234
  **Response Mode Override:**
235
235
  Use query parameter `?mode=sync|async` to override automatic strategy selection.
@@ -282,7 +282,7 @@ async def asyncio(
282
282
  **Execution Strategies (automatic):**
283
283
  - Fast operations (<5s): Immediate synchronous response
284
284
  - Medium operations (5-30s): SSE streaming with progress updates
285
- - Long operations (>30s): Async Celery worker with operation tracking
285
+ - Long operations (>30s): Background queue with operation tracking
286
286
 
287
287
  **Response Mode Override:**
288
288
  Use query parameter `?mode=sync|async` to override automatic strategy selection.
@@ -103,7 +103,7 @@ def sync_detailed(
103
103
  **What Happens (status='uploaded'):**
104
104
  1. File validated in S3
105
105
  2. Row count calculated
106
- 3. DuckDB staging triggered immediately (Celery task)
106
+ 3. DuckDB staging triggered immediately (background task)
107
107
  4. If ingest_to_graph=true, graph ingestion queued
108
108
  5. File queryable in DuckDB within seconds
109
109
 
@@ -165,7 +165,7 @@ def sync(
165
165
  **What Happens (status='uploaded'):**
166
166
  1. File validated in S3
167
167
  2. Row count calculated
168
- 3. DuckDB staging triggered immediately (Celery task)
168
+ 3. DuckDB staging triggered immediately (background task)
169
169
  4. If ingest_to_graph=true, graph ingestion queued
170
170
  5. File queryable in DuckDB within seconds
171
171
 
@@ -222,7 +222,7 @@ async def asyncio_detailed(
222
222
  **What Happens (status='uploaded'):**
223
223
  1. File validated in S3
224
224
  2. Row count calculated
225
- 3. DuckDB staging triggered immediately (Celery task)
225
+ 3. DuckDB staging triggered immediately (background task)
226
226
  4. If ingest_to_graph=true, graph ingestion queued
227
227
  5. File queryable in DuckDB within seconds
228
228
 
@@ -282,7 +282,7 @@ async def asyncio(
282
282
  **What Happens (status='uploaded'):**
283
283
  1. File validated in S3
284
284
  2. Row count calculated
285
- 3. DuckDB staging triggered immediately (Celery task)
285
+ 3. DuckDB staging triggered immediately (background task)
286
286
  4. If ingest_to_graph=true, graph ingestion queued
287
287
  5. File queryable in DuckDB within seconds
288
288
 
@@ -1,6 +1,6 @@
1
1
  # RoboSystems Python Client Extensions
2
2
 
3
- 🚀 **Production-Ready Extensions** for the RoboSystems Financial Knowledge Graph API
3
+ **Production-Ready Extensions** for the RoboSystems Financial Knowledge Graph API
4
4
 
5
5
  [![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -17,7 +17,7 @@ The RoboSystems Python Client Extensions provide enhanced functionality for the
17
17
  - **Caching** with TTL and LRU eviction
18
18
  - **Full Async/Await Support** throughout
19
19
 
20
- ## 🚀 Quick Start
20
+ ## Quick Start
21
21
 
22
22
  ### Installation
23
23
 
@@ -84,7 +84,7 @@ async def main():
84
84
  asyncio.run(main())
85
85
  ```
86
86
 
87
- ## 🔐 Authentication
87
+ ## Authentication
88
88
 
89
89
  ### API Key Authentication (Recommended)
90
90
 
@@ -139,7 +139,7 @@ dev_ext = create_extensions(
139
139
  )
140
140
  ```
141
141
 
142
- ## 🛠 Advanced Features
142
+ ## Advanced Features
143
143
 
144
144
  ### Query Builder
145
145
 
@@ -182,7 +182,7 @@ print(f"Complexity: {cost['complexity_category']} (score: {cost['complexity_scor
182
182
 
183
183
  # Get optimization recommendations
184
184
  for rec in cost['recommendations']:
185
- print(f"💡 {rec}")
185
+ print(f"Tip: {rec}")
186
186
  ```
187
187
 
188
188
  ### Result Processing
@@ -271,7 +271,7 @@ client.connect("operation_id")
271
271
  client.close()
272
272
  ```
273
273
 
274
- ## 📊 Examples
274
+ ## Examples
275
275
 
276
276
  ### Financial Data Analysis
277
277
 
@@ -332,7 +332,7 @@ if final_batch:
332
332
  process_transaction_batch(final_batch)
333
333
  total_processed += len(final_batch)
334
334
 
335
- print(f"Processed {total_processed:,} transactions total")
335
+ print(f"Processed {total_processed:,} transactions total")
336
336
  ```
337
337
 
338
338
  ### Error Handling
@@ -361,7 +361,7 @@ except Exception as e:
361
361
  print(f"Query failed: {e}")
362
362
  ```
363
363
 
364
- ## Performance Optimization
364
+ ## Performance Optimization
365
365
 
366
366
  ### Connection Pooling
367
367
 
@@ -391,21 +391,21 @@ query = "MATCH (c:Company) WHERE c.revenue > 1000000 RETURN c"
391
391
  # Check syntax
392
392
  validation = validate_cypher_query(query)
393
393
  if not validation['valid']:
394
- print("Query has syntax errors:", validation['issues'])
394
+ print("Query has syntax errors:", validation['issues'])
395
395
 
396
396
  # Estimate cost
397
397
  cost = estimate_query_cost(query)
398
- print(f"📊 Query complexity: {cost['complexity_category']}")
398
+ print(f"Query complexity: {cost['complexity_category']}")
399
399
 
400
400
  # Follow recommendations
401
401
  for rec in cost['recommendations']:
402
- print(f"💡 Optimization tip: {rec}")
402
+ print(f"Optimization tip: {rec}")
403
403
 
404
404
  # Execute only if reasonable complexity
405
405
  if cost['complexity_category'] in ['low', 'medium']:
406
406
  result = extensions.execute_query("graph_id", query)
407
407
  else:
408
- print("⚠️ Query may be too expensive - consider optimization")
408
+ print("Query may be too expensive - consider optimization")
409
409
  ```
410
410
 
411
411
  ### Caching Strategy
@@ -425,7 +425,7 @@ else:
425
425
  cache = results_cache
426
426
  ```
427
427
 
428
- ## 🧪 Testing
428
+ ## Testing
429
429
 
430
430
  Run the test suite:
431
431
 
@@ -457,7 +457,7 @@ def test_query_execution():
457
457
  assert result["data"] == [{"count": 100}]
458
458
  ```
459
459
 
460
- ## 🔧 Configuration
460
+ ## Configuration
461
461
 
462
462
  ### Environment Variables
463
463
 
@@ -492,7 +492,7 @@ config = RoboSystemsExtensionConfig(
492
492
  extensions = RoboSystemsExtensions(config)
493
493
  ```
494
494
 
495
- ## 📚 API Reference
495
+ ## API Reference
496
496
 
497
497
  ### Core Classes
498
498
 
@@ -528,7 +528,7 @@ extensions = RoboSystemsExtensions(config)
528
528
  - **`format_duration(milliseconds)`** - Human-readable time formatting
529
529
  - **`create_extensions(method, **kwargs)`** - Extensions factory
530
530
 
531
- ## 🐛 Troubleshooting
531
+ ## Troubleshooting
532
532
 
533
533
  ### Common Issues
534
534
 
@@ -545,9 +545,9 @@ pip install pandas # For DataFrame conversion (optional)
545
545
  extensions = AuthenticatedExtensions("your-api-key")
546
546
  try:
547
547
  result = extensions.execute_query("graph_id", "MATCH (n) RETURN count(n) LIMIT 1")
548
- print("Authentication successful")
548
+ print("Authentication successful")
549
549
  except Exception as e:
550
- print(f"Auth failed: {e}")
550
+ print(f"Auth failed: {e}")
551
551
  ```
552
552
 
553
553
  **Connection Issues**
@@ -585,11 +585,7 @@ logging.basicConfig(level=logging.DEBUG)
585
585
  extensions = AuthenticatedExtensions("your-key")
586
586
  ```
587
587
 
588
- ## 📄 License
589
-
590
- MIT License - see [LICENSE](LICENSE) file for details.
591
-
592
- ## 🤝 Contributing
588
+ ## Contributing
593
589
 
594
590
  1. Fork the repository
595
591
  2. Create a feature branch
@@ -597,7 +593,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
597
593
  4. Run the test suite: `python run_tests.py`
598
594
  5. Submit a pull request
599
595
 
600
- ## 📞 Support
596
+ ## Support
601
597
 
602
598
  - **API Reference**: [api.robosystems.ai](https://api.robosystems.ai)
603
599
  - **Issues**: [GitHub Issues](https://github.com/RoboFinSystems/robosystems-python-client/issues)
@@ -177,7 +177,7 @@ class AgentClient:
177
177
  else datetime.now().isoformat(),
178
178
  )
179
179
 
180
- # Check if this is a queued response (async Celery execution)
180
+ # Check if this is a queued response (async background task execution)
181
181
  is_queued = False
182
182
  queued_response = None
183
183
 
@@ -25,6 +25,7 @@ class RoboSystemsExtensionConfig:
25
25
  max_retries: int = 5
26
26
  retry_delay: int = 1000
27
27
  timeout: int = 30
28
+ s3_endpoint_url: Optional[str] = None # Override S3 endpoint (e.g., for LocalStack)
28
29
 
29
30
 
30
31
  class RoboSystemsExtensions:
@@ -41,6 +42,7 @@ class RoboSystemsExtensions:
41
42
  "max_retries": config.max_retries,
42
43
  "retry_delay": config.retry_delay,
43
44
  "timeout": config.timeout,
45
+ "s3_endpoint_url": config.s3_endpoint_url,
44
46
  }
45
47
 
46
48
  # Extract token from headers if it was set by auth classes
@@ -37,7 +37,6 @@ class FileUploadOptions:
37
37
  """Options for file upload operations"""
38
38
 
39
39
  on_progress: Optional[Callable[[str], None]] = None
40
- fix_localstack_url: bool = True
41
40
  ingest_to_graph: bool = False
42
41
 
43
42
 
@@ -78,6 +77,9 @@ class FileClient:
78
77
  self.base_url = config["base_url"]
79
78
  self.headers = config.get("headers", {})
80
79
  self.token = config.get("token")
80
+ self.s3_endpoint_url = config.get(
81
+ "s3_endpoint_url"
82
+ ) # Optional S3 endpoint override
81
83
  self._http_client = httpx.Client(timeout=120.0)
82
84
 
83
85
  def upload(
@@ -171,9 +173,23 @@ class FileClient:
171
173
  upload_url = upload_data.upload_url
172
174
  file_id = upload_data.file_id
173
175
 
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")
176
+ # Override S3 endpoint if configured (e.g., for LocalStack)
177
+ if self.s3_endpoint_url:
178
+ from urllib.parse import urlparse, urlunparse
179
+
180
+ parsed_url = urlparse(upload_url)
181
+ override_parsed = urlparse(self.s3_endpoint_url)
182
+ # Replace scheme, host, and port with the override endpoint
183
+ upload_url = urlunparse(
184
+ (
185
+ override_parsed.scheme or parsed_url.scheme,
186
+ override_parsed.netloc,
187
+ parsed_url.path,
188
+ parsed_url.params,
189
+ parsed_url.query,
190
+ parsed_url.fragment,
191
+ )
192
+ )
177
193
 
178
194
  # Step 2: Upload file to S3
179
195
  if options.on_progress:
@@ -15,6 +15,7 @@ from ..api.materialize.get_materialization_status import (
15
15
  sync_detailed as get_materialization_status,
16
16
  )
17
17
  from ..models.materialize_request import MaterializeRequest
18
+ from .operation_client import OperationClient, OperationProgress, MonitorOptions
18
19
 
19
20
  logger = logging.getLogger(__name__)
20
21
 
@@ -27,6 +28,7 @@ class MaterializationOptions:
27
28
  rebuild: bool = False
28
29
  force: bool = False
29
30
  on_progress: Optional[Callable[[str], None]] = None
31
+ timeout: Optional[int] = 600 # 10 minute default timeout
30
32
 
31
33
 
32
34
  @dataclass
@@ -66,6 +68,14 @@ class MaterializationClient:
66
68
  self.base_url = config["base_url"]
67
69
  self.headers = config.get("headers", {})
68
70
  self.token = config.get("token")
71
+ self._operation_client = None
72
+
73
+ @property
74
+ def operation_client(self) -> OperationClient:
75
+ """Get or create the operation client for SSE monitoring."""
76
+ if self._operation_client is None:
77
+ self._operation_client = OperationClient(self.config)
78
+ return self._operation_client
69
79
 
70
80
  def materialize(
71
81
  self,
@@ -75,13 +85,13 @@ class MaterializationClient:
75
85
  """
76
86
  Materialize graph from DuckDB staging tables.
77
87
 
78
- Rebuilds the complete graph database from the current state of DuckDB
79
- staging tables. Automatically discovers all tables, materializes them in
80
- the correct order (nodes before relationships), and clears the staleness flag.
88
+ Submits a materialization job to Dagster and monitors progress via SSE.
89
+ The operation runs asynchronously on the server but this method waits
90
+ for completion and returns the final result.
81
91
 
82
92
  Args:
83
93
  graph_id: Graph database identifier
84
- options: Materialization options (ignore_errors, rebuild, force)
94
+ options: Materialization options (ignore_errors, rebuild, force, timeout)
85
95
 
86
96
  Returns:
87
97
  MaterializationResult with detailed execution information
@@ -96,7 +106,7 @@ class MaterializationClient:
96
106
 
97
107
  try:
98
108
  if options.on_progress:
99
- options.on_progress("Starting graph materialization...")
109
+ options.on_progress("Submitting materialization job...")
100
110
 
101
111
  request = MaterializeRequest(
102
112
  ignore_errors=options.ignore_errors,
@@ -125,6 +135,7 @@ class MaterializationClient:
125
135
 
126
136
  response = materialize_graph(**kwargs)
127
137
 
138
+ # Handle non-200 status codes
128
139
  if response.status_code != 200 or not response.parsed:
129
140
  error_msg = f"Materialization failed: {response.status_code}"
130
141
  if hasattr(response, "content"):
@@ -148,25 +159,68 @@ class MaterializationClient:
148
159
  error=error_msg,
149
160
  )
150
161
 
162
+ # Get the operation_id from the queued response
151
163
  result_data = response.parsed
164
+ operation_id = result_data.operation_id
152
165
 
153
166
  if options.on_progress:
154
- options.on_progress(
155
- f"✅ Materialization complete: {len(result_data.tables_materialized)} tables, "
156
- f"{result_data.total_rows:,} rows in {result_data.execution_time_ms:.2f}ms"
157
- )
158
-
159
- return MaterializationResult(
160
- status=result_data.status,
161
- was_stale=result_data.was_stale,
162
- stale_reason=result_data.stale_reason,
163
- tables_materialized=result_data.tables_materialized,
164
- total_rows=result_data.total_rows,
165
- execution_time_ms=result_data.execution_time_ms,
166
- message=result_data.message,
167
- success=True,
167
+ options.on_progress(f"Materialization queued (operation: {operation_id})")
168
+
169
+ # Monitor the operation via SSE until completion
170
+ def on_sse_progress(progress: OperationProgress):
171
+ if options.on_progress:
172
+ msg = progress.message
173
+ if progress.percentage is not None:
174
+ msg += f" ({progress.percentage:.0f}%)"
175
+ options.on_progress(msg)
176
+
177
+ monitor_options = MonitorOptions(
178
+ on_progress=on_sse_progress,
179
+ timeout=options.timeout,
168
180
  )
169
181
 
182
+ op_result = self.operation_client.monitor_operation(operation_id, monitor_options)
183
+
184
+ # Convert operation result to materialization result
185
+ if op_result.status.value == "completed":
186
+ # Extract details from SSE completion event result
187
+ sse_result = op_result.result or {}
188
+
189
+ if options.on_progress:
190
+ tables = sse_result.get("tables_materialized", [])
191
+ rows = sse_result.get("total_rows", 0)
192
+ time_ms = sse_result.get("execution_time_ms", 0)
193
+ options.on_progress(
194
+ f"✅ Materialization complete: {len(tables)} tables, "
195
+ f"{rows:,} rows in {time_ms:.2f}ms"
196
+ )
197
+
198
+ return MaterializationResult(
199
+ status="success",
200
+ was_stale=sse_result.get("was_stale", False),
201
+ stale_reason=sse_result.get("stale_reason"),
202
+ tables_materialized=sse_result.get("tables_materialized", []),
203
+ total_rows=sse_result.get("total_rows", 0),
204
+ execution_time_ms=sse_result.get(
205
+ "execution_time_ms", op_result.execution_time_ms or 0
206
+ ),
207
+ message=sse_result.get("message", "Graph materialized successfully"),
208
+ success=True,
209
+ )
210
+ else:
211
+ # Operation failed or was cancelled
212
+ return MaterializationResult(
213
+ status=op_result.status.value,
214
+ was_stale=False,
215
+ stale_reason=None,
216
+ tables_materialized=[],
217
+ total_rows=0,
218
+ execution_time_ms=op_result.execution_time_ms or 0,
219
+ message=op_result.error or f"Operation {op_result.status.value}",
220
+ success=False,
221
+ error=op_result.error,
222
+ )
223
+
170
224
  except Exception as e:
171
225
  logger.error(f"Materialization failed: {e}")
172
226
  return MaterializationResult(
@@ -104,8 +104,10 @@ class SSEClient:
104
104
 
105
105
  try:
106
106
  self.client = httpx.Client(timeout=self.config.timeout)
107
- self._response = self.client.stream("GET", url, params=params, headers=headers)
108
- self._response.__enter__()
107
+ self._context_manager = self.client.stream(
108
+ "GET", url, params=params, headers=headers
109
+ )
110
+ self._response = self._context_manager.__enter__()
109
111
 
110
112
  self.reconnect_attempts = 0
111
113
  self.emit("connected", None)
@@ -124,11 +126,9 @@ class SSEClient:
124
126
 
125
127
  try:
126
128
  event_buffer = {"event": None, "data": [], "id": None, "retry": None}
127
- print("[SSE DEBUG] Starting to process events...")
128
129
 
129
130
  for line in self._response.iter_lines():
130
131
  if self.closed:
131
- print("[SSE DEBUG] Stream closed, breaking out of loop")
132
132
  break
133
133
 
134
134
  line = line.strip()
@@ -136,7 +136,6 @@ class SSEClient:
136
136
  # Empty line indicates end of event
137
137
  if not line:
138
138
  if event_buffer["data"] or event_buffer["event"]:
139
- print(f"[SSE DEBUG] Dispatching event: {event_buffer.get('event')}")
140
139
  self._dispatch_event(event_buffer)
141
140
  event_buffer = {"event": None, "data": [], "id": None, "retry": None}
142
141
  continue
@@ -172,13 +171,9 @@ class SSEClient:
172
171
 
173
172
  # Handle final event if stream ends without empty line
174
173
  if event_buffer["data"] or event_buffer["event"]:
175
- print("[SSE DEBUG] Dispatching final event after stream end")
176
174
  self._dispatch_event(event_buffer)
177
175
 
178
- print("[SSE DEBUG] Event processing loop ended")
179
-
180
176
  except Exception as error:
181
- print(f"[SSE DEBUG] Exception in event processing: {error}")
182
177
  if not self.closed:
183
178
  self.emit("error", error)
184
179
 
@@ -214,13 +209,14 @@ class SSEClient:
214
209
  # Emit typed event
215
210
  self.emit(event_type, parsed_data)
216
211
 
217
- # Check for completion events
212
+ # Check for completion events - just set flag, don't close from within loop
213
+ # The loop will break on next iteration and close() will be called in finally
218
214
  if event_type in [
219
215
  EventType.OPERATION_COMPLETED.value,
220
216
  EventType.OPERATION_ERROR.value,
221
217
  EventType.OPERATION_CANCELLED.value,
222
218
  ]:
223
- self.close()
219
+ self.closed = True
224
220
 
225
221
  def _handle_error(
226
222
  self, error: Exception, operation_id: str, from_sequence: int
@@ -285,12 +281,13 @@ class SSEClient:
285
281
  """Close the SSE connection"""
286
282
  self.closed = True
287
283
 
288
- if self._response:
284
+ if hasattr(self, "_context_manager") and self._context_manager:
289
285
  try:
290
- self._response.__exit__(None, None, None)
286
+ self._context_manager.__exit__(None, None, None)
291
287
  except Exception:
292
288
  pass
293
- self._response = None
289
+ self._context_manager = None
290
+ self._response = None
294
291
 
295
292
  if self.client:
296
293
  self.client.close()
@@ -334,10 +331,10 @@ class AsyncSSEClient:
334
331
 
335
332
  try:
336
333
  self.client = httpx.AsyncClient(timeout=self.config.timeout)
337
- self._response = await self.client.stream(
334
+ self._context_manager = self.client.stream(
338
335
  "GET", url, params=params, headers=headers
339
336
  )
340
- await self._response.__aenter__()
337
+ self._response = await self._context_manager.__aenter__()
341
338
 
342
339
  self.reconnect_attempts = 0
343
340
  self.emit("connected", None)
@@ -401,13 +398,9 @@ class AsyncSSEClient:
401
398
 
402
399
  # Handle final event if stream ends without empty line
403
400
  if event_buffer["data"] or event_buffer["event"]:
404
- print("[SSE DEBUG] Dispatching final event after stream end")
405
401
  self._dispatch_event(event_buffer)
406
402
 
407
- print("[SSE DEBUG] Event processing loop ended")
408
-
409
403
  except Exception as error:
410
- print(f"[SSE DEBUG] Exception in event processing: {error}")
411
404
  if not self.closed:
412
405
  self.emit("error", error)
413
406
 
@@ -443,13 +436,14 @@ class AsyncSSEClient:
443
436
  # Emit typed event
444
437
  self.emit(event_type, parsed_data)
445
438
 
446
- # Check for completion events
439
+ # Check for completion events - just set flag, don't close from within loop
440
+ # The loop will break on next iteration and close() will be called in finally
447
441
  if event_type in [
448
442
  EventType.OPERATION_COMPLETED.value,
449
443
  EventType.OPERATION_ERROR.value,
450
444
  EventType.OPERATION_CANCELLED.value,
451
445
  ]:
452
- asyncio.create_task(self.close())
446
+ self.closed = True
453
447
 
454
448
  async def _handle_error(
455
449
  self, error: Exception, operation_id: str, from_sequence: int
@@ -512,12 +506,13 @@ class AsyncSSEClient:
512
506
  """Close the SSE connection (async)"""
513
507
  self.closed = True
514
508
 
515
- if self._response:
509
+ if hasattr(self, "_context_manager") and self._context_manager:
516
510
  try:
517
- await self._response.__aexit__(None, None, None)
511
+ await self._context_manager.__aexit__(None, None, None)
518
512
  except Exception:
519
513
  pass
520
- self._response = None
514
+ self._context_manager = None
515
+ self._response = None
521
516
 
522
517
  if self.client:
523
518
  await self.client.aclose()
@@ -197,7 +197,9 @@ class SubgraphWorkspaceClient:
197
197
  sse_url = f"{self.api._base_url}/v1/operations/{operation_id}/stream"
198
198
  headers = {"X-API-Key": self.api.token}
199
199
 
200
- async with httpx.AsyncClient() as client:
200
+ # Use longer timeout for SSE streaming (Dagster jobs can take time)
201
+ timeout = httpx.Timeout(connect=30.0, read=120.0, write=30.0, pool=30.0)
202
+ async with httpx.AsyncClient(timeout=timeout) as client:
201
203
  async with client.stream("GET", sse_url, headers=headers) as sse_response:
202
204
  async for line in sse_response.aiter_lines():
203
205
  if line.startswith("data: "):
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Mapping
4
- from typing import Any, TypeVar, cast
4
+ from typing import Any, TypeVar
5
5
 
6
6
  from attrs import define as _attrs_define
7
7
  from attrs import field as _attrs_field
@@ -13,102 +13,64 @@ T = TypeVar("T", bound="MaterializeResponse")
13
13
 
14
14
  @_attrs_define
15
15
  class MaterializeResponse:
16
- """
16
+ """Response for queued materialization operation.
17
+
18
+ Example:
19
+ {'graph_id': 'kg_abc123', 'message': 'Materialization queued. Monitor via SSE stream.', 'operation_id':
20
+ '550e8400-e29b-41d4-a716-446655440000', 'status': 'queued'}
21
+
17
22
  Attributes:
18
- status (str): Materialization status
19
23
  graph_id (str): Graph database identifier
20
- was_stale (bool): Whether graph was stale before materialization
21
- tables_materialized (list[str]): List of tables successfully materialized
22
- total_rows (int): Total rows materialized across all tables
23
- execution_time_ms (float): Total materialization time
24
+ operation_id (str): SSE operation ID for progress tracking
24
25
  message (str): Human-readable status message
25
- stale_reason (None | str | Unset): Reason graph was stale
26
+ status (str | Unset): Operation status Default: 'queued'.
26
27
  """
27
28
 
28
- status: str
29
29
  graph_id: str
30
- was_stale: bool
31
- tables_materialized: list[str]
32
- total_rows: int
33
- execution_time_ms: float
30
+ operation_id: str
34
31
  message: str
35
- stale_reason: None | str | Unset = UNSET
32
+ status: str | Unset = "queued"
36
33
  additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
37
34
 
38
35
  def to_dict(self) -> dict[str, Any]:
39
- status = self.status
40
-
41
36
  graph_id = self.graph_id
42
37
 
43
- was_stale = self.was_stale
44
-
45
- tables_materialized = self.tables_materialized
46
-
47
- total_rows = self.total_rows
48
-
49
- execution_time_ms = self.execution_time_ms
38
+ operation_id = self.operation_id
50
39
 
51
40
  message = self.message
52
41
 
53
- stale_reason: None | str | Unset
54
- if isinstance(self.stale_reason, Unset):
55
- stale_reason = UNSET
56
- else:
57
- stale_reason = self.stale_reason
42
+ status = self.status
58
43
 
59
44
  field_dict: dict[str, Any] = {}
60
45
  field_dict.update(self.additional_properties)
61
46
  field_dict.update(
62
47
  {
63
- "status": status,
64
48
  "graph_id": graph_id,
65
- "was_stale": was_stale,
66
- "tables_materialized": tables_materialized,
67
- "total_rows": total_rows,
68
- "execution_time_ms": execution_time_ms,
49
+ "operation_id": operation_id,
69
50
  "message": message,
70
51
  }
71
52
  )
72
- if stale_reason is not UNSET:
73
- field_dict["stale_reason"] = stale_reason
53
+ if status is not UNSET:
54
+ field_dict["status"] = status
74
55
 
75
56
  return field_dict
76
57
 
77
58
  @classmethod
78
59
  def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
79
60
  d = dict(src_dict)
80
- status = d.pop("status")
81
-
82
61
  graph_id = d.pop("graph_id")
83
62
 
84
- was_stale = d.pop("was_stale")
85
-
86
- tables_materialized = cast(list[str], d.pop("tables_materialized"))
87
-
88
- total_rows = d.pop("total_rows")
89
-
90
- execution_time_ms = d.pop("execution_time_ms")
63
+ operation_id = d.pop("operation_id")
91
64
 
92
65
  message = d.pop("message")
93
66
 
94
- def _parse_stale_reason(data: object) -> None | str | Unset:
95
- if data is None:
96
- return data
97
- if isinstance(data, Unset):
98
- return data
99
- return cast(None | str | Unset, data)
100
-
101
- stale_reason = _parse_stale_reason(d.pop("stale_reason", UNSET))
67
+ status = d.pop("status", UNSET)
102
68
 
103
69
  materialize_response = cls(
104
- status=status,
105
70
  graph_id=graph_id,
106
- was_stale=was_stale,
107
- tables_materialized=tables_materialized,
108
- total_rows=total_rows,
109
- execution_time_ms=execution_time_ms,
71
+ operation_id=operation_id,
110
72
  message=message,
111
- stale_reason=stale_reason,
73
+ status=status,
112
74
  )
113
75
 
114
76
  materialize_response.additional_properties = d
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robosystems-client
3
- Version: 0.2.21
3
+ Version: 0.2.23
4
4
  Summary: Python Client for RoboSystems financial graph database API
5
5
  Author: RFS LLC
6
6
  License: MIT
@@ -6,9 +6,9 @@ robosystems_client/sdk-config.yaml,sha256=Y_A8qSC2zHLYy6d443Rlgdkw2GleOSFjYvq_Qm
6
6
  robosystems_client/types.py,sha256=p37iLHBX-cpNh7jZmbG4Xl1UI6hexvRS-HkY_QBlZWE,1301
7
7
  robosystems_client/api/__init__.py,sha256=zTSiG_ujSjAqWPyc435YXaX9XTlpMjiJWBbV-f-YtdA,45
8
8
  robosystems_client/api/agent/__init__.py,sha256=5vd9uJWAjRqa9xzxzYkLD1yoZ12Ld_bAaNB5WX4fbE8,56
9
- robosystems_client/api/agent/auto_select_agent.py,sha256=yeav6I1-DAL7g-E0f3AhClva_BEsKRXL0HQzKvnDroU,16408
9
+ robosystems_client/api/agent/auto_select_agent.py,sha256=uHoIjrKoA6e7GyQu1DMAIPGZ1JAx-xA8sYodyLTFUfU,16396
10
10
  robosystems_client/api/agent/batch_process_queries.py,sha256=d0htQmRZoTig8XnSQHINhPDf10w_kgPfTSBNGDUrg1c,6697
11
- robosystems_client/api/agent/execute_specific_agent.py,sha256=70Y9dVy84z-xuQlwDFUUULi-mb-tBCmDO67Avmny6eo,9440
11
+ robosystems_client/api/agent/execute_specific_agent.py,sha256=pZWf6dsWtMjJ2qkV0tOLEhWB8s9HlsEXUtu2DZKv_RE,9428
12
12
  robosystems_client/api/agent/get_agent_metadata.py,sha256=MS4MCA5fQE2F654iVTTnWGxckDx3YlPvYScMtsyKkAk,5730
13
13
  robosystems_client/api/agent/list_agents.py,sha256=BZ0H9ZfpwyXY57TPvkMVSsm72d6JcOY9qDUU4LxTvyE,6183
14
14
  robosystems_client/api/agent/recommend_agent.py,sha256=fy4cm-DA_FHiDsX9gtrHfITOmHJFGNekOkxAhPWz6I8,6601
@@ -67,7 +67,7 @@ robosystems_client/api/files/create_file_upload.py,sha256=OVMbGuJ004lqYrHmUFzr2g
67
67
  robosystems_client/api/files/delete_file.py,sha256=wAt0wHIYgX1D57HwQvUUb1GMr2GQzXNq3wz0MuCSI3I,9815
68
68
  robosystems_client/api/files/get_file.py,sha256=cZv2YurkjF8bhBzC_pprblPUguunV0ciOjuB4ygQS1Q,10558
69
69
  robosystems_client/api/files/list_files.py,sha256=3JnJ4gwZS7k4KdjnHp7fv0jq8JuTGzV0xRhZcMEfBcA,10541
70
- robosystems_client/api/files/update_file.py,sha256=PTFUwT8qsI8tUFo7Cno4iG7ouicIBGYWme0BiLy6Ogc,9027
70
+ robosystems_client/api/files/update_file.py,sha256=X80_TbYmQvL7mLdQejCy-UzUiyVx96SpCE2EHNJJSr8,9043
71
71
  robosystems_client/api/graph_health/__init__.py,sha256=5vd9uJWAjRqa9xzxzYkLD1yoZ12Ld_bAaNB5WX4fbE8,56
72
72
  robosystems_client/api/graph_health/get_database_health.py,sha256=rGMjLUjD5DfIGv1Qu8BARWRafZhTqyrsb65YCIFe1k8,8738
73
73
  robosystems_client/api/graph_info/__init__.py,sha256=5vd9uJWAjRqa9xzxzYkLD1yoZ12Ld_bAaNB5WX4fbE8,56
@@ -141,20 +141,20 @@ robosystems_client/api/user/update_user_password.py,sha256=0couyI63dlspqvtO62cI4
141
141
  robosystems_client/api/views/__init__.py,sha256=5vd9uJWAjRqa9xzxzYkLD1yoZ12Ld_bAaNB5WX4fbE8,56
142
142
  robosystems_client/api/views/create_view.py,sha256=aBjk0FsAj6A3K2iqjE7DcTLKjDRsxegFbs5RC0hmbqA,6316
143
143
  robosystems_client/api/views/save_view.py,sha256=T7PUUNvp0JIeOYWG2n8Nm4Y9hhEWV7i-Gl2Vl8Oz4Ls,9224
144
- robosystems_client/extensions/README.md,sha256=qfHFjdgA_J-zNXziNZE6M1MKJiwVkocBi01w_HhvzEk,16136
144
+ robosystems_client/extensions/README.md,sha256=NUkPte5GM8rJkT-tVrVXbujbKVxP-sw-QocYaLZfAL0,15971
145
145
  robosystems_client/extensions/__init__.py,sha256=eTuJQGygQTOWC51YVhJOWUWFUMLcPo7MpZ0H3GaxoR0,7076
146
- robosystems_client/extensions/agent_client.py,sha256=Db2C4hrakVsf6ScnBcNk6rte3Kwn4cQBEHsR_joWMTs,17750
146
+ robosystems_client/extensions/agent_client.py,sha256=kvC5UO5JjTpsVB_1ARFN2H7MSDZYjw016zJrnNTMZUM,17759
147
147
  robosystems_client/extensions/auth_integration.py,sha256=ABOJ8aVjfHehNGNzim1iR9-Cdh7Mr22ce-WgWWeqJt0,6535
148
148
  robosystems_client/extensions/dataframe_utils.py,sha256=gK1bgkVqBF0TvWVdGQvqWrt-ur_Rw11j8uNtMoulLWE,12312
149
149
  robosystems_client/extensions/element_mapping_client.py,sha256=yuh0QPQBPM33E7r6QWWDiKm3T4TfCdbn2kvO3Jlw4Cs,18516
150
- robosystems_client/extensions/extensions.py,sha256=7vsD3QeIKbwhC1UqNskFjsfKkg_ZO3PPDnc6TxV3PoA,6722
151
- robosystems_client/extensions/file_client.py,sha256=WNttw8BtdpLsb3N2fkvim-a8eDSshi8LM3nMtfEmLt0,11843
150
+ robosystems_client/extensions/extensions.py,sha256=IqTuVJNt6vvO5LhnH-9SexCF5k9Abqig1sBdY4SdD1I,6858
151
+ robosystems_client/extensions/file_client.py,sha256=bknqmbYrIqxc2PnWskeDvcWYP2bemBQaxY_4A6zr5LY,12326
152
152
  robosystems_client/extensions/graph_client.py,sha256=OBi0xj0SLIRKLeSu_DiGt2ZakCmhggvNrMP3jdRfEgQ,10326
153
- robosystems_client/extensions/materialization_client.py,sha256=Pvq4Gz5fS4B2eTxFrpCGTj7-diOZwOtWnNo7wKT9mVE,6811
153
+ robosystems_client/extensions/materialization_client.py,sha256=3PzzEYn5NqENbb4PggGb1Z31SWmcW9D2mJ83uem22_c,8923
154
154
  robosystems_client/extensions/operation_client.py,sha256=B1qju-wWQrnrnVJixKGgsA_KEInviwJwdlJxzm_i7P0,13359
155
155
  robosystems_client/extensions/query_client.py,sha256=cX3e8EBoTeg4Lwm6edJYRULM2UmGpfqNX3f48S8TQbE,19430
156
- robosystems_client/extensions/sse_client.py,sha256=XvQIq3JQ0Yiax11E7cwclhupShYOpEMURM2cYQodiz8,15058
157
- robosystems_client/extensions/subgraph_workspace_client.py,sha256=Ioc7FNJEKaD_kAJBeymwtFlVI-U9t47RouD5ibUHv4g,24036
156
+ robosystems_client/extensions/sse_client.py,sha256=rqYs3mfnLhE3kwdhEWcbeiAFfT_6AFZRY5rpXpV5KHg,14956
157
+ robosystems_client/extensions/subgraph_workspace_client.py,sha256=ISMo7xkOB1Py35njIV_qjw94RJJKEZk8Ag4RGkII3-o,24208
158
158
  robosystems_client/extensions/table_client.py,sha256=hegYiPlR-HMacyinDDACZaMeNn0bamkYtbkzDkVawVI,4521
159
159
  robosystems_client/extensions/token_utils.py,sha256=qCK_s1vBzRnSYwtgncPZRLJVIw3WXmzqNTWjdEEpdgs,10899
160
160
  robosystems_client/extensions/utils.py,sha256=vhmUnEsq-UEAMgNhmkqlbJg4oJj096QPiHALEHJ-y4A,16207
@@ -300,7 +300,7 @@ robosystems_client/models/list_table_files_response.py,sha256=D4SuwlQy11oOhEq9k2
300
300
  robosystems_client/models/login_request.py,sha256=8LbzTIfoUWAKzkEYs7E6AXr4TEPAeejoX8kIMBRm7XQ,1579
301
301
  robosystems_client/models/logout_user_response_logoutuser.py,sha256=dpvqyIqCDQdXVyx-gDyl3qex2KuyjJUE0OuldQcL4k0,1257
302
302
  robosystems_client/models/materialize_request.py,sha256=t3TXLcPJabN_lSx6BwEeku2v2zGceHFRVSS6TpK59Iw,1483
303
- robosystems_client/models/materialize_response.py,sha256=RQSN9CqXKL1HvRlZP7DGj_unMWMp4vTcsFv_BBjsIMg,3540
303
+ robosystems_client/models/materialize_response.py,sha256=Luuf3FjUj1cZQ0QmcONR5ulMbKKvjhL5SkkMx7lBp-Q,2386
304
304
  robosystems_client/models/materialize_status_response.py,sha256=eXNmPgudtCneoVb2rbSW9DGGTG10DFoGN-oaiHuYjLo,5402
305
305
  robosystems_client/models/mcp_tool_call.py,sha256=dz--5OucQNcbFT7GaUR1uw-ZgSxHI0-zVYmz0J9fixE,2215
306
306
  robosystems_client/models/mcp_tool_call_arguments.py,sha256=FX9fVbI_8tzM1XF4oKaH7Tf2WFChsSuIHn03yZY0Q5w,1245
@@ -414,7 +414,7 @@ robosystems_client/models/view_axis_config_member_labels_type_0.py,sha256=kkzpHx
414
414
  robosystems_client/models/view_config.py,sha256=HQnqYjLMXRhjZLOc5ypwILriMFKuvPzu0hPQi2vyNoM,3795
415
415
  robosystems_client/models/view_source.py,sha256=h66cASj-P_-qOptKv26uAIe9PtIewU2nTs42Ls-lFFk,4098
416
416
  robosystems_client/models/view_source_type.py,sha256=KpgczHUeOinV01jdLvytZ2URKwcsRcp1doPx2D3USyw,169
417
- robosystems_client-0.2.21.dist-info/METADATA,sha256=_OwBXwn_Y6UnpnHTr-iZ6NgzDj6jvbvqhlbPOT1N5PM,3950
418
- robosystems_client-0.2.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
419
- robosystems_client-0.2.21.dist-info/licenses/LICENSE,sha256=LjFqQPU4eQh7jAQ04SmE9eC0j74HCdXvzbo0hjW4mWo,1063
420
- robosystems_client-0.2.21.dist-info/RECORD,,
417
+ robosystems_client-0.2.23.dist-info/METADATA,sha256=z_qtJfV6oiE6r4anQJcG7tf6OfZ7mXoevVJv7rc2UwE,3950
418
+ robosystems_client-0.2.23.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
419
+ robosystems_client-0.2.23.dist-info/licenses/LICENSE,sha256=LjFqQPU4eQh7jAQ04SmE9eC0j74HCdXvzbo0hjW4mWo,1063
420
+ robosystems_client-0.2.23.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any