|
7 | 7 | import random |
8 | 8 | import logging |
9 | 9 | from types import TracebackType |
10 | | -from typing import TYPE_CHECKING, Any, Dict, Callable, Iterator, Awaitable, cast |
| 10 | +from typing import TYPE_CHECKING, Any, Dict, Union, Callable, Iterator, Awaitable, cast |
11 | 11 | from typing_extensions import AsyncIterator |
12 | 12 |
|
13 | 13 | import httpx |
|
27 | 27 | from ...pagination import SyncCursorPage, AsyncCursorPage |
28 | 28 | from ..._exceptions import DedalusError |
29 | 29 | from ..._base_client import AsyncPaginator, _merge_mappings, make_request_options |
| 30 | +from ..._event_handler import EventHandlerRegistry |
30 | 31 | from ...types.machines import terminal_list_params, terminal_create_params |
31 | 32 | from ...types.machines.terminal import Terminal |
32 | 33 | from ...types.websocket_reconnection import ReconnectingEvent, ReconnectingOverrides, is_recoverable_close |
33 | 34 | from ...types.websocket_connection_options import WebSocketConnectionOptions |
| 35 | +from ...types.machines.terminal_error_event import TerminalErrorEvent |
34 | 36 | from ...types.machines.terminal_client_event import TerminalClientEvent |
35 | 37 | from ...types.machines.terminal_server_event import TerminalServerEvent |
36 | 38 | from ...types.machines.terminal_client_event_param import TerminalClientEventParam |
@@ -606,6 +608,7 @@ def __init__( |
606 | 608 | self._extra_query = extra_query |
607 | 609 | self._extra_headers = extra_headers |
608 | 610 | self._intentionally_closed = False |
| 611 | + self._event_handler_registry = EventHandlerRegistry(use_lock=False) |
609 | 612 |
|
610 | 613 | async def __aiter__(self) -> AsyncIterator[TerminalServerEvent]: |
611 | 614 | """ |
@@ -736,6 +739,86 @@ async def _reconnect(self, exc: Exception) -> bool: |
736 | 739 |
|
737 | 740 | return False |
738 | 741 |
|
| 742 | + def on( |
| 743 | + self, event_type: str, handler: Callable[..., Any] | None = None |
| 744 | + ) -> Union[AsyncTerminalsResourceConnection, Callable[[Callable[..., Any]], Callable[..., Any]]]: |
| 745 | + """Adds the handler to the end of the handlers list for the given event type. |
| 746 | +
|
| 747 | + No checks are made to see if the handler has already been added. Multiple calls |
| 748 | + passing the same combination of event type and handler will result in the handler |
| 749 | + being added, and called, multiple times. |
| 750 | +
|
| 751 | + Can be used as a method (returns ``self`` for chaining):: |
| 752 | +
|
| 753 | + connection.on("output", my_handler) |
| 754 | +
|
| 755 | + Or as a decorator:: |
| 756 | +
|
| 757 | + @connection.on("output") |
| 758 | + async def my_handler(event): ... |
| 759 | + """ |
| 760 | + if handler is not None: |
| 761 | + self._event_handler_registry.add(event_type, handler) |
| 762 | + return self |
| 763 | + |
| 764 | + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: |
| 765 | + self._event_handler_registry.add(event_type, fn) |
| 766 | + return fn |
| 767 | + |
| 768 | + return decorator |
| 769 | + |
| 770 | + def off(self, event_type: str, handler: Callable[..., Any]) -> AsyncTerminalsResourceConnection: |
| 771 | + """Remove a previously registered event handler.""" |
| 772 | + self._event_handler_registry.remove(event_type, handler) |
| 773 | + return self |
| 774 | + |
| 775 | + def once( |
| 776 | + self, event_type: str, handler: Callable[..., Any] | None = None |
| 777 | + ) -> Union[AsyncTerminalsResourceConnection, Callable[[Callable[..., Any]], Callable[..., Any]]]: |
| 778 | + """Register a one-time event handler. |
| 779 | +
|
| 780 | + Automatically removed after first invocation. |
| 781 | + """ |
| 782 | + if handler is not None: |
| 783 | + self._event_handler_registry.add(event_type, handler, once=True) |
| 784 | + return self |
| 785 | + |
| 786 | + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: |
| 787 | + self._event_handler_registry.add(event_type, fn, once=True) |
| 788 | + return fn |
| 789 | + |
| 790 | + return decorator |
| 791 | + |
| 792 | + async def dispatch_events(self) -> None: |
| 793 | + """Run the event loop, dispatching received events to registered handlers. |
| 794 | +
|
| 795 | + Blocks until the connection is closed. This is the push-based |
| 796 | + alternative to iterating with ``async for event in connection``. |
| 797 | +
|
| 798 | + If an ``"error"`` event arrives and no handler is registered for |
| 799 | + ``"error"`` or ``"event"``, an ``DedalusError`` is raised. |
| 800 | + """ |
| 801 | + import asyncio |
| 802 | + |
| 803 | + async for event in self: |
| 804 | + event_type = event.type |
| 805 | + specific = self._event_handler_registry.get_handlers(event_type) |
| 806 | + generic = self._event_handler_registry.get_handlers("event") |
| 807 | + |
| 808 | + if event_type == "error" and not specific and not generic: |
| 809 | + if isinstance(event, TerminalErrorEvent): |
| 810 | + raise DedalusError(f"WebSocket error: {event}") |
| 811 | + |
| 812 | + for handler in specific: |
| 813 | + result = handler(event) |
| 814 | + if asyncio.iscoroutine(result): |
| 815 | + await result |
| 816 | + |
| 817 | + for handler in generic: |
| 818 | + result = handler(event) |
| 819 | + if asyncio.iscoroutine(result): |
| 820 | + await result |
| 821 | + |
739 | 822 |
|
740 | 823 | class AsyncTerminalsResourceConnectionManager: |
741 | 824 | """ |
@@ -785,7 +868,7 @@ def __init__( |
785 | 868 |
|
786 | 869 | async def __aenter__(self) -> AsyncTerminalsResourceConnection: |
787 | 870 | """ |
788 | | - 👋 If your application doesn't work well with the context manager approach then you |
| 871 | + If your application doesn't work well with the context manager approach then you |
789 | 872 | can call this method directly to initiate a connection. |
790 | 873 |
|
791 | 874 | **Warning**: You must remember to close the connection with `.close()`. |
@@ -893,6 +976,7 @@ def __init__( |
893 | 976 | self._extra_query = extra_query |
894 | 977 | self._extra_headers = extra_headers |
895 | 978 | self._intentionally_closed = False |
| 979 | + self._event_handler_registry = EventHandlerRegistry(use_lock=True) |
896 | 980 |
|
897 | 981 | def __iter__(self) -> Iterator[TerminalServerEvent]: |
898 | 982 | """ |
@@ -1021,6 +1105,80 @@ def _reconnect(self, exc: Exception) -> bool: |
1021 | 1105 |
|
1022 | 1106 | return False |
1023 | 1107 |
|
| 1108 | + def on( |
| 1109 | + self, event_type: str, handler: Callable[..., Any] | None = None |
| 1110 | + ) -> Union[TerminalsResourceConnection, Callable[[Callable[..., Any]], Callable[..., Any]]]: |
| 1111 | + """Adds the handler to the end of the handlers list for the given event type. |
| 1112 | +
|
| 1113 | + No checks are made to see if the handler has already been added. Multiple calls |
| 1114 | + passing the same combination of event type and handler will result in the handler |
| 1115 | + being added, and called, multiple times. |
| 1116 | +
|
| 1117 | + Can be used as a method (returns ``self`` for chaining):: |
| 1118 | +
|
| 1119 | + connection.on("output", my_handler) |
| 1120 | +
|
| 1121 | + Or as a decorator:: |
| 1122 | +
|
| 1123 | + @connection.on("output") |
| 1124 | + def my_handler(event): ... |
| 1125 | + """ |
| 1126 | + if handler is not None: |
| 1127 | + self._event_handler_registry.add(event_type, handler) |
| 1128 | + return self |
| 1129 | + |
| 1130 | + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: |
| 1131 | + self._event_handler_registry.add(event_type, fn) |
| 1132 | + return fn |
| 1133 | + |
| 1134 | + return decorator |
| 1135 | + |
| 1136 | + def off(self, event_type: str, handler: Callable[..., Any]) -> TerminalsResourceConnection: |
| 1137 | + """Remove a previously registered event handler.""" |
| 1138 | + self._event_handler_registry.remove(event_type, handler) |
| 1139 | + return self |
| 1140 | + |
| 1141 | + def once( |
| 1142 | + self, event_type: str, handler: Callable[..., Any] | None = None |
| 1143 | + ) -> Union[TerminalsResourceConnection, Callable[[Callable[..., Any]], Callable[..., Any]]]: |
| 1144 | + """Register a one-time event handler. |
| 1145 | +
|
| 1146 | + Automatically removed after first invocation. |
| 1147 | + """ |
| 1148 | + if handler is not None: |
| 1149 | + self._event_handler_registry.add(event_type, handler, once=True) |
| 1150 | + return self |
| 1151 | + |
| 1152 | + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: |
| 1153 | + self._event_handler_registry.add(event_type, fn, once=True) |
| 1154 | + return fn |
| 1155 | + |
| 1156 | + return decorator |
| 1157 | + |
| 1158 | + def dispatch_events(self) -> None: |
| 1159 | + """Run the event loop, dispatching received events to registered handlers. |
| 1160 | +
|
| 1161 | + Blocks the current thread until the connection is closed. This is the push-based |
| 1162 | + alternative to iterating with ``for event in connection``. |
| 1163 | +
|
| 1164 | + If an ``"error"`` event arrives and no handler is registered for |
| 1165 | + ``"error"`` or ``"event"``, an ``DedalusError`` is raised. |
| 1166 | + """ |
| 1167 | + for event in self: |
| 1168 | + event_type = event.type |
| 1169 | + specific = self._event_handler_registry.get_handlers(event_type) |
| 1170 | + generic = self._event_handler_registry.get_handlers("event") |
| 1171 | + |
| 1172 | + if event_type == "error" and not specific and not generic: |
| 1173 | + if isinstance(event, TerminalErrorEvent): |
| 1174 | + raise DedalusError(f"WebSocket error: {event}") |
| 1175 | + |
| 1176 | + for handler in specific: |
| 1177 | + handler(event) |
| 1178 | + |
| 1179 | + for handler in generic: |
| 1180 | + handler(event) |
| 1181 | + |
1024 | 1182 |
|
1025 | 1183 | class TerminalsResourceConnectionManager: |
1026 | 1184 | """ |
@@ -1070,7 +1228,7 @@ def __init__( |
1070 | 1228 |
|
1071 | 1229 | def __enter__(self) -> TerminalsResourceConnection: |
1072 | 1230 | """ |
1073 | | - 👋 If your application doesn't work well with the context manager approach then you |
| 1231 | + If your application doesn't work well with the context manager approach then you |
1074 | 1232 | can call this method directly to initiate a connection. |
1075 | 1233 |
|
1076 | 1234 | **Warning**: You must remember to close the connection with `.close()`. |
|
0 commit comments