syndesi 0.4.2__py3-none-any.whl → 0.5.0__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.
Files changed (57) hide show
  1. syndesi/__init__.py +22 -2
  2. syndesi/adapters/adapter.py +332 -489
  3. syndesi/adapters/adapter_worker.py +820 -0
  4. syndesi/adapters/auto.py +58 -25
  5. syndesi/adapters/descriptors.py +38 -0
  6. syndesi/adapters/ip.py +203 -71
  7. syndesi/adapters/serialport.py +154 -25
  8. syndesi/adapters/stop_conditions.py +354 -0
  9. syndesi/adapters/timeout.py +58 -21
  10. syndesi/adapters/visa.py +236 -11
  11. syndesi/cli/console.py +51 -16
  12. syndesi/cli/shell.py +95 -47
  13. syndesi/cli/terminal_tools.py +8 -8
  14. syndesi/component.py +315 -0
  15. syndesi/protocols/delimited.py +92 -107
  16. syndesi/protocols/modbus.py +2368 -868
  17. syndesi/protocols/protocol.py +186 -33
  18. syndesi/protocols/raw.py +45 -62
  19. syndesi/protocols/scpi.py +65 -102
  20. syndesi/remote/remote.py +188 -0
  21. syndesi/scripts/syndesi.py +12 -2
  22. syndesi/tools/errors.py +49 -31
  23. syndesi/tools/log_settings.py +21 -8
  24. syndesi/tools/{log.py → logmanager.py} +24 -13
  25. syndesi/tools/types.py +9 -7
  26. syndesi/version.py +5 -1
  27. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/METADATA +1 -1
  28. syndesi-0.5.0.dist-info/RECORD +41 -0
  29. syndesi/adapters/backend/__init__.py +0 -0
  30. syndesi/adapters/backend/adapter_backend.py +0 -438
  31. syndesi/adapters/backend/adapter_manager.py +0 -48
  32. syndesi/adapters/backend/adapter_session.py +0 -346
  33. syndesi/adapters/backend/backend.py +0 -438
  34. syndesi/adapters/backend/backend_status.py +0 -0
  35. syndesi/adapters/backend/backend_tools.py +0 -66
  36. syndesi/adapters/backend/descriptors.py +0 -153
  37. syndesi/adapters/backend/ip_backend.py +0 -149
  38. syndesi/adapters/backend/serialport_backend.py +0 -241
  39. syndesi/adapters/backend/stop_condition_backend.py +0 -219
  40. syndesi/adapters/backend/timed_queue.py +0 -39
  41. syndesi/adapters/backend/timeout.py +0 -252
  42. syndesi/adapters/backend/visa_backend.py +0 -197
  43. syndesi/adapters/ip_server.py +0 -102
  44. syndesi/adapters/stop_condition.py +0 -90
  45. syndesi/cli/backend_console.py +0 -96
  46. syndesi/cli/backend_status.py +0 -274
  47. syndesi/cli/backend_wrapper.py +0 -61
  48. syndesi/scripts/syndesi_backend.py +0 -37
  49. syndesi/tools/backend_api.py +0 -175
  50. syndesi/tools/backend_logger.py +0 -64
  51. syndesi/tools/exceptions.py +0 -16
  52. syndesi/tools/internal.py +0 -0
  53. syndesi-0.4.2.dist-info/RECORD +0 -60
  54. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/WHEEL +0 -0
  55. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/entry_points.txt +0 -0
  56. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/licenses/LICENSE +0 -0
  57. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,354 @@
1
+ # File : stop_condition.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+
5
+ """
6
+ Stop-condition module
7
+
8
+ This is the frontend of the stop-conditions, the part that is imported by the user
9
+ """
10
+
11
+ # from abc import abstractmethod
12
+ from abc import abstractmethod
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+
16
+
17
+ @dataclass
18
+ class Fragment:
19
+ """
20
+ Fragment class, holds a piece of data (bytes) and the time at which it was received
21
+ """
22
+
23
+ data: bytes
24
+ timestamp: float
25
+
26
+ def __str__(self) -> str:
27
+ return f"{self.data!r}@{self.timestamp}"
28
+
29
+ def __repr__(self) -> str:
30
+ return f"Fragment({self.data!r}@{self.timestamp})"
31
+
32
+ def __getitem__(self, key: slice) -> "Fragment":
33
+ # if self.data is None:
34
+ # raise IndexError('Cannot index invalid fragment')
35
+ return Fragment(self.data[key], self.timestamp)
36
+
37
+
38
+ class StopConditionType(Enum):
39
+ """
40
+ Stop-condition type
41
+ """
42
+
43
+ TERMINATION = "termination"
44
+ LENGTH = "length"
45
+ CONTINUATION = "continuation"
46
+ TOTAL = "total"
47
+ FRAGMENT = "fragment"
48
+
49
+
50
+ class StopCondition:
51
+ """
52
+ Stop-condition base class, cannot be used on its own
53
+ """
54
+
55
+ # @abstractmethod
56
+ # def type(self) -> StopConditionType:
57
+ # pass
58
+
59
+ @abstractmethod
60
+ def initiate_read(self, timestamp: float) -> None:
61
+ """
62
+ Prepare the stop-condition for the next read
63
+ """
64
+
65
+ @abstractmethod
66
+ def evaluate(
67
+ self, raw_fragment: Fragment
68
+ ) -> tuple[bool, Fragment, Fragment, float | None]:
69
+ """
70
+ Evaluate incoming fragment and return read information for the next fragment
71
+ """
72
+
73
+ @abstractmethod
74
+ def type(self) -> StopConditionType:
75
+ """
76
+ Helper function to determine the which type of stop-condition generated a stop
77
+ """
78
+
79
+ @abstractmethod
80
+ def flush_read(self) -> None:
81
+ """
82
+ Reset read operation
83
+ """
84
+
85
+
86
+ class Termination(StopCondition):
87
+ """
88
+ Termination stop-condition, used to stop when a specified sequence is received
89
+
90
+ Parameters
91
+ ----------
92
+ sequence : bytes | str
93
+ """
94
+
95
+ def __init__(self, sequence: bytes | str) -> None:
96
+ super().__init__()
97
+ if isinstance(sequence, str):
98
+ self._sequence = sequence.encode("utf-8")
99
+ else:
100
+ self._sequence = sequence
101
+ self._sequence_found_length = 0
102
+
103
+ # TYPE = StopConditionType.TERMINATION
104
+
105
+ # def __init__(self, sequence: bytes | str) -> None:
106
+ # """
107
+ # Instanciate a new Termination class
108
+ # """
109
+ # self.sequence: bytes
110
+ # if isinstance(sequence, str):
111
+ # self.sequence = sequence.encode("utf-8")
112
+ # elif isinstance(sequence, bytes):
113
+ # self.sequence = sequence
114
+ # else:
115
+ # raise ValueError(f"Invalid termination sequence type : {type(sequence)}")
116
+
117
+ def __str__(self) -> str:
118
+ return f"Termination({repr(self._sequence)})"
119
+
120
+ def __repr__(self) -> str:
121
+ return self.__str__()
122
+
123
+ def initiate_read(self, timestamp: float) -> None:
124
+ self._sequence_found_length = 0
125
+
126
+ def flush_read(self) -> None:
127
+ self._sequence_found_length = 0
128
+
129
+ def evaluate(
130
+ self, raw_fragment: Fragment
131
+ ) -> tuple[bool, Fragment, Fragment, float | None]:
132
+ if raw_fragment.data is None:
133
+ raise RuntimeError("Trying to evaluate an invalid fragment")
134
+
135
+ position, length = termination_in_data(
136
+ self._sequence[self._sequence_found_length :], raw_fragment.data
137
+ )
138
+ stop = False
139
+ deferred = Fragment(b"", raw_fragment.timestamp)
140
+
141
+ if position is None:
142
+ # Nothing was found, keep everything
143
+ kept = raw_fragment
144
+ else:
145
+ self._sequence_found_length += length
146
+
147
+ if self._sequence_found_length == len(self._sequence):
148
+ # The sequence was found entirely
149
+ deferred = raw_fragment[position + length :]
150
+ self._sequence_found_length = 0
151
+ stop = True
152
+ elif position + length == len(raw_fragment.data):
153
+ # Part of the sequence was found at the end
154
+ # Return what's before the sequence
155
+ deferred = Fragment(b"", raw_fragment.timestamp)
156
+
157
+ kept = raw_fragment[: position + length]
158
+
159
+ return stop, kept, deferred, None
160
+
161
+ def type(self) -> StopConditionType:
162
+ return StopConditionType.TERMINATION
163
+
164
+
165
+ class Length(StopCondition):
166
+ """
167
+ Length stop-condition, used to stop when the specified number of bytes (or more) have been read
168
+
169
+ Parameters
170
+ ----------
171
+ n : int
172
+ Number of bytes
173
+ """
174
+
175
+ # TYPE = StopConditionType.LENGTH
176
+ def __init__(self, n: int) -> None:
177
+ super().__init__()
178
+ self._n = n
179
+ self._counter = 0
180
+
181
+ def __str__(self) -> str:
182
+ return f"Length({self._n})"
183
+
184
+ def __repr__(self) -> str:
185
+ return self.__str__()
186
+
187
+ def initiate_read(self, timestamp: float) -> None:
188
+ # Length
189
+ self._counter = 0
190
+
191
+ def type(self) -> StopConditionType:
192
+ return StopConditionType.LENGTH
193
+
194
+ def flush_read(self) -> None:
195
+ self._counter = 0
196
+
197
+ def evaluate(
198
+ self, raw_fragment: Fragment
199
+ ) -> tuple[bool, Fragment, Fragment, float | None]:
200
+ remaining_bytes = self._n - self._counter
201
+ kept_fragment = raw_fragment[:remaining_bytes]
202
+ deferred_fragment = raw_fragment[remaining_bytes:]
203
+ self._counter += len(kept_fragment.data)
204
+ remaining_bytes = self._n - self._counter
205
+ # TODO : remaining_bytes <= 0 ? Alongside above TODO maybe
206
+ return remaining_bytes == 0, kept_fragment, deferred_fragment, None
207
+
208
+
209
+ class Continuation(StopCondition):
210
+ """
211
+ Continuation stop-condition, used to stop reading when data has already been received
212
+ and nothing has been received since then for the specified amount of time
213
+
214
+ Parameters
215
+ ----------
216
+ continuation : float
217
+ """
218
+
219
+ def __init__(self, continuation: float) -> None:
220
+ super().__init__()
221
+ self.continuation = continuation
222
+ self._last_fragment: float | None = None
223
+
224
+ def __str__(self) -> str:
225
+ return f"Continuation({self.continuation})"
226
+
227
+ def __repr__(self) -> str:
228
+ return self.__str__()
229
+
230
+ def initiate_read(self, timestamp: float) -> None:
231
+ self._last_fragment = timestamp
232
+
233
+ def flush_read(self) -> None:
234
+ self._last_fragment = None
235
+
236
+ def evaluate(
237
+ self, raw_fragment: Fragment
238
+ ) -> tuple[bool, Fragment, Fragment, float | None]:
239
+ deferred = Fragment(b"", raw_fragment.timestamp)
240
+ kept = raw_fragment
241
+
242
+ # if raw_fragment.timestamp is None:
243
+ # raise RuntimeError("Cannot evaluate fragment with no timestamp")
244
+ # last_fragment can be none if no data was ever received
245
+ if self._last_fragment is not None:
246
+ continuation_timestamp = self._last_fragment + self.continuation
247
+ stop = continuation_timestamp <= raw_fragment.timestamp
248
+ next_event_timeout = continuation_timestamp
249
+ else:
250
+ stop = False
251
+ next_event_timeout = None
252
+
253
+ return stop, kept, deferred, next_event_timeout
254
+
255
+ def type(self) -> StopConditionType:
256
+ return StopConditionType.CONTINUATION
257
+
258
+
259
+ class Total(StopCondition):
260
+ """
261
+ Total stop-condition, used to stop reading when data has already been received
262
+ and the total read time exceeds the specified amount
263
+
264
+ """
265
+
266
+ def __init__(self, total: float) -> None:
267
+ super().__init__()
268
+ self.total = total
269
+ self._start_time: float | None = None
270
+
271
+ def __str__(self) -> str:
272
+ return f"Total({self.total})"
273
+
274
+ def __repr__(self) -> str:
275
+ return self.__str__()
276
+
277
+ def initiate_read(self, timestamp: float) -> None:
278
+ self._start_time = timestamp
279
+
280
+ def flush_read(self) -> None:
281
+ self._start_time = None
282
+
283
+ def evaluate(
284
+ self, raw_fragment: Fragment
285
+ ) -> tuple[bool, Fragment, Fragment, float | None]:
286
+ kept = raw_fragment
287
+ deferred = Fragment(b"", raw_fragment.timestamp)
288
+
289
+ # if raw_fragment.timestamp is None:
290
+ # raise RuntimeError("Cannot evaluate fragment with no timestamp")
291
+
292
+ if self._start_time is None:
293
+ raise RuntimeError("Invalid start time")
294
+ total_timestamp = self._start_time + self.total
295
+ stop = total_timestamp <= raw_fragment.timestamp
296
+
297
+ return stop, kept, deferred, total_timestamp
298
+
299
+ def type(self) -> StopConditionType:
300
+ return StopConditionType.TOTAL
301
+
302
+
303
+ class FragmentStopCondition(StopCondition):
304
+ """
305
+ Fragment stop-condition, used to stop on each piece of data received by the
306
+ adapter
307
+
308
+ """
309
+
310
+ def __init__(self) -> None: ...
311
+
312
+ def __str__(self) -> str:
313
+ return "FragmentStopCondition()"
314
+
315
+ def __repr__(self) -> str:
316
+ return self.__str__()
317
+
318
+ def initiate_read(self, timestamp: float) -> None:
319
+ pass
320
+
321
+ def flush_read(self) -> None:
322
+ pass
323
+
324
+ def evaluate(
325
+ self, raw_fragment: Fragment
326
+ ) -> tuple[bool, Fragment, Fragment, float | None]:
327
+
328
+ return True, raw_fragment, Fragment(b"", raw_fragment.timestamp), None
329
+
330
+ def type(self) -> StopConditionType:
331
+ return StopConditionType.FRAGMENT
332
+
333
+
334
+ def termination_in_data(termination: bytes, data: bytes) -> tuple[int | None, int]:
335
+ """
336
+ Return the position (if it exists) and length of the termination (or part of it) inside data
337
+ """
338
+ p = None
339
+ length = len(termination)
340
+ # First check if the full termination is somewhere. If that's the case, data will be split
341
+ try:
342
+ p = data.index(termination)
343
+ # If found, return that
344
+ except ValueError:
345
+ # If not, we'll try to find if part of the sequence is at the end, in that case
346
+ # we'll return the length of the sequence that was found
347
+ length -= 1
348
+ while length > 0:
349
+ if data[-length:] == termination[:length]:
350
+ p = len(data) - length # - 1
351
+ break
352
+ length -= 1
353
+
354
+ return p, length
@@ -1,23 +1,43 @@
1
1
  # File : timeout.py
2
2
  # Author : Sébastien Deriaz
3
3
  # License : GPL
4
+ """
5
+ This module holds the Timeout class, this class is meant for the user as a frontend for the
6
+ backend timeout management
7
+ """
4
8
 
5
9
  from enum import Enum
6
10
  from types import EllipsisType
7
- from typing import Any, Protocol
11
+ from typing import Any
8
12
 
9
13
  from ..tools.types import NumberLike, is_number
10
14
 
15
+
11
16
  class TimeoutAction(Enum):
17
+ """
18
+ Action on timeout expiration
19
+ """
20
+
12
21
  ERROR = "error"
13
22
  RETURN_EMPTY = "return_empty"
14
-
15
-
16
- class IsInitialized(Protocol):
17
- response: NumberLike | None
23
+ RETURN_NONE = "return_none"
18
24
 
19
25
 
20
26
  class Timeout:
27
+ """
28
+ This class holds timeout information
29
+
30
+ Parameters
31
+ ----------
32
+ response : float
33
+ Time before the device responds
34
+ action : str
35
+ Action performed when a timeout occurs
36
+ * ``error`` : raise a AdapterTimeoutError
37
+ * ``return_empty`` : return b''
38
+ * ``return_none`` : return None
39
+ """
40
+
21
41
  DEFAULT_ACTION = TimeoutAction.ERROR
22
42
 
23
43
  def __init__(
@@ -25,16 +45,7 @@ class Timeout:
25
45
  response: NumberLike | None | EllipsisType = ...,
26
46
  action: str | EllipsisType | TimeoutAction = ...,
27
47
  ) -> None:
28
- """
29
- This class holds timeout information
30
48
 
31
- Parameters
32
- ----------
33
- response : float
34
- Time before the device responds
35
- action : str
36
- Action performed when a timeout occurs. 'error' -> raise an error, 'return' -> return b''
37
- """
38
49
  super().__init__()
39
50
 
40
51
  self._is_default_response = response is ...
@@ -63,29 +74,55 @@ class Timeout:
63
74
  return self.__str__()
64
75
 
65
76
  def set_default(self, default_timeout: "Timeout") -> None:
77
+ """
78
+ Set the default timeout (no effect if timeout is already set)
79
+
80
+ Parameters
81
+ ----------
82
+ default_timeout : Timeout
83
+ """
66
84
  if self._is_default_response:
67
85
  self._response = default_timeout.response()
68
86
  if self._is_default_action:
69
87
  self.action = default_timeout.action
70
88
 
71
89
  def response(self) -> NumberLike | None:
90
+ """
91
+ Return timeout response if it has been configured
92
+
93
+ Returns
94
+ -------
95
+ response : NumberLike | None
96
+ """
72
97
  if self._response is ...:
73
98
  return None
74
- elif self._response is None:
99
+ if self._response is None:
75
100
  return None
76
- else:
77
- return self._response
101
+ return self._response
78
102
 
79
103
  def is_initialized(self) -> bool:
104
+ """
105
+ Return True if the Timeout has been initialized, False otherwise
106
+ """
80
107
  return self._response is not Ellipsis
81
108
 
82
109
 
83
110
  def any_to_timeout(value: Any) -> Timeout:
111
+ """
112
+ Convert any input to a timeout (if possible)
113
+
114
+ Parameters
115
+ ----------
116
+ value : None | NumberLike | Timeout
117
+
118
+ Returns
119
+ -------
120
+ timeout : Timeout
121
+ """
84
122
  if value is None:
85
123
  return Timeout(response=None)
86
- elif is_number(value):
124
+ if is_number(value):
87
125
  return Timeout(response=float(value))
88
- elif isinstance(value, Timeout):
126
+ if isinstance(value, Timeout):
89
127
  return value
90
- else:
91
- raise ValueError(f"Could not convert {value} to Timeout")
128
+ raise ValueError(f"Could not convert {value} to Timeout")