vsslctrl 0.1.0.dev1__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.
vsslctrl/api_base.py ADDED
@@ -0,0 +1,321 @@
1
+ from abc import ABC, abstractmethod
2
+ import asyncio
3
+ from random import randrange
4
+ from asyncio.exceptions import IncompleteReadError
5
+ import logging
6
+ from typing import Callable, final
7
+
8
+ from .utils import cancel_task
9
+ from .exceptions import ZoneConnectionError
10
+ from .decorators import logging_helpers
11
+
12
+
13
+ class APITaskGroup:
14
+ def __init__(self):
15
+ self._tasks = []
16
+
17
+ @property
18
+ def tasks(self):
19
+ return self._tasks
20
+
21
+ def add(self, task_cr):
22
+ task = asyncio.create_task(task_cr)
23
+ self._tasks.append(task)
24
+
25
+ def extend(self, tasks: list):
26
+ for task in tasks:
27
+ self.add(task)
28
+
29
+ async def cancel(self):
30
+ # Cancel all tasks in the group
31
+ for task in self._tasks:
32
+ cancel_task(task)
33
+ self._tasks = []
34
+
35
+ async def wait():
36
+ return await asyncio.gather(*self._tasks)
37
+
38
+
39
+ @logging_helpers("Base API:")
40
+ class APIBase(ABC):
41
+ TIMEOUT = 5 # seconds
42
+ KEEP_ALIVE = 10 # seconds
43
+ BACKOFF_MIN = 15 # seconds
44
+ BACKOFF_MAX = 300 # 5 minutes
45
+
46
+ FRIST_BYTE = 1
47
+
48
+ def __init__(self, host, port):
49
+ self.host = host
50
+ self.port = port
51
+
52
+ self._reader = None
53
+ self._writer = None
54
+ self._writer_queue: asyncio.Queue = asyncio.Queue()
55
+
56
+ self._disconnecting = False
57
+ self._connecting = False
58
+ self.connection_event = asyncio.Event()
59
+
60
+ self._keep_alive_received = False
61
+ self._keep_connected_task = None
62
+ self._reconnection_attempts = 0
63
+
64
+ self._task_group = APITaskGroup()
65
+
66
+ @property
67
+ def connected(self):
68
+ if self.connection_event is None:
69
+ return False
70
+ return self.connection_event.is_set()
71
+
72
+ @property
73
+ def _reconnecting(self):
74
+ return self._disconnecting or self._connecting
75
+
76
+ #
77
+ # Send a request
78
+ #
79
+ def send(self, data):
80
+ if self._writer_queue and self.connected:
81
+ self._writer_queue.put_nowait(data)
82
+
83
+ #
84
+ # Connect
85
+ #
86
+ @final
87
+ async def connect(self):
88
+ if self.connected:
89
+ return self.connected
90
+
91
+ self._connecting = True
92
+
93
+ try:
94
+ self._log_debug(f"Attemping connection to {self.host}:{self.port}")
95
+ self._reader, self._writer = await asyncio.wait_for(
96
+ asyncio.open_connection(self.host, self.port), self.TIMEOUT
97
+ )
98
+
99
+ # Connected
100
+ self.connection_event.set()
101
+
102
+ # cancel any reconnecting loops
103
+ self._cancel_keep_connected()
104
+
105
+ # Groups tasks for easy handling
106
+ self._task_group.extend(
107
+ [
108
+ self._receive_first_byte(),
109
+ self._send_bytes(),
110
+ self._send_keepalive_base(),
111
+ ]
112
+ )
113
+
114
+ self._log_info(f"Connected to {self.host}:{self.port}")
115
+
116
+ except (asyncio.TimeoutError, asyncio.CancelledError):
117
+ message = f"Connection to {self.host}:{self.port} timed out"
118
+ self._log_error(message)
119
+ self.connection_event.clear()
120
+ raise ZoneConnectionError(message)
121
+ except ConnectionRefusedError:
122
+ message = f"Connection to {self.host}:{self.port} refused"
123
+ self._log_error(message)
124
+ self.connection_event.clear()
125
+ raise ZoneConnectionError(message)
126
+ except Exception as e:
127
+ self._log_error(
128
+ f"Connection to {self.host}:{self.port} failed with exception {e}"
129
+ )
130
+ self.connection_event.clear()
131
+ raise
132
+ finally:
133
+ self._connecting = False
134
+
135
+ return self.connected
136
+
137
+ #
138
+ # Disconnect
139
+ #
140
+ @final
141
+ async def disconnect(self):
142
+ self._disconnecting = True
143
+
144
+ # cancel any reconnecting loops
145
+ self._cancel_keep_connected()
146
+
147
+ # Break Loops
148
+ if self.connection_event:
149
+ self.connection_event.clear()
150
+
151
+ await self._task_group.cancel()
152
+
153
+ if self._writer:
154
+ try:
155
+ # Writer hangs on disconnect sometimes
156
+ self._writer.close()
157
+ await asyncio.wait_for(self._writer.wait_closed(), self.TIMEOUT)
158
+
159
+ except asyncio.CancelledError:
160
+ self._log_debug(f"writer close timeout")
161
+
162
+ self._log_info(f"{self.host}:{self.port}: disconnected")
163
+
164
+ self._disconnecting = False
165
+
166
+ return not self.connected
167
+
168
+ #
169
+ # Reconnect
170
+ #
171
+ @final
172
+ async def reconnect(self):
173
+ if not self._reconnecting and not self._is_keep_connected_running():
174
+ await self.disconnect()
175
+ self._keep_connected()
176
+
177
+ #
178
+ # Is keep connected task running
179
+ #
180
+ @final
181
+ def _is_keep_connected_running(self):
182
+ return (
183
+ isinstance(self._keep_connected_task, asyncio.Task)
184
+ and not self._keep_connected_task.done()
185
+ )
186
+
187
+ #
188
+ # Keep connected
189
+ #
190
+ @final
191
+ def _keep_connected(self):
192
+ if not self._is_keep_connected_running():
193
+ self._log_debug(f"{self.host}:{self.port}: creating keep_connected task")
194
+ self._keep_connected_task = asyncio.create_task(self._keep_connected_loop())
195
+ return self._keep_connected_task
196
+
197
+ #
198
+ # Keep connected loop
199
+ #
200
+ @final
201
+ async def _keep_connected_loop(self):
202
+ # break if we try to disconnect
203
+ while not self.connected:
204
+ try:
205
+ await self.connect()
206
+ self._cancel_keep_connected()
207
+ except ZoneConnectionError as e:
208
+ self._reconnection_attempts += 1
209
+
210
+ backoff = min(
211
+ max(
212
+ self.BACKOFF_MIN,
213
+ self.BACKOFF_MIN * self._reconnection_attempts,
214
+ ),
215
+ self.BACKOFF_MAX,
216
+ )
217
+
218
+ self._log_info(
219
+ f"{self.host}:{self.port}: reconnecting in {backoff} seconds"
220
+ )
221
+ await asyncio.sleep(backoff)
222
+
223
+ #
224
+ # Cancel keep connected tasks
225
+ #
226
+ @final
227
+ def _cancel_keep_connected(self):
228
+ self._log_debug(f"{self.host}:{self.port}: canceling keep_connected task")
229
+ cancel_task(self._keep_connected_task)
230
+ self._reconnection_attempts = 0
231
+
232
+ #
233
+ # Send Bytes
234
+ #
235
+ @final
236
+ async def _send_bytes(self):
237
+ try:
238
+ self._log_debug(f"Send task started for {self.host}:{self.port}")
239
+ while self.connected:
240
+ # Wait until there's data in the queue
241
+ data = await self._writer_queue.get()
242
+
243
+ if not isinstance(data, bytearray):
244
+ Exception("Currently only accept Bytearray!")
245
+
246
+ # Send the data
247
+ self._writer.write(data)
248
+ await self._writer.drain()
249
+
250
+ self._log_debug(f"Sent to {self.host}:{self.port}: {data.hex()}")
251
+
252
+ # VSSL cant handle too many requests.
253
+ await asyncio.sleep(0.2)
254
+
255
+ except asyncio.CancelledError:
256
+ self._log_debug(f"Cancelled send task for {self.host}:{self.port}")
257
+ except (BrokenPipeError, ConnectionError, TimeoutError, OSError) as e:
258
+ self._log_error(f"Lost connection to host {self.host}:{self.port}")
259
+ await self.reconnect()
260
+
261
+ #
262
+ # Response Task Loop
263
+ #
264
+ @final
265
+ async def _receive_first_byte(self):
266
+ try:
267
+ self._log_debug(f"Receive task started for {self.host}:{self.port}")
268
+ while self.connected:
269
+ data = await self._reader.readexactly(self.FRIST_BYTE)
270
+
271
+ if not data:
272
+ continue
273
+
274
+ self._keep_alive_received = True
275
+
276
+ await self._read_byte_stream(self._reader, data)
277
+
278
+ except asyncio.CancelledError:
279
+ self._log_debug(f"Cancelled receive task for {self.host}:{self.port}")
280
+ except (
281
+ IncompleteReadError,
282
+ TimeoutError,
283
+ ConnectionResetError,
284
+ OSError,
285
+ ) as e:
286
+ self._log_error(f"Lost connection to host {self.host}:{self.port}")
287
+ await self.reconnect()
288
+
289
+ #
290
+ # Read the byte stream
291
+ #
292
+ @abstractmethod
293
+ async def _read_byte_stream(self):
294
+ pass
295
+
296
+ #
297
+ # Send a keep alive
298
+ #
299
+ async def _send_keepalive_base(self):
300
+ try:
301
+ while self.connected:
302
+ self._keep_alive_received = False
303
+
304
+ # Send first then sleep
305
+ self._send_keepalive()
306
+
307
+ await asyncio.sleep(self.KEEP_ALIVE)
308
+
309
+ if not self._keep_alive_received:
310
+ self._log_error("Keep-alive not received")
311
+ await self.reconnect()
312
+
313
+ except asyncio.CancelledError:
314
+ self._log_debug(f"Cancelled the keepalive task for {self.host}:{self.port}")
315
+
316
+ #
317
+ # Send a keep alive
318
+ #
319
+ @abstractmethod
320
+ def _send_keepalive(self):
321
+ pass