cgse-common 0.16.14__py3-none-any.whl → 0.17.1__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.
egse/scpi.py CHANGED
@@ -15,6 +15,9 @@ DEFAULT_READ_TIMEOUT = 1.0 # seconds
15
15
  DEFAULT_CONNECT_TIMEOUT = 3.0 # seconds
16
16
  IDENTIFICATION_QUERY = "*IDN?"
17
17
 
18
+ SEPARATOR = b"\n"
19
+ SEPARATOR_STR = SEPARATOR.decode()
20
+
18
21
 
19
22
  class SCPICommand:
20
23
  """Base class for SCPI commands."""
@@ -57,7 +60,8 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
57
60
  id_validation: String that should appear in the device's identification response
58
61
  """
59
62
  super().__init__()
60
- self.device_name = device_name
63
+
64
+ self._device_name = device_name
61
65
  self.hostname = hostname
62
66
  self.port = port
63
67
  self.settings = settings or {}
@@ -77,7 +81,11 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
77
81
  def is_simulator(self) -> bool:
78
82
  return False
79
83
 
80
- async def initialize(self, commands: list[tuple[str, bool]] = None, reset_device: bool = False):
84
+ @property
85
+ def device_name(self) -> str:
86
+ return self._device_name
87
+
88
+ async def initialize(self, commands: list[tuple[str, bool]] = None, reset_device: bool = False) -> list[str | None]:
81
89
  """Initialize the device with optional reset and command sequence.
82
90
 
83
91
  Performs device initialization by optionally resetting the device and then
@@ -92,34 +100,42 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
92
100
  the command sequence. Defaults to False.
93
101
 
94
102
  Returns:
95
- None
103
+ Response for each of the commands, or None when no response was expected.
96
104
 
97
105
  Raises:
98
106
  Any exceptions raised by the underlying write() or trans() methods,
99
107
  typically communication errors or device timeouts.
100
108
 
101
109
  Example:
102
- >>> await device.initialize([
103
- ... ("*IDN?", True), # Query device ID, expect response
104
- ... ("SYST:ERR?", True), # Check for errors, expect response
105
- ... ("OUTP ON", False) # Enable output, no response expected
106
- ... ], reset_device=True)
110
+ await device.initialize(
111
+ [
112
+ ("*IDN?", True), # Query device ID, expect response
113
+ ("SYST:ERR?", True), # Check for errors, expect response
114
+ ("OUTP ON", False), # Enable output, no response expected
115
+ ],
116
+ reset_device=True
117
+ )
107
118
  """
108
119
 
109
120
  commands = commands or []
121
+ responses = []
110
122
 
111
123
  if reset_device:
112
- logger.info(f"Resetting the {self.device_name}...")
124
+ logger.info(f"Resetting the {self._device_name}...")
113
125
  await self.write("*RST") # this also resets the user-defined buffer
114
126
 
115
127
  for cmd, expects_response in commands:
116
128
  if expects_response:
117
129
  logger.debug(f"Sending {cmd}...")
118
130
  response = (await self.trans(cmd)).decode().strip()
131
+ responses.append(response)
119
132
  logger.debug(f"{response = }")
120
133
  else:
121
134
  logger.debug(f"Sending {cmd}...")
122
135
  await self.write(cmd)
136
+ responses.append(None)
137
+
138
+ return responses
123
139
 
124
140
  async def connect(self) -> None:
125
141
  """Connect to the device asynchronously.
@@ -132,47 +148,47 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
132
148
  async with self._connect_lock:
133
149
  # Sanity checks
134
150
  if self._is_connection_open:
135
- logger.warning(f"{self.device_name}: Trying to connect to an already connected device.")
151
+ logger.warning(f"{self._device_name}: Trying to connect to an already connected device.")
136
152
  return
137
153
 
138
154
  if not self.hostname:
139
- raise ValueError(f"{self.device_name}: Hostname is not initialized.")
155
+ raise ValueError(f"{self._device_name}: Hostname is not initialized.")
140
156
 
141
157
  if not self.port:
142
- raise ValueError(f"{self.device_name}: Port number is not initialized.")
158
+ raise ValueError(f"{self._device_name}: Port number is not initialized.")
143
159
 
144
160
  # Attempt to establish a connection
145
161
  try:
146
- logger.debug(f'Connecting to {self.device_name} at "{self.hostname}" using port {self.port}')
162
+ logger.debug(f'Connecting to {self._device_name} at "{self.hostname}" using port {self.port}')
147
163
 
148
164
  connect_task = asyncio.open_connection(self.hostname, self.port)
149
165
  self._reader, self._writer = await asyncio.wait_for(connect_task, timeout=self.connect_timeout)
150
166
 
151
167
  self._is_connection_open = True
152
168
 
153
- logger.debug(f"Successfully connected to {self.device_name}.")
169
+ logger.debug(f"Successfully connected to {self._device_name}.")
154
170
 
155
171
  except asyncio.TimeoutError as exc:
156
172
  raise DeviceTimeoutError(
157
- self.device_name, f"Connection to {self.hostname}:{self.port} timed out"
173
+ self._device_name, f"Connection to {self.hostname}:{self.port} timed out"
158
174
  ) from exc
159
175
  except ConnectionRefusedError as exc:
160
176
  raise DeviceConnectionError(
161
- self.device_name, f"Connection refused to {self.hostname}:{self.port}"
177
+ self._device_name, f"Connection refused to {self.hostname}:{self.port}"
162
178
  ) from exc
163
179
  except socket.gaierror as exc:
164
- raise DeviceConnectionError(self.device_name, f"Address resolution error for {self.hostname}") from exc
180
+ raise DeviceConnectionError(self._device_name, f"Address resolution error for {self.hostname}") from exc
165
181
  except socket.herror as exc:
166
- raise DeviceConnectionError(self.device_name, f"Host address error for {self.hostname}") from exc
182
+ raise DeviceConnectionError(self._device_name, f"Host address error for {self.hostname}") from exc
167
183
  except OSError as exc:
168
- raise DeviceConnectionError(self.device_name, f"OS error: {exc}") from exc
184
+ raise DeviceConnectionError(self._device_name, f"OS error: {exc}") from exc
169
185
 
170
186
  # Validate device identity if requested
171
187
  if self.id_validation:
172
188
  logger.debug("Validating connection..")
173
189
  if not await self.is_connected():
174
190
  await self.disconnect()
175
- raise DeviceConnectionError(self.device_name, "Device connected but failed identity verification")
191
+ raise DeviceConnectionError(self._device_name, "Device connected but failed identity verification")
176
192
 
177
193
  async def disconnect(self) -> None:
178
194
  """Disconnect from the device asynchronously.
@@ -183,14 +199,14 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
183
199
  async with self._connect_lock:
184
200
  try:
185
201
  if self._is_connection_open and self._writer is not None:
186
- logger.debug(f"Disconnecting from {self.device_name} at {self.hostname}")
202
+ logger.debug(f"Disconnecting from {self._device_name} at {self.hostname}")
187
203
  self._writer.close()
188
204
  await self._writer.wait_closed()
189
205
  self._writer = None
190
206
  self._reader = None
191
207
  self._is_connection_open = False
192
208
  except Exception as exc:
193
- raise DeviceConnectionError(self.device_name, f"Could not close connection: {exc}") from exc
209
+ raise DeviceConnectionError(self._device_name, f"Could not close connection: {exc}") from exc
194
210
 
195
211
  async def reconnect(self) -> None:
196
212
  """Reconnect to the device asynchronously."""
@@ -214,7 +230,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
214
230
  # Validate the response if validation string is provided
215
231
  if self.id_validation and self.id_validation not in id_response:
216
232
  logger.error(
217
- f"{self.device_name}: Device did not respond correctly to identification query. "
233
+ f"{self._device_name}: Device did not respond correctly to identification query. "
218
234
  f'Expected "{self.id_validation}" in response, got: {id_response}'
219
235
  )
220
236
  await self.disconnect()
@@ -223,7 +239,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
223
239
  return True
224
240
 
225
241
  except DeviceError as exc:
226
- logger.error(f"{self.device_name}: Connection test failed: {exc}", exc_info=True)
242
+ logger.error(f"{self._device_name}: Connection test failed: {exc}", exc_info=True)
227
243
  await self.disconnect()
228
244
  return False
229
245
 
@@ -240,20 +256,20 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
240
256
  async with self._io_lock:
241
257
  try:
242
258
  if not self._is_connection_open or self._writer is None:
243
- raise DeviceConnectionError(self.device_name, "Device not connected, use connect() first")
259
+ raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
244
260
 
245
- # Ensure command ends with newline
246
- if not command.endswith("\n"):
247
- command += "\n"
261
+ # Ensure command ends with the proper separator or terminator
262
+ if not command.endswith(SEPARATOR_STR):
263
+ command += SEPARATOR_STR
248
264
 
249
265
  logger.info(f"-----> {command}")
250
266
  self._writer.write(command.encode())
251
267
  await self._writer.drain()
252
268
 
253
269
  except asyncio.TimeoutError as exc:
254
- raise DeviceTimeoutError(self.device_name, "Write operation timed out") from exc
270
+ raise DeviceTimeoutError(self._device_name, "Write operation timed out") from exc
255
271
  except (ConnectionError, OSError) as exc:
256
- raise DeviceConnectionError(self.device_name, f"Communication error: {exc}") from exc
272
+ raise DeviceConnectionError(self._device_name, f"Communication error: {exc}") from exc
257
273
 
258
274
  async def read(self) -> bytes:
259
275
  """
@@ -268,7 +284,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
268
284
  """
269
285
  async with self._io_lock:
270
286
  if not self._is_connection_open or self._reader is None:
271
- raise DeviceConnectionError(self.device_name, "Device not connected, use connect() first")
287
+ raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
272
288
 
273
289
  try:
274
290
  # First, small delay to allow device to prepare response
@@ -277,19 +293,19 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
277
293
  # Try to read until newline (common SCPI terminator)
278
294
  try:
279
295
  response = await asyncio.wait_for(
280
- self._reader.readuntil(separator=b"\n"), timeout=self.read_timeout
296
+ self._reader.readuntil(separator=SEPARATOR), timeout=self.read_timeout
281
297
  )
282
298
  logger.info(f"<----- {response}")
283
299
  return response
284
300
 
285
301
  except asyncio.IncompleteReadError as exc:
286
302
  # Connection closed before receiving full response
287
- logger.warning(f"{self.device_name}: Incomplete read, got {len(exc.partial)} bytes")
288
- return exc.partial if exc.partial else b"\r\n"
303
+ logger.warning(f"{self._device_name}: Incomplete read, got {len(exc.partial)} bytes")
304
+ return exc.partial if exc.partial else SEPARATOR
289
305
 
290
306
  except asyncio.LimitOverrunError:
291
307
  # Response too large for buffer
292
- logger.warning(f"{self.device_name}: Response exceeded buffer limits")
308
+ logger.warning(f"{self._device_name}: Response exceeded buffer limits")
293
309
  # Fall back to reading a large chunk
294
310
  return await asyncio.wait_for(
295
311
  self._reader.read(8192), # Larger buffer for exceptional cases
@@ -297,9 +313,9 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
297
313
  )
298
314
 
299
315
  except asyncio.TimeoutError as exc:
300
- raise DeviceTimeoutError(self.device_name, "Read operation timed out") from exc
316
+ raise DeviceTimeoutError(self._device_name, "Read operation timed out") from exc
301
317
  except Exception as exc:
302
- raise DeviceConnectionError(self.device_name, f"Read error: {exc}") from exc
318
+ raise DeviceConnectionError(self._device_name, f"Read error: {exc}") from exc
303
319
 
304
320
  async def trans(self, command: str) -> bytes:
305
321
  """
@@ -320,11 +336,11 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
320
336
  async with self._io_lock:
321
337
  try:
322
338
  if not self._is_connection_open or self._writer is None:
323
- raise DeviceConnectionError(self.device_name, "Device not connected, use connect() first")
339
+ raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
324
340
 
325
341
  # Ensure command ends with newline
326
- if not command.endswith("\n"):
327
- command += "\n"
342
+ if not command.endswith(SEPARATOR_STR):
343
+ command += SEPARATOR_STR
328
344
 
329
345
  logger.info(f"-----> {command}")
330
346
  self._writer.write(command.encode())
@@ -336,19 +352,19 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
336
352
  # Try to read until newline (common SCPI terminator)
337
353
  try:
338
354
  response = await asyncio.wait_for(
339
- self._reader.readuntil(separator=b"\n"), timeout=self.read_timeout
355
+ self._reader.readuntil(separator=SEPARATOR), timeout=self.read_timeout
340
356
  )
341
357
  logger.info(f"<----- {response}")
342
358
  return response
343
359
 
344
360
  except asyncio.IncompleteReadError as exc:
345
361
  # Connection closed before receiving full response
346
- logger.warning(f"{self.device_name}: Incomplete read, got {len(exc.partial)} bytes")
347
- return exc.partial if exc.partial else b"\r\n"
362
+ logger.warning(f"{self._device_name}: Incomplete read, got {len(exc.partial)} bytes")
363
+ return exc.partial if exc.partial else SEPARATOR
348
364
 
349
365
  except asyncio.LimitOverrunError:
350
366
  # Response too large for buffer
351
- logger.warning(f"{self.device_name}: Response exceeded buffer limits")
367
+ logger.warning(f"{self._device_name}: Response exceeded buffer limits")
352
368
  # Fall back to reading a large chunk
353
369
  return await asyncio.wait_for(
354
370
  self._reader.read(8192), # Larger buffer for exceptional cases
@@ -356,11 +372,11 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
356
372
  )
357
373
 
358
374
  except asyncio.TimeoutError as exc:
359
- raise DeviceTimeoutError(self.device_name, "Communication timed out") from exc
375
+ raise DeviceTimeoutError(self._device_name, "Communication timed out") from exc
360
376
  except (ConnectionError, OSError) as exc:
361
- raise DeviceConnectionError(self.device_name, f"Communication error: {exc}") from exc
377
+ raise DeviceConnectionError(self._device_name, f"Communication error: {exc}") from exc
362
378
  except Exception as exc:
363
- raise DeviceConnectionError(self.device_name, f"Transaction error: {exc}") from exc
379
+ raise DeviceConnectionError(self._device_name, f"Transaction error: {exc}") from exc
364
380
 
365
381
  async def __aenter__(self):
366
382
  """Async context manager entry."""
egse/settings.py CHANGED
@@ -66,22 +66,20 @@ The above code will read the YAML file from the given location and not from the
66
66
 
67
67
  """
68
68
 
69
- from __future__ import annotations
70
-
71
- import logging
72
69
  import re
73
70
  from pathlib import Path
74
71
  from typing import Any
75
72
 
76
73
  import yaml # This module is provided by the pip package PyYaml - pip install pyyaml
77
74
 
75
+ from egse.env import bool_env
78
76
  from egse.env import get_local_settings_env_name
79
77
  from egse.env import get_local_settings_path
80
78
  from egse.log import logger
81
79
  from egse.system import attrdict
82
80
  from egse.system import recursive_dict_update
83
81
 
84
- _HERE = Path(__file__).resolve().parent
82
+ VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
85
83
 
86
84
 
87
85
  class SettingsError(Exception):
@@ -195,9 +193,12 @@ def load_local_settings(force: bool = False) -> attrdict:
195
193
  local_settings = attrdict()
196
194
 
197
195
  local_settings_path = get_local_settings_path()
196
+ if VERBOSE_DEBUG:
197
+ logger.debug(f"{get_local_settings_env_name()=}")
198
+ logger.debug(f"{local_settings_path=}")
198
199
 
199
200
  if local_settings_path:
200
- path = Path(local_settings_path)
201
+ path = Path(local_settings_path).expanduser()
201
202
  local_settings = load_settings_file(path.parent, path.name, force)
202
203
 
203
204
  return local_settings
@@ -221,7 +222,8 @@ def read_configuration_file(filename: Path, *, force=False) -> dict:
221
222
  filename = str(filename)
222
223
 
223
224
  if force or not Settings.is_memoized(filename):
224
- logger.debug(f"Parsing YAML configuration file {filename}.")
225
+ if VERBOSE_DEBUG:
226
+ logger.debug(f"Parsing YAML configuration file {filename}.")
225
227
 
226
228
  with open(filename, "r") as stream:
227
229
  try:
@@ -386,7 +388,9 @@ def main(args: list | None = None): # pragma: no cover
386
388
  #
387
389
  # Use the '--help' option to see what your choices are.
388
390
 
389
- logging.basicConfig(level=20)
391
+ from egse.env import setup_env
392
+
393
+ setup_env()
390
394
 
391
395
  import argparse
392
396
 
egse/setup.py CHANGED
@@ -644,7 +644,7 @@ def _check_conditions_for_get_path_of_setup_file(site_id: str) -> Path:
644
644
 
645
645
  print_env()
646
646
 
647
- repo_location = Path(repo_location)
647
+ repo_location = Path(repo_location).expanduser()
648
648
  setup_location = repo_location / "data" / site_id / "conf"
649
649
 
650
650
  if not repo_location.is_dir():
@@ -689,7 +689,7 @@ def get_path_of_setup_file(setup_id: int, site_id: str) -> Path:
689
689
  """
690
690
 
691
691
  if not has_conf_repo_location():
692
- setup_location = Path(get_conf_data_location(site_id))
692
+ setup_location = Path(get_conf_data_location(site_id)).expanduser()
693
693
  else:
694
694
  setup_location = _check_conditions_for_get_path_of_setup_file(site_id)
695
695
 
@@ -823,10 +823,11 @@ def submit_setup(setup: Setup, description: str, **kwargs) -> str | None:
823
823
 
824
824
  def main(args: list = None): # pragma: no cover
825
825
  import argparse
826
-
827
826
  from rich import print
828
-
829
827
  from egse.config import find_files
828
+ from egse.env import setup_env
829
+
830
+ setup_env()
830
831
 
831
832
  site_id = get_site_id()
832
833
  location = get_conf_data_location()
@@ -882,7 +883,7 @@ def main(args: list = None): # pragma: no cover
882
883
 
883
884
 
884
885
  class SetupManager:
885
- """Unified manager that routes Setup access to appropriate providers.
886
+ """A unified manager that routes Setup access to appropriate providers.
886
887
 
887
888
  Providers are loaded from the `cgse.extension.setup_providers` entrypoints.
888
889
  Providers serve different purposes, the default provider accesses Setups from
@@ -958,6 +959,11 @@ _setup_manager = SetupManager()
958
959
 
959
960
 
960
961
  if __name__ == "__main__":
962
+ from egse.env import setup_env
963
+
964
+ setup_env()
965
+
966
+ # import sys
961
967
  # main(sys.argv[1:])
962
- #
968
+
963
969
  rich.print(load_setup(site_id=get_site_id()))