syndesi 0.2.4__py3-none-any.whl → 0.3.2__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 (100) hide show
  1. syndesi/__init__.py +9 -3
  2. syndesi/__main__.py +4 -0
  3. syndesi/adapters/__init__.py +0 -8
  4. syndesi/adapters/adapter.py +541 -244
  5. syndesi/adapters/auto.py +28 -23
  6. syndesi/adapters/backend/adapter_backend.py +387 -0
  7. syndesi/adapters/backend/adapter_manager.py +48 -0
  8. syndesi/adapters/backend/adapter_session.py +447 -0
  9. syndesi/adapters/backend/backend.py +438 -0
  10. syndesi/adapters/backend/backend_tools.py +66 -0
  11. syndesi/adapters/backend/descriptors.py +152 -0
  12. syndesi/adapters/backend/ip_backend.py +148 -0
  13. syndesi/adapters/backend/serialport_backend.py +236 -0
  14. syndesi/adapters/backend/stop_condition_backend.py +342 -0
  15. syndesi/adapters/backend/timed_queue.py +39 -0
  16. syndesi/adapters/backend/timeout.py +252 -0
  17. syndesi/adapters/backend/visa_backend.py +197 -0
  18. syndesi/adapters/ip.py +89 -154
  19. syndesi/adapters/ip_server.py +87 -93
  20. syndesi/adapters/serialport.py +56 -178
  21. syndesi/adapters/stop_condition.py +142 -0
  22. syndesi/adapters/timeout.py +85 -296
  23. syndesi/adapters/visa.py +39 -98
  24. syndesi/cli/backend_console.py +96 -0
  25. syndesi/cli/backend_status.py +276 -0
  26. syndesi/cli/backend_wrapper.py +61 -0
  27. syndesi/cli/console.py +281 -0
  28. syndesi/cli/shell.py +239 -151
  29. syndesi/cli/shell_tools.py +107 -0
  30. syndesi/cli/terminal_tools.py +14 -0
  31. syndesi/protocols/__init__.py +0 -6
  32. syndesi/protocols/delimited.py +151 -45
  33. syndesi/protocols/modbus.py +672 -377
  34. syndesi/protocols/protocol.py +67 -11
  35. syndesi/protocols/raw.py +67 -9
  36. syndesi/protocols/scpi.py +90 -36
  37. syndesi/{descriptors/syndesi → scripts}/__init__.py +0 -0
  38. syndesi/scripts/syndesi.py +52 -0
  39. syndesi/scripts/syndesi_backend.py +37 -0
  40. syndesi/tools/backend_api.py +209 -0
  41. syndesi/tools/backend_logger.py +65 -0
  42. syndesi/tools/errors.py +23 -0
  43. syndesi/tools/exceptions.py +7 -1
  44. syndesi/tools/log.py +207 -55
  45. syndesi/tools/log_settings.py +17 -0
  46. syndesi/tools/types.py +67 -40
  47. syndesi/version.py +3 -1
  48. {syndesi-0.2.4.dist-info → syndesi-0.3.2.dist-info}/METADATA +39 -11
  49. syndesi-0.3.2.dist-info/RECORD +59 -0
  50. {syndesi-0.2.4.dist-info → syndesi-0.3.2.dist-info}/WHEEL +1 -1
  51. syndesi-0.3.2.dist-info/entry_points.txt +3 -0
  52. syndesi-0.3.2.dist-info/licenses/LICENSE +674 -0
  53. {syndesi-0.2.4.dist-info → syndesi-0.3.2.dist-info}/top_level.txt +0 -0
  54. syndesi/_version.py +0 -1
  55. syndesi/adapters/IP.py +0 -81
  56. syndesi/adapters/VISA.py +0 -50
  57. syndesi/adapters/iadapter.py +0 -211
  58. syndesi/adapters/proxy.py +0 -99
  59. syndesi/adapters/remote.py +0 -18
  60. syndesi/adapters/serial.py +0 -37
  61. syndesi/adapters/stop_conditions.py +0 -210
  62. syndesi/adapters/timed_queue.py +0 -32
  63. syndesi/api/api.py +0 -77
  64. syndesi/cli/adapter.py +0 -134
  65. syndesi/cli/command.py +0 -29
  66. syndesi/cli/serial.py +0 -18
  67. syndesi/cli/syndesi.py +0 -45
  68. syndesi/descriptors/IP.py +0 -9
  69. syndesi/descriptors/Serial.py +0 -10
  70. syndesi/descriptors/VISA.py +0 -31
  71. syndesi/descriptors/__init__.py +0 -1
  72. syndesi/descriptors/descriptor.py +0 -9
  73. syndesi/descriptors/ip.py +0 -9
  74. syndesi/descriptors/syndesi/Syndesi.py +0 -9
  75. syndesi/descriptors/syndesi/_device.py +0 -25
  76. syndesi/descriptors/syndesi/devices.py +0 -10
  77. syndesi/descriptors/syndesi/frame.py +0 -133
  78. syndesi/descriptors/syndesi/network.py +0 -41
  79. syndesi/descriptors/syndesi/payload.py +0 -11
  80. syndesi/descriptors/syndesi/sdid.py +0 -21
  81. syndesi/descriptors/visa.py +0 -31
  82. syndesi/protocols/commands.py +0 -56
  83. syndesi/protocols/iprotocol.py +0 -14
  84. syndesi/protocols/sdp.py +0 -14
  85. syndesi/proxy/__init__.py +0 -0
  86. syndesi/proxy/proxy.py +0 -136
  87. syndesi/proxy/proxy_api.py +0 -122
  88. syndesi/tools/logger.py +0 -113
  89. syndesi/tools/others.py +0 -2
  90. syndesi/tools/remote_api.py +0 -113
  91. syndesi/tools/remote_server.py +0 -133
  92. syndesi/tools/shell.py +0 -111
  93. syndesi/tools/stop_conditions.py +0 -148
  94. syndesi-0.2.4.dist-info/RECORD +0 -73
  95. syndesi-0.2.4.dist-info/entry_points.txt +0 -3
  96. tests/__init__.py +0 -0
  97. {experiments → syndesi/adapters/backend}/__init__.py +0 -0
  98. /syndesi/{api/__init__.py → cli/terminal.py} +0 -0
  99. /syndesi/cli/{ip.py → terminal_apps.py} +0 -0
  100. /syndesi/{cli/modbus.py → tools/internal.py} +0 -0
@@ -1,24 +1,80 @@
1
- from ..adapters import Adapter
2
- from ..adapters import Timeout
3
- from ..adapters.auto import auto_adapter
1
+ # File : protocol.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+
4
5
  import logging
5
- from ..tools.log import LoggerAlias
6
+ from abc import abstractmethod
7
+ from collections.abc import Callable
8
+ from types import EllipsisType
9
+ from typing import Any
10
+
11
+ from ..adapters.adapter import Adapter
12
+ from ..adapters.auto import auto_adapter
13
+ from ..adapters.backend.adapter_backend import AdapterReadPayload, AdapterSignal
14
+
15
+ # from syndesi.adapters.stop_condition import StopCondition
16
+ from ..adapters.timeout import Timeout
17
+ from ..tools.log_settings import LoggerAlias
18
+
6
19
 
7
20
  class Protocol:
8
- def __init__(self, adapter : Adapter, timeout : Timeout = ...) -> None:
21
+ def __init__(
22
+ self,
23
+ adapter: Adapter,
24
+ timeout: Timeout | None | EllipsisType = ...,
25
+ event_callback: Callable[[AdapterSignal], None] | None = None,
26
+ ) -> None:
27
+ # TODO : Convert the callable from AdapterSignal to ProtocolSignal or something similar
9
28
  self._adapter = auto_adapter(adapter)
10
- if timeout != ...:
29
+
30
+ self._event_callback = event_callback
31
+ self._adapter.set_event_callback(self.event_callback)
32
+
33
+ if timeout is not ...:
11
34
  self._adapter.set_default_timeout(timeout)
12
35
  self._logger = logging.getLogger(LoggerAlias.PROTOCOL.value)
13
36
 
14
- def flushRead(self):
37
+ if timeout is ...:
38
+ self._adapter.set_timeout(self._default_timeout())
39
+ else:
40
+ self._adapter.set_timeout(timeout)
41
+
42
+ @abstractmethod
43
+ def _default_timeout(self) -> Timeout | None:
44
+ pass
45
+
46
+ def flushRead(self) -> None:
15
47
  self._adapter.flushRead()
16
48
 
17
- def write(self, data):
49
+ def event_callback(self, event: AdapterSignal) -> None:
50
+ if self._event_callback is not None:
51
+ self._event_callback(event)
52
+
53
+ @abstractmethod
54
+ def _on_data_ready_event(self, data: AdapterReadPayload) -> None:
55
+ pass
56
+
57
+ @abstractmethod
58
+ def write(self, *args: Any, **kwargs: Any) -> None:
18
59
  pass
19
60
 
20
- def query(self, data, timeout : Timeout = ...):
61
+ def open(self) -> None:
62
+ self._adapter.open()
63
+
64
+ def close(self) -> None:
65
+ self._adapter.close()
66
+
67
+ @abstractmethod
68
+ def query(
69
+ self,
70
+ *args: Any,
71
+ **kwargs: Any,
72
+ # timeout: Union[Timeout, EllipsisType, None],
73
+ # stop_condition: Union[StopCondition, None, EllipsisType],
74
+ # full_output : bool,
75
+ ) -> Any:
21
76
  pass
22
77
 
23
- def read(self):
24
- pass
78
+ @abstractmethod
79
+ def read(self, *args: Any, **kwargs: Any) -> Any:
80
+ pass
syndesi/protocols/raw.py CHANGED
@@ -1,12 +1,28 @@
1
- from ..adapters import Adapter, Timeout
2
- from .protocol import Protocol
1
+ # File : raw.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+
5
+ from collections.abc import Callable
6
+ from types import EllipsisType
7
+
8
+ from syndesi.adapters.backend.adapter_backend import AdapterReadPayload, AdapterSignal
9
+ from syndesi.adapters.stop_condition import StopCondition
3
10
 
11
+ from ..adapters.adapter import Adapter
12
+ from ..adapters.timeout import Timeout
13
+ from .protocol import Protocol
4
14
 
5
15
  # Raw protocols provide the user with the binary data directly,
6
16
  # without converting it to string first
7
17
 
18
+
8
19
  class Raw(Protocol):
9
- def __init__(self, adapter: Adapter, timeout : Timeout = ...) -> None:
20
+ def __init__(
21
+ self,
22
+ adapter: Adapter,
23
+ timeout: Timeout | None | EllipsisType = ...,
24
+ event_callback: Callable[[AdapterSignal], None] | None = None,
25
+ ) -> None:
10
26
  """
11
27
  Raw device, no presentation and application layers
12
28
 
@@ -14,15 +30,57 @@ class Raw(Protocol):
14
30
  ----------
15
31
  adapter : IAdapter
16
32
  """
17
- super().__init__(adapter, timeout)
33
+ super().__init__(
34
+ adapter,
35
+ timeout,
36
+ event_callback,
37
+ )
38
+
39
+ # Connect the adapter if it wasn't done already
40
+ self._adapter.connect()
18
41
 
19
- def write(self, data : bytes):
42
+ def _default_timeout(self) -> Timeout | None:
43
+ return Timeout(response=2, action="error")
44
+
45
+ def write(self, data: bytes) -> None:
20
46
  self._adapter.write(data)
21
47
 
22
- def query(self, data : bytes, timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
48
+ def query(
49
+ self,
50
+ data: bytes,
51
+ timeout: Timeout | None | EllipsisType = ...,
52
+ stop_condition: StopCondition | None | EllipsisType = ...,
53
+ full_output: bool = False,
54
+ ) -> bytes | tuple[bytes, AdapterSignal]:
23
55
  self._adapter.flushRead()
24
56
  self.write(data)
25
- return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
57
+ return self.read(
58
+ timeout=timeout,
59
+ stop_condition=stop_condition,
60
+ full_output=full_output,
61
+ )
62
+
63
+ def read(
64
+ self,
65
+ timeout: Timeout | None | EllipsisType = ...,
66
+ stop_condition: StopCondition | None | EllipsisType = ...,
67
+ full_output: bool = False,
68
+ ) -> bytes | tuple[bytes, AdapterSignal]:
69
+
70
+ return self.read_detailed(timeout=timeout, stop_condition=stop_condition)[0]
71
+
72
+ def read_detailed(
73
+ self,
74
+ timeout: Timeout | None | EllipsisType = ...,
75
+ stop_condition: StopCondition | None | EllipsisType = ...,
76
+ ) -> tuple[bytes, AdapterSignal | None]:
77
+ return self._adapter.read_detailed(
78
+ timeout=timeout, stop_condition=stop_condition
79
+ )
80
+
81
+ def _on_data_ready_event(self, data: AdapterReadPayload) -> None:
82
+ # TODO : Call the callback here ?
83
+ pass
26
84
 
27
- def read(self, timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
28
- return self._adapter.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
85
+ def __str__(self) -> str:
86
+ return f"Raw({self._adapter})"
syndesi/protocols/scpi.py CHANGED
@@ -1,12 +1,30 @@
1
- from ..adapters import Adapter, IP, Timeout, Termination, StopCondition
1
+ # File : scpi.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+
5
+
6
+ from types import EllipsisType
7
+
8
+ from syndesi.adapters.backend.adapter_backend import AdapterSignal
9
+
10
+ from ..adapters.adapter import Adapter
11
+ from ..adapters.ip import IP
12
+ from ..adapters.stop_condition import StopCondition, Termination
13
+ from ..adapters.timeout import Timeout, TimeoutAction
2
14
  from .protocol import Protocol
3
- from ..tools.types import is_byte_instance
4
15
 
5
- DEFAULT_TIMEOUT = Timeout(response=10, continuation=0.5, total=None, on_response='error', on_continuation='error')
6
16
 
7
17
  class SCPI(Protocol):
8
18
  DEFAULT_PORT = 5025
9
- def __init__(self, adapter: Adapter, send_termination = '\n', receive_termination = None, timeout : Timeout = ..., encoding : str = 'utf-8') -> None:
19
+
20
+ def __init__(
21
+ self,
22
+ adapter: Adapter,
23
+ send_termination: str = "\n",
24
+ receive_termination: str | None = None,
25
+ timeout: Timeout | None | EllipsisType = ...,
26
+ encoding: str = "utf-8",
27
+ ) -> None:
10
28
  """
11
29
  SDP (Syndesi Device Protocol) compatible device
12
30
 
@@ -22,9 +40,7 @@ class SCPI(Protocol):
22
40
  """
23
41
  self._encoding = encoding
24
42
  # Set the default timeout
25
- if timeout is Ellipsis:
26
- timeout = DEFAULT_TIMEOUT
27
-
43
+
28
44
  if receive_termination is None:
29
45
  self._receive_termination = send_termination
30
46
  else:
@@ -32,56 +48,94 @@ class SCPI(Protocol):
32
48
  self._send_termination = send_termination
33
49
  # Configure the adapter for stop-condition mode (timeouts will raise errors)
34
50
  if not adapter._default_stop_condition:
35
- raise ValueError('No stop-conditions can be set for an adapter used by SCPI protocol')
36
- adapter.set_stop_condition(Termination(self._receive_termination.encode(self._encoding)))
37
- adapter.set_timeout(timeout)
51
+ raise ValueError(
52
+ "No stop-conditions can be set for an adapter used by SCPI protocol"
53
+ )
54
+ adapter.set_stop_conditions(
55
+ Termination(self._receive_termination.encode(self._encoding))
56
+ )
57
+
58
+ # adapter.set_timeout(self.timeout)
38
59
  if isinstance(adapter, IP):
39
60
  adapter.set_default_port(self.DEFAULT_PORT)
40
61
  # Give the adapter to the Protocol base class
41
62
  super().__init__(adapter=adapter, timeout=timeout)
42
63
 
43
- def _to_bytes(self, command):
64
+ # Connect the adapter if it wasn't done already
65
+ self._adapter.connect()
66
+
67
+ def _default_timeout(self) -> Timeout | None:
68
+ return Timeout(response=5, action=TimeoutAction.ERROR.value)
69
+
70
+ def _to_bytes(self, command: str) -> bytes:
44
71
  if isinstance(command, str):
45
- return command.encode('ASCII')
72
+ return command.encode("ASCII")
46
73
  else:
47
- raise ValueError(f'Invalid command type : {type(command)}')
48
-
49
- def _from_bytes(self, payload : bytes):
50
- if is_byte_instance(payload):
51
- return payload.decode('ASCII')
74
+ raise ValueError(f"Invalid command type : {type(command)}")
75
+
76
+ def _from_bytes(self, payload: bytes) -> str:
77
+ if isinstance(payload, bytes):
78
+ return payload.decode("ASCII")
52
79
  else:
53
80
  raise ValueError(f"Invalid payload type : {type(payload)}")
54
81
 
55
- def _formatCommand(self, command):
82
+ def _formatCommand(self, command: str) -> str:
56
83
  return command + self._send_termination
57
84
 
58
- def _unformatCommand(self, payload):
59
- return payload.replace(self._receive_termination, '')
60
-
61
- def _checkCommand(self, command : str):
62
- for c in ['\n', '\r']:
85
+ def _unformatCommand(self, payload: str) -> str:
86
+ return payload.replace(self._receive_termination, "")
87
+
88
+ def _checkCommand(self, command: str) -> None:
89
+ for c in ["\n", "\r"]:
63
90
  if c in command:
64
91
  raise ValueError(f"Invalid char {repr(c)} in command")
65
92
 
66
- def write(self, command : str) -> None:
93
+ def write(self, command: str) -> None:
67
94
  self._checkCommand(command)
68
95
  payload = self._to_bytes(self._formatCommand(command))
69
96
  self._adapter.write(payload)
70
97
 
71
- def write_raw(self, data : bytes, termination : bool = False):
72
- self._adapter.write(data + (self._send_termination if termination else b''))
98
+ def write_raw(self, data: bytes, termination: bool = False) -> None:
99
+ self._adapter.write(
100
+ data
101
+ + (self._send_termination.encode(self._encoding) if termination else b"")
102
+ )
73
103
 
74
- def query(self, command : str, timeout : Timeout = ..., stop_condition : StopCondition = ..., return_metrics : bool = False) -> str:
104
+ def query(
105
+ self,
106
+ command: str,
107
+ timeout: Timeout | None | EllipsisType = ...,
108
+ stop_condition: StopCondition | None | EllipsisType = ...,
109
+ ) -> str:
75
110
  self._adapter.flushRead()
76
111
  self.write(command)
77
- return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
78
112
 
79
- def read(self, timeout : Timeout = ..., stop_condition : StopCondition = None, return_metrics : bool = False) -> str:
80
- output = self._from_bytes(self._adapter.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics))
81
- return self._unformatCommand(output)
113
+ return self.read(timeout=timeout, stop_condition=stop_condition)
82
114
 
83
- def read_raw(self, timeout=None, stop_condition=None, return_metrics : bool = False) -> str:
84
- """
85
- Return the raw bytes instead of str
86
- """
87
- return self._adapter.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
115
+ def read_raw(
116
+ self,
117
+ timeout: Timeout | None | EllipsisType = ...,
118
+ stop_condition: StopCondition | None | EllipsisType = ...,
119
+ ) -> tuple[bytes, AdapterSignal | None]:
120
+ return self._adapter.read_detailed(
121
+ timeout=timeout,
122
+ stop_condition=stop_condition,
123
+ )
124
+
125
+ def read_detailed(
126
+ self,
127
+ timeout: Timeout | None | EllipsisType = ...,
128
+ stop_condition: StopCondition | None | EllipsisType = ...,
129
+ ) -> tuple[str, AdapterSignal | None]:
130
+
131
+ raw_data, signal = self.read_raw(timeout=timeout, stop_condition=stop_condition)
132
+ data_out = self._unformatCommand(self._from_bytes(raw_data))
133
+
134
+ return data_out, signal
135
+
136
+ def read(
137
+ self,
138
+ timeout: Timeout | None | EllipsisType = ...,
139
+ stop_condition: StopCondition | None | EllipsisType = ...,
140
+ ) -> str:
141
+ return self.read_detailed(timeout=timeout, stop_condition=stop_condition)[0]
File without changes
@@ -0,0 +1,52 @@
1
+ # File : syndesi.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+
5
+ import argparse
6
+ from enum import Enum
7
+
8
+ from ..cli.shell import AdapterShell, AdapterType
9
+ from ..tools.log import log
10
+ from ..version import __version__
11
+
12
+
13
+ class SyndesiCommands(Enum):
14
+ SERIAL = "serial"
15
+ IP = "ip"
16
+ MODBUS = "modbus"
17
+ VISA = "visa"
18
+
19
+
20
+ def main() -> None:
21
+ parser = argparse.ArgumentParser(
22
+ prog="syndesi", description="Syndesi command line tool", epilog=""
23
+ )
24
+
25
+ parser.add_argument(
26
+ "--version", action="version", version=f"%(prog)s {__version__}"
27
+ )
28
+ parser.add_argument("-v", "--verbose", action="store_true")
29
+ parser.add_argument(
30
+ "command",
31
+ choices=[x.value for x in SyndesiCommands],
32
+ help="Command, use syndesi <command> -h for help",
33
+ )
34
+
35
+ args, remaining_args = parser.parse_known_args()
36
+ command = SyndesiCommands(args.command)
37
+
38
+ if args.verbose:
39
+ log("DEBUG", console=True)
40
+
41
+ if command == SyndesiCommands.SERIAL:
42
+ AdapterShell(AdapterType.SERIAL, remaining_args).run()
43
+ elif command == SyndesiCommands.IP:
44
+ AdapterShell(AdapterType.IP, remaining_args).run()
45
+ elif command == SyndesiCommands.VISA:
46
+ AdapterShell(AdapterType.VISA, remaining_args).run()
47
+ else:
48
+ raise RuntimeError(f"Command '{command.value}' is not supported yet")
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,37 @@
1
+ # File : syndesi_backend.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+
5
+ import argparse
6
+
7
+ from ..cli.backend_console import BackendConsole
8
+ from ..cli.backend_status import BackendStatus
9
+ from ..cli.backend_wrapper import BackendWrapper
10
+
11
+
12
+ def main() -> None:
13
+ argument_parser = argparse.ArgumentParser(prog="syndesi-backend")
14
+
15
+ argument_parser.add_argument(
16
+ "--status", action="store_true", help="Show backend status"
17
+ )
18
+
19
+ argument_parser.add_argument(
20
+ "--console", action="store_true", help="Run backend console"
21
+ )
22
+
23
+ args, remaining_args = argument_parser.parse_known_args()
24
+
25
+ if args.status:
26
+ status = BackendStatus(remaining_args)
27
+ status.run()
28
+ elif args.console:
29
+ console = BackendConsole(remaining_args)
30
+ console.run()
31
+ else:
32
+ backend = BackendWrapper(remaining_args)
33
+ backend.run()
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,209 @@
1
+ # File : backend_api.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from multiprocessing.connection import Connection, wait
8
+ from typing import Any, Protocol
9
+
10
+ from syndesi.tools.errors import BackendCommunicationError, BackendError
11
+
12
+ BACKEND_PORT = 59677
13
+ LOCALHOST = "127.0.0.1"
14
+
15
+ default_host = LOCALHOST
16
+ # AUTHKEY = b'syndesi'
17
+
18
+ # Backend protocol format
19
+ # (action, arguments)
20
+ # If the command succeeds, it is returned as :
21
+ # (action, other arguments)
22
+ # If the command fails, it is returned as :
23
+ # (error, error_description)
24
+
25
+ # The backend links the client with an adapter when SELECT_ADAPTER is sent along with an adapter descriptor
26
+
27
+ STANDARD_RESPONSE_TIMEOUT = 2
28
+
29
+
30
+ class Action(Enum):
31
+ # All adapters
32
+ SELECT_ADAPTER = "select"
33
+ OPEN = "open" # (descriptor,stop_condition) -> ()
34
+ CLOSE = (
35
+ "close" # (descriptor,) -> () # Notify the backend that this client will close
36
+ )
37
+ FORCE_CLOSE = "force_close" # (descriptor,) -> ()
38
+ WRITE = "write" # (descriptor,data) -> ()
39
+ READ = "read" # (descriptor,full_output,temporary_timeout,temporary_stop_condition) -> (data,metrics)
40
+ SET_STOP_CONDITION = "set_stop_condition" # (descriptor,stop_condition)
41
+ FLUSHREAD = "flushread"
42
+ #START_READ = "start_read" # Start a read (descriptor,response_time)
43
+ GET_BACKEND_TIME = "get_time"
44
+
45
+ # Events
46
+ ADAPTER_EVENT_DATA_READY = "event_adapter_data_ready"
47
+ ADAPTER_EVENT_DISCONNECTED = "event_adapter_disconnected"
48
+ ADAPTER_EVENT_READ_INIT = "event_adapter_read_init"
49
+
50
+ # Other
51
+ SET_ROLE_ADAPTER = (
52
+ "set_role_adapter" # Define the client as an adapter (exchange of data)
53
+ )
54
+ SET_ROLE_MONITORING = "set_role_monitoring" # The client queries for backend info
55
+ SET_ROLE_LOGGER = "set_role_logger" # The client receives logs
56
+ SET_LOG_LEVEL = "set_log_level"
57
+ PING = "ping"
58
+ STOP = "stop"
59
+
60
+ # Backend debugger
61
+ ENUMERATE_ADAPTER_CONNECTIONS = "enumerate_adapter_connections"
62
+ ENUMERATE_MONITORING_CONNECTIONS = "enumerate_monitoring_connections"
63
+ BACKEND_STATS = "backend_stats"
64
+
65
+ # Errors
66
+ ERROR_GENERIC = "error_generic"
67
+ ERROR_UNKNOWN_ACTION = "error_unknown_action"
68
+ ERROR_INVALID_REQUEST = "error_invalid_request"
69
+ ERROR_ADAPTER_NOT_OPENED = "error_adapter_not_opened"
70
+ ERROR_INVALID_ROLE = "error_invalid_role"
71
+ ERROR_ADAPTER_DISCONNECTED = "error_adapter_disconnected"
72
+ ERROR_BACKEND_DISCONNECTED = "error_backend_disconnected"
73
+ ERROR_FAILED_TO_OPEN = "error_failed_to_open"
74
+
75
+
76
+ def is_event(action: Action) -> bool:
77
+ return action.value.startswith("event_")
78
+
79
+
80
+ def is_action_error(action: Action) -> bool:
81
+ return action.value.startswith("error_")
82
+
83
+
84
+ class BackendException(Exception):
85
+ pass
86
+
87
+
88
+ class ValidFragment(Protocol):
89
+ data: bytes
90
+
91
+
92
+ @dataclass
93
+ class Fragment:
94
+ data: bytes
95
+ timestamp: float | None
96
+
97
+ def __str__(self) -> str:
98
+ return f"{self.data!r}"
99
+
100
+ def __repr__(self) -> str:
101
+ return self.__str__()
102
+
103
+ def __getitem__(self, key: slice) -> "Fragment":
104
+ # if self.data is None:
105
+ # raise IndexError('Cannot index invalid fragment')
106
+ return Fragment(self.data[key], self.timestamp)
107
+
108
+
109
+ # def get_conn_addresses(conn: Connection) -> tuple[str, str]:
110
+ # try:
111
+ # fd = conn.fileno()
112
+ # except OSError:
113
+ # return ("closed", "closed")
114
+ # else:
115
+ # sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
116
+ # try:
117
+ # # address, port = sock.getpeername() # (IP, port) tuple
118
+ # peer_address = sock.getpeername()
119
+ # sock_address = sock.getsockname()
120
+ # return sock_address, peer_address
121
+ # except Exception:
122
+ # return ("error", "closed")
123
+
124
+
125
+ BackendResponse = tuple[object, ...]
126
+
127
+
128
+ def frontend_send(conn: Connection, action: Action, *args: Any) -> bool:
129
+ try:
130
+ conn.send((action.value, *args))
131
+ except (BrokenPipeError, OSError):
132
+ return False
133
+ else:
134
+ return True
135
+
136
+
137
+ def backend_request(
138
+ conn: Connection,
139
+ action: Action,
140
+ *args: Any,
141
+ timeout: float = STANDARD_RESPONSE_TIMEOUT,
142
+ ) -> BackendResponse:
143
+ try:
144
+ conn.send((action.value, *args))
145
+ except (BrokenPipeError, OSError) as err:
146
+ raise BackendCommunicationError("Failed to communicate with backend") from err
147
+ else:
148
+ ready = wait([conn], timeout=timeout)
149
+ if conn not in ready:
150
+ raise BackendCommunicationError(
151
+ "Failed to receive backend response in time"
152
+ )
153
+
154
+ try:
155
+ raw_response: object = conn.recv()
156
+ except (EOFError, ConnectionResetError) as err:
157
+ raise BackendCommunicationError(
158
+ f"Failed to receive backend response to {action.value}"
159
+ ) from err
160
+
161
+ # Check if the response is correctly formatted
162
+ if not (isinstance(raw_response, tuple) and isinstance(raw_response[0], str)):
163
+ raise BackendCommunicationError(
164
+ f"Invalid response received from backend : {raw_response}"
165
+ )
166
+
167
+ response_action: Action = Action(raw_response[0])
168
+ arguments: tuple[Any, ...] = raw_response[1:]
169
+
170
+ if is_action_error(response_action):
171
+ if len(arguments) > 0:
172
+ if isinstance(arguments[0], str):
173
+ error_message: str = arguments[0]
174
+ else:
175
+ error_message = "failed to read error message"
176
+ else:
177
+ error_message = "Missing error message"
178
+ raise BackendError(f"{response_action} : {error_message}")
179
+ return arguments
180
+
181
+
182
+ backend_send = frontend_send
183
+
184
+
185
+ def raise_if_error(response: BackendResponse) -> None:
186
+ action = Action(response[0])
187
+ if is_action_error(action):
188
+ if len(response) > 1:
189
+ description = response[1]
190
+ else:
191
+ description = f"{action}"
192
+ raise BackendException(f"{action.name}/{description}")
193
+ return
194
+
195
+
196
+ class AdapterBackendStatus(Enum):
197
+ DISCONNECTED = 0
198
+ CONNECTED = 1
199
+
200
+
201
+ class ClientStatus(Enum):
202
+ DISCONNECTED = 0
203
+ CONNECTED = 1
204
+
205
+
206
+ # class StatusSnapshot(TypedDict):
207
+ # type : Literal['snapshot']
208
+ # adapters :
209
+ # #clients : List[str]