hat.juggler

Juggler communication protocol

 1"""Juggler communication protocol"""
 2
 3from hat.juggler.client import (NotifyCb,
 4                                JugglerError,
 5                                connect,
 6                                Client)
 7from hat.juggler.server import (ConnectionCb,
 8                                RequestCb,
 9                                listen,
10                                Server,
11                                Connection)
12
13
14__all__ = ['NotifyCb',
15           'JugglerError',
16           'connect',
17           'Client',
18           'ConnectionCb',
19           'RequestCb',
20           'listen',
21           'Server',
22           'Connection']
NotifyCb = typing.Callable[[ForwardRef('Client'), str, None | bool | int | float | str | typing.List[ForwardRef('Data')] | typing.Dict[str, ForwardRef('Data')]], None | collections.abc.Awaitable[None]]
class JugglerError(builtins.Exception):
28class JugglerError(Exception):
29    """Juggler error"""
30
31    def __init__(self, data: json.Data):
32        self.__data = data
33
34    @property
35    def data(self) -> json.Data:
36        """Error data"""
37        return self.__data

Juggler error

JugglerError( data: None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')])
31    def __init__(self, data: json.Data):
32        self.__data = data
data: None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')]
34    @property
35    def data(self) -> json.Data:
36        """Error data"""
37        return self.__data

Error data

async def connect( address: str, notify_cb: Callable[[ForwardRef('Client'), str, None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')]], None | Awaitable[None]] | None = None, *, auth: aiohttp.helpers.BasicAuth | None = None, ssl_ctx: ssl.SSLContext | None = None, send_queue_size: int = 1024, max_segment_size: int = 65536, ping_delay: float = 30, ping_timeout: float = 30, **kwargs) -> Client:
40async def connect(address: str,
41                  notify_cb: NotifyCb | None = None,
42                  *,
43                  auth: aiohttp.BasicAuth | None = None,
44                  ssl_ctx: ssl.SSLContext | None = None,
45                  send_queue_size: int = 1024,
46                  max_segment_size: int = 64 * 1024,
47                  ping_delay: float = 30,
48                  ping_timeout: float = 30,
49                  **kwargs
50                  ) -> 'Client':
51    """Connect to remote server
52
53    `address` represents remote WebSocket URL formated as
54    ``<schema>://<host>:<port>/<path>`` where ``<schema>`` is ``ws`` or
55    ``wss``.
56
57    Additional arguments are passed directly to `aiohttp.ClientSession`.
58
59    """
60    client = Client()
61    client._notify_cb = notify_cb
62    client._loop = asyncio.get_running_loop()
63    client._async_group = aio.Group()
64    client._state = json.Storage()
65    client._res_futures = {}
66    client._next_req_ids = itertools.count(1)
67    client._session = aiohttp.ClientSession(**kwargs)
68
69    try:
70        ws = await client._session.ws_connect(address,
71                                              auth=auth,
72                                              ssl=ssl_ctx or False,
73                                              max_msg_size=0)
74
75    except BaseException:
76        await aio.uncancellable(client._session.close())
77        raise
78
79    client._transport = Transport(ws=ws,
80                                  msg_cb=client._on_msg,
81                                  send_queue_size=send_queue_size,
82                                  max_segment_size=max_segment_size,
83                                  ping_delay=ping_delay,
84                                  ping_timeout=ping_timeout,
85                                  remote=_get_remote(address, ws))
86
87    client.async_group.spawn(aio.call_on_cancel, client._on_close)
88    client.async_group.spawn(aio.call_on_done,
89                             client._transport.wait_closing(), client.close)
90
91    return client

Connect to remote server

address represents remote WebSocket URL formated as <schema>://<host>:<port>/<path> where <schema> is ws or wss.

Additional arguments are passed directly to aiohttp.ClientSession.

class Client(hat.aio.group.Resource):
 94class Client(aio.Resource):
 95    """Client
 96
 97    For creating new client see `connect` coroutine.
 98
 99    """
100
101    @property
102    def async_group(self) -> aio.Group:
103        """Async group"""
104        return self._async_group
105
106    @property
107    def state(self) -> json.Storage:
108        """Remote server state"""
109        return self._state
110
111    async def send(self,
112                   name: str,
113                   data: json.Data
114                   ) -> json.Data:
115        """Send request and wait for response
116
117        Args:
118            name: request name
119            data: request payload
120
121        Raises:
122            JugglerError
123            ConnectionError
124
125        """
126        if not self.is_open:
127            raise ConnectionError()
128
129        req_id = next(self._next_req_ids)
130        res_future = self._loop.create_future()
131        self._res_futures[req_id] = res_future
132
133        try:
134            await self._transport.send({'type': 'request',
135                                        'id': req_id,
136                                        'name': name,
137                                        'data': data})
138            return await res_future
139
140        finally:
141            self._res_futures.pop(req_id)
142
143    async def _on_close(self):
144        for f in self._res_futures.values():
145            if not f.done():
146                f.set_exception(ConnectionError())
147
148        await self._transport.async_close()
149        await self._session.close()
150
151    async def _on_msg(self, msg):
152        if msg['type'] == 'response':
153            res_future = self._res_futures.get(msg['id'])
154            if not res_future or res_future.done():
155                return
156
157            if msg['success']:
158                res_future.set_result(msg['data'])
159
160            else:
161                res_future.set_exception(JugglerError(msg['data']))
162
163        elif msg['type'] == 'state':
164            data = json.patch(self._state.data, msg['diff'])
165            self._state.set([], data)
166
167        elif msg['type'] == 'notify':
168            if not self._notify_cb:
169                return
170
171            await aio.call(self._notify_cb, self, msg['name'],
172                           msg['data'])
173
174        else:
175            raise Exception("invalid message type")

Client

For creating new client see connect coroutine.

async_group: hat.aio.group.Group
101    @property
102    def async_group(self) -> aio.Group:
103        """Async group"""
104        return self._async_group

Async group

state: hat.json.path.Storage
106    @property
107    def state(self) -> json.Storage:
108        """Remote server state"""
109        return self._state

Remote server state

async def send( self, name: str, data: None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')]) -> None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')]:
111    async def send(self,
112                   name: str,
113                   data: json.Data
114                   ) -> json.Data:
115        """Send request and wait for response
116
117        Args:
118            name: request name
119            data: request payload
120
121        Raises:
122            JugglerError
123            ConnectionError
124
125        """
126        if not self.is_open:
127            raise ConnectionError()
128
129        req_id = next(self._next_req_ids)
130        res_future = self._loop.create_future()
131        self._res_futures[req_id] = res_future
132
133        try:
134            await self._transport.send({'type': 'request',
135                                        'id': req_id,
136                                        'name': name,
137                                        'data': data})
138            return await res_future
139
140        finally:
141            self._res_futures.pop(req_id)

Send request and wait for response

Arguments:
  • name: request name
  • data: request payload
Raises:
  • JugglerError
  • ConnectionError
ConnectionCb = typing.Callable[[ForwardRef('Connection')], None | collections.abc.Awaitable[None]]
RequestCb = typing.Callable[[ForwardRef('Connection'), str, None | bool | int | float | str | typing.List[ForwardRef('Data')] | typing.Dict[str, ForwardRef('Data')]], None | bool | int | float | str | typing.List[ForwardRef('Data')] | typing.Dict[str, ForwardRef('Data')] | collections.abc.Awaitable[None | bool | int | float | str | typing.List[ForwardRef('Data')] | typing.Dict[str, ForwardRef('Data')]]]
async def listen( host: str, port: int, connection_cb: Callable[[Connection], None | Awaitable[None]] | None = None, request_cb: Callable[[ForwardRef('Connection'), str, None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')]], None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')] | Awaitable[None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')]]] | None = None, *, ws_path: str | None = '/ws', static_dir: pathlib.PurePath | None = None, index_path: str | None = '/index.html', htpasswd_file: pathlib.PurePath | None = None, ssl_ctx: ssl.SSLContext | None = None, autoflush_delay: float | None = 0.2, shutdown_timeout: float = 0.1, state: hat.json.path.Storage | None = None, parallel_requests: bool = False, additional_routes: Iterable[aiohttp.web_routedef.RouteDef] = [], send_queue_size: int = 1024, max_segment_size: int = 65536, ping_delay: float = 30, ping_timeout: float = 30, no_cache: bool = True) -> Server:
 32async def listen(host: str,
 33                 port: int,
 34                 connection_cb: ConnectionCb | None = None,
 35                 request_cb: RequestCb | None = None,
 36                 *,
 37                 ws_path: str | None = '/ws',
 38                 static_dir: pathlib.PurePath | None = None,
 39                 index_path: str | None = '/index.html',
 40                 htpasswd_file: pathlib.PurePath | None = None,
 41                 ssl_ctx: ssl.SSLContext | None = None,
 42                 autoflush_delay: float | None = 0.2,
 43                 shutdown_timeout: float = 0.1,
 44                 state: json.Storage | None = None,
 45                 parallel_requests: bool = False,
 46                 additional_routes: Iterable[aiohttp.web.RouteDef] = [],
 47                 send_queue_size: int = 1024,
 48                 max_segment_size: int = 64 * 1024,
 49                 ping_delay: float = 30,
 50                 ping_timeout: float = 30,
 51                 no_cache: bool = True
 52                 ) -> 'Server':
 53    """Create listening server
 54
 55    Each time server receives new incoming juggler connection, `connection_cb`
 56    is called with newly created connection.
 57
 58    For each connection, when server receives `request` message, `request_cb`
 59    is called with associated connection, request name and request data.
 60    If `request_cb` returns value, successful `response` message is sent
 61    with resulting value as data. If `request_cb` raises exception,
 62    unsuccessful `response` message is sent with raised exception as data.
 63    If `request_cb` is ``None``, each `request` message causes sending
 64    of unsuccessful `response` message.
 65
 66    If `static_dir` is set, server serves static files is addition to providing
 67    juggler communication.
 68
 69    If `index_path` is set, request for url path ``/`` are redirected to
 70    `index_path`.
 71
 72    If `htpasswd_file` is set, HTTP Basic Authentication is enabled.
 73    All requests are checked for ``Authorization`` header and only users
 74    specified by `htpassword_file` are accepted. `htpasswd_file` is read
 75    during initialization and changes to it's content, after initialization
 76    finishes, are not monitored.
 77
 78    If `ssl_ctx` is set, server provides `https/wss` communication instead
 79    of `http/ws` communication.
 80
 81    Argument `autoflush_delay` is associated with all connections associated
 82    with this server. `autoflush_delay` defines maximum time delay for
 83    automatic synchronization of `state` changes. If `autoflush_delay` is set
 84    to ``None``, automatic synchronization is disabled and user is responsible
 85    for calling :meth:`Connection.flush`. If `autoflush_delay` is set to ``0``,
 86    synchronization of `state` is performed on each change of `state` data.
 87
 88    `shutdown_timeout` defines maximum time duration server will wait for
 89    regular connection close procedures during server shutdown. All connections
 90    that are not closed during this period are forcefully closed.
 91
 92    If `state` is ``None``, each connection is initialized with it's own
 93    instance of server state. If `state` is set, provided state is shared
 94    between all connections.
 95
 96    If `parallel_requests` is set to ``True``, incoming requests will be
 97    processed in parallel - processing of subsequent requests can start (and
 98    finish) before prior responses are generated.
 99
100    Argument `additional_routes` can be used for providing addition aiohttp
101    route definitions handled by running web server.
102
103    `send_queue_size` limits number of messages that can be put in send queue.
104    This limit can impact blocking of :meth:`Connection.notify`.
105
106    `max_segment_size` limits maximum size of single segment
107    (transport payload size).
108
109    When connection doesn't receive incoming data,
110    `ping_delay` is time (in seconds) that connection waits before sending
111    ping request.
112
113    `ping_timeout` is time (in seconds), that connection waits for any kind
114    of incoming traffic before closed connection is assumed.
115
116    If `no_cache` is set to ``True``, server will include
117    ``Cache-Control: no-cache`` header in all responses.
118
119    Args:
120        host: listening hostname
121        port: listening TCP port
122        connection_cb: connection callback
123        request_cb: request callback
124        ws_path: WebSocket url path segment
125        static_dir: static files directory path
126        index_path: index path
127        htpasswd_file: htpasswd file path
128        ssl_ctx: SSL context
129        autoflush_delay: autoflush delay
130        shutdown_timeout: shutdown timeout
131        state: shared server state
132        parallel_requests: parallel request processing
133        additional_routes: additional route definitions
134        send_queue_size: send queue size
135        max_segment_size: maximum segment size
136        ping_delay: ping delay
137        ping_timeout: ping timeout
138        no_cache: no cache header
139
140    """
141    server = Server()
142    server._connection_cb = connection_cb
143    server._request_cb = request_cb
144    server._autoflush_delay = autoflush_delay
145    server._state = state
146    server._parallel_requests = parallel_requests
147    server._send_queue_size = send_queue_size
148    server._max_segment_size = max_segment_size
149    server._ping_delay = ping_delay
150    server._ping_timeout = ping_timeout
151    server._async_group = aio.Group()
152
153    middlewares = []
154
155    if htpasswd_file:
156        middlewares.append(BasicAuthMiddleware(htpasswd_file))
157
158    routes = []
159
160    if ws_path:
161        routes.append(aiohttp.web.get(ws_path, server._ws_handler))
162
163    if index_path:
164
165        async def root_handler(request):
166            raise aiohttp.web.HTTPFound(index_path)
167
168        routes.append(aiohttp.web.get('/', root_handler))
169
170    routes.extend(additional_routes)
171
172    if static_dir:
173        routes.append(aiohttp.web.static('/', static_dir))
174
175    app = aiohttp.web.Application(middlewares=middlewares)
176    app.add_routes(routes)
177
178    if no_cache:
179        app.on_response_prepare.append(_no_cache_prepare)
180
181    runner = aiohttp.web.AppRunner(app,
182                                   shutdown_timeout=shutdown_timeout)
183    await runner.setup()
184    server.async_group.spawn(aio.call_on_cancel, runner.cleanup)
185
186    try:
187        site = aiohttp.web.TCPSite(runner=runner,
188                                   host=host,
189                                   port=port,
190                                   ssl_context=ssl_ctx,
191                                   reuse_address=True)
192        await site.start()
193
194    except BaseException:
195        await aio.uncancellable(server.async_close())
196        raise
197
198    return server

Create listening server

Each time server receives new incoming juggler connection, connection_cb is called with newly created connection.

For each connection, when server receives request message, request_cb is called with associated connection, request name and request data. If request_cb returns value, successful response message is sent with resulting value as data. If request_cb raises exception, unsuccessful response message is sent with raised exception as data. If request_cb is None, each request message causes sending of unsuccessful response message.

If static_dir is set, server serves static files is addition to providing juggler communication.

If index_path is set, request for url path / are redirected to index_path.

If htpasswd_file is set, HTTP Basic Authentication is enabled. All requests are checked for Authorization header and only users specified by htpassword_file are accepted. htpasswd_file is read during initialization and changes to it's content, after initialization finishes, are not monitored.

If ssl_ctx is set, server provides https/wss communication instead of http/ws communication.

Argument autoflush_delay is associated with all connections associated with this server. autoflush_delay defines maximum time delay for automatic synchronization of state changes. If autoflush_delay is set to None, automatic synchronization is disabled and user is responsible for calling Connection.flush(). If autoflush_delay is set to 0, synchronization of state is performed on each change of state data.

shutdown_timeout defines maximum time duration server will wait for regular connection close procedures during server shutdown. All connections that are not closed during this period are forcefully closed.

If state is None, each connection is initialized with it's own instance of server state. If state is set, provided state is shared between all connections.

If parallel_requests is set to True, incoming requests will be processed in parallel - processing of subsequent requests can start (and finish) before prior responses are generated.

Argument additional_routes can be used for providing addition aiohttp route definitions handled by running web server.

send_queue_size limits number of messages that can be put in send queue. This limit can impact blocking of Connection.notify().

max_segment_size limits maximum size of single segment (transport payload size).

When connection doesn't receive incoming data, ping_delay is time (in seconds) that connection waits before sending ping request.

ping_timeout is time (in seconds), that connection waits for any kind of incoming traffic before closed connection is assumed.

If no_cache is set to True, server will include Cache-Control: no-cache header in all responses.

Arguments:
  • host: listening hostname
  • port: listening TCP port
  • connection_cb: connection callback
  • request_cb: request callback
  • ws_path: WebSocket url path segment
  • static_dir: static files directory path
  • index_path: index path
  • htpasswd_file: htpasswd file path
  • ssl_ctx: SSL context
  • autoflush_delay: autoflush delay
  • shutdown_timeout: shutdown timeout
  • state: shared server state
  • parallel_requests: parallel request processing
  • additional_routes: additional route definitions
  • send_queue_size: send queue size
  • max_segment_size: maximum segment size
  • ping_delay: ping delay
  • ping_timeout: ping timeout
  • no_cache: no cache header
class Server(hat.aio.group.Resource):
201class Server(aio.Resource):
202    """Server
203
204    For creating new server see `listen` coroutine.
205
206    When server is closed, all incoming connections are also closed.
207
208    """
209
210    @property
211    def async_group(self) -> aio.Group:
212        """Async group"""
213        return self._async_group
214
215    async def create_connection(self,
216                                request: aiohttp.web.Request
217                                ) -> 'Connection':
218        """Create connection"""
219        conn = Connection()
220
221        conn._ws = aiohttp.web.WebSocketResponse()
222        await conn._ws.prepare(request)
223
224        conn._remote = _get_remote(request)
225        conn._async_group = self.async_group.create_subgroup()
226        conn._request_cb = self._request_cb
227        conn._autoflush_delay = self._autoflush_delay
228        conn._state = self._state or json.Storage()
229        conn._parallel_requests = self._parallel_requests
230        conn._flush_queue = aio.Queue()
231
232        conn._transport = Transport(ws=conn._ws,
233                                    msg_cb=conn._on_msg,
234                                    send_queue_size=self._send_queue_size,
235                                    max_segment_size=self._max_segment_size,
236                                    ping_delay=self._ping_delay,
237                                    ping_timeout=self._ping_timeout,
238                                    remote=conn._remote)
239
240        conn.async_group.spawn(aio.call_on_cancel, conn._transport.async_close)
241        conn.async_group.spawn(aio.call_on_done,
242                               conn._transport.wait_closing(), conn.close)
243
244        conn.async_group.spawn(conn._sync_loop)
245
246        if self._connection_cb:
247            conn.async_group.spawn(aio.call, self._connection_cb, conn)
248
249        return conn
250
251    async def _ws_handler(self, request):
252        conn = await self.create_connection(request)
253
254        await conn.wait_closed()
255
256        return conn.ws

Server

For creating new server see listen coroutine.

When server is closed, all incoming connections are also closed.

async_group: hat.aio.group.Group
210    @property
211    def async_group(self) -> aio.Group:
212        """Async group"""
213        return self._async_group

Async group

async def create_connection( self, request: aiohttp.web_request.Request) -> Connection:
215    async def create_connection(self,
216                                request: aiohttp.web.Request
217                                ) -> 'Connection':
218        """Create connection"""
219        conn = Connection()
220
221        conn._ws = aiohttp.web.WebSocketResponse()
222        await conn._ws.prepare(request)
223
224        conn._remote = _get_remote(request)
225        conn._async_group = self.async_group.create_subgroup()
226        conn._request_cb = self._request_cb
227        conn._autoflush_delay = self._autoflush_delay
228        conn._state = self._state or json.Storage()
229        conn._parallel_requests = self._parallel_requests
230        conn._flush_queue = aio.Queue()
231
232        conn._transport = Transport(ws=conn._ws,
233                                    msg_cb=conn._on_msg,
234                                    send_queue_size=self._send_queue_size,
235                                    max_segment_size=self._max_segment_size,
236                                    ping_delay=self._ping_delay,
237                                    ping_timeout=self._ping_timeout,
238                                    remote=conn._remote)
239
240        conn.async_group.spawn(aio.call_on_cancel, conn._transport.async_close)
241        conn.async_group.spawn(aio.call_on_done,
242                               conn._transport.wait_closing(), conn.close)
243
244        conn.async_group.spawn(conn._sync_loop)
245
246        if self._connection_cb:
247            conn.async_group.spawn(aio.call, self._connection_cb, conn)
248
249        return conn

Create connection

class Connection(hat.aio.group.Resource):
259class Connection(aio.Resource):
260    """Connection
261
262    For creating new connection see `listen` coroutine.
263
264    """
265
266    @property
267    def async_group(self) -> aio.Group:
268        """Async group"""
269        return self._async_group
270
271    @property
272    def remote(self) -> str:
273        """Remote IP address
274
275        Address is obtained from Forwarded or X-Forwarded-For headers. If
276        these headers are not available, socket's remote address is used.
277
278        """
279        return self._remote
280
281    @property
282    def state(self) -> json.Storage:
283        """Server state"""
284        return self._state
285
286    @property
287    def ws(self) -> aiohttp.web.WebSocketResponse:
288        """Associated WebSocket"""
289        return self._ws
290
291    async def flush(self):
292        """Force synchronization of state data
293
294        Raises:
295            ConnectionError
296
297        """
298        try:
299            flush_future = asyncio.Future()
300            self._flush_queue.put_nowait(flush_future)
301            await flush_future
302
303        except aio.QueueClosedError:
304            raise ConnectionError()
305
306    async def notify(self,
307                     name: str,
308                     data: json.Data):
309        """Send notification
310
311        Raises:
312            ConnectionError
313
314        """
315        if not self.is_open:
316            raise ConnectionError()
317
318        await self._transport.send({'type': 'notify',
319                                    'name': name,
320                                    'data': data})
321
322    async def _on_msg(self, msg):
323        if msg['type'] != 'request':
324            raise Exception("invalid message type")
325
326        if self._parallel_requests:
327            self.async_group.spawn(self._process_request, msg)
328
329        else:
330            await self._process_request(msg)
331
332    async def _process_request(self, req):
333        try:
334            res = {'type': 'response',
335                   'id': req['id']}
336
337            if req['name']:
338                try:
339                    if not self._request_cb:
340                        raise Exception('request handler not implemented')
341
342                    res['data'] = await aio.call(self._request_cb, self,
343                                                 req['name'], req['data'])
344                    res['success'] = True
345
346                except Exception as e:
347                    res['data'] = str(e)
348                    res['success'] = False
349
350            else:
351                res['data'] = req['data']
352                res['success'] = True
353
354            await self._transport.send(res)
355
356        except ConnectionError:
357            self.close()
358
359        except Exception as e:
360            self.close()
361            mlog.error("process request error: %s", e, exc_info=e)
362
363    async def _sync_loop(self):
364        flush_future = None
365        data = None
366        synced_data = None
367        data_queue = aio.Queue()
368
369        try:
370            with self._state.register_change_cb(data_queue.put_nowait):
371                data_queue.put_nowait(self._state.data)
372
373                if not self.is_open:
374                    return
375
376                get_data_future = self.async_group.spawn(data_queue.get)
377                get_flush_future = self.async_group.spawn(
378                    self._flush_queue.get)
379
380                while True:
381                    await asyncio.wait([get_data_future, get_flush_future],
382                                       return_when=asyncio.FIRST_COMPLETED)
383
384                    if get_flush_future.done():
385                        flush_future = get_flush_future.result()
386                        get_flush_future = self.async_group.spawn(
387                            self._flush_queue.get)
388
389                    else:
390                        await asyncio.wait([get_flush_future],
391                                           timeout=self._autoflush_delay)
392
393                        if get_flush_future.done():
394                            flush_future = get_flush_future.result()
395                            get_flush_future = self.async_group.spawn(
396                                self._flush_queue.get)
397
398                        else:
399                            flush_future = None
400
401                    if get_data_future.done():
402                        data = get_data_future.result()
403                        get_data_future = self.async_group.spawn(
404                            data_queue.get)
405
406                    if self._autoflush_delay != 0:
407                        if not data_queue.empty():
408                            data = data_queue.get_nowait_until_empty()
409
410                    if synced_data is not data:
411                        diff = json.diff(synced_data, data)
412                        synced_data = data
413
414                        if diff:
415                            await self._transport.send({'type': 'state',
416                                                        'diff': diff})
417
418                    if flush_future and not flush_future.done():
419                        flush_future.set_result(True)
420
421        except Exception as e:
422            mlog.error("sync loop error: %s", e, exc_info=e)
423
424        finally:
425            self.close()
426
427            self._flush_queue.close()
428            while True:
429                if flush_future and not flush_future.done():
430                    flush_future.set_exception(ConnectionError())
431
432                if self._flush_queue.empty():
433                    break
434
435                flush_future = self._flush_queue.get_nowait()

Connection

For creating new connection see listen coroutine.

async_group: hat.aio.group.Group
266    @property
267    def async_group(self) -> aio.Group:
268        """Async group"""
269        return self._async_group

Async group

remote: str
271    @property
272    def remote(self) -> str:
273        """Remote IP address
274
275        Address is obtained from Forwarded or X-Forwarded-For headers. If
276        these headers are not available, socket's remote address is used.
277
278        """
279        return self._remote

Remote IP address

Address is obtained from Forwarded or X-Forwarded-For headers. If these headers are not available, socket's remote address is used.

state: hat.json.path.Storage
281    @property
282    def state(self) -> json.Storage:
283        """Server state"""
284        return self._state

Server state

ws: aiohttp.web_ws.WebSocketResponse
286    @property
287    def ws(self) -> aiohttp.web.WebSocketResponse:
288        """Associated WebSocket"""
289        return self._ws

Associated WebSocket

async def flush(self):
291    async def flush(self):
292        """Force synchronization of state data
293
294        Raises:
295            ConnectionError
296
297        """
298        try:
299            flush_future = asyncio.Future()
300            self._flush_queue.put_nowait(flush_future)
301            await flush_future
302
303        except aio.QueueClosedError:
304            raise ConnectionError()

Force synchronization of state data

Raises:
  • ConnectionError
async def notify( self, name: str, data: None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')]):
306    async def notify(self,
307                     name: str,
308                     data: json.Data):
309        """Send notification
310
311        Raises:
312            ConnectionError
313
314        """
315        if not self.is_open:
316            raise ConnectionError()
317
318        await self._transport.send({'type': 'notify',
319                                    'name': name,
320                                    'data': data})

Send notification

Raises:
  • ConnectionError