cgse-common 0.17.0__tar.gz → 0.17.2__tar.gz
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.
- {cgse_common-0.17.0 → cgse_common-0.17.2}/.gitignore +5 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/PKG-INFO +1 -1
- {cgse_common-0.17.0 → cgse_common-0.17.2}/pyproject.toml +17 -6
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/bits.py +14 -17
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/config.py +3 -2
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/decorators.py +6 -3
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/env.py +4 -1
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/exceptions.py +3 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/log.py +7 -8
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/scpi.py +61 -47
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/settings.py +38 -2
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/socketdevice.py +4 -2
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/system.py +63 -24
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/version.py +24 -13
- {cgse_common-0.17.0 → cgse_common-0.17.2}/README.md +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/justfile +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/noxfile.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/service_registry.db +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/cgse_common/__init__.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/cgse_common/cgse.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/cgse_common/settings.yaml +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/calibration.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/counter.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/device.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/dicts.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/heartbeat.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/hk.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/metrics.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/observer.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/obsid.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/persistence.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/plugin.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/plugins/metrics/duckdb.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/plugins/metrics/influxdb.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/plugins/metrics/timescaledb.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/process.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/py.typed +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/randomwalk.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/ratelimit.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/reload.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/resource.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/response.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/settings.yaml +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/setup.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/signal.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/state.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/task.py +0 -0
- {cgse_common-0.17.0 → cgse_common-0.17.2}/src/egse/zmq_ser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cgse-common"
|
|
3
|
-
version = "0.17.
|
|
3
|
+
version = "0.17.2"
|
|
4
4
|
description = "Software framework to support hardware testing"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "IvS KU Leuven"}
|
|
@@ -64,20 +64,27 @@ log_cli_level = "DEBUG"
|
|
|
64
64
|
filterwarnings = [
|
|
65
65
|
"ignore::DeprecationWarning"
|
|
66
66
|
]
|
|
67
|
+
markers = [
|
|
68
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')"
|
|
69
|
+
]
|
|
67
70
|
|
|
71
|
+
# Some of these omitted files are only temporary omitted until refactored
|
|
68
72
|
[tool.coverage.run]
|
|
69
73
|
omit = [
|
|
70
74
|
"tests/*",
|
|
71
75
|
"conftest.py",
|
|
76
|
+
"src/egse/ratelimit.py",
|
|
77
|
+
"src/egse/plugins/metrics/duckdb.py",
|
|
78
|
+
"src/egse/plugins/metrics/timescaledb.py",
|
|
72
79
|
]
|
|
73
80
|
|
|
74
81
|
[tool.hatch.build.targets.sdist]
|
|
75
82
|
exclude = [
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
83
|
+
"tests",
|
|
84
|
+
"pytest.ini",
|
|
85
|
+
".gitignore",
|
|
86
|
+
"*poetry*",
|
|
87
|
+
"*setuptools*",
|
|
81
88
|
]
|
|
82
89
|
|
|
83
90
|
[tool.hatch.build.targets.wheel]
|
|
@@ -87,8 +94,12 @@ packages = ["src/egse", "src/scripts", "src/cgse_common"]
|
|
|
87
94
|
line-length = 120
|
|
88
95
|
|
|
89
96
|
[tool.ruff.lint]
|
|
97
|
+
select = ["F", "I"]
|
|
90
98
|
extend-select = ["E", "W"]
|
|
91
99
|
|
|
100
|
+
[tool.ruff.lint.isort]
|
|
101
|
+
force-single-line = true
|
|
102
|
+
|
|
92
103
|
[build-system]
|
|
93
104
|
requires = ["hatchling"]
|
|
94
105
|
build-backend = "hatchling.build"
|
|
@@ -5,7 +5,6 @@ This module contains a number of convenience functions to work with bits, bytes
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import ctypes
|
|
8
|
-
from collections.abc import Iterable
|
|
9
8
|
from typing import Union
|
|
10
9
|
|
|
11
10
|
|
|
@@ -33,7 +32,7 @@ def extract_bits(value: int, start_position: int, num_bits: int) -> int:
|
|
|
33
32
|
return extracted_bits
|
|
34
33
|
|
|
35
34
|
|
|
36
|
-
def set_bit(value: int, bit) -> int:
|
|
35
|
+
def set_bit(value: int, bit: int) -> int:
|
|
37
36
|
"""
|
|
38
37
|
Set bit to 1 for the given value.
|
|
39
38
|
|
|
@@ -47,7 +46,7 @@ def set_bit(value: int, bit) -> int:
|
|
|
47
46
|
return value | (1 << bit)
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
def set_bits(value: int, bits: tuple) -> int:
|
|
49
|
+
def set_bits(value: int, bits: tuple[int, int]) -> int:
|
|
51
50
|
"""
|
|
52
51
|
Set the given bits in value to 1.
|
|
53
52
|
|
|
@@ -63,7 +62,7 @@ def set_bits(value: int, bits: tuple) -> int:
|
|
|
63
62
|
return value
|
|
64
63
|
|
|
65
64
|
|
|
66
|
-
def clear_bit(value: int, bit) -> int:
|
|
65
|
+
def clear_bit(value: int, bit: int) -> int:
|
|
67
66
|
"""
|
|
68
67
|
Set bit to 0 for the given value.
|
|
69
68
|
|
|
@@ -77,7 +76,7 @@ def clear_bit(value: int, bit) -> int:
|
|
|
77
76
|
return value & ~(1 << bit)
|
|
78
77
|
|
|
79
78
|
|
|
80
|
-
def clear_bits(value: int, bits: tuple) -> int:
|
|
79
|
+
def clear_bits(value: int, bits: tuple[int, int]) -> int:
|
|
81
80
|
"""
|
|
82
81
|
Set the given bits in value to 0.
|
|
83
82
|
|
|
@@ -93,7 +92,7 @@ def clear_bits(value: int, bits: tuple) -> int:
|
|
|
93
92
|
return value
|
|
94
93
|
|
|
95
94
|
|
|
96
|
-
def toggle_bit(value: int, bit) -> int:
|
|
95
|
+
def toggle_bit(value: int, bit: int) -> int:
|
|
97
96
|
"""
|
|
98
97
|
Toggle the bit in the given value.
|
|
99
98
|
|
|
@@ -107,7 +106,7 @@ def toggle_bit(value: int, bit) -> int:
|
|
|
107
106
|
return value ^ (1 << bit)
|
|
108
107
|
|
|
109
108
|
|
|
110
|
-
def bit_set(value: int, bit) -> bool:
|
|
109
|
+
def bit_set(value: int, bit: int) -> bool:
|
|
111
110
|
"""
|
|
112
111
|
Return True if the bit is set.
|
|
113
112
|
|
|
@@ -122,27 +121,25 @@ def bit_set(value: int, bit) -> bool:
|
|
|
122
121
|
return value & bit_value == bit_value
|
|
123
122
|
|
|
124
123
|
|
|
125
|
-
def bits_set(value: int, *args:
|
|
124
|
+
def bits_set(value: int, *args: int) -> bool:
|
|
126
125
|
"""
|
|
127
126
|
Return True if all the bits are set.
|
|
128
127
|
|
|
129
128
|
Args:
|
|
130
129
|
value (int): the value to check
|
|
131
130
|
args: a set of indices of the bits to check, starting from 0 at the LSB.
|
|
132
|
-
All the indices
|
|
133
|
-
|
|
131
|
+
All the indices shall be given as separate arguments, i.e. unpack a list
|
|
132
|
+
if needed.
|
|
134
133
|
|
|
135
134
|
Returns:
|
|
136
135
|
True if all the bits are set (1).
|
|
137
136
|
|
|
138
137
|
Examples:
|
|
139
|
-
>>> assert bits_set(0b0101_0000_1011,
|
|
140
|
-
>>> assert bits_set(0b0101_0000_1011,
|
|
141
|
-
>>> assert not bits_set(0b0101_0000_1011, [1, 2, 3])
|
|
138
|
+
>>> assert bits_set(0b0101_0000_1011, 0, 1, 3, 8, 10)
|
|
139
|
+
>>> assert bits_set(0b0101_0000_1011, 3, 8)
|
|
140
|
+
>>> assert not bits_set(0b0101_0000_1011, *[1, 2, 3])
|
|
142
141
|
"""
|
|
143
142
|
|
|
144
|
-
if len(args) == 1 and isinstance(args[0], list):
|
|
145
|
-
args = args[0]
|
|
146
143
|
return all([bit_set(value, bit) for bit in args])
|
|
147
144
|
|
|
148
145
|
|
|
@@ -506,7 +503,7 @@ def crc_calc(data: list[bytes | int], start: int, len_: int) -> int:
|
|
|
506
503
|
Reference:
|
|
507
504
|
The description of the CRC calculation for RMAP is given in the ECSS document
|
|
508
505
|
_Space Engineering: SpaceWire - Remote Memory Access Protocol_, section A.3
|
|
509
|
-
on page 80 [ECSS
|
|
506
|
+
on page 80 [ECSS-E-ST-50-52C].
|
|
510
507
|
|
|
511
508
|
"""
|
|
512
509
|
crc: int = 0
|
|
@@ -548,7 +545,7 @@ def s16(value: int) -> int:
|
|
|
548
545
|
>>> s16(0b1000_0000_0001_0001)
|
|
549
546
|
-32751
|
|
550
547
|
|
|
551
|
-
The 'bin()'
|
|
548
|
+
The 'bin()' function will return a strange representation of this number:
|
|
552
549
|
|
|
553
550
|
>>> bin(-32751)
|
|
554
551
|
'-0b111111111101111'
|
|
@@ -154,13 +154,14 @@ def find_files(pattern: str, root: Path | str, in_dir: str = None) -> Generator[
|
|
|
154
154
|
Returns:
|
|
155
155
|
Paths of files matching pattern, from root.
|
|
156
156
|
"""
|
|
157
|
-
root = Path(root).resolve()
|
|
157
|
+
root = Path(root).expanduser().resolve()
|
|
158
158
|
if not root.is_dir():
|
|
159
159
|
root = root.parent
|
|
160
160
|
if not root.exists():
|
|
161
161
|
raise ValueError(f"The root argument didn't resolve into a valid directory: {root}.")
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
# FIXME: at some point we might want to make this configurable, through env?
|
|
164
|
+
exclude_dirs = ("venv", "venv38", ".venv", ".nox", ".git", ".idea", ".DS_Store", "__pycache__")
|
|
164
165
|
|
|
165
166
|
for path, folders, files in os.walk(root):
|
|
166
167
|
folders[:] = list(filter(lambda x: x not in exclude_dirs, folders))
|
|
@@ -15,8 +15,8 @@ from typing import Optional
|
|
|
15
15
|
|
|
16
16
|
import rich
|
|
17
17
|
|
|
18
|
-
from egse.system import get_caller_info
|
|
19
18
|
from egse.log import logger
|
|
19
|
+
from egse.system import get_caller_info
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def static_vars(**kwargs):
|
|
@@ -316,7 +316,10 @@ def debug(func):
|
|
|
316
316
|
|
|
317
317
|
|
|
318
318
|
def profile_func(
|
|
319
|
-
output_file: str
|
|
319
|
+
output_file: str | None = None,
|
|
320
|
+
sort_by: str = "cumulative",
|
|
321
|
+
lines_to_print: int | None = None,
|
|
322
|
+
strip_dirs: bool = False,
|
|
320
323
|
) -> Callable:
|
|
321
324
|
"""A time profiler decorator.
|
|
322
325
|
|
|
@@ -346,7 +349,7 @@ def profile_func(
|
|
|
346
349
|
decorator](https://gist.github.com/ekhoda/2de44cf60d29ce24ad29758ce8635b78).
|
|
347
350
|
|
|
348
351
|
Inspired by and modified the profile decorator of Giampaolo Rodola:
|
|
349
|
-
[profile
|
|
352
|
+
[profile decorator](http://code.activestate.com/recipes/577817-profile-decorator/).
|
|
350
353
|
|
|
351
354
|
|
|
352
355
|
"""
|
|
@@ -72,8 +72,8 @@ import os
|
|
|
72
72
|
import warnings
|
|
73
73
|
from pathlib import Path
|
|
74
74
|
|
|
75
|
-
from dotenv import load_dotenv as _load_dotenv
|
|
76
75
|
from dotenv import find_dotenv
|
|
76
|
+
from dotenv import load_dotenv as _load_dotenv
|
|
77
77
|
from rich.console import Console
|
|
78
78
|
|
|
79
79
|
from egse.decorators import static_vars
|
|
@@ -678,6 +678,7 @@ def env_var(**kwargs: str | int | float | bool | None):
|
|
|
678
678
|
else:
|
|
679
679
|
os.environ[k] = v
|
|
680
680
|
|
|
681
|
+
setup_env.is_initialized = False
|
|
681
682
|
setup_env()
|
|
682
683
|
|
|
683
684
|
yield
|
|
@@ -689,12 +690,14 @@ def env_var(**kwargs: str | int | float | bool | None):
|
|
|
689
690
|
else:
|
|
690
691
|
os.environ[k] = v
|
|
691
692
|
|
|
693
|
+
setup_env.is_initialized = False
|
|
692
694
|
setup_env()
|
|
693
695
|
|
|
694
696
|
|
|
695
697
|
def main(args: list | None = None): # pragma: no cover
|
|
696
698
|
import argparse
|
|
697
699
|
import sys
|
|
700
|
+
|
|
698
701
|
import rich
|
|
699
702
|
|
|
700
703
|
parser = argparse.ArgumentParser()
|
|
@@ -18,6 +18,7 @@ __all__ = [
|
|
|
18
18
|
"LOG_DATE_FORMAT_FULL",
|
|
19
19
|
"LOG_DATE_FORMAT_CLEAN",
|
|
20
20
|
"logger",
|
|
21
|
+
"logging", # export to guarantee that this module is imported and initialized before users use logging
|
|
21
22
|
"root_logger",
|
|
22
23
|
"egse_logger",
|
|
23
24
|
"get_log_level_from_env",
|
|
@@ -84,7 +85,7 @@ class NonEGSEFilter(logging.Filter):
|
|
|
84
85
|
return not record.name.startswith("egse")
|
|
85
86
|
|
|
86
87
|
|
|
87
|
-
def get_log_level_from_env(env_var: str = "LOG_LEVEL", default:
|
|
88
|
+
def get_log_level_from_env(env_var: str = "LOG_LEVEL", default: str = "INFO") -> int:
|
|
88
89
|
"""Read the log level from an environment variable."""
|
|
89
90
|
log_level_str = os.getenv(env_var, default)
|
|
90
91
|
|
|
@@ -95,18 +96,16 @@ def get_log_level_from_env(env_var: str = "LOG_LEVEL", default: int = logging.IN
|
|
|
95
96
|
if 10 <= log_level <= 50:
|
|
96
97
|
return log_level
|
|
97
98
|
else:
|
|
98
|
-
logging.warning(
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
return default
|
|
99
|
+
logging.warning(f"Log level {log_level} outside standard range (10-50). Using {default.upper()}.")
|
|
100
|
+
return logging._nameToLevel[default.upper()]
|
|
102
101
|
|
|
103
102
|
except ValueError:
|
|
104
103
|
log_level_str = log_level_str.upper()
|
|
105
104
|
try:
|
|
106
105
|
return getattr(logging, log_level_str)
|
|
107
106
|
except AttributeError:
|
|
108
|
-
logging.error(f"Invalid LOG_LEVEL '{log_level_str}'. Using {
|
|
109
|
-
return default
|
|
107
|
+
logging.error(f"Invalid LOG_LEVEL '{log_level_str}'. Using {default.upper()}.")
|
|
108
|
+
return logging._nameToLevel[default.upper()]
|
|
110
109
|
|
|
111
110
|
|
|
112
111
|
egse_logger = logging.getLogger("egse")
|
|
@@ -145,7 +144,7 @@ if __name__ == "__main__":
|
|
|
145
144
|
Environment variables:
|
|
146
145
|
- LOG_LEVEL=debug|info|warning|critical
|
|
147
146
|
- LOG_FORMAT=full|clean
|
|
148
|
-
|
|
147
|
+
|
|
149
148
|
Example logging statements
|
|
150
149
|
- logging level set to INFO
|
|
151
150
|
- logging format set to full
|
|
@@ -9,12 +9,18 @@ from egse.device import AsyncDeviceTransport
|
|
|
9
9
|
from egse.device import DeviceConnectionError
|
|
10
10
|
from egse.device import DeviceError
|
|
11
11
|
from egse.device import DeviceTimeoutError
|
|
12
|
+
from egse.env import bool_env
|
|
12
13
|
from egse.log import logger
|
|
13
14
|
|
|
14
15
|
DEFAULT_READ_TIMEOUT = 1.0 # seconds
|
|
15
16
|
DEFAULT_CONNECT_TIMEOUT = 3.0 # seconds
|
|
16
17
|
IDENTIFICATION_QUERY = "*IDN?"
|
|
17
18
|
|
|
19
|
+
VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
|
|
20
|
+
|
|
21
|
+
SEPARATOR = b"\n"
|
|
22
|
+
SEPARATOR_STR = SEPARATOR.decode()
|
|
23
|
+
|
|
18
24
|
|
|
19
25
|
class SCPICommand:
|
|
20
26
|
"""Base class for SCPI commands."""
|
|
@@ -57,7 +63,8 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
57
63
|
id_validation: String that should appear in the device's identification response
|
|
58
64
|
"""
|
|
59
65
|
super().__init__()
|
|
60
|
-
|
|
66
|
+
|
|
67
|
+
self._device_name = device_name
|
|
61
68
|
self.hostname = hostname
|
|
62
69
|
self.port = port
|
|
63
70
|
self.settings = settings or {}
|
|
@@ -71,12 +78,15 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
71
78
|
self._connect_lock = asyncio.Lock()
|
|
72
79
|
"""Prevents multiple, simultaneous connect or disconnect attempts."""
|
|
73
80
|
self._io_lock = asyncio.Lock()
|
|
74
|
-
"""Prevents multiple coroutines from attempting to read, write or query from the same stream
|
|
75
|
-
at the same time."""
|
|
81
|
+
"""Prevents multiple coroutines from attempting to read, write or query from the same stream at the same time."""
|
|
76
82
|
|
|
77
83
|
def is_simulator(self) -> bool:
|
|
78
84
|
return False
|
|
79
85
|
|
|
86
|
+
@property
|
|
87
|
+
def device_name(self) -> str:
|
|
88
|
+
return self._device_name
|
|
89
|
+
|
|
80
90
|
async def initialize(self, commands: list[tuple[str, bool]] = None, reset_device: bool = False) -> list[str | None]:
|
|
81
91
|
"""Initialize the device with optional reset and command sequence.
|
|
82
92
|
|
|
@@ -113,7 +123,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
113
123
|
responses = []
|
|
114
124
|
|
|
115
125
|
if reset_device:
|
|
116
|
-
logger.info(f"Resetting the {self.
|
|
126
|
+
logger.info(f"Resetting the {self._device_name}...")
|
|
117
127
|
await self.write("*RST") # this also resets the user-defined buffer
|
|
118
128
|
|
|
119
129
|
for cmd, expects_response in commands:
|
|
@@ -140,47 +150,51 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
140
150
|
async with self._connect_lock:
|
|
141
151
|
# Sanity checks
|
|
142
152
|
if self._is_connection_open:
|
|
143
|
-
logger.warning(f"{self.
|
|
153
|
+
logger.warning(f"{self._device_name}: Trying to connect to an already connected device.")
|
|
144
154
|
return
|
|
145
155
|
|
|
146
156
|
if not self.hostname:
|
|
147
|
-
raise ValueError(f"{self.
|
|
157
|
+
raise ValueError(f"{self._device_name}: Hostname is not initialized.")
|
|
148
158
|
|
|
149
159
|
if not self.port:
|
|
150
|
-
raise ValueError(f"{self.
|
|
160
|
+
raise ValueError(f"{self._device_name}: Port number is not initialized.")
|
|
151
161
|
|
|
152
162
|
# Attempt to establish a connection
|
|
153
163
|
try:
|
|
154
|
-
logger.debug(f'Connecting to {self.
|
|
164
|
+
logger.debug(f'Connecting to {self._device_name} at "{self.hostname}" using port {self.port}')
|
|
155
165
|
|
|
156
166
|
connect_task = asyncio.open_connection(self.hostname, self.port)
|
|
157
167
|
self._reader, self._writer = await asyncio.wait_for(connect_task, timeout=self.connect_timeout)
|
|
158
168
|
|
|
159
169
|
self._is_connection_open = True
|
|
160
170
|
|
|
161
|
-
|
|
171
|
+
response = await self.read_string()
|
|
172
|
+
if VERBOSE_DEBUG:
|
|
173
|
+
logger.debug(f"Response after connection: {response}")
|
|
174
|
+
|
|
175
|
+
logger.debug(f"Successfully connected to {self._device_name}.")
|
|
162
176
|
|
|
163
177
|
except asyncio.TimeoutError as exc:
|
|
164
178
|
raise DeviceTimeoutError(
|
|
165
|
-
self.
|
|
179
|
+
self._device_name, f"Connection to {self.hostname}:{self.port} timed out"
|
|
166
180
|
) from exc
|
|
167
181
|
except ConnectionRefusedError as exc:
|
|
168
182
|
raise DeviceConnectionError(
|
|
169
|
-
self.
|
|
183
|
+
self._device_name, f"Connection refused to {self.hostname}:{self.port}"
|
|
170
184
|
) from exc
|
|
171
185
|
except socket.gaierror as exc:
|
|
172
|
-
raise DeviceConnectionError(self.
|
|
186
|
+
raise DeviceConnectionError(self._device_name, f"Address resolution error for {self.hostname}") from exc
|
|
173
187
|
except socket.herror as exc:
|
|
174
|
-
raise DeviceConnectionError(self.
|
|
188
|
+
raise DeviceConnectionError(self._device_name, f"Host address error for {self.hostname}") from exc
|
|
175
189
|
except OSError as exc:
|
|
176
|
-
raise DeviceConnectionError(self.
|
|
190
|
+
raise DeviceConnectionError(self._device_name, f"OS error: {exc}") from exc
|
|
177
191
|
|
|
178
192
|
# Validate device identity if requested
|
|
179
193
|
if self.id_validation:
|
|
180
194
|
logger.debug("Validating connection..")
|
|
181
195
|
if not await self.is_connected():
|
|
182
196
|
await self.disconnect()
|
|
183
|
-
raise DeviceConnectionError(self.
|
|
197
|
+
raise DeviceConnectionError(self._device_name, "Device connected but failed identity verification")
|
|
184
198
|
|
|
185
199
|
async def disconnect(self) -> None:
|
|
186
200
|
"""Disconnect from the device asynchronously.
|
|
@@ -191,14 +205,14 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
191
205
|
async with self._connect_lock:
|
|
192
206
|
try:
|
|
193
207
|
if self._is_connection_open and self._writer is not None:
|
|
194
|
-
logger.debug(f"Disconnecting from {self.
|
|
208
|
+
logger.debug(f"Disconnecting from {self._device_name} at {self.hostname}")
|
|
195
209
|
self._writer.close()
|
|
196
210
|
await self._writer.wait_closed()
|
|
197
211
|
self._writer = None
|
|
198
212
|
self._reader = None
|
|
199
213
|
self._is_connection_open = False
|
|
200
214
|
except Exception as exc:
|
|
201
|
-
raise DeviceConnectionError(self.
|
|
215
|
+
raise DeviceConnectionError(self._device_name, f"Could not close connection: {exc}") from exc
|
|
202
216
|
|
|
203
217
|
async def reconnect(self) -> None:
|
|
204
218
|
"""Reconnect to the device asynchronously."""
|
|
@@ -222,7 +236,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
222
236
|
# Validate the response if validation string is provided
|
|
223
237
|
if self.id_validation and self.id_validation not in id_response:
|
|
224
238
|
logger.error(
|
|
225
|
-
f"{self.
|
|
239
|
+
f"{self._device_name}: Device did not respond correctly to identification query. "
|
|
226
240
|
f'Expected "{self.id_validation}" in response, got: {id_response}'
|
|
227
241
|
)
|
|
228
242
|
await self.disconnect()
|
|
@@ -231,7 +245,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
231
245
|
return True
|
|
232
246
|
|
|
233
247
|
except DeviceError as exc:
|
|
234
|
-
logger.error(f"{self.
|
|
248
|
+
logger.error(f"{self._device_name}: Connection test failed: {exc}", exc_info=True)
|
|
235
249
|
await self.disconnect()
|
|
236
250
|
return False
|
|
237
251
|
|
|
@@ -248,20 +262,20 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
248
262
|
async with self._io_lock:
|
|
249
263
|
try:
|
|
250
264
|
if not self._is_connection_open or self._writer is None:
|
|
251
|
-
raise DeviceConnectionError(self.
|
|
265
|
+
raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
|
|
252
266
|
|
|
253
|
-
# Ensure command ends with
|
|
254
|
-
if not command.endswith(
|
|
255
|
-
command +=
|
|
267
|
+
# Ensure command ends with the proper separator or terminator
|
|
268
|
+
if not command.endswith(SEPARATOR_STR):
|
|
269
|
+
command += SEPARATOR_STR
|
|
256
270
|
|
|
257
271
|
logger.info(f"-----> {command}")
|
|
258
272
|
self._writer.write(command.encode())
|
|
259
273
|
await self._writer.drain()
|
|
260
274
|
|
|
261
275
|
except asyncio.TimeoutError as exc:
|
|
262
|
-
raise DeviceTimeoutError(self.
|
|
276
|
+
raise DeviceTimeoutError(self._device_name, "Write operation timed out") from exc
|
|
263
277
|
except (ConnectionError, OSError) as exc:
|
|
264
|
-
raise DeviceConnectionError(self.
|
|
278
|
+
raise DeviceConnectionError(self._device_name, f"Communication error: {exc}") from exc
|
|
265
279
|
|
|
266
280
|
async def read(self) -> bytes:
|
|
267
281
|
"""
|
|
@@ -276,7 +290,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
276
290
|
"""
|
|
277
291
|
async with self._io_lock:
|
|
278
292
|
if not self._is_connection_open or self._reader is None:
|
|
279
|
-
raise DeviceConnectionError(self.
|
|
293
|
+
raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
|
|
280
294
|
|
|
281
295
|
try:
|
|
282
296
|
# First, small delay to allow device to prepare response
|
|
@@ -285,19 +299,19 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
285
299
|
# Try to read until newline (common SCPI terminator)
|
|
286
300
|
try:
|
|
287
301
|
response = await asyncio.wait_for(
|
|
288
|
-
self._reader.readuntil(separator=
|
|
302
|
+
self._reader.readuntil(separator=SEPARATOR), timeout=self.read_timeout
|
|
289
303
|
)
|
|
290
304
|
logger.info(f"<----- {response}")
|
|
291
305
|
return response
|
|
292
306
|
|
|
293
307
|
except asyncio.IncompleteReadError as exc:
|
|
294
308
|
# Connection closed before receiving full response
|
|
295
|
-
logger.warning(f"{self.
|
|
296
|
-
return exc.partial if exc.partial else
|
|
309
|
+
logger.warning(f"{self._device_name}: Incomplete read, got {len(exc.partial)} bytes")
|
|
310
|
+
return exc.partial if exc.partial else SEPARATOR
|
|
297
311
|
|
|
298
312
|
except asyncio.LimitOverrunError:
|
|
299
313
|
# Response too large for buffer
|
|
300
|
-
logger.warning(f"{self.
|
|
314
|
+
logger.warning(f"{self._device_name}: Response exceeded buffer limits")
|
|
301
315
|
# Fall back to reading a large chunk
|
|
302
316
|
return await asyncio.wait_for(
|
|
303
317
|
self._reader.read(8192), # Larger buffer for exceptional cases
|
|
@@ -305,9 +319,9 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
305
319
|
)
|
|
306
320
|
|
|
307
321
|
except asyncio.TimeoutError as exc:
|
|
308
|
-
raise DeviceTimeoutError(self.
|
|
322
|
+
raise DeviceTimeoutError(self._device_name, "Read operation timed out") from exc
|
|
309
323
|
except Exception as exc:
|
|
310
|
-
raise DeviceConnectionError(self.
|
|
324
|
+
raise DeviceConnectionError(self._device_name, f"Read error: {exc}") from exc
|
|
311
325
|
|
|
312
326
|
async def trans(self, command: str) -> bytes:
|
|
313
327
|
"""
|
|
@@ -328,35 +342,35 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
328
342
|
async with self._io_lock:
|
|
329
343
|
try:
|
|
330
344
|
if not self._is_connection_open or self._writer is None:
|
|
331
|
-
raise DeviceConnectionError(self.
|
|
345
|
+
raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
|
|
332
346
|
|
|
333
|
-
# Ensure command ends with
|
|
334
|
-
if not command.endswith(
|
|
335
|
-
command +=
|
|
347
|
+
# Ensure command ends with the required terminator
|
|
348
|
+
if not command.endswith(SEPARATOR_STR):
|
|
349
|
+
command += SEPARATOR_STR
|
|
336
350
|
|
|
337
|
-
logger.info(f"-----> {command}")
|
|
351
|
+
logger.info(f"-----> {command=}")
|
|
338
352
|
self._writer.write(command.encode())
|
|
339
353
|
await self._writer.drain()
|
|
340
354
|
|
|
341
|
-
# First, small delay to allow device to prepare response
|
|
355
|
+
# First, small delay to allow the device to prepare a response
|
|
342
356
|
await asyncio.sleep(0.01)
|
|
343
357
|
|
|
344
|
-
# Try to read until
|
|
358
|
+
# Try to read until the required separator (common SCPI terminator)
|
|
345
359
|
try:
|
|
346
360
|
response = await asyncio.wait_for(
|
|
347
|
-
self._reader.readuntil(separator=
|
|
361
|
+
self._reader.readuntil(separator=SEPARATOR), timeout=self.read_timeout
|
|
348
362
|
)
|
|
349
|
-
logger.info(f"<----- {response}")
|
|
363
|
+
logger.info(f"<----- {response=}")
|
|
350
364
|
return response
|
|
351
365
|
|
|
352
366
|
except asyncio.IncompleteReadError as exc:
|
|
353
367
|
# Connection closed before receiving full response
|
|
354
|
-
logger.warning(f"{self.
|
|
355
|
-
return exc.partial if exc.partial else
|
|
368
|
+
logger.warning(f"{self._device_name}: Incomplete read, got {len(exc.partial)} bytes")
|
|
369
|
+
return exc.partial if exc.partial else SEPARATOR
|
|
356
370
|
|
|
357
371
|
except asyncio.LimitOverrunError:
|
|
358
372
|
# Response too large for buffer
|
|
359
|
-
logger.warning(f"{self.
|
|
373
|
+
logger.warning(f"{self._device_name}: Response exceeded buffer limits")
|
|
360
374
|
# Fall back to reading a large chunk
|
|
361
375
|
return await asyncio.wait_for(
|
|
362
376
|
self._reader.read(8192), # Larger buffer for exceptional cases
|
|
@@ -364,11 +378,11 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
364
378
|
)
|
|
365
379
|
|
|
366
380
|
except asyncio.TimeoutError as exc:
|
|
367
|
-
raise DeviceTimeoutError(self.
|
|
381
|
+
raise DeviceTimeoutError(self._device_name, "Communication timed out") from exc
|
|
368
382
|
except (ConnectionError, OSError) as exc:
|
|
369
|
-
raise DeviceConnectionError(self.
|
|
383
|
+
raise DeviceConnectionError(self._device_name, f"Communication error: {exc}") from exc
|
|
370
384
|
except Exception as exc:
|
|
371
|
-
raise DeviceConnectionError(self.
|
|
385
|
+
raise DeviceConnectionError(self._device_name, f"Transaction error: {exc}") from exc
|
|
372
386
|
|
|
373
387
|
async def __aenter__(self):
|
|
374
388
|
"""Async context manager entry."""
|
|
@@ -72,13 +72,14 @@ from typing import Any
|
|
|
72
72
|
|
|
73
73
|
import yaml # This module is provided by the pip package PyYaml - pip install pyyaml
|
|
74
74
|
|
|
75
|
+
from egse.env import bool_env
|
|
75
76
|
from egse.env import get_local_settings_env_name
|
|
76
77
|
from egse.env import get_local_settings_path
|
|
77
78
|
from egse.log import logger
|
|
78
79
|
from egse.system import attrdict
|
|
79
80
|
from egse.system import recursive_dict_update
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
|
|
82
83
|
|
|
83
84
|
|
|
84
85
|
class SettingsError(Exception):
|
|
@@ -192,6 +193,9 @@ def load_local_settings(force: bool = False) -> attrdict:
|
|
|
192
193
|
local_settings = attrdict()
|
|
193
194
|
|
|
194
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=}")
|
|
195
199
|
|
|
196
200
|
if local_settings_path:
|
|
197
201
|
path = Path(local_settings_path).expanduser()
|
|
@@ -218,7 +222,8 @@ def read_configuration_file(filename: Path, *, force=False) -> dict:
|
|
|
218
222
|
filename = str(filename)
|
|
219
223
|
|
|
220
224
|
if force or not Settings.is_memoized(filename):
|
|
221
|
-
|
|
225
|
+
if VERBOSE_DEBUG:
|
|
226
|
+
logger.debug(f"Parsing YAML configuration file {filename}.")
|
|
222
227
|
|
|
223
228
|
with open(filename, "r") as stream:
|
|
224
229
|
try:
|
|
@@ -375,6 +380,37 @@ class Settings:
|
|
|
375
380
|
|
|
376
381
|
return msg.rstrip()
|
|
377
382
|
|
|
383
|
+
@classmethod
|
|
384
|
+
def from_string(cls, yaml_string: str, group: str | None = None) -> attrdict:
|
|
385
|
+
"""
|
|
386
|
+
Creates a Settings object from a YAML string.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
yaml_string (str): the YAML configuration as a string
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
An attribute dictionary with all the settings from the given string.
|
|
393
|
+
"""
|
|
394
|
+
try:
|
|
395
|
+
yaml_document = yaml.load(yaml_string, Loader=SAFE_LOADER)
|
|
396
|
+
settings = attrdict({name: value for name, value in yaml_document.items()}, label="Settings")
|
|
397
|
+
except yaml.YAMLError as exc:
|
|
398
|
+
logger.error(exc)
|
|
399
|
+
raise SettingsError("Error loading YAML document from string") from exc
|
|
400
|
+
|
|
401
|
+
if not settings:
|
|
402
|
+
logger.warning(
|
|
403
|
+
"The Settings YAML string is empty. No local settings were loaded, an empty dictionary is returned."
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if group:
|
|
407
|
+
if group in settings:
|
|
408
|
+
settings = attrdict({name: value for name, value in settings[group].items()}, label=group)
|
|
409
|
+
else:
|
|
410
|
+
raise SettingsError(f"Group name '{group}' is not defined in the provided YAML string.")
|
|
411
|
+
|
|
412
|
+
return settings
|
|
413
|
+
|
|
378
414
|
|
|
379
415
|
def main(args: list | None = None): # pragma: no cover
|
|
380
416
|
# We provide convenience to inspect the settings by calling this module directly from Python.
|
|
@@ -8,6 +8,8 @@ import socket
|
|
|
8
8
|
import time
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
|
+
from egse.device import AsyncDeviceInterface
|
|
12
|
+
from egse.device import AsyncDeviceTransport
|
|
11
13
|
from egse.device import DeviceConnectionError
|
|
12
14
|
from egse.device import DeviceConnectionInterface
|
|
13
15
|
from egse.device import DeviceTimeoutError
|
|
@@ -153,7 +155,7 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
|
|
|
153
155
|
|
|
154
156
|
try:
|
|
155
157
|
while True:
|
|
156
|
-
# compute remaining timeout for select, this is needed because we read in different parts
|
|
158
|
+
# compute the remaining timeout for select, this is needed because we read in different parts
|
|
157
159
|
# until ETX is received, and we want to receive the complete messages including ETX within
|
|
158
160
|
# the read timeout.
|
|
159
161
|
if end_time is None:
|
|
@@ -252,7 +254,7 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
|
|
|
252
254
|
raise DeviceConnectionError(self.device_name, "Socket communication error.") from exc
|
|
253
255
|
|
|
254
256
|
|
|
255
|
-
class AsyncSocketDevice(
|
|
257
|
+
class AsyncSocketDevice(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
256
258
|
"""
|
|
257
259
|
Async socket-backed device using asyncio streams.
|
|
258
260
|
|
|
@@ -10,8 +10,6 @@ The module has external dependencies to:
|
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
13
|
import asyncio
|
|
16
14
|
import builtins
|
|
17
15
|
import collections
|
|
@@ -30,6 +28,7 @@ import os
|
|
|
30
28
|
import platform # For getting the operating system name
|
|
31
29
|
import re
|
|
32
30
|
import shutil
|
|
31
|
+
import signal
|
|
33
32
|
import socket
|
|
34
33
|
import subprocess # For executing a shell command
|
|
35
34
|
import sys
|
|
@@ -60,7 +59,6 @@ from rich.text import Text
|
|
|
60
59
|
from rich.tree import Tree
|
|
61
60
|
from typer.core import TyperCommand
|
|
62
61
|
|
|
63
|
-
import signal
|
|
64
62
|
from egse.log import logger
|
|
65
63
|
|
|
66
64
|
EPOCH_1958_1970 = 378691200
|
|
@@ -107,7 +105,7 @@ class Periodic:
|
|
|
107
105
|
interval: float,
|
|
108
106
|
*,
|
|
109
107
|
name: str | None = None,
|
|
110
|
-
callback: Callable = None,
|
|
108
|
+
callback: Callable | None = None,
|
|
111
109
|
repeat: int | None = None,
|
|
112
110
|
skip: bool = True,
|
|
113
111
|
pause: bool = False,
|
|
@@ -202,7 +200,7 @@ class Periodic:
|
|
|
202
200
|
"""Triggers the Timer's action: either call its callback, or logs a message."""
|
|
203
201
|
|
|
204
202
|
if self._callback is None:
|
|
205
|
-
self._logger.warning(f"Periodic
|
|
203
|
+
self._logger.warning(f"Periodic - No callback provided for interval timer {self.name}.")
|
|
206
204
|
return
|
|
207
205
|
|
|
208
206
|
try:
|
|
@@ -211,7 +209,7 @@ class Periodic:
|
|
|
211
209
|
self._logger.debug("Caught CancelledError on callback function in Periodic.")
|
|
212
210
|
raise
|
|
213
211
|
except Exception as exc:
|
|
214
|
-
self._logger.error(f"{
|
|
212
|
+
self._logger.error(f"{type_name(exc)} caught: {exc}")
|
|
215
213
|
|
|
216
214
|
@property
|
|
217
215
|
def interval(self):
|
|
@@ -370,8 +368,8 @@ def ignore_m_warning(modules=None):
|
|
|
370
368
|
modules = [modules]
|
|
371
369
|
|
|
372
370
|
try:
|
|
373
|
-
import warnings
|
|
374
371
|
import re
|
|
372
|
+
import warnings
|
|
375
373
|
|
|
376
374
|
msg = "'{module}' found in sys.modules after import of package"
|
|
377
375
|
for module in modules:
|
|
@@ -390,7 +388,10 @@ def now(utc: bool = True):
|
|
|
390
388
|
|
|
391
389
|
|
|
392
390
|
def format_datetime(
|
|
393
|
-
dt:
|
|
391
|
+
dt: str | datetime.datetime | datetime.date | None = None,
|
|
392
|
+
fmt: str | None = None,
|
|
393
|
+
width: int = 6,
|
|
394
|
+
precision: int = 3,
|
|
394
395
|
) -> str:
|
|
395
396
|
"""Format a datetime as YYYY-mm-ddTHH:MM:SS.μs+0000.
|
|
396
397
|
|
|
@@ -452,6 +453,10 @@ def format_datetime(
|
|
|
452
453
|
if fmt:
|
|
453
454
|
timestamp = dt.strftime(fmt)
|
|
454
455
|
else:
|
|
456
|
+
# If dt is a date (not datetime), convert to datetime at midnight
|
|
457
|
+
if isinstance(dt, datetime.date) and not isinstance(dt, datetime.datetime):
|
|
458
|
+
dt = datetime.datetime.combine(dt, datetime.time.min)
|
|
459
|
+
|
|
455
460
|
width = min(width, precision)
|
|
456
461
|
timestamp = (
|
|
457
462
|
f"{dt.strftime('%Y-%m-%dT%H:%M')}:"
|
|
@@ -727,18 +732,29 @@ def get_host_ip() -> Optional[str]:
|
|
|
727
732
|
return None
|
|
728
733
|
|
|
729
734
|
|
|
730
|
-
def get_current_location():
|
|
735
|
+
def get_current_location() -> tuple[str, int, str]:
|
|
731
736
|
"""
|
|
732
737
|
Returns the location where this function is called, i.e. the filename, line number, and function name.
|
|
738
|
+
|
|
739
|
+
If the location cannot be determined, ("", 0, "") is returned.
|
|
733
740
|
"""
|
|
734
|
-
frame = inspect.currentframe()
|
|
741
|
+
frame = inspect.currentframe()
|
|
742
|
+
logger.debug(f"{frame = }")
|
|
743
|
+
if frame is None:
|
|
744
|
+
return "", 0, ""
|
|
735
745
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
746
|
+
previous_frame = frame.f_back
|
|
747
|
+
logger.debug(f"{previous_frame = }")
|
|
748
|
+
if previous_frame is None:
|
|
749
|
+
return "", 0, ""
|
|
750
|
+
|
|
751
|
+
filename = inspect.getframeinfo(previous_frame).filename
|
|
752
|
+
line_number = inspect.getframeinfo(previous_frame).lineno
|
|
753
|
+
function_name = inspect.getframeinfo(previous_frame).function
|
|
739
754
|
|
|
740
755
|
# Clean up to prevent reference cycles
|
|
741
756
|
del frame
|
|
757
|
+
del previous_frame
|
|
742
758
|
|
|
743
759
|
return filename, line_number, function_name
|
|
744
760
|
|
|
@@ -1364,7 +1380,7 @@ def chdir(dirname=None):
|
|
|
1364
1380
|
|
|
1365
1381
|
|
|
1366
1382
|
@contextlib.contextmanager
|
|
1367
|
-
def env_var(**kwargs:
|
|
1383
|
+
def env_var(**kwargs: str):
|
|
1368
1384
|
"""
|
|
1369
1385
|
Context manager to run some code that need alternate settings for environment variables.
|
|
1370
1386
|
|
|
@@ -1540,6 +1556,7 @@ def read_last_lines(filename: str | Path, num_lines: int) -> List[str]:
|
|
|
1540
1556
|
sanity_check(num_lines >= 0, "the number of lines to read shall be a positive number or zero.")
|
|
1541
1557
|
|
|
1542
1558
|
if not filename.exists():
|
|
1559
|
+
logger.warning(f"File does not exist: {filename}")
|
|
1543
1560
|
return []
|
|
1544
1561
|
|
|
1545
1562
|
# Declaring variable to implement exponential search
|
|
@@ -2321,22 +2338,44 @@ def caffeinate(pid: int = None):
|
|
|
2321
2338
|
subprocess.Popen([shutil.which("caffeinate"), "-i", "-w", str(pid)])
|
|
2322
2339
|
|
|
2323
2340
|
|
|
2324
|
-
def redirect_output_to_log(output_fn: str, append: bool = False) -> TextIO:
|
|
2341
|
+
def redirect_output_to_log(output_fn: str, append: bool = False, overwrite=True) -> TextIO:
|
|
2325
2342
|
"""
|
|
2326
|
-
Open file in the log folder where process output will be redirected.
|
|
2327
|
-
|
|
2343
|
+
Open the file in the log folder where the current process output will be redirected.
|
|
2328
2344
|
When no location can be determined, the user's home directory will be used.
|
|
2329
2345
|
|
|
2330
|
-
The file
|
|
2346
|
+
The file will be opened in text mode.
|
|
2347
|
+
|
|
2348
|
+
Args:
|
|
2349
|
+
output_fn: the name of the output file
|
|
2350
|
+
append: True to append to the file, False to overwrite
|
|
2351
|
+
overwrite: when False and the file exists, an exception is raised
|
|
2352
|
+
|
|
2353
|
+
Returns:
|
|
2354
|
+
The file stream (TextIO) where output can be redirected to.
|
|
2355
|
+
|
|
2356
|
+
Raises:
|
|
2357
|
+
FileExistsError: when the output file exists, append is False and overwrite is False.
|
|
2331
2358
|
"""
|
|
2332
2359
|
|
|
2333
|
-
|
|
2334
|
-
|
|
2360
|
+
if Path(output_fn).is_absolute():
|
|
2361
|
+
output_path = Path(output_fn)
|
|
2362
|
+
else:
|
|
2363
|
+
try:
|
|
2364
|
+
from egse.env import get_log_file_location
|
|
2365
|
+
|
|
2366
|
+
location = get_log_file_location()
|
|
2367
|
+
output_path = Path(location, output_fn).expanduser()
|
|
2368
|
+
except ValueError:
|
|
2369
|
+
output_path = Path.home() / output_fn
|
|
2335
2370
|
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2371
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2372
|
+
|
|
2373
|
+
if output_path.exists() and not append:
|
|
2374
|
+
if not overwrite:
|
|
2375
|
+
raise FileExistsError(
|
|
2376
|
+
f"Output file {output_path!s} already exists and will be overwritten. "
|
|
2377
|
+
f"Use overwrite=True to allow overwriting."
|
|
2378
|
+
)
|
|
2340
2379
|
|
|
2341
2380
|
out = open(output_path, "a" if append else "w")
|
|
2342
2381
|
|
|
@@ -31,7 +31,7 @@ __all__ = [
|
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def get_version_from_settings_file_raw(group_name: str, location: Path | str = None) -> str:
|
|
34
|
+
def get_version_from_settings_file_raw(group_name: str, location: Path | str | None = None) -> str:
|
|
35
35
|
"""
|
|
36
36
|
Reads the VERSION field from the `settings.yaml` file in raw mode, meaning the file
|
|
37
37
|
is not read using the PyYAML module, but using the `readline()` function of the file
|
|
@@ -66,7 +66,7 @@ def get_version_from_settings_file_raw(group_name: str, location: Path | str = N
|
|
|
66
66
|
return version
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def get_version_from_settings(group_name: str, location: Path = None):
|
|
69
|
+
def get_version_from_settings(group_name: str, location: Path | None = None, yaml_string: str | None = None) -> str:
|
|
70
70
|
"""
|
|
71
71
|
Reads the VERSION field from the `settings.yaml` file. This function first tries to load the proper Settings
|
|
72
72
|
and Group and if that fails uses the raw method.
|
|
@@ -74,6 +74,7 @@ def get_version_from_settings(group_name: str, location: Path = None):
|
|
|
74
74
|
Args:
|
|
75
75
|
group_name: major group name that contains the VERSION field, i.e. Common-EGSE or PLATO_TEST_SCRIPTS.
|
|
76
76
|
location: the location of the `settings.yaml` file or None in which case the location of this file is used.
|
|
77
|
+
yaml_string: optional YAML string to read the settings from instead of a file.
|
|
77
78
|
|
|
78
79
|
Raises:
|
|
79
80
|
A RuntimeError when the group_name is incorrect and unknown or the VERSION field is not found.
|
|
@@ -81,10 +82,14 @@ def get_version_from_settings(group_name: str, location: Path = None):
|
|
|
81
82
|
Returns:
|
|
82
83
|
The version from the `settings.yaml` file as a string.
|
|
83
84
|
"""
|
|
84
|
-
from egse.settings import Settings
|
|
85
|
+
from egse.settings import Settings
|
|
86
|
+
from egse.settings import SettingsError
|
|
85
87
|
|
|
86
88
|
try:
|
|
87
|
-
|
|
89
|
+
if location is None and yaml_string is not None:
|
|
90
|
+
settings = Settings.from_string(yaml_string, group=group_name)
|
|
91
|
+
else:
|
|
92
|
+
settings = Settings.load(group_name, location=location)
|
|
88
93
|
version = settings.VERSION
|
|
89
94
|
except (ModuleNotFoundError, SettingsError):
|
|
90
95
|
version = get_version_from_settings_file_raw(group_name, location=location)
|
|
@@ -92,7 +97,7 @@ def get_version_from_settings(group_name: str, location: Path = None):
|
|
|
92
97
|
return version
|
|
93
98
|
|
|
94
99
|
|
|
95
|
-
def get_version_from_git(location: str = None):
|
|
100
|
+
def get_version_from_git(location: str | Path | None = None) -> str:
|
|
96
101
|
"""
|
|
97
102
|
Returns the Git version number for the repository at the given location.
|
|
98
103
|
|
|
@@ -121,12 +126,12 @@ def get_version_from_git(location: str = None):
|
|
|
121
126
|
proc = subprocess.run(
|
|
122
127
|
["git", "describe", "--tags", "--long", "--always"], stderr=subprocess.PIPE, stdout=subprocess.PIPE
|
|
123
128
|
)
|
|
124
|
-
if proc.stderr:
|
|
125
|
-
version = None
|
|
126
129
|
if proc.stdout:
|
|
127
130
|
version = proc.stdout.strip().decode("ascii")
|
|
131
|
+
else:
|
|
132
|
+
version = "0.0.0"
|
|
128
133
|
except subprocess.CalledProcessError:
|
|
129
|
-
version =
|
|
134
|
+
version = "0.0.0"
|
|
130
135
|
|
|
131
136
|
return version
|
|
132
137
|
|
|
@@ -144,12 +149,13 @@ def get_version_installed(package_name: str) -> str:
|
|
|
144
149
|
from egse.system import chdir
|
|
145
150
|
|
|
146
151
|
with chdir(Path(__file__).parent):
|
|
147
|
-
from importlib.metadata import
|
|
152
|
+
from importlib.metadata import PackageNotFoundError
|
|
153
|
+
from importlib.metadata import version as get_version
|
|
148
154
|
|
|
149
155
|
try:
|
|
150
|
-
version =
|
|
156
|
+
version = get_version(package_name)
|
|
151
157
|
except PackageNotFoundError:
|
|
152
|
-
version =
|
|
158
|
+
version = "0.0.0"
|
|
153
159
|
|
|
154
160
|
return version
|
|
155
161
|
|
|
@@ -162,11 +168,12 @@ VERSION = get_version_installed("cgse-common")
|
|
|
162
168
|
# The __PYPI_VERSION__ is the version number that will be used for the installation.
|
|
163
169
|
# The version will appear in PyPI and also as the `installed version`.
|
|
164
170
|
|
|
165
|
-
__PYPI_VERSION__ = VERSION.split("+")[0]
|
|
171
|
+
__PYPI_VERSION__ = VERSION.split("+")[0] if VERSION else "0.0.0"
|
|
166
172
|
|
|
167
173
|
|
|
168
|
-
|
|
174
|
+
def main():
|
|
169
175
|
import rich
|
|
176
|
+
|
|
170
177
|
from egse.plugin import entry_points
|
|
171
178
|
|
|
172
179
|
if VERSION:
|
|
@@ -178,3 +185,7 @@ if __name__ == "__main__":
|
|
|
178
185
|
for ep in entry_points("cgse.version"):
|
|
179
186
|
if installed_version := get_version_installed(ep.name):
|
|
180
187
|
rich.print(f"Installed version for {ep.name}= [bold default]{installed_version}[/]")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|