robosystems-client 0.1.9__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/__init__.py +14 -0
- robosystems_client/api/__init__.py +1 -0
- robosystems_client/api/agent/__init__.py +1 -0
- robosystems_client/api/agent/query_financial_agent.py +423 -0
- robosystems_client/api/auth/__init__.py +1 -0
- robosystems_client/api/auth/check_password_strength.py +172 -0
- robosystems_client/api/auth/complete_sso_auth.py +177 -0
- robosystems_client/api/auth/generate_sso_token.py +174 -0
- robosystems_client/api/auth/get_captcha_config.py +87 -0
- robosystems_client/api/auth/get_current_auth_user.py +220 -0
- robosystems_client/api/auth/get_password_policy.py +134 -0
- robosystems_client/api/auth/login_user.py +181 -0
- robosystems_client/api/auth/logout_user.py +169 -0
- robosystems_client/api/auth/refresh_session.py +174 -0
- robosystems_client/api/auth/register_user.py +189 -0
- robosystems_client/api/auth/sso_login.py +177 -0
- robosystems_client/api/auth/sso_token_exchange.py +181 -0
- robosystems_client/api/backup/__init__.py +1 -0
- robosystems_client/api/backup/create_backup.py +401 -0
- robosystems_client/api/backup/export_backup.py +225 -0
- robosystems_client/api/backup/get_backup_download_url.py +258 -0
- robosystems_client/api/backup/get_backup_stats.py +182 -0
- robosystems_client/api/backup/kuzu_backup_health.py +202 -0
- robosystems_client/api/backup/list_backups.py +217 -0
- robosystems_client/api/backup/restore_backup.py +401 -0
- robosystems_client/api/billing/__init__.py +1 -0
- robosystems_client/api/billing/get_available_subscription_plans_v1_graph_id_billing_available_plans_get.py +198 -0
- robosystems_client/api/billing/get_credit_billing_info_v1_graph_id_billing_credits_get.py +210 -0
- robosystems_client/api/billing/get_current_graph_bill.py +285 -0
- robosystems_client/api/billing/get_graph_billing_history.py +329 -0
- robosystems_client/api/billing/get_graph_monthly_bill.py +315 -0
- robosystems_client/api/billing/get_graph_pricing_info_v1_graph_id_billing_pricing_get.py +198 -0
- robosystems_client/api/billing/get_graph_subscription_v1_graph_id_billing_subscription_get.py +198 -0
- robosystems_client/api/billing/get_graph_usage_details.py +350 -0
- robosystems_client/api/billing/upgrade_graph_subscription_v1_graph_id_billing_subscription_upgrade_post.py +216 -0
- robosystems_client/api/connections/__init__.py +1 -0
- robosystems_client/api/connections/create_connection.py +327 -0
- robosystems_client/api/connections/create_link_token.py +281 -0
- robosystems_client/api/connections/delete_connection.py +278 -0
- robosystems_client/api/connections/exchange_link_token.py +301 -0
- robosystems_client/api/connections/get_connection.py +262 -0
- robosystems_client/api/connections/get_connection_options.py +285 -0
- robosystems_client/api/connections/init_o_auth.py +230 -0
- robosystems_client/api/connections/list_connections.py +314 -0
- robosystems_client/api/connections/oauth_callback.py +318 -0
- robosystems_client/api/connections/sync_connection.py +362 -0
- robosystems_client/api/create/__init__.py +1 -0
- robosystems_client/api/create/create_graph.py +375 -0
- robosystems_client/api/create/get_available_extensions.py +134 -0
- robosystems_client/api/credits_/__init__.py +1 -0
- robosystems_client/api/credits_/check_credit_balance.py +299 -0
- robosystems_client/api/credits_/check_storage_limits.py +249 -0
- robosystems_client/api/credits_/get_credit_summary.py +245 -0
- robosystems_client/api/credits_/get_storage_usage.py +279 -0
- robosystems_client/api/credits_/list_credit_transactions.py +392 -0
- robosystems_client/api/graph_analytics/__init__.py +1 -0
- robosystems_client/api/graph_analytics/get_graph_metrics.py +285 -0
- robosystems_client/api/graph_analytics/get_graph_usage_stats.py +329 -0
- robosystems_client/api/graph_status/__init__.py +1 -0
- robosystems_client/api/graph_status/get_database_health.py +273 -0
- robosystems_client/api/graph_status/get_database_info.py +277 -0
- robosystems_client/api/mcp/__init__.py +1 -0
- robosystems_client/api/mcp/call_mcp_tool.py +432 -0
- robosystems_client/api/mcp/list_mcp_tools.py +265 -0
- robosystems_client/api/operations/__init__.py +1 -0
- robosystems_client/api/operations/cancel_operation.py +246 -0
- robosystems_client/api/operations/get_operation_status.py +273 -0
- robosystems_client/api/operations/stream_operation_events.py +415 -0
- robosystems_client/api/query/__init__.py +1 -0
- robosystems_client/api/query/execute_cypher_query.py +482 -0
- robosystems_client/api/schema/__init__.py +1 -0
- robosystems_client/api/schema/export_graph_schema.py +239 -0
- robosystems_client/api/schema/get_graph_schema_info.py +277 -0
- robosystems_client/api/schema/list_schema_extensions.py +216 -0
- robosystems_client/api/schema/validate_schema.py +326 -0
- robosystems_client/api/service_offerings/__init__.py +1 -0
- robosystems_client/api/service_offerings/get_service_offerings.py +197 -0
- robosystems_client/api/status/__init__.py +1 -0
- robosystems_client/api/status/get_mcp_health.py +136 -0
- robosystems_client/api/status/get_service_status.py +134 -0
- robosystems_client/api/user/__init__.py +1 -0
- robosystems_client/api/user/create_user_api_key.py +205 -0
- robosystems_client/api/user/get_all_credit_summaries.py +256 -0
- robosystems_client/api/user/get_current_user.py +187 -0
- robosystems_client/api/user/get_user_graphs.py +187 -0
- robosystems_client/api/user/list_user_api_keys.py +187 -0
- robosystems_client/api/user/revoke_user_api_key.py +209 -0
- robosystems_client/api/user/select_user_graph.py +213 -0
- robosystems_client/api/user/update_user.py +205 -0
- robosystems_client/api/user/update_user_api_key.py +218 -0
- robosystems_client/api/user/update_user_password.py +218 -0
- robosystems_client/api/user_analytics/__init__.py +1 -0
- robosystems_client/api/user_analytics/get_detailed_user_analytics.py +222 -0
- robosystems_client/api/user_analytics/get_user_usage_overview.py +187 -0
- robosystems_client/api/user_limits/__init__.py +1 -0
- robosystems_client/api/user_limits/get_user_limits.py +190 -0
- robosystems_client/api/user_limits/get_user_usage.py +187 -0
- robosystems_client/api/user_subscriptions/__init__.py +1 -0
- robosystems_client/api/user_subscriptions/cancel_shared_repository_subscription.py +209 -0
- robosystems_client/api/user_subscriptions/get_repository_credits.py +206 -0
- robosystems_client/api/user_subscriptions/get_shared_repository_credits.py +193 -0
- robosystems_client/api/user_subscriptions/get_user_shared_subscriptions.py +213 -0
- robosystems_client/api/user_subscriptions/subscribe_to_shared_repository.py +214 -0
- robosystems_client/api/user_subscriptions/upgrade_shared_repository_subscription.py +228 -0
- robosystems_client/client.py +278 -0
- robosystems_client/errors.py +16 -0
- robosystems_client/extensions/README.md +611 -0
- robosystems_client/extensions/__init__.py +108 -0
- robosystems_client/extensions/auth_integration.py +210 -0
- robosystems_client/extensions/extensions.py +170 -0
- robosystems_client/extensions/operation_client.py +368 -0
- robosystems_client/extensions/query_client.py +375 -0
- robosystems_client/extensions/sse_client.py +520 -0
- robosystems_client/extensions/tests/__init__.py +1 -0
- robosystems_client/extensions/tests/test_integration.py +490 -0
- robosystems_client/extensions/tests/test_unit.py +560 -0
- robosystems_client/extensions/utils.py +526 -0
- robosystems_client/models/__init__.py +379 -0
- robosystems_client/models/account_info.py +79 -0
- robosystems_client/models/add_on_credit_info.py +119 -0
- robosystems_client/models/agent_message.py +68 -0
- robosystems_client/models/agent_request.py +132 -0
- robosystems_client/models/agent_request_context_type_0.py +44 -0
- robosystems_client/models/agent_response.py +132 -0
- robosystems_client/models/agent_response_metadata_type_0.py +44 -0
- robosystems_client/models/api_key_info.py +134 -0
- robosystems_client/models/api_keys_response.py +74 -0
- robosystems_client/models/auth_response.py +82 -0
- robosystems_client/models/auth_response_user.py +44 -0
- robosystems_client/models/available_extension.py +78 -0
- robosystems_client/models/available_extensions_response.py +73 -0
- robosystems_client/models/backup_create_request.py +117 -0
- robosystems_client/models/backup_export_request.py +72 -0
- robosystems_client/models/backup_list_response.py +90 -0
- robosystems_client/models/backup_response.py +200 -0
- robosystems_client/models/backup_restore_request.py +81 -0
- robosystems_client/models/backup_stats_response.py +156 -0
- robosystems_client/models/backup_stats_response_backup_formats.py +44 -0
- robosystems_client/models/cancel_operation_response_canceloperation.py +44 -0
- robosystems_client/models/cancellation_response.py +76 -0
- robosystems_client/models/check_credit_balance_response_checkcreditbalance.py +44 -0
- robosystems_client/models/connection_options_response.py +82 -0
- robosystems_client/models/connection_provider_info.py +203 -0
- robosystems_client/models/connection_provider_info_auth_type.py +11 -0
- robosystems_client/models/connection_provider_info_provider.py +10 -0
- robosystems_client/models/connection_response.py +149 -0
- robosystems_client/models/connection_response_metadata.py +44 -0
- robosystems_client/models/connection_response_provider.py +10 -0
- robosystems_client/models/create_api_key_request.py +82 -0
- robosystems_client/models/create_api_key_response.py +74 -0
- robosystems_client/models/create_connection_request.py +179 -0
- robosystems_client/models/create_connection_request_provider.py +10 -0
- robosystems_client/models/create_graph_request.py +183 -0
- robosystems_client/models/credit_check_request.py +82 -0
- robosystems_client/models/credit_summary.py +128 -0
- robosystems_client/models/credit_summary_response.py +140 -0
- robosystems_client/models/credits_summary_response.py +122 -0
- robosystems_client/models/credits_summary_response_credits_by_addon_item.py +44 -0
- robosystems_client/models/custom_schema_definition.py +194 -0
- robosystems_client/models/custom_schema_definition_metadata.py +49 -0
- robosystems_client/models/custom_schema_definition_nodes_item.py +44 -0
- robosystems_client/models/custom_schema_definition_relationships_item.py +44 -0
- robosystems_client/models/cypher_query_request.py +128 -0
- robosystems_client/models/cypher_query_request_parameters_type_0.py +44 -0
- robosystems_client/models/database_health_response.py +181 -0
- robosystems_client/models/database_info_response.py +191 -0
- robosystems_client/models/detailed_transactions_response.py +124 -0
- robosystems_client/models/detailed_transactions_response_date_range.py +44 -0
- robosystems_client/models/detailed_transactions_response_summary.py +59 -0
- robosystems_client/models/enhanced_credit_transaction_response.py +192 -0
- robosystems_client/models/enhanced_credit_transaction_response_metadata.py +44 -0
- robosystems_client/models/error_response.py +145 -0
- robosystems_client/models/exchange_token_request.py +116 -0
- robosystems_client/models/exchange_token_request_metadata_type_0.py +44 -0
- robosystems_client/models/get_all_credit_summaries_response_getallcreditsummaries.py +44 -0
- robosystems_client/models/get_backup_download_url_response_getbackupdownloadurl.py +44 -0
- robosystems_client/models/get_current_auth_user_response_getcurrentauthuser.py +44 -0
- robosystems_client/models/get_current_graph_bill_response_getcurrentgraphbill.py +44 -0
- robosystems_client/models/get_graph_billing_history_response_getgraphbillinghistory.py +44 -0
- robosystems_client/models/get_graph_monthly_bill_response_getgraphmonthlybill.py +44 -0
- robosystems_client/models/get_graph_schema_info_response_getgraphschemainfo.py +44 -0
- robosystems_client/models/get_graph_usage_details_response_getgraphusagedetails.py +44 -0
- robosystems_client/models/get_mcp_health_response_getmcphealth.py +44 -0
- robosystems_client/models/get_operation_status_response_getoperationstatus.py +44 -0
- robosystems_client/models/get_storage_usage_response_getstorageusage.py +44 -0
- robosystems_client/models/graph_info.py +92 -0
- robosystems_client/models/graph_metadata.py +105 -0
- robosystems_client/models/graph_metrics_response.py +188 -0
- robosystems_client/models/graph_metrics_response_estimated_size.py +44 -0
- robosystems_client/models/graph_metrics_response_health_status.py +44 -0
- robosystems_client/models/graph_metrics_response_node_counts.py +44 -0
- robosystems_client/models/graph_metrics_response_relationship_counts.py +44 -0
- robosystems_client/models/graph_usage_response.py +116 -0
- robosystems_client/models/graph_usage_response_query_statistics.py +44 -0
- robosystems_client/models/graph_usage_response_recent_activity.py +44 -0
- robosystems_client/models/graph_usage_response_storage_usage.py +44 -0
- robosystems_client/models/health_status.py +110 -0
- robosystems_client/models/health_status_details_type_0.py +44 -0
- robosystems_client/models/http_validation_error.py +75 -0
- robosystems_client/models/initial_entity_data.py +212 -0
- robosystems_client/models/kuzu_backup_health_response_kuzubackuphealth.py +44 -0
- robosystems_client/models/link_token_request.py +174 -0
- robosystems_client/models/link_token_request_options_type_0.py +44 -0
- robosystems_client/models/link_token_request_provider_type_0.py +10 -0
- robosystems_client/models/list_connections_provider_type_0.py +10 -0
- robosystems_client/models/list_schema_extensions_response_listschemaextensions.py +44 -0
- robosystems_client/models/login_request.py +68 -0
- robosystems_client/models/logout_user_response_logoutuser.py +44 -0
- robosystems_client/models/mcp_tool_call.py +84 -0
- robosystems_client/models/mcp_tool_call_arguments.py +44 -0
- robosystems_client/models/mcp_tools_response.py +74 -0
- robosystems_client/models/mcp_tools_response_tools_item.py +44 -0
- robosystems_client/models/o_auth_callback_request.py +130 -0
- robosystems_client/models/o_auth_init_request.py +128 -0
- robosystems_client/models/o_auth_init_request_additional_params_type_0.py +44 -0
- robosystems_client/models/o_auth_init_response.py +78 -0
- robosystems_client/models/password_check_request.py +82 -0
- robosystems_client/models/password_check_response.py +112 -0
- robosystems_client/models/password_check_response_character_types.py +44 -0
- robosystems_client/models/password_policy_response.py +66 -0
- robosystems_client/models/password_policy_response_policy.py +44 -0
- robosystems_client/models/plaid_connection_config.py +209 -0
- robosystems_client/models/plaid_connection_config_accounts_type_0_item.py +44 -0
- robosystems_client/models/plaid_connection_config_institution_type_0.py +44 -0
- robosystems_client/models/quick_books_connection_config.py +92 -0
- robosystems_client/models/register_request.py +98 -0
- robosystems_client/models/repository_credits_response.py +101 -0
- robosystems_client/models/repository_plan.py +10 -0
- robosystems_client/models/repository_type.py +10 -0
- robosystems_client/models/response_mode.py +11 -0
- robosystems_client/models/schema_export_response.py +163 -0
- robosystems_client/models/schema_export_response_data_stats_type_0.py +44 -0
- robosystems_client/models/schema_export_response_schema_definition_type_0.py +44 -0
- robosystems_client/models/schema_validation_request.py +142 -0
- robosystems_client/models/schema_validation_request_schema_definition_type_0.py +44 -0
- robosystems_client/models/schema_validation_response.py +227 -0
- robosystems_client/models/schema_validation_response_compatibility_type_0.py +44 -0
- robosystems_client/models/schema_validation_response_stats_type_0.py +44 -0
- robosystems_client/models/sec_connection_config.py +82 -0
- robosystems_client/models/sso_complete_request.py +60 -0
- robosystems_client/models/sso_exchange_request.py +90 -0
- robosystems_client/models/sso_exchange_response.py +78 -0
- robosystems_client/models/sso_login_request.py +60 -0
- robosystems_client/models/sso_token_response.py +78 -0
- robosystems_client/models/storage_limit_response.py +149 -0
- robosystems_client/models/subscription_info.py +180 -0
- robosystems_client/models/subscription_info_metadata.py +44 -0
- robosystems_client/models/subscription_request.py +89 -0
- robosystems_client/models/subscription_response.py +82 -0
- robosystems_client/models/success_response.py +112 -0
- robosystems_client/models/success_response_data_type_0.py +44 -0
- robosystems_client/models/sync_connection_request.py +106 -0
- robosystems_client/models/sync_connection_request_sync_options_type_0.py +44 -0
- robosystems_client/models/sync_connection_response_syncconnection.py +44 -0
- robosystems_client/models/tier_upgrade_request.py +62 -0
- robosystems_client/models/transaction_summary_response.py +126 -0
- robosystems_client/models/update_api_key_request.py +92 -0
- robosystems_client/models/update_password_request.py +76 -0
- robosystems_client/models/update_user_request.py +92 -0
- robosystems_client/models/upgrade_subscription_request.py +82 -0
- robosystems_client/models/user_analytics_response.py +132 -0
- robosystems_client/models/user_analytics_response_api_usage.py +44 -0
- robosystems_client/models/user_analytics_response_graph_usage.py +44 -0
- robosystems_client/models/user_analytics_response_limits.py +44 -0
- robosystems_client/models/user_analytics_response_recent_activity_item.py +44 -0
- robosystems_client/models/user_analytics_response_user_info.py +44 -0
- robosystems_client/models/user_graph_summary.py +134 -0
- robosystems_client/models/user_graphs_response.py +96 -0
- robosystems_client/models/user_limits_response.py +95 -0
- robosystems_client/models/user_response.py +132 -0
- robosystems_client/models/user_subscriptions_response.py +90 -0
- robosystems_client/models/user_usage_response.py +90 -0
- robosystems_client/models/user_usage_response_graphs.py +44 -0
- robosystems_client/models/user_usage_summary_response.py +130 -0
- robosystems_client/models/user_usage_summary_response_usage_vs_limits.py +44 -0
- robosystems_client/models/validation_error.py +88 -0
- robosystems_client/py.typed +1 -0
- robosystems_client/sdk-config.yaml +5 -0
- robosystems_client/types.py +54 -0
- robosystems_client-0.1.9.dist-info/METADATA +302 -0
- robosystems_client-0.1.9.dist-info/RECORD +282 -0
- robosystems_client-0.1.9.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""Core SSE (Server-Sent Events) client for RoboSystems API
|
|
2
|
+
|
|
3
|
+
Provides automatic reconnection, event replay, and type-safe event handling.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import asyncio
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Dict, Any, Optional, Callable, Set, TYPE_CHECKING
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from urllib.parse import urljoin
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import sseclient
|
|
17
|
+
except ImportError:
|
|
18
|
+
sseclient = None
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import httpx
|
|
22
|
+
except ImportError:
|
|
23
|
+
httpx = None
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
if httpx:
|
|
27
|
+
from httpx import Client, AsyncClient
|
|
28
|
+
else:
|
|
29
|
+
Client = Any
|
|
30
|
+
AsyncClient = Any
|
|
31
|
+
else:
|
|
32
|
+
Client = Any
|
|
33
|
+
AsyncClient = Any
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class SSEConfig:
|
|
38
|
+
"""Configuration for SSE client"""
|
|
39
|
+
|
|
40
|
+
base_url: str
|
|
41
|
+
headers: Optional[Dict[str, str]] = None
|
|
42
|
+
max_retries: int = 5
|
|
43
|
+
retry_delay: int = 1000 # milliseconds
|
|
44
|
+
heartbeat_interval: int = 30000 # milliseconds
|
|
45
|
+
timeout: int = 30 # seconds
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class SSEEvent:
|
|
50
|
+
"""Represents an SSE event"""
|
|
51
|
+
|
|
52
|
+
event: str
|
|
53
|
+
data: Any
|
|
54
|
+
id: Optional[str] = None
|
|
55
|
+
retry: Optional[int] = None
|
|
56
|
+
timestamp: datetime = None
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
if self.timestamp is None:
|
|
60
|
+
self.timestamp = datetime.now()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class EventType(Enum):
|
|
64
|
+
"""Standard event types from RoboSystems API"""
|
|
65
|
+
|
|
66
|
+
OPERATION_STARTED = "operation_started"
|
|
67
|
+
OPERATION_PROGRESS = "operation_progress"
|
|
68
|
+
OPERATION_COMPLETED = "operation_completed"
|
|
69
|
+
OPERATION_ERROR = "operation_error"
|
|
70
|
+
OPERATION_CANCELLED = "operation_cancelled"
|
|
71
|
+
DATA_CHUNK = "data_chunk"
|
|
72
|
+
METADATA = "metadata"
|
|
73
|
+
HEARTBEAT = "heartbeat"
|
|
74
|
+
QUEUE_UPDATE = "queue_update"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SSEClient:
|
|
78
|
+
"""SSE client for RoboSystems API with automatic reconnection"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, config: SSEConfig) -> None:
|
|
81
|
+
if not httpx:
|
|
82
|
+
raise ImportError(
|
|
83
|
+
"httpx is required for SSE client. Install with: pip install httpx"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self.config = config
|
|
87
|
+
self.client: Optional[Client] = None
|
|
88
|
+
self.reconnect_attempts = 0
|
|
89
|
+
self.last_event_id: Optional[str] = None
|
|
90
|
+
self.closed = False
|
|
91
|
+
self.listeners: Dict[str, Set[Callable]] = {}
|
|
92
|
+
self._response = None
|
|
93
|
+
|
|
94
|
+
def connect(self, operation_id: str, from_sequence: int = 0) -> None:
|
|
95
|
+
"""Connect to SSE stream for the given operation"""
|
|
96
|
+
url = urljoin(self.config.base_url, f"/v1/operations/{operation_id}/stream")
|
|
97
|
+
params = {"from_sequence": from_sequence}
|
|
98
|
+
|
|
99
|
+
headers = {
|
|
100
|
+
"Accept": "text/event-stream",
|
|
101
|
+
"Cache-Control": "no-cache",
|
|
102
|
+
**(self.config.headers or {}),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try:
|
|
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__()
|
|
109
|
+
|
|
110
|
+
self.reconnect_attempts = 0
|
|
111
|
+
self.emit("connected", None)
|
|
112
|
+
|
|
113
|
+
# Start processing events
|
|
114
|
+
self._process_events()
|
|
115
|
+
|
|
116
|
+
except Exception as error:
|
|
117
|
+
if not self.closed:
|
|
118
|
+
self._handle_error(error, operation_id, from_sequence)
|
|
119
|
+
|
|
120
|
+
def _process_events(self) -> None:
|
|
121
|
+
"""Process incoming SSE events according to SSE specification"""
|
|
122
|
+
if not self._response:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
event_buffer = {"event": None, "data": [], "id": None, "retry": None}
|
|
127
|
+
|
|
128
|
+
for line in self._response.iter_lines():
|
|
129
|
+
if self.closed:
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
line = line.strip()
|
|
133
|
+
|
|
134
|
+
# Empty line indicates end of event
|
|
135
|
+
if not line:
|
|
136
|
+
if event_buffer["data"] or event_buffer["event"]:
|
|
137
|
+
self._dispatch_event(event_buffer)
|
|
138
|
+
event_buffer = {"event": None, "data": [], "id": None, "retry": None}
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Skip comment lines
|
|
142
|
+
if line.startswith(":"):
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Parse field
|
|
146
|
+
if ":" in line:
|
|
147
|
+
field, value = line.split(":", 1)
|
|
148
|
+
field = field.strip()
|
|
149
|
+
value = value.lstrip() # Remove leading space but keep others
|
|
150
|
+
|
|
151
|
+
if field == "data":
|
|
152
|
+
event_buffer["data"].append(value)
|
|
153
|
+
elif field == "event":
|
|
154
|
+
event_buffer["event"] = value
|
|
155
|
+
elif field == "id":
|
|
156
|
+
event_buffer["id"] = value
|
|
157
|
+
self.last_event_id = value
|
|
158
|
+
elif field == "retry":
|
|
159
|
+
try:
|
|
160
|
+
event_buffer["retry"] = int(value)
|
|
161
|
+
except ValueError:
|
|
162
|
+
pass # Ignore invalid retry values
|
|
163
|
+
else:
|
|
164
|
+
# Field with no value
|
|
165
|
+
if line == "data":
|
|
166
|
+
event_buffer["data"].append("")
|
|
167
|
+
elif line in ["event", "id", "retry"]:
|
|
168
|
+
event_buffer[line] = ""
|
|
169
|
+
|
|
170
|
+
# Handle final event if stream ends without empty line
|
|
171
|
+
if event_buffer["data"] or event_buffer["event"]:
|
|
172
|
+
self._dispatch_event(event_buffer)
|
|
173
|
+
|
|
174
|
+
except Exception as error:
|
|
175
|
+
if not self.closed:
|
|
176
|
+
self.emit("error", error)
|
|
177
|
+
|
|
178
|
+
def _dispatch_event(self, event_buffer: Dict[str, Any]) -> None:
|
|
179
|
+
"""Dispatch a complete SSE event"""
|
|
180
|
+
# Join data lines with newlines as per SSE spec
|
|
181
|
+
data_str = "\n".join(event_buffer["data"])
|
|
182
|
+
|
|
183
|
+
if not data_str and not event_buffer["event"]:
|
|
184
|
+
return # Skip empty events
|
|
185
|
+
|
|
186
|
+
event_type = event_buffer["event"] or "message"
|
|
187
|
+
|
|
188
|
+
# Parse JSON data if possible
|
|
189
|
+
parsed_data = data_str
|
|
190
|
+
try:
|
|
191
|
+
if data_str:
|
|
192
|
+
parsed_data = json.loads(data_str)
|
|
193
|
+
except json.JSONDecodeError:
|
|
194
|
+
# Keep as string if not valid JSON
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
sse_event = SSEEvent(
|
|
198
|
+
event=event_type,
|
|
199
|
+
data=parsed_data,
|
|
200
|
+
id=event_buffer["id"],
|
|
201
|
+
timestamp=datetime.now(),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Emit generic event
|
|
205
|
+
self.emit("event", sse_event)
|
|
206
|
+
|
|
207
|
+
# Emit typed event
|
|
208
|
+
self.emit(event_type, parsed_data)
|
|
209
|
+
|
|
210
|
+
# Check for completion events
|
|
211
|
+
if event_type in [
|
|
212
|
+
EventType.OPERATION_COMPLETED.value,
|
|
213
|
+
EventType.OPERATION_ERROR.value,
|
|
214
|
+
EventType.OPERATION_CANCELLED.value,
|
|
215
|
+
]:
|
|
216
|
+
self.close()
|
|
217
|
+
|
|
218
|
+
def _handle_error(
|
|
219
|
+
self, error: Exception, operation_id: str, from_sequence: int
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Handle connection errors with retry logic"""
|
|
222
|
+
if self.closed:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if self.reconnect_attempts < self.config.max_retries:
|
|
226
|
+
self.reconnect_attempts += 1
|
|
227
|
+
delay_ms = self.config.retry_delay * (2 ** (self.reconnect_attempts - 1))
|
|
228
|
+
delay_seconds = delay_ms / 1000
|
|
229
|
+
|
|
230
|
+
self.emit(
|
|
231
|
+
"reconnecting",
|
|
232
|
+
{
|
|
233
|
+
"attempt": self.reconnect_attempts,
|
|
234
|
+
"delay": delay_ms,
|
|
235
|
+
"last_event_id": self.last_event_id,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
time.sleep(delay_seconds)
|
|
240
|
+
|
|
241
|
+
# Resume from last event if available
|
|
242
|
+
resume_from = 0
|
|
243
|
+
if self.last_event_id:
|
|
244
|
+
try:
|
|
245
|
+
resume_from = int(self.last_event_id) + 1
|
|
246
|
+
except ValueError:
|
|
247
|
+
resume_from = from_sequence
|
|
248
|
+
else:
|
|
249
|
+
resume_from = from_sequence
|
|
250
|
+
|
|
251
|
+
self.connect(operation_id, resume_from)
|
|
252
|
+
else:
|
|
253
|
+
self.emit("max_retries_exceeded", error)
|
|
254
|
+
self.close()
|
|
255
|
+
|
|
256
|
+
def on(self, event: str, listener: Callable[[Any], None]) -> None:
|
|
257
|
+
"""Add event listener"""
|
|
258
|
+
if event not in self.listeners:
|
|
259
|
+
self.listeners[event] = set()
|
|
260
|
+
self.listeners[event].add(listener)
|
|
261
|
+
|
|
262
|
+
def off(self, event: str, listener: Callable[[Any], None]) -> None:
|
|
263
|
+
"""Remove event listener"""
|
|
264
|
+
if event in self.listeners:
|
|
265
|
+
self.listeners[event].discard(listener)
|
|
266
|
+
|
|
267
|
+
def emit(self, event: str, data: Any) -> None:
|
|
268
|
+
"""Emit event to all listeners"""
|
|
269
|
+
if event in self.listeners:
|
|
270
|
+
for listener in self.listeners[event]:
|
|
271
|
+
try:
|
|
272
|
+
listener(data)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
# Log error but don't stop other listeners
|
|
275
|
+
print(f"Error in event listener for {event}: {e}")
|
|
276
|
+
|
|
277
|
+
def close(self):
|
|
278
|
+
"""Close the SSE connection"""
|
|
279
|
+
self.closed = True
|
|
280
|
+
|
|
281
|
+
if self._response:
|
|
282
|
+
try:
|
|
283
|
+
self._response.__exit__(None, None, None)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
self._response = None
|
|
287
|
+
|
|
288
|
+
if self.client:
|
|
289
|
+
self.client.close()
|
|
290
|
+
self.client = None
|
|
291
|
+
|
|
292
|
+
self.emit("closed", None)
|
|
293
|
+
self.listeners.clear()
|
|
294
|
+
|
|
295
|
+
def is_connected(self) -> bool:
|
|
296
|
+
"""Check if the connection is active"""
|
|
297
|
+
return self.client is not None and not self.closed
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class AsyncSSEClient:
|
|
301
|
+
"""Async version of SSE client"""
|
|
302
|
+
|
|
303
|
+
def __init__(self, config: SSEConfig) -> None:
|
|
304
|
+
if not httpx:
|
|
305
|
+
raise ImportError(
|
|
306
|
+
"httpx is required for async SSE client. Install with: pip install httpx"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
self.config = config
|
|
310
|
+
self.client: Optional[AsyncClient] = None
|
|
311
|
+
self.reconnect_attempts = 0
|
|
312
|
+
self.last_event_id: Optional[str] = None
|
|
313
|
+
self.closed = False
|
|
314
|
+
self.listeners: Dict[str, Set[Callable]] = {}
|
|
315
|
+
self._response = None
|
|
316
|
+
|
|
317
|
+
async def connect(self, operation_id: str, from_sequence: int = 0) -> None:
|
|
318
|
+
"""Connect to SSE stream for the given operation (async)"""
|
|
319
|
+
url = urljoin(self.config.base_url, f"/v1/operations/{operation_id}/stream")
|
|
320
|
+
params = {"from_sequence": from_sequence}
|
|
321
|
+
|
|
322
|
+
headers = {
|
|
323
|
+
"Accept": "text/event-stream",
|
|
324
|
+
"Cache-Control": "no-cache",
|
|
325
|
+
**(self.config.headers or {}),
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
self.client = httpx.AsyncClient(timeout=self.config.timeout)
|
|
330
|
+
self._response = await self.client.stream(
|
|
331
|
+
"GET", url, params=params, headers=headers
|
|
332
|
+
)
|
|
333
|
+
await self._response.__aenter__()
|
|
334
|
+
|
|
335
|
+
self.reconnect_attempts = 0
|
|
336
|
+
self.emit("connected", None)
|
|
337
|
+
|
|
338
|
+
# Start processing events
|
|
339
|
+
await self._process_events()
|
|
340
|
+
|
|
341
|
+
except Exception as error:
|
|
342
|
+
if not self.closed:
|
|
343
|
+
await self._handle_error(error, operation_id, from_sequence)
|
|
344
|
+
|
|
345
|
+
async def _process_events(self) -> None:
|
|
346
|
+
"""Process incoming SSE events according to SSE specification (async)"""
|
|
347
|
+
if not self._response:
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
event_buffer = {"event": None, "data": [], "id": None, "retry": None}
|
|
352
|
+
|
|
353
|
+
async for line in self._response.aiter_lines():
|
|
354
|
+
if self.closed:
|
|
355
|
+
break
|
|
356
|
+
|
|
357
|
+
line = line.strip()
|
|
358
|
+
|
|
359
|
+
# Empty line indicates end of event
|
|
360
|
+
if not line:
|
|
361
|
+
if event_buffer["data"] or event_buffer["event"]:
|
|
362
|
+
self._dispatch_event(event_buffer)
|
|
363
|
+
event_buffer = {"event": None, "data": [], "id": None, "retry": None}
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
# Skip comment lines
|
|
367
|
+
if line.startswith(":"):
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
# Parse field
|
|
371
|
+
if ":" in line:
|
|
372
|
+
field, value = line.split(":", 1)
|
|
373
|
+
field = field.strip()
|
|
374
|
+
value = value.lstrip()
|
|
375
|
+
|
|
376
|
+
if field == "data":
|
|
377
|
+
event_buffer["data"].append(value)
|
|
378
|
+
elif field == "event":
|
|
379
|
+
event_buffer["event"] = value
|
|
380
|
+
elif field == "id":
|
|
381
|
+
event_buffer["id"] = value
|
|
382
|
+
self.last_event_id = value
|
|
383
|
+
elif field == "retry":
|
|
384
|
+
try:
|
|
385
|
+
event_buffer["retry"] = int(value)
|
|
386
|
+
except ValueError:
|
|
387
|
+
pass
|
|
388
|
+
else:
|
|
389
|
+
# Field with no value
|
|
390
|
+
if line == "data":
|
|
391
|
+
event_buffer["data"].append("")
|
|
392
|
+
elif line in ["event", "id", "retry"]:
|
|
393
|
+
event_buffer[line] = ""
|
|
394
|
+
|
|
395
|
+
# Handle final event if stream ends without empty line
|
|
396
|
+
if event_buffer["data"] or event_buffer["event"]:
|
|
397
|
+
self._dispatch_event(event_buffer)
|
|
398
|
+
|
|
399
|
+
except Exception as error:
|
|
400
|
+
if not self.closed:
|
|
401
|
+
self.emit("error", error)
|
|
402
|
+
|
|
403
|
+
def _dispatch_event(self, event_buffer: Dict[str, Any]):
|
|
404
|
+
"""Dispatch a complete SSE event (same as sync version)"""
|
|
405
|
+
# Join data lines with newlines as per SSE spec
|
|
406
|
+
data_str = "\n".join(event_buffer["data"])
|
|
407
|
+
|
|
408
|
+
if not data_str and not event_buffer["event"]:
|
|
409
|
+
return # Skip empty events
|
|
410
|
+
|
|
411
|
+
event_type = event_buffer["event"] or "message"
|
|
412
|
+
|
|
413
|
+
# Parse JSON data if possible
|
|
414
|
+
parsed_data = data_str
|
|
415
|
+
try:
|
|
416
|
+
if data_str:
|
|
417
|
+
parsed_data = json.loads(data_str)
|
|
418
|
+
except json.JSONDecodeError:
|
|
419
|
+
# Keep as string if not valid JSON
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
sse_event = SSEEvent(
|
|
423
|
+
event=event_type,
|
|
424
|
+
data=parsed_data,
|
|
425
|
+
id=event_buffer["id"],
|
|
426
|
+
timestamp=datetime.now(),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Emit generic event
|
|
430
|
+
self.emit("event", sse_event)
|
|
431
|
+
|
|
432
|
+
# Emit typed event
|
|
433
|
+
self.emit(event_type, parsed_data)
|
|
434
|
+
|
|
435
|
+
# Check for completion events
|
|
436
|
+
if event_type in [
|
|
437
|
+
EventType.OPERATION_COMPLETED.value,
|
|
438
|
+
EventType.OPERATION_ERROR.value,
|
|
439
|
+
EventType.OPERATION_CANCELLED.value,
|
|
440
|
+
]:
|
|
441
|
+
asyncio.create_task(self.close())
|
|
442
|
+
|
|
443
|
+
async def _handle_error(
|
|
444
|
+
self, error: Exception, operation_id: str, from_sequence: int
|
|
445
|
+
) -> None:
|
|
446
|
+
"""Handle connection errors with retry logic (async)"""
|
|
447
|
+
if self.closed:
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
if self.reconnect_attempts < self.config.max_retries:
|
|
451
|
+
self.reconnect_attempts += 1
|
|
452
|
+
delay_ms = self.config.retry_delay * (2 ** (self.reconnect_attempts - 1))
|
|
453
|
+
delay_seconds = delay_ms / 1000
|
|
454
|
+
|
|
455
|
+
self.emit(
|
|
456
|
+
"reconnecting",
|
|
457
|
+
{
|
|
458
|
+
"attempt": self.reconnect_attempts,
|
|
459
|
+
"delay": delay_ms,
|
|
460
|
+
"last_event_id": self.last_event_id,
|
|
461
|
+
},
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
await asyncio.sleep(delay_seconds)
|
|
465
|
+
|
|
466
|
+
resume_from = 0
|
|
467
|
+
if self.last_event_id:
|
|
468
|
+
try:
|
|
469
|
+
resume_from = int(self.last_event_id) + 1
|
|
470
|
+
except ValueError:
|
|
471
|
+
resume_from = from_sequence
|
|
472
|
+
else:
|
|
473
|
+
resume_from = from_sequence
|
|
474
|
+
|
|
475
|
+
await self.connect(operation_id, resume_from)
|
|
476
|
+
else:
|
|
477
|
+
self.emit("max_retries_exceeded", error)
|
|
478
|
+
await self.close()
|
|
479
|
+
|
|
480
|
+
def on(self, event: str, listener: Callable[[Any], None]) -> None:
|
|
481
|
+
"""Add event listener"""
|
|
482
|
+
if event not in self.listeners:
|
|
483
|
+
self.listeners[event] = set()
|
|
484
|
+
self.listeners[event].add(listener)
|
|
485
|
+
|
|
486
|
+
def off(self, event: str, listener: Callable[[Any], None]) -> None:
|
|
487
|
+
"""Remove event listener"""
|
|
488
|
+
if event in self.listeners:
|
|
489
|
+
self.listeners[event].discard(listener)
|
|
490
|
+
|
|
491
|
+
def emit(self, event: str, data: Any) -> None:
|
|
492
|
+
"""Emit event to all listeners"""
|
|
493
|
+
if event in self.listeners:
|
|
494
|
+
for listener in self.listeners[event]:
|
|
495
|
+
try:
|
|
496
|
+
listener(data)
|
|
497
|
+
except Exception as e:
|
|
498
|
+
print(f"Error in event listener for {event}: {e}")
|
|
499
|
+
|
|
500
|
+
async def close(self):
|
|
501
|
+
"""Close the SSE connection (async)"""
|
|
502
|
+
self.closed = True
|
|
503
|
+
|
|
504
|
+
if self._response:
|
|
505
|
+
try:
|
|
506
|
+
await self._response.__aexit__(None, None, None)
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
self._response = None
|
|
510
|
+
|
|
511
|
+
if self.client:
|
|
512
|
+
await self.client.aclose()
|
|
513
|
+
self.client = None
|
|
514
|
+
|
|
515
|
+
self.emit("closed", None)
|
|
516
|
+
self.listeners.clear()
|
|
517
|
+
|
|
518
|
+
def is_connected(self) -> bool:
|
|
519
|
+
"""Check if the connection is active"""
|
|
520
|
+
return self.client is not None and not self.closed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for RoboSystems SDK Extensions"""
|