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/__init__.py +4 -0
- vsslctrl/api_alpha.py +1040 -0
- vsslctrl/api_base.py +321 -0
- vsslctrl/api_bravo.py +419 -0
- vsslctrl/core.py +322 -0
- vsslctrl/data_structure.py +242 -0
- vsslctrl/decorators.py +61 -0
- vsslctrl/discovery.py +193 -0
- vsslctrl/event_bus.py +159 -0
- vsslctrl/exceptions.py +21 -0
- vsslctrl/group.py +187 -0
- vsslctrl/io.py +229 -0
- vsslctrl/settings.py +655 -0
- vsslctrl/track.py +337 -0
- vsslctrl/transport.py +242 -0
- vsslctrl/utils.py +75 -0
- vsslctrl/zone.py +452 -0
- vsslctrl-0.1.0.dev1.dist-info/LICENSE +21 -0
- vsslctrl-0.1.0.dev1.dist-info/METADATA +385 -0
- vsslctrl-0.1.0.dev1.dist-info/RECORD +22 -0
- vsslctrl-0.1.0.dev1.dist-info/WHEEL +5 -0
- vsslctrl-0.1.0.dev1.dist-info/top_level.txt +1 -0
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
|