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.

Files changed (282) hide show
  1. robosystems_client/__init__.py +14 -0
  2. robosystems_client/api/__init__.py +1 -0
  3. robosystems_client/api/agent/__init__.py +1 -0
  4. robosystems_client/api/agent/query_financial_agent.py +423 -0
  5. robosystems_client/api/auth/__init__.py +1 -0
  6. robosystems_client/api/auth/check_password_strength.py +172 -0
  7. robosystems_client/api/auth/complete_sso_auth.py +177 -0
  8. robosystems_client/api/auth/generate_sso_token.py +174 -0
  9. robosystems_client/api/auth/get_captcha_config.py +87 -0
  10. robosystems_client/api/auth/get_current_auth_user.py +220 -0
  11. robosystems_client/api/auth/get_password_policy.py +134 -0
  12. robosystems_client/api/auth/login_user.py +181 -0
  13. robosystems_client/api/auth/logout_user.py +169 -0
  14. robosystems_client/api/auth/refresh_session.py +174 -0
  15. robosystems_client/api/auth/register_user.py +189 -0
  16. robosystems_client/api/auth/sso_login.py +177 -0
  17. robosystems_client/api/auth/sso_token_exchange.py +181 -0
  18. robosystems_client/api/backup/__init__.py +1 -0
  19. robosystems_client/api/backup/create_backup.py +401 -0
  20. robosystems_client/api/backup/export_backup.py +225 -0
  21. robosystems_client/api/backup/get_backup_download_url.py +258 -0
  22. robosystems_client/api/backup/get_backup_stats.py +182 -0
  23. robosystems_client/api/backup/kuzu_backup_health.py +202 -0
  24. robosystems_client/api/backup/list_backups.py +217 -0
  25. robosystems_client/api/backup/restore_backup.py +401 -0
  26. robosystems_client/api/billing/__init__.py +1 -0
  27. robosystems_client/api/billing/get_available_subscription_plans_v1_graph_id_billing_available_plans_get.py +198 -0
  28. robosystems_client/api/billing/get_credit_billing_info_v1_graph_id_billing_credits_get.py +210 -0
  29. robosystems_client/api/billing/get_current_graph_bill.py +285 -0
  30. robosystems_client/api/billing/get_graph_billing_history.py +329 -0
  31. robosystems_client/api/billing/get_graph_monthly_bill.py +315 -0
  32. robosystems_client/api/billing/get_graph_pricing_info_v1_graph_id_billing_pricing_get.py +198 -0
  33. robosystems_client/api/billing/get_graph_subscription_v1_graph_id_billing_subscription_get.py +198 -0
  34. robosystems_client/api/billing/get_graph_usage_details.py +350 -0
  35. robosystems_client/api/billing/upgrade_graph_subscription_v1_graph_id_billing_subscription_upgrade_post.py +216 -0
  36. robosystems_client/api/connections/__init__.py +1 -0
  37. robosystems_client/api/connections/create_connection.py +327 -0
  38. robosystems_client/api/connections/create_link_token.py +281 -0
  39. robosystems_client/api/connections/delete_connection.py +278 -0
  40. robosystems_client/api/connections/exchange_link_token.py +301 -0
  41. robosystems_client/api/connections/get_connection.py +262 -0
  42. robosystems_client/api/connections/get_connection_options.py +285 -0
  43. robosystems_client/api/connections/init_o_auth.py +230 -0
  44. robosystems_client/api/connections/list_connections.py +314 -0
  45. robosystems_client/api/connections/oauth_callback.py +318 -0
  46. robosystems_client/api/connections/sync_connection.py +362 -0
  47. robosystems_client/api/create/__init__.py +1 -0
  48. robosystems_client/api/create/create_graph.py +375 -0
  49. robosystems_client/api/create/get_available_extensions.py +134 -0
  50. robosystems_client/api/credits_/__init__.py +1 -0
  51. robosystems_client/api/credits_/check_credit_balance.py +299 -0
  52. robosystems_client/api/credits_/check_storage_limits.py +249 -0
  53. robosystems_client/api/credits_/get_credit_summary.py +245 -0
  54. robosystems_client/api/credits_/get_storage_usage.py +279 -0
  55. robosystems_client/api/credits_/list_credit_transactions.py +392 -0
  56. robosystems_client/api/graph_analytics/__init__.py +1 -0
  57. robosystems_client/api/graph_analytics/get_graph_metrics.py +285 -0
  58. robosystems_client/api/graph_analytics/get_graph_usage_stats.py +329 -0
  59. robosystems_client/api/graph_status/__init__.py +1 -0
  60. robosystems_client/api/graph_status/get_database_health.py +273 -0
  61. robosystems_client/api/graph_status/get_database_info.py +277 -0
  62. robosystems_client/api/mcp/__init__.py +1 -0
  63. robosystems_client/api/mcp/call_mcp_tool.py +432 -0
  64. robosystems_client/api/mcp/list_mcp_tools.py +265 -0
  65. robosystems_client/api/operations/__init__.py +1 -0
  66. robosystems_client/api/operations/cancel_operation.py +246 -0
  67. robosystems_client/api/operations/get_operation_status.py +273 -0
  68. robosystems_client/api/operations/stream_operation_events.py +415 -0
  69. robosystems_client/api/query/__init__.py +1 -0
  70. robosystems_client/api/query/execute_cypher_query.py +482 -0
  71. robosystems_client/api/schema/__init__.py +1 -0
  72. robosystems_client/api/schema/export_graph_schema.py +239 -0
  73. robosystems_client/api/schema/get_graph_schema_info.py +277 -0
  74. robosystems_client/api/schema/list_schema_extensions.py +216 -0
  75. robosystems_client/api/schema/validate_schema.py +326 -0
  76. robosystems_client/api/service_offerings/__init__.py +1 -0
  77. robosystems_client/api/service_offerings/get_service_offerings.py +197 -0
  78. robosystems_client/api/status/__init__.py +1 -0
  79. robosystems_client/api/status/get_mcp_health.py +136 -0
  80. robosystems_client/api/status/get_service_status.py +134 -0
  81. robosystems_client/api/user/__init__.py +1 -0
  82. robosystems_client/api/user/create_user_api_key.py +205 -0
  83. robosystems_client/api/user/get_all_credit_summaries.py +256 -0
  84. robosystems_client/api/user/get_current_user.py +187 -0
  85. robosystems_client/api/user/get_user_graphs.py +187 -0
  86. robosystems_client/api/user/list_user_api_keys.py +187 -0
  87. robosystems_client/api/user/revoke_user_api_key.py +209 -0
  88. robosystems_client/api/user/select_user_graph.py +213 -0
  89. robosystems_client/api/user/update_user.py +205 -0
  90. robosystems_client/api/user/update_user_api_key.py +218 -0
  91. robosystems_client/api/user/update_user_password.py +218 -0
  92. robosystems_client/api/user_analytics/__init__.py +1 -0
  93. robosystems_client/api/user_analytics/get_detailed_user_analytics.py +222 -0
  94. robosystems_client/api/user_analytics/get_user_usage_overview.py +187 -0
  95. robosystems_client/api/user_limits/__init__.py +1 -0
  96. robosystems_client/api/user_limits/get_user_limits.py +190 -0
  97. robosystems_client/api/user_limits/get_user_usage.py +187 -0
  98. robosystems_client/api/user_subscriptions/__init__.py +1 -0
  99. robosystems_client/api/user_subscriptions/cancel_shared_repository_subscription.py +209 -0
  100. robosystems_client/api/user_subscriptions/get_repository_credits.py +206 -0
  101. robosystems_client/api/user_subscriptions/get_shared_repository_credits.py +193 -0
  102. robosystems_client/api/user_subscriptions/get_user_shared_subscriptions.py +213 -0
  103. robosystems_client/api/user_subscriptions/subscribe_to_shared_repository.py +214 -0
  104. robosystems_client/api/user_subscriptions/upgrade_shared_repository_subscription.py +228 -0
  105. robosystems_client/client.py +278 -0
  106. robosystems_client/errors.py +16 -0
  107. robosystems_client/extensions/README.md +611 -0
  108. robosystems_client/extensions/__init__.py +108 -0
  109. robosystems_client/extensions/auth_integration.py +210 -0
  110. robosystems_client/extensions/extensions.py +170 -0
  111. robosystems_client/extensions/operation_client.py +368 -0
  112. robosystems_client/extensions/query_client.py +375 -0
  113. robosystems_client/extensions/sse_client.py +520 -0
  114. robosystems_client/extensions/tests/__init__.py +1 -0
  115. robosystems_client/extensions/tests/test_integration.py +490 -0
  116. robosystems_client/extensions/tests/test_unit.py +560 -0
  117. robosystems_client/extensions/utils.py +526 -0
  118. robosystems_client/models/__init__.py +379 -0
  119. robosystems_client/models/account_info.py +79 -0
  120. robosystems_client/models/add_on_credit_info.py +119 -0
  121. robosystems_client/models/agent_message.py +68 -0
  122. robosystems_client/models/agent_request.py +132 -0
  123. robosystems_client/models/agent_request_context_type_0.py +44 -0
  124. robosystems_client/models/agent_response.py +132 -0
  125. robosystems_client/models/agent_response_metadata_type_0.py +44 -0
  126. robosystems_client/models/api_key_info.py +134 -0
  127. robosystems_client/models/api_keys_response.py +74 -0
  128. robosystems_client/models/auth_response.py +82 -0
  129. robosystems_client/models/auth_response_user.py +44 -0
  130. robosystems_client/models/available_extension.py +78 -0
  131. robosystems_client/models/available_extensions_response.py +73 -0
  132. robosystems_client/models/backup_create_request.py +117 -0
  133. robosystems_client/models/backup_export_request.py +72 -0
  134. robosystems_client/models/backup_list_response.py +90 -0
  135. robosystems_client/models/backup_response.py +200 -0
  136. robosystems_client/models/backup_restore_request.py +81 -0
  137. robosystems_client/models/backup_stats_response.py +156 -0
  138. robosystems_client/models/backup_stats_response_backup_formats.py +44 -0
  139. robosystems_client/models/cancel_operation_response_canceloperation.py +44 -0
  140. robosystems_client/models/cancellation_response.py +76 -0
  141. robosystems_client/models/check_credit_balance_response_checkcreditbalance.py +44 -0
  142. robosystems_client/models/connection_options_response.py +82 -0
  143. robosystems_client/models/connection_provider_info.py +203 -0
  144. robosystems_client/models/connection_provider_info_auth_type.py +11 -0
  145. robosystems_client/models/connection_provider_info_provider.py +10 -0
  146. robosystems_client/models/connection_response.py +149 -0
  147. robosystems_client/models/connection_response_metadata.py +44 -0
  148. robosystems_client/models/connection_response_provider.py +10 -0
  149. robosystems_client/models/create_api_key_request.py +82 -0
  150. robosystems_client/models/create_api_key_response.py +74 -0
  151. robosystems_client/models/create_connection_request.py +179 -0
  152. robosystems_client/models/create_connection_request_provider.py +10 -0
  153. robosystems_client/models/create_graph_request.py +183 -0
  154. robosystems_client/models/credit_check_request.py +82 -0
  155. robosystems_client/models/credit_summary.py +128 -0
  156. robosystems_client/models/credit_summary_response.py +140 -0
  157. robosystems_client/models/credits_summary_response.py +122 -0
  158. robosystems_client/models/credits_summary_response_credits_by_addon_item.py +44 -0
  159. robosystems_client/models/custom_schema_definition.py +194 -0
  160. robosystems_client/models/custom_schema_definition_metadata.py +49 -0
  161. robosystems_client/models/custom_schema_definition_nodes_item.py +44 -0
  162. robosystems_client/models/custom_schema_definition_relationships_item.py +44 -0
  163. robosystems_client/models/cypher_query_request.py +128 -0
  164. robosystems_client/models/cypher_query_request_parameters_type_0.py +44 -0
  165. robosystems_client/models/database_health_response.py +181 -0
  166. robosystems_client/models/database_info_response.py +191 -0
  167. robosystems_client/models/detailed_transactions_response.py +124 -0
  168. robosystems_client/models/detailed_transactions_response_date_range.py +44 -0
  169. robosystems_client/models/detailed_transactions_response_summary.py +59 -0
  170. robosystems_client/models/enhanced_credit_transaction_response.py +192 -0
  171. robosystems_client/models/enhanced_credit_transaction_response_metadata.py +44 -0
  172. robosystems_client/models/error_response.py +145 -0
  173. robosystems_client/models/exchange_token_request.py +116 -0
  174. robosystems_client/models/exchange_token_request_metadata_type_0.py +44 -0
  175. robosystems_client/models/get_all_credit_summaries_response_getallcreditsummaries.py +44 -0
  176. robosystems_client/models/get_backup_download_url_response_getbackupdownloadurl.py +44 -0
  177. robosystems_client/models/get_current_auth_user_response_getcurrentauthuser.py +44 -0
  178. robosystems_client/models/get_current_graph_bill_response_getcurrentgraphbill.py +44 -0
  179. robosystems_client/models/get_graph_billing_history_response_getgraphbillinghistory.py +44 -0
  180. robosystems_client/models/get_graph_monthly_bill_response_getgraphmonthlybill.py +44 -0
  181. robosystems_client/models/get_graph_schema_info_response_getgraphschemainfo.py +44 -0
  182. robosystems_client/models/get_graph_usage_details_response_getgraphusagedetails.py +44 -0
  183. robosystems_client/models/get_mcp_health_response_getmcphealth.py +44 -0
  184. robosystems_client/models/get_operation_status_response_getoperationstatus.py +44 -0
  185. robosystems_client/models/get_storage_usage_response_getstorageusage.py +44 -0
  186. robosystems_client/models/graph_info.py +92 -0
  187. robosystems_client/models/graph_metadata.py +105 -0
  188. robosystems_client/models/graph_metrics_response.py +188 -0
  189. robosystems_client/models/graph_metrics_response_estimated_size.py +44 -0
  190. robosystems_client/models/graph_metrics_response_health_status.py +44 -0
  191. robosystems_client/models/graph_metrics_response_node_counts.py +44 -0
  192. robosystems_client/models/graph_metrics_response_relationship_counts.py +44 -0
  193. robosystems_client/models/graph_usage_response.py +116 -0
  194. robosystems_client/models/graph_usage_response_query_statistics.py +44 -0
  195. robosystems_client/models/graph_usage_response_recent_activity.py +44 -0
  196. robosystems_client/models/graph_usage_response_storage_usage.py +44 -0
  197. robosystems_client/models/health_status.py +110 -0
  198. robosystems_client/models/health_status_details_type_0.py +44 -0
  199. robosystems_client/models/http_validation_error.py +75 -0
  200. robosystems_client/models/initial_entity_data.py +212 -0
  201. robosystems_client/models/kuzu_backup_health_response_kuzubackuphealth.py +44 -0
  202. robosystems_client/models/link_token_request.py +174 -0
  203. robosystems_client/models/link_token_request_options_type_0.py +44 -0
  204. robosystems_client/models/link_token_request_provider_type_0.py +10 -0
  205. robosystems_client/models/list_connections_provider_type_0.py +10 -0
  206. robosystems_client/models/list_schema_extensions_response_listschemaextensions.py +44 -0
  207. robosystems_client/models/login_request.py +68 -0
  208. robosystems_client/models/logout_user_response_logoutuser.py +44 -0
  209. robosystems_client/models/mcp_tool_call.py +84 -0
  210. robosystems_client/models/mcp_tool_call_arguments.py +44 -0
  211. robosystems_client/models/mcp_tools_response.py +74 -0
  212. robosystems_client/models/mcp_tools_response_tools_item.py +44 -0
  213. robosystems_client/models/o_auth_callback_request.py +130 -0
  214. robosystems_client/models/o_auth_init_request.py +128 -0
  215. robosystems_client/models/o_auth_init_request_additional_params_type_0.py +44 -0
  216. robosystems_client/models/o_auth_init_response.py +78 -0
  217. robosystems_client/models/password_check_request.py +82 -0
  218. robosystems_client/models/password_check_response.py +112 -0
  219. robosystems_client/models/password_check_response_character_types.py +44 -0
  220. robosystems_client/models/password_policy_response.py +66 -0
  221. robosystems_client/models/password_policy_response_policy.py +44 -0
  222. robosystems_client/models/plaid_connection_config.py +209 -0
  223. robosystems_client/models/plaid_connection_config_accounts_type_0_item.py +44 -0
  224. robosystems_client/models/plaid_connection_config_institution_type_0.py +44 -0
  225. robosystems_client/models/quick_books_connection_config.py +92 -0
  226. robosystems_client/models/register_request.py +98 -0
  227. robosystems_client/models/repository_credits_response.py +101 -0
  228. robosystems_client/models/repository_plan.py +10 -0
  229. robosystems_client/models/repository_type.py +10 -0
  230. robosystems_client/models/response_mode.py +11 -0
  231. robosystems_client/models/schema_export_response.py +163 -0
  232. robosystems_client/models/schema_export_response_data_stats_type_0.py +44 -0
  233. robosystems_client/models/schema_export_response_schema_definition_type_0.py +44 -0
  234. robosystems_client/models/schema_validation_request.py +142 -0
  235. robosystems_client/models/schema_validation_request_schema_definition_type_0.py +44 -0
  236. robosystems_client/models/schema_validation_response.py +227 -0
  237. robosystems_client/models/schema_validation_response_compatibility_type_0.py +44 -0
  238. robosystems_client/models/schema_validation_response_stats_type_0.py +44 -0
  239. robosystems_client/models/sec_connection_config.py +82 -0
  240. robosystems_client/models/sso_complete_request.py +60 -0
  241. robosystems_client/models/sso_exchange_request.py +90 -0
  242. robosystems_client/models/sso_exchange_response.py +78 -0
  243. robosystems_client/models/sso_login_request.py +60 -0
  244. robosystems_client/models/sso_token_response.py +78 -0
  245. robosystems_client/models/storage_limit_response.py +149 -0
  246. robosystems_client/models/subscription_info.py +180 -0
  247. robosystems_client/models/subscription_info_metadata.py +44 -0
  248. robosystems_client/models/subscription_request.py +89 -0
  249. robosystems_client/models/subscription_response.py +82 -0
  250. robosystems_client/models/success_response.py +112 -0
  251. robosystems_client/models/success_response_data_type_0.py +44 -0
  252. robosystems_client/models/sync_connection_request.py +106 -0
  253. robosystems_client/models/sync_connection_request_sync_options_type_0.py +44 -0
  254. robosystems_client/models/sync_connection_response_syncconnection.py +44 -0
  255. robosystems_client/models/tier_upgrade_request.py +62 -0
  256. robosystems_client/models/transaction_summary_response.py +126 -0
  257. robosystems_client/models/update_api_key_request.py +92 -0
  258. robosystems_client/models/update_password_request.py +76 -0
  259. robosystems_client/models/update_user_request.py +92 -0
  260. robosystems_client/models/upgrade_subscription_request.py +82 -0
  261. robosystems_client/models/user_analytics_response.py +132 -0
  262. robosystems_client/models/user_analytics_response_api_usage.py +44 -0
  263. robosystems_client/models/user_analytics_response_graph_usage.py +44 -0
  264. robosystems_client/models/user_analytics_response_limits.py +44 -0
  265. robosystems_client/models/user_analytics_response_recent_activity_item.py +44 -0
  266. robosystems_client/models/user_analytics_response_user_info.py +44 -0
  267. robosystems_client/models/user_graph_summary.py +134 -0
  268. robosystems_client/models/user_graphs_response.py +96 -0
  269. robosystems_client/models/user_limits_response.py +95 -0
  270. robosystems_client/models/user_response.py +132 -0
  271. robosystems_client/models/user_subscriptions_response.py +90 -0
  272. robosystems_client/models/user_usage_response.py +90 -0
  273. robosystems_client/models/user_usage_response_graphs.py +44 -0
  274. robosystems_client/models/user_usage_summary_response.py +130 -0
  275. robosystems_client/models/user_usage_summary_response_usage_vs_limits.py +44 -0
  276. robosystems_client/models/validation_error.py +88 -0
  277. robosystems_client/py.typed +1 -0
  278. robosystems_client/sdk-config.yaml +5 -0
  279. robosystems_client/types.py +54 -0
  280. robosystems_client-0.1.9.dist-info/METADATA +302 -0
  281. robosystems_client-0.1.9.dist-info/RECORD +282 -0
  282. 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"""