cgse-common 2025.0.3__py3-none-any.whl → 2025.0.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgse-common
3
- Version: 2025.0.3
3
+ Version: 2025.0.5
4
4
  Summary: Software framework to support hardware testing
5
5
  Author: IVS KU Leuven
6
6
  Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
@@ -1,13 +1,13 @@
1
1
  egse/bits.py,sha256=1DPcC5R-8w-KLfbr2WLx-4Ezfwm6jpRMz-E5QfQ_YKw,11138
2
2
  egse/calibration.py,sha256=DZ-LgEwamTWDukMqTr2JLCPt3M1lw6W0SfpHQfG7cXg,8989
3
- egse/command.py,sha256=hGY-M618clxW5x5cLx79hHFiFgENkJ7S3Nj5Wacow4M,22879
3
+ egse/command.py,sha256=su35PU_0Bru1Es4up1lJFps9FTQpJ7xcxl9Y4w8suxA,22880
4
4
  egse/config.py,sha256=Ib1Ddt0wFwRrioBvMs7Md-wgEEPdK3eaXCt0H7HJEow,14888
5
- egse/control.py,sha256=gM9II-BgtgJjG7NqxyY8nBtMiRD0b9azGYFeD9_WU6s,12816
5
+ egse/control.py,sha256=cH0EA5mqaS3nQc35x1Sa0o2iM2Q4_hsrak6GOdcRMMU,12793
6
6
  egse/decorators.py,sha256=M0PavrPcTwhWS6yLlEo-mOS20lcy90Mym7HUVzPNmhs,13404
7
7
  egse/device.py,sha256=SIMROwB9YdNdFowTcZp1HW_el8LWVWDJ5tDUxs58YuY,8500
8
- egse/env.py,sha256=rNuqyQh2fRNIYC9tIwubnKVr7ctzUyQYqouJoyZKzmk,21968
8
+ egse/env.py,sha256=RnqUb_LF_9pjrt2Vqf7b-Pa66XstiKyqgzA_HFlp7Mc,27777
9
9
  egse/exceptions.py,sha256=Tc1xqUrWxV6SXxc9xB9viK_O-GVa_SpzqZUVEhZkwzA,1801
10
- egse/hk.py,sha256=MOX9KgkBkEpAtWniHXc8IWHyravp3OqTV4ZIuzmr93s,31141
10
+ egse/hk.py,sha256=5WMOGlx043XmEPj7ML1TkDzA8ZaTYLD-i5OcNUxE268,31163
11
11
  egse/metrics.py,sha256=cZKMEe3OTT2uomj7vXjEl54JD0CStfEC4nCgS6U5YSM,3794
12
12
  egse/mixin.py,sha256=J3yu5lPnm0grJqIm5LiacBUCZujJsdcKBNRaOQcMnNo,17345
13
13
  egse/monitoring.py,sha256=-pwXqPoiNKzQYKQSpKddFlaPkCTJZYdxvG1d2MBN3l0,3033
@@ -16,21 +16,21 @@ egse/obsid.py,sha256=-HPuHApZrr3Nj1J2-qqnIiE814C-gm4FSHdM2afKdRY,5883
16
16
  egse/persistence.py,sha256=Lx6LMJ1-dh8N43XF7tTM6RwD0sSETiGQ9DNqug-G-zQ,2160
17
17
  egse/plugin.py,sha256=Fd_QnABm33_dLjaj1zDgEZr3RKy-N88U5Hz5OZm9Jj0,2684
18
18
  egse/process.py,sha256=mQ2ojeL_9oE_QkMJlQDPd1290z0j2mOrGXrlrWtOtzI,16615
19
- egse/protocol.py,sha256=Psy0iOLPTgARn1VqeKtPCSKepHr_S2KW58UYwjOA6J0,23827
19
+ egse/protocol.py,sha256=G1HTU7xfS4xE1Oi1diDOTWQUTkPLyuooawWCOY6p-b0,23828
20
20
  egse/proxy.py,sha256=pMKdnF62SXm0quLoKfgvK9GFkH2mLMB5fWNrZenfqQQ,18100
21
21
  egse/reload.py,sha256=rDT0bC6RFeRhW38wSgFcxr30h8FvaKkoGp_OE-AwBB4,4388
22
22
  egse/resource.py,sha256=VoB7BVrQULT_SJ1XioDzB59-uH47nUcN-KNVLvFxiFE,15163
23
- egse/response.py,sha256=WdUUgYe1ABhPAy1HL8lVCM3Gu0Vgy3niaRi_twZuLgo,2700
24
- egse/services.py,sha256=ZgkF0Rx_PykOVHAOV1cKduJdUhuY6A4DgwjPJWRGj3U,7642
23
+ egse/response.py,sha256=WFtWftovrmmn92NW4mhJjibMtCWteZmAzNuPLVsV-lg,2716
24
+ egse/services.py,sha256=rGS_5mSUFDw8wISEU_nuS9P9_XNtHvpViD9oSH9FqKo,7709
25
25
  egse/services.yaml,sha256=p8QBF56zLI21iJ9skt65VlNz4rIqRoFfBTZxOIUZCZ4,1853
26
- egse/settings.py,sha256=iu17w0xvLhZPXImOt_C3duy2XamClQpKuP-OkCEIZVo,13481
26
+ egse/settings.py,sha256=E8i9nYyz6n931XP0BQu8xmtP3gHICBaSP4VMKL47IXs,15160
27
27
  egse/settings.yaml,sha256=M6WMtTbNqNuXsdEf-OzKz0dAxkFTRabpugmIJ2HKyZk,290
28
- egse/setup.py,sha256=NLS0RhMTvKpdHnnojSPCcfRJwPGkpijt5VLFcDfnbt8,42916
28
+ egse/setup.py,sha256=wFHqRQtDdkdbJeXH6hF28xUYnYhv-TYPIV-PunUoEow,43789
29
29
  egse/state.py,sha256=ekcCZu_DZKkKYn-5iWG7ij7Aif2WYMNVs5h3cia-cVc,5352
30
- egse/system.py,sha256=t2NrtTAPXbMgBxsqicGjti-b8oEmiHtQu2Ydm77q4lI,48941
30
+ egse/system.py,sha256=KCXz8eH3PNwLaB7ww3OCkK_tQoaTYhh2FEwX1mvN8RQ,48996
31
31
  egse/version.py,sha256=-EMuiSn2eEp8QJX6csmBEu1m2yGqlXHJj2Hj49w6a2k,6217
32
32
  egse/zmq_ser.py,sha256=2-nwVUBWZ3vvosKNmlWobHJrIJA2HlM3V5a63Gz2JY0,1819
33
- cgse_common-2025.0.3.dist-info/METADATA,sha256=prFc4fx-X9MsjZi1aiM1IXdU1313CKQYZun9m4WR-x4,2574
34
- cgse_common-2025.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
- cgse_common-2025.0.3.dist-info/entry_points.txt,sha256=E-KaQ9NGWAP1XvLHncNxq5oa22EAf9sOpBZWphXcxiE,34
36
- cgse_common-2025.0.3.dist-info/RECORD,,
33
+ cgse_common-2025.0.5.dist-info/METADATA,sha256=RBKcR3sDE90ugMy7tQy4c28Qfqae2f6vzEkCWwQ7kjA,2574
34
+ cgse_common-2025.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
+ cgse_common-2025.0.5.dist-info/entry_points.txt,sha256=E-KaQ9NGWAP1XvLHncNxq5oa22EAf9sOpBZWphXcxiE,34
36
+ cgse_common-2025.0.5.dist-info/RECORD,,
egse/command.py CHANGED
@@ -95,7 +95,7 @@ import re
95
95
  from collections import namedtuple
96
96
  from typing import Callable
97
97
 
98
- from egse.control import Success
98
+ from egse.response import Success
99
99
  from egse.exceptions import Error
100
100
 
101
101
  logger = logging.getLogger(__name__)
egse/control.py CHANGED
@@ -5,7 +5,6 @@ import abc
5
5
  import logging
6
6
  import pickle
7
7
  import threading
8
- from typing import Any
9
8
 
10
9
  import zmq
11
10
 
egse/env.py CHANGED
@@ -40,30 +40,38 @@ WARNING:
40
40
  from __future__ import annotations
41
41
 
42
42
  __all__ = [
43
- "get_data_storage_location",
44
- "set_data_storage_location",
45
- "get_data_storage_location_env_name",
43
+ "env_var",
46
44
  "get_conf_data_location",
47
- "set_conf_data_location",
48
45
  "get_conf_data_location_env_name",
49
46
  "get_conf_repo_location",
50
- "set_conf_repo_location",
51
47
  "get_conf_repo_location_env_name",
48
+ "get_data_storage_location",
49
+ "get_data_storage_location_env_name",
50
+ "get_local_settings",
51
+ "get_local_settings_env_name",
52
52
  "get_log_file_location",
53
- "set_log_file_location",
54
53
  "get_log_file_location_env_name",
55
- "get_local_settings",
54
+ "get_project_name",
55
+ "get_site_id",
56
+ "set_conf_data_location",
57
+ "set_conf_repo_location",
58
+ "set_data_storage_location",
56
59
  "set_local_settings",
57
- "get_local_settings_env_name",
60
+ "set_log_file_location",
58
61
  ]
59
62
 
63
+ import contextlib
60
64
  import os
61
65
  import warnings
62
66
  from pathlib import Path
63
67
 
64
68
  from egse.system import all_logging_disabled
69
+ from egse.system import get_caller_info
65
70
  from egse.system import ignore_m_warning
66
71
 
72
+ from rich.console import Console
73
+ console = Console(width=100)
74
+
67
75
  # Every project shall have a PROJECT and a SITE_ID environment variable set. This variable will be used to
68
76
  # create the other environment variables that are specific to the project.
69
77
 
@@ -121,11 +129,18 @@ class _Env:
121
129
  self._env = {}
122
130
 
123
131
  def set(self, key, value):
124
- self._env[key] = value
132
+ if value is None:
133
+ if key in self._env:
134
+ del self._env[key]
135
+ else:
136
+ self._env[key] = value
125
137
 
126
138
  def get(self, key) -> str:
127
139
  return self._env.get(key, NoValue())
128
140
 
141
+ def __rich__(self):
142
+ return self._env
143
+
129
144
 
130
145
  _env = _Env()
131
146
 
@@ -157,20 +172,60 @@ def _check_no_value(var_name, value):
157
172
  """Raise a ValueError when the value for the variable is NoValue."""
158
173
  if value == NoValue():
159
174
  project = _env.get("PROJECT")
160
- env_name = var_name if var_name == "SITE_ID" else f"{project}_{var_name}"
175
+ env_name = var_name if var_name in ("PROJECT", "SITE_ID") else f"{project}_{var_name}"
161
176
  raise ValueError(
162
177
  f"The environment variable {env_name} is not set. "
163
178
  f"Please set the environment variable before proceeding."
164
179
  )
165
180
 
166
181
 
182
+ def set_default_environment(
183
+ project: str,
184
+ site_id: str,
185
+ data_storage_location: str | Path,
186
+ conf_data_location: str | Path | None = None,
187
+ conf_repo_location: str | Path | None = None,
188
+ log_file_location: str | Path | None = None,
189
+ local_settings: str | Path | None = None,
190
+ ):
191
+ set_project_name(project)
192
+ set_site_id(site_id)
193
+ set_data_storage_location(data_storage_location)
194
+ set_conf_data_location(conf_data_location)
195
+ set_conf_repo_location(conf_repo_location)
196
+ set_log_file_location(log_file_location)
197
+ set_local_settings(local_settings)
198
+
199
+
200
+ def get_project_name():
201
+ """Get the PROJECT name. Return None when the PROJECT is not set."""
202
+ return _env.get("PROJECT") or None
203
+
204
+
205
+ def set_project_name(name: str):
206
+ """Set the environment variable PROJECT and its internal representation."""
207
+ os.environ["PROJECT"] = name
208
+ _env.set("PROJECT", name)
209
+
210
+
211
+ def get_site_id():
212
+ """Get the SITE_ID. Return None if the SITE_ID is not set."""
213
+ return _env.get("SITE_ID") or None
214
+
215
+
216
+ def set_site_id(name: str):
217
+ """Set the environment variable SITE_ID and its internal representation."""
218
+ os.environ["SITE_ID"] = name
219
+ _env.set("SITE_ID", name)
220
+
221
+
167
222
  def get_data_storage_location_env_name() -> str:
168
223
  """Returns the name of the environment variable for the project."""
169
224
  project = _env.get("PROJECT")
170
225
  return f"{project}_DATA_STORAGE_LOCATION"
171
226
 
172
227
 
173
- def set_data_storage_location(location: str | Path):
228
+ def set_data_storage_location(location: str | Path | None):
174
229
  """
175
230
  Sets the environment variable and the internal representation to the given value.
176
231
 
@@ -179,14 +234,19 @@ def set_data_storage_location(location: str | Path):
179
234
  """
180
235
  env_name = get_data_storage_location_env_name()
181
236
 
182
- # Check if location exists and is readable
237
+ if location is None:
238
+ if env_name in os.environ:
239
+ del os.environ[env_name]
240
+ _env.set("DATA_STORAGE_LOCATION", None)
241
+ return
242
+
183
243
  if not Path(location).exists():
184
244
  warnings.warn(
185
245
  f"The location you provided for the environment variable {env_name} doesn't exist: {location}."
186
246
  )
187
247
 
188
- os.environ[env_name] = location
189
- _env.set('DATA_STORAGE_LOCATION', location)
248
+ os.environ[env_name] = str(location)
249
+ _env.set('DATA_STORAGE_LOCATION', str(location))
190
250
 
191
251
 
192
252
  def get_data_storage_location(site_id: str = None) -> str:
@@ -213,6 +273,9 @@ def get_data_storage_location(site_id: str = None) -> str:
213
273
  """
214
274
  global _env
215
275
 
276
+ project = _env.get("PROJECT")
277
+ _check_no_value("PROJECT", project)
278
+
216
279
  site_id = site_id or _env.get("SITE_ID")
217
280
  _check_no_value("SITE_ID", site_id)
218
281
 
@@ -230,7 +293,7 @@ def get_conf_data_location_env_name() -> str:
230
293
  return f"{project}_CONF_DATA_LOCATION"
231
294
 
232
295
 
233
- def set_conf_data_location(location: str | Path):
296
+ def set_conf_data_location(location: str | Path | None):
234
297
  """
235
298
  Sets the environment variable and the internal representation to the given value.
236
299
 
@@ -240,7 +303,12 @@ def set_conf_data_location(location: str | Path):
240
303
 
241
304
  env_name = get_conf_data_location_env_name()
242
305
 
243
- # Check if location exists and is readable
306
+ if location is None:
307
+ if env_name in os.environ:
308
+ del os.environ[env_name]
309
+ _env.set("CONF_DATA_LOCATION", None)
310
+ return
311
+
244
312
  if not Path(location).exists():
245
313
  warnings.warn(
246
314
  f"The location you provided for the environment variable {env_name} doesn't exist: {location}."
@@ -293,7 +361,7 @@ def get_log_file_location_env_name():
293
361
  return f"{project}_LOG_FILE_LOCATION"
294
362
 
295
363
 
296
- def set_log_file_location(location: str | Path):
364
+ def set_log_file_location(location: str | Path | None):
297
365
  """
298
366
  Sets the environment variable and the internal representation to the given value.
299
367
 
@@ -303,7 +371,12 @@ def set_log_file_location(location: str | Path):
303
371
 
304
372
  env_name = get_log_file_location_env_name()
305
373
 
306
- # Check if location exists and is readable
374
+ if location is None:
375
+ if env_name in os.environ:
376
+ del os.environ[env_name]
377
+ _env.set("LOG_FILE_LOCATION", None)
378
+ return
379
+
307
380
  if not Path(location).exists():
308
381
  warnings.warn(
309
382
  f"The location you provided for the environment variable {env_name} doesn't exist: {location}."
@@ -355,17 +428,24 @@ def get_local_settings_env_name() -> str:
355
428
  return f"{project}_LOCAL_SETTINGS"
356
429
 
357
430
 
358
- def set_local_settings(path: str | Path):
431
+ def set_local_settings(path: str | Path | None):
359
432
  """
360
433
  Sets the environment variable and the internal representation to the given value.
361
434
 
435
+ When the path is set to None, the environment variable will be unset.
436
+
362
437
  Warnings:
363
438
  Issues a warning when the given path doesn't exist.
364
439
  """
365
440
 
366
441
  env_name = get_local_settings_env_name()
367
442
 
368
- # Check if location exists and is readable
443
+ if path is None:
444
+ if env_name in os.environ:
445
+ del os.environ[env_name]
446
+ _env.set('LOCAL_SETTINGS', None)
447
+ return
448
+
369
449
  if not Path(path).exists():
370
450
  warnings.warn(
371
451
  f"The location you provided for the environment variable {env_name} doesn't exist: {path}."
@@ -389,34 +469,61 @@ def get_local_settings() -> str:
389
469
  return local_settings or None
390
470
 
391
471
 
472
+ def has_conf_repo_location() -> bool:
473
+ location = _env.get("CONF_REPO_LOCATION")
474
+ return True if location else False
475
+
476
+
392
477
  def get_conf_repo_location_env_name() -> str:
393
478
  """Returns the name of the environment variable for the project."""
394
479
  project = _env.get("PROJECT")
395
480
  return f"{project}_CONF_REPO_LOCATION"
396
481
 
397
482
 
398
- def get_conf_repo_location() -> str:
399
- """Returns the fully qualified name of the location of the repository with configuration and calibration data."""
483
+ def get_conf_repo_location() -> str | None:
484
+ """
485
+ Returns the fully qualified name of the location of the repository with
486
+ configuration and calibration data.
487
+
488
+ Returns None if no environment variable was defined or if the location doesn't exist.
489
+ In both cases a Warning is issued.
490
+ """
400
491
 
401
492
  location = _env.get("CONF_REPO_LOCATION")
402
493
 
403
- if location and not Path(location).exists():
494
+ if location in (None, NoValue()):
495
+ warnings.warn(
496
+ f"The environment variable for the configuration data repository is "
497
+ f"not defined ({get_conf_repo_location_env_name()})."
498
+ )
499
+ return None
500
+
501
+ if not Path(location).exists():
404
502
  warnings.warn(f"The location of the configuration data repository doesn't exist: {location}.")
503
+ return None
405
504
 
406
- return location or None
505
+ return location
407
506
 
408
507
 
409
- def set_conf_repo_location(location: str):
508
+ def set_conf_repo_location(location: str | Path | None):
410
509
  """
411
510
  Sets the environment variable and the internal representation to the given value.
412
511
 
512
+ When the location is None, the environment variable will be unset and its internal
513
+ representation will be NoValue().
514
+
413
515
  Warnings:
414
516
  Issues a warning when the given location doesn't exist.
415
517
  """
416
518
 
417
519
  env_name = get_conf_repo_location_env_name()
418
520
 
419
- # Check if location exists and is readable
521
+ if location is None:
522
+ if env_name in os.environ:
523
+ del os.environ[env_name]
524
+ _env.set("CONF_REPO_LOCATION", None)
525
+ return
526
+
420
527
  if not Path(location).exists():
421
528
  warnings.warn(
422
529
  f"The location you provided for the environment variable {env_name} doesn't exist: {location}."
@@ -426,7 +533,69 @@ def set_conf_repo_location(location: str):
426
533
  _env.set('CONF_REPO_LOCATION', location)
427
534
 
428
535
 
429
- ignore_m_warning('egse.env')
536
+ def print_env():
537
+ """
538
+ Prints out the mandatory and known environment variables at the time of the
539
+ function call. The function and lineno is also printed for information.
540
+ """
541
+ col_width = 30
542
+
543
+ console = Console(width=200)
544
+
545
+ with warnings.catch_warnings():
546
+ warnings.simplefilter("ignore")
547
+ caller_info = get_caller_info(level=2)
548
+ console.print(f"[b]Environment as in {caller_info.filename}:{caller_info.lineno}[/]")
549
+ console.print(f" {'PROJECT':{col_width}s}: {get_project_name()}")
550
+ console.print(f" {'SITE_ID':{col_width}s}: {get_site_id()}")
551
+ console.print(f" {get_data_storage_location_env_name():{col_width}s}: {get_data_storage_location()}")
552
+ console.print(f" {get_log_file_location_env_name():{col_width}s}: {get_log_file_location()}")
553
+ console.print(f" {get_conf_data_location_env_name():{col_width}s}: {get_conf_data_location()}")
554
+ console.print(f" {get_conf_repo_location_env_name():{col_width}s}: {get_conf_repo_location()}")
555
+ console.print(f" {get_local_settings_env_name():{col_width}s}: {get_local_settings()}")
556
+
557
+
558
+ @contextlib.contextmanager
559
+ def env_var(**kwargs):
560
+ """
561
+ Context manager to run some code that need alternate settings for environment variables.
562
+ This will automatically initialize the CGSE environment upon entry and re-initialize upon exit.
563
+
564
+ NOTE: This context manager is different from the one from `egse.system` because of the CGSE environment changes.
565
+
566
+ Args:
567
+ **kwargs: dictionary with environment variables that are needed
568
+
569
+ Examples:
570
+
571
+ >>> from egse.env import env_var
572
+ >>> with env_var(PLATO_DATA_STORAGE_LOCATION="/Users/rik/data"):
573
+ ... # do stuff that needs these alternate setting
574
+ ... pass
575
+
576
+ """
577
+ saved_env = {}
578
+
579
+ for k, v in kwargs.items():
580
+ saved_env[k] = os.environ.get(k)
581
+ if v is None:
582
+ if k in os.environ:
583
+ del os.environ[k]
584
+ else:
585
+ os.environ[k] = v
586
+
587
+ initialize()
588
+
589
+ yield
590
+
591
+ for k, v in saved_env.items():
592
+ if v is None:
593
+ if k in os.environ:
594
+ del os.environ[k]
595
+ else:
596
+ os.environ[k] = v
597
+
598
+ initialize()
430
599
 
431
600
 
432
601
  def main(args: list | None = None): # pragma: no cover
@@ -507,8 +676,10 @@ def main(args: list | None = None): # pragma: no cover
507
676
  location = get_data_storage_location()
508
677
  if not Path(location).exists():
509
678
  if args.mkdir:
510
- rich.print(f" [green]⟶ Creating data storage location: {location}[/]")
679
+ rich.print(f" [green]⟶ Creating data storage location: {location} (+ daily + obs)[/]")
511
680
  Path(location).mkdir(parents=True)
681
+ (Path(location) / "daily").mkdir()
682
+ (Path(location) / "obs").mkdir()
512
683
  else:
513
684
  rich.print(" [red]⟶ ERROR: The data storage location doesn't exist![/]")
514
685
  else:
@@ -530,6 +701,16 @@ def main(args: list | None = None): # pragma: no cover
530
701
  except ValueError as exc:
531
702
  rich.print(f" get_conf_data_location() = [red]{exc}[/]")
532
703
 
704
+ try:
705
+ rich.print(f" {get_conf_repo_location() = }", flush=True, end="")
706
+ location = get_conf_repo_location()
707
+ if location is None or not Path(location).exists():
708
+ rich.print(" [red]⟶ ERROR: The configuration repository location doesn't exist![/]")
709
+ else:
710
+ rich.print()
711
+ except ValueError as exc:
712
+ rich.print(f" get_conf_repo_location() = [red]{exc}[/]")
713
+
533
714
  try:
534
715
  rich.print(f" {get_log_file_location() = }", flush=True, end="")
535
716
  location = get_log_file_location()
@@ -613,6 +794,9 @@ def main(args: list | None = None): # pragma: no cover
613
794
  # PLATO_COMMON_EGSE_PATH - YES
614
795
 
615
796
 
797
+ ignore_m_warning('egse.env')
798
+
799
+
616
800
  if __name__ == "__main__":
617
801
  import sys
618
802
  main(sys.argv[1:])
egse/hk.py CHANGED
@@ -16,9 +16,9 @@ import dateutil.parser as date_parser
16
16
  import numpy as np
17
17
  from egse.config import find_files
18
18
  from egse.env import get_data_storage_location
19
+ from egse.env import get_site_id
19
20
  from egse.obsid import ObservationIdentifier
20
21
  from egse.obsid import obsid_from_storage
21
- from egse.settings import Settings
22
22
  from egse.setup import Setup
23
23
  from egse.setup import SetupError
24
24
  from egse.setup import load_setup
@@ -30,8 +30,6 @@ from egse.system import time_since_epoch_1958
30
30
 
31
31
  _LOGGER = logging.getLogger(__name__)
32
32
 
33
- SITE_ID = Settings.load("SITE").ID
34
-
35
33
 
36
34
  class TmDictionaryColumns(str, Enum):
37
35
  """ Enumeration of the relevant columns in the TM dictionary spreadsheet.
@@ -50,7 +48,7 @@ class TmDictionaryColumns(str, Enum):
50
48
  STORAGE_MNEMONIC = "Storage mnemonic"
51
49
  CORRECT_HK_NAMES = "CAM EGSE mnemonic"
52
50
  ORIGINAL_EGSE_HK_NAMES = "Original name in EGSE"
53
- SYNOPTICS_ORIGIN = f"Origin of synoptics at {SITE_ID}"
51
+ SYNOPTICS_ORIGIN = f"Origin of synoptics at {get_site_id()}"
54
52
  TIMESTAMP_NAMES = "Name of corresponding timestamp"
55
53
  DESCRIPTION = "Description"
56
54
  DASHBOARD = "MON screen"
@@ -120,7 +118,7 @@ def get_housekeeping(hk_name: str, obsid: Union[ObservationIdentifier, str, int]
120
118
  try:
121
119
  return _get_housekeeping_obsid(hk_name, data_dir, obsid=obsid, time_window=time_window, setup=setup)
122
120
  except (ValueError, StopIteration, FileNotFoundError) as exc:
123
- raise HKError(f"No HK found for {hk_name} for obsid {obsid} at {SITE_ID}") from exc
121
+ raise HKError(f"No HK found for {hk_name} for obsid {obsid} at {get_site_id()}") from exc
124
122
 
125
123
  # Specified OD
126
124
 
@@ -129,14 +127,14 @@ def get_housekeeping(hk_name: str, obsid: Union[ObservationIdentifier, str, int]
129
127
  try:
130
128
  return _get_housekeeping_od(hk_name, data_dir, od=od, time_window=time_window, setup=setup)
131
129
  except (ValueError, StopIteration, FileNotFoundError) as exc:
132
- raise HKError(f"No HK found for {hk_name} for OD {od} at {SITE_ID}") from exc
130
+ raise HKError(f"No HK found for {hk_name} for OD {od} at {get_site_id()}") from exc
133
131
 
134
132
  # Didn't specify neither the obsid nor the OD
135
133
 
136
134
  try:
137
135
  return _get_housekeeping_daily(hk_name, data_dir, time_window=time_window, setup=setup)
138
136
  except (ValueError, StopIteration, FileNotFoundError) as exc:
139
- raise HKError(f"No HK found for {hk_name} for today at {SITE_ID}") from exc
137
+ raise HKError(f"No HK found for {hk_name} for today at {get_site_id()}") from exc
140
138
 
141
139
 
142
140
  def _get_housekeeping(hk_name: str, timestamp_name: str, hk_dir: str, files, time_window: int = None):
@@ -272,7 +270,7 @@ def _get_housekeeping_od(hk_name: str, data_dir, od: str, time_window: int = Non
272
270
  raise HKError(f"Cannot determine which EGSE component generated HK parameter {hk_name}")
273
271
 
274
272
  hk_dir += f"{od}/"
275
- hk_files = [f"{od}_{SITE_ID}_{origin}.csv"]
273
+ hk_files = [f"{od}_{get_site_id()}_{origin}.csv"]
276
274
 
277
275
  return _get_housekeeping(hk_name, timestamp_name, hk_dir, hk_files, time_window=time_window)
278
276
 
@@ -326,7 +324,7 @@ def _get_housekeeping_obsid(hk_name: str, data_dir, obsid: Union[ObservationIden
326
324
 
327
325
  if len(hk_files) == 0:
328
326
 
329
- raise HKError(f"No HK found for the {origin} at {SITE_ID} for obsid {obsid}")
327
+ raise HKError(f"No HK found for the {origin} at {get_site_id()} for obsid {obsid}")
330
328
 
331
329
  hk_files = [hk_files[-1].name]
332
330
 
@@ -387,7 +385,7 @@ def _get_housekeeping_daily(hk_name: str, data_dir, time_window: int = None, set
387
385
 
388
386
  timestamp = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y%m%d")
389
387
  hk_dir += f"{timestamp}/"
390
- filename = f"{timestamp}_{SITE_ID}_{origin}.csv"
388
+ filename = f"{timestamp}_{get_site_id()}_{origin}.csv"
391
389
 
392
390
  timestamp_index, hk_index = get_indices(hk_dir + filename, hk_name, timestamp_name)
393
391
  return get_last_non_empty(hk_dir + filename, timestamp_index, hk_index)
@@ -412,7 +410,7 @@ def _get_housekeeping_daily(hk_name: str, data_dir, time_window: int = None, set
412
410
 
413
411
  # Determine which columns will be needed from which file
414
412
 
415
- filename = f"{start_od}/{start_od}_{SITE_ID}_{origin}.csv"
413
+ filename = f"{start_od}/{start_od}_{get_site_id()}_{origin}.csv"
416
414
 
417
415
  if Path(hk_dir + filename).exists():
418
416
 
@@ -456,7 +454,7 @@ def _get_housekeeping_daily(hk_name: str, data_dir, time_window: int = None, set
456
454
  while day <= last_day:
457
455
 
458
456
  od = f"{day.year}{day.month:02d}{day.day:02d}"
459
- filename = f"{od}/{od}_{SITE_ID}_{origin}.csv"
457
+ filename = f"{od}/{od}_{get_site_id()}_{origin}.csv"
460
458
 
461
459
  if Path(hk_dir + filename).exists():
462
460
 
@@ -684,7 +682,7 @@ def read_conversion_dict(storage_mnemonic: str, use_site: bool = False, setup: O
684
682
 
685
683
  if use_site:
686
684
 
687
- th_prefix = f"G{SITE_ID}_"
685
+ th_prefix = f"G{get_site_id()}_"
688
686
 
689
687
  th_conversion_dict = {}
690
688
 
egse/protocol.py CHANGED
@@ -20,7 +20,7 @@ from prometheus_client import Summary
20
20
  from egse.command import Command
21
21
  from egse.command import CommandExecution
22
22
  from egse.control import ControlServer
23
- from egse.control import Failure
23
+ from egse.response import Failure
24
24
  from egse.device import DeviceConnectionObserver
25
25
  from egse.system import format_datetime
26
26
  from egse.zmq_ser import bind_address
egse/response.py CHANGED
@@ -2,9 +2,10 @@
2
2
  This module provides functionality to handle responses from the control servers.
3
3
  """
4
4
  __all__ = [
5
- "Success",
6
5
  "Failure",
7
6
  "Message",
7
+ "Response",
8
+ "Success",
8
9
  ]
9
10
 
10
11
  from typing import Any
egse/services.py CHANGED
@@ -10,6 +10,7 @@ logging levels, or to quit the control server in a controlled way.
10
10
 
11
11
  import inspect
12
12
  import logging
13
+ from pathlib import Path
13
14
 
14
15
  from egse.command import ClientServerCommand
15
16
  from egse.control import ControlServer
@@ -22,7 +23,9 @@ from egse.zmq_ser import connect_address
22
23
 
23
24
  LOGGER = logging.getLogger(__name__)
24
25
 
25
- SERVICE_SETTINGS = Settings.load(filename="services.yaml")
26
+ HERE = Path(__file__).parent
27
+
28
+ SERVICE_SETTINGS = Settings.load(filename=str(HERE / "services.yaml"))
26
29
 
27
30
 
28
31
  class ServiceCommand(ClientServerCommand):
egse/settings.py CHANGED
@@ -8,7 +8,7 @@ settings are loaded from a file called ``settings.yaml``. The default yaml confi
8
8
  located in the same directory as this module.
9
9
 
10
10
  The YAML file is read and the configuration parameters for the given group are
11
- made available as instance variables of the returned class.
11
+ available as instance variables of the returned class.
12
12
 
13
13
  The intended use is as follows:
14
14
 
@@ -54,24 +54,27 @@ If that file is located at a specific location, also use the ``location=`` keywo
54
54
  The above code will read the complete YAML file, i.e. all the groups into a dictionary.
55
55
 
56
56
  """
57
+ from __future__ import annotations
57
58
 
58
59
  import logging
59
- import os
60
- import pathlib
61
60
  import re
61
+ from pathlib import Path
62
+ from typing import Any
62
63
 
63
64
  import yaml # This module is provided by the pip package PyYaml - pip install pyyaml
64
65
 
65
66
  from egse.env import get_local_settings
66
67
  from egse.env import get_local_settings_env_name
67
68
  from egse.exceptions import FileIsEmptyError
68
- from egse.system import AttributeDict
69
- from egse.system import get_caller_info
69
+ from egse.system import attrdict
70
+ from egse.system import get_package_location
70
71
  from egse.system import ignore_m_warning
71
72
  from egse.system import recursive_dict_update
72
73
 
73
74
  _LOGGER = logging.getLogger(__name__)
74
75
 
76
+ _HERE = Path(__file__).resolve().parent
77
+
75
78
 
76
79
  class SettingsError(Exception):
77
80
  pass
@@ -109,6 +112,107 @@ SAFE_LOADER.add_implicit_resolver(
109
112
  list(u'-+0123456789.'))
110
113
 
111
114
 
115
+ def get_settings_locations(location: str | Path = None, filename: str = "settings.yaml") -> list[Path]:
116
+
117
+ yaml_locations: set[Path] = set()
118
+
119
+ if location is None:
120
+ package_locations = get_package_location("egse") # `egse` is a namespace
121
+
122
+ for package_location in package_locations:
123
+ if (package_location / filename).exists():
124
+ yaml_locations.add(package_location)
125
+
126
+ yaml_locations.add(_HERE)
127
+ _LOGGER.debug(f"yaml_locations in Settings.load(): {yaml_locations}")
128
+
129
+ else:
130
+
131
+ package_location = Path(location).resolve()
132
+ if (package_location / filename).exists():
133
+ yaml_locations.add(package_location)
134
+ else:
135
+ _LOGGER.warning(f"No '{filename}' file found at {package_location}.")
136
+
137
+ return list(yaml_locations)
138
+
139
+
140
+ def load_global_settings(locations, filename: str = "settings.yaml", force: bool = False) -> attrdict:
141
+
142
+ global_settings = attrdict()
143
+
144
+ for yaml_location in locations:
145
+ try:
146
+ yaml_document = read_configuration_file(str(yaml_location / filename), force=force)
147
+ recursive_dict_update(global_settings, yaml_document)
148
+ except FileNotFoundError as exc:
149
+ raise SettingsError(
150
+ f"Filename {filename} not found at location {locations}."
151
+ ) from exc
152
+
153
+ return global_settings
154
+
155
+
156
+ def load_local_settings(force: bool = False):
157
+
158
+ local_settings = {}
159
+ try:
160
+ local_settings_location = get_local_settings()
161
+
162
+ if local_settings_location:
163
+ _LOGGER.debug(f"Using {local_settings_location} to update global settings.")
164
+ try:
165
+ yaml_document_local = read_configuration_file(local_settings_location, force=force)
166
+ if yaml_document_local is None:
167
+ raise FileIsEmptyError()
168
+ local_settings = attrdict(
169
+ {name: value for name, value in yaml_document_local.items()}
170
+ )
171
+ except FileNotFoundError as exc:
172
+ raise SettingsError(
173
+ f"Local settings YAML file '{local_settings_location}' not found. "
174
+ f"Check your environment variable {get_local_settings_env_name()}."
175
+ ) from exc
176
+ except FileIsEmptyError:
177
+ _LOGGER.warning(
178
+ f"Local settings YAML file '{local_settings_location}' is empty. "
179
+ f"No local settings were loaded.")
180
+
181
+ except KeyError as exc:
182
+ _LOGGER.warning(f"The environment variable {get_local_settings_env_name()} is not defined. (from "
183
+ f"{exc.__class__.__name__}: {exc})")
184
+
185
+ return local_settings
186
+
187
+
188
+ def read_configuration_file(filename: str, *, force=False):
189
+ """
190
+ Read the YAML input configuration file. The configuration file is only read
191
+ once and memoized as load optimization.
192
+
193
+ Args:
194
+ filename (str): the fully qualified filename of the YAML file
195
+ force (bool): force reloading the file
196
+
197
+ Returns:
198
+ a dictionary containing all the configuration settings from the YAML file.
199
+ """
200
+ if force or not Settings.is_memoized(filename):
201
+
202
+ _LOGGER.debug(f"Parsing YAML configuration file {filename}.")
203
+
204
+ with open(filename, "r") as stream:
205
+ try:
206
+ yaml_document = yaml.load(stream, Loader=SAFE_LOADER)
207
+ except yaml.YAMLError as exc:
208
+ _LOGGER.error(exc)
209
+ raise SettingsError(f"Error loading YAML document {filename}") from exc
210
+
211
+ Settings.add_memoized(filename, yaml_document)
212
+
213
+ return Settings.get_memoized(filename) or {}
214
+
215
+
112
216
  class Settings:
113
217
  """
114
218
  The Settings class provides a load() method that loads configuration settings for a group
@@ -116,9 +220,7 @@ class Settings:
116
220
  """
117
221
 
118
222
  __memoized_yaml = {} # Memoized settings yaml files
119
-
120
223
  __profile = False # Used for profiling methods and functions
121
- __simulation = False # Use simulation mode where applicable and possible
122
224
 
123
225
  LOG_FORMAT_DEFAULT = "%(levelname)s:%(module)s:%(lineno)d:%(message)s"
124
226
  LOG_FORMAT_FULL = "%(asctime)23s:%(levelname)8s:%(lineno)5d:%(name)-20s: %(message)s"
@@ -132,40 +234,35 @@ class Settings:
132
234
  LOG_FORMAT_DATE = "%d/%m/%Y %H:%M:%S"
133
235
 
134
236
  @classmethod
135
- def read_configuration_file(cls, filename: str, *, force=False):
136
- """
137
- Read the YAML input configuration file. The configuration file is only read
138
- once and memoized as load optimization.
139
-
140
- Args:
141
- filename (str): the fully qualified filename of the YAML file
142
- force (bool): force reloading the file
237
+ def get_memoized_locations(cls) -> list:
238
+ return list(cls.__memoized_yaml.keys())
143
239
 
144
- Returns:
145
- a dictionary containing all the configuration settings from the YAML file.
146
- """
147
- if force or filename not in cls.__memoized_yaml:
240
+ @classmethod
241
+ def is_memoized(cls, filename: str) -> bool:
242
+ return filename in cls.__memoized_yaml
148
243
 
149
- _LOGGER.debug(f"Parsing YAML configuration file {filename}.")
244
+ @classmethod
245
+ def add_memoized(cls, filename: str, yaml_document: Any):
246
+ cls.__memoized_yaml[filename] = yaml_document
150
247
 
151
- with open(filename, "r") as stream:
152
- try:
153
- yaml_document = yaml.load(stream, Loader=SAFE_LOADER)
154
- except yaml.YAMLError as exc:
155
- _LOGGER.error(exc)
156
- raise SettingsError(f"Error loading YAML document {filename}") from exc
248
+ @classmethod
249
+ def get_memoized(cls, filename: str):
250
+ return cls.__memoized_yaml.get(filename)
157
251
 
158
- cls.__memoized_yaml[filename] = yaml_document
252
+ @classmethod
253
+ def clear_memoized(cls):
254
+ cls.__memoized_yaml.clear()
159
255
 
160
- return cls.__memoized_yaml[filename]
256
+ @classmethod
257
+ def set_profiling(cls, flag):
258
+ cls.__profile = flag
161
259
 
162
260
  @classmethod
163
- def get_memoized_locations(cls):
164
- return cls.__memoized_yaml.keys()
261
+ def profiling(cls):
262
+ return cls.__profile
165
263
 
166
264
  @classmethod
167
- def load(cls, group_name=None, filename="settings.yaml", location=None, *, force=False,
168
- add_local_settings=True):
265
+ def load(cls, group_name=None, filename="settings.yaml", location=None, *, force=False, add_local_settings=True):
169
266
  """
170
267
  Load the settings for the given group from YAML configuration file.
171
268
  When no group is provided, the complete configuration is returned.
@@ -186,7 +283,7 @@ class Settings:
186
283
  Args:
187
284
  group_name (str): the name of one of the main groups from the YAML file
188
285
  filename (str): the name of the YAML file to read
189
- location (str): the path to the location of the YAML file
286
+ location (str, Path): the path to the location of the YAML file
190
287
  force (bool): force reloading the file
191
288
  add_local_settings (bool): update the Settings with site specific local settings
192
289
 
@@ -197,99 +294,58 @@ class Settings:
197
294
  a SettingsError when the group is not defined in the YAML file.
198
295
  """
199
296
 
200
- _THIS_FILE_LOCATION = pathlib.Path(__file__).resolve().parent
201
-
202
- if location is None:
203
-
204
- # Check if the yaml file is located at the location of the caller,
205
- # if not, use the file that is located where the Settings module is located.
206
-
207
- caller_dir = get_caller_info(level=2).filename
208
- caller_dir = pathlib.Path(caller_dir).resolve().parent
209
-
210
- if (caller_dir / filename).is_file():
211
- yaml_location = caller_dir
212
- else:
213
- yaml_location = _THIS_FILE_LOCATION
214
- else:
215
-
216
- # The location was given as an argument
217
-
218
- yaml_location = pathlib.Path(location).resolve()
219
-
220
- _LOGGER.debug(f"yaml_location in Settings.load(location={location}) is {yaml_location}")
297
+ # Load all detected YAML documents, these are considered global settings
221
298
 
222
- # Load the YAML global document
299
+ yaml_locations = get_settings_locations(location, filename)
300
+ global_settings = load_global_settings(yaml_locations, filename, force)
223
301
 
224
- try:
225
- yaml_document_global = cls.read_configuration_file(
226
- str(yaml_location / filename), force=force
227
- )
228
- except FileNotFoundError as exc:
229
- raise SettingsError(
230
- f"Filename {filename} not found at location {yaml_location}."
231
- ) from exc
232
-
233
- # Check if there were any groups defined in the YAML document
234
-
235
- if not yaml_document_global:
236
- raise SettingsError(f"Empty YAML document {filename} at {yaml_location}.")
302
+ if not global_settings:
303
+ raise SettingsError(f"There were no global settings defined for {filename} at {yaml_locations}.")
237
304
 
238
305
  # Load the LOCAL settings YAML file
239
306
 
240
- local_settings = {}
241
-
242
307
  if add_local_settings:
243
- try:
244
- local_settings_location = get_local_settings()
245
- if local_settings_location:
246
- _LOGGER.debug(f"Using {local_settings_location} to update global settings.")
247
- try:
248
- yaml_document_local = cls.read_configuration_file(
249
- local_settings_location, force=force
250
- )
251
- if yaml_document_local is None:
252
- raise FileIsEmptyError()
253
- local_settings = AttributeDict(
254
- {name: value for name, value in yaml_document_local.items()}
255
- )
256
- except FileNotFoundError as exc:
257
- raise SettingsError(
258
- f"Local settings YAML file '{local_settings_location}' not found. "
259
- f"Check your environment variable {get_local_settings_env_name()}."
260
- ) from exc
261
- except FileIsEmptyError:
262
- _LOGGER.warning(f"Local settings YAML file '{local_settings_location}' is empty. "
263
- f"No local settings were loaded.")
264
- except KeyError:
265
- _LOGGER.debug(f"The environment variable {get_local_settings_env_name()} is not defined.")
308
+ local_settings = load_local_settings(force)
309
+ else:
310
+ local_settings = {}
266
311
 
267
312
  if group_name in (None, ""):
268
- global_settings = AttributeDict(
269
- {name: value for name, value in yaml_document_global.items()}
313
+ global_settings = attrdict(
314
+ {name: value for name, value in global_settings.items()},
315
+ label="Settings"
270
316
  )
271
317
  if add_local_settings:
272
318
  recursive_dict_update(global_settings, local_settings)
273
319
  return global_settings
274
320
 
275
- # Check if the requested group is defined in the YAML document
321
+ if group_name in global_settings:
322
+ include_global_settings = True
323
+ else:
324
+ include_global_settings = False
325
+ if group_name in local_settings:
326
+ include_local_settings = True
327
+ else:
328
+ include_local_settings = False
276
329
 
277
- if group_name not in yaml_document_global:
330
+ if not include_global_settings and not include_local_settings:
278
331
  raise SettingsError(
279
- f"Group name '{group_name}' is not defined in the YAML "
280
- f"document '{filename}' at '{yaml_location}."
332
+ f"Group name '{group_name}' is not defined in the global nor in the local settings."
281
333
  )
282
334
 
283
335
  # Check if the group has any settings
284
336
 
285
- if not yaml_document_global[group_name]:
286
- raise SettingsError(f"Empty group in YAML document {filename} at {yaml_location}.")
337
+ if include_global_settings and not global_settings[group_name]:
338
+ _LOGGER.warning(f"Empty group in YAML document {filename} for {group_name}.")
287
339
 
288
- group_settings = AttributeDict(
289
- {name: value for name, value in yaml_document_global[group_name].items()}
290
- )
340
+ if include_global_settings:
341
+ group_settings = attrdict(
342
+ {name: value for name, value in global_settings[group_name].items()},
343
+ label=group_name
344
+ )
345
+ else:
346
+ group_settings = attrdict(label=group_name)
291
347
 
292
- if add_local_settings and group_name in local_settings:
348
+ if add_local_settings and include_local_settings:
293
349
  recursive_dict_update(group_settings, local_settings[group_name])
294
350
 
295
351
  return group_settings
@@ -312,34 +368,15 @@ class Settings:
312
368
  trunc += " ..."
313
369
  msg += f" {field}: {trunc}\n"
314
370
 
315
- return msg
316
-
317
- @classmethod
318
- def set_profiling(cls, flag):
319
- cls.__profile = flag
320
-
321
- @classmethod
322
- def profiling(cls):
323
- return cls.__profile
371
+ return msg.rstrip()
324
372
 
325
- @classmethod
326
- def set_simulation_mode(cls, flag: bool):
327
- cls.__simulation = flag
328
-
329
- @classmethod
330
- def simulation_mode(cls) -> bool:
331
- return cls.__simulation
332
-
333
-
334
- ignore_m_warning('egse.settings')
335
-
336
- if __name__ == "__main__":
337
373
 
374
+ def main(args: list | None = None): # pragma: no cover
338
375
  # We provide convenience to inspect the settings by calling this module directly from Python.
339
376
  #
340
377
  # python -m egse.settings
341
378
  #
342
- # Use the '--help' option to see the what your choices are.
379
+ # Use the '--help' option to see what your choices are.
343
380
 
344
381
  logging.basicConfig(level=20)
345
382
 
@@ -352,7 +389,10 @@ if __name__ == "__main__":
352
389
  ),
353
390
  )
354
391
  parser.add_argument("--local", action="store_true", help="print only the local settings.")
355
- args = parser.parse_args()
392
+ parser.add_argument("--global", action="store_true",
393
+ help="print only the global settings, don't include local settings.")
394
+ parser.add_argument("--group", help="print only settings for this group")
395
+ args = parser.parse_args(args or [])
356
396
 
357
397
  # The following import will activate the pretty printing of the AttributeDict
358
398
  # through the __rich__ method.
@@ -362,13 +402,20 @@ if __name__ == "__main__":
362
402
  if args.local:
363
403
  location = get_local_settings()
364
404
  if location:
405
+ location = str(Path(location).expanduser().resolve())
365
406
  settings = Settings.load(filename=location)
366
407
  print(settings)
367
408
  print(f"Loaded from [purple]{location}.")
368
409
  else:
369
410
  print("[red]No local settings defined.")
370
411
  else:
371
- settings = Settings.load()
412
+ # if the global option is given we don't want to include local settings
413
+ add_local_settings = False if vars(args)["global"] else True
414
+
415
+ if args.group:
416
+ settings = Settings.load(args.group, add_local_settings=add_local_settings)
417
+ else:
418
+ settings = Settings.load(add_local_settings=add_local_settings)
372
419
  print(settings)
373
420
  print("[blue]Memoized locations:")
374
421
  locations = Settings.get_memoized_locations()
@@ -379,3 +426,9 @@ def get_site_id() -> str:
379
426
 
380
427
  site = Settings.load("SITE")
381
428
  return site.ID
429
+
430
+
431
+ # ignore_m_warning('egse.settings')
432
+
433
+ if __name__ == "__main__":
434
+ main()
egse/setup.py CHANGED
@@ -133,6 +133,8 @@ from rich.tree import Tree
133
133
  from egse.env import get_conf_repo_location
134
134
  from egse.env import get_conf_repo_location_env_name
135
135
  from egse.env import get_data_storage_location
136
+ from egse.env import has_conf_repo_location
137
+ from egse.env import print_env
136
138
  from egse.response import Failure
137
139
  from egse.env import get_conf_data_location
138
140
  from egse.system import format_datetime
@@ -227,7 +229,11 @@ def _load_yaml(resource_name: str):
227
229
 
228
230
 
229
231
  def _load_pandas(resource_name: str, separator: str):
230
- """ Find and return the content of the given files as a pandas DataFrame object.
232
+ """
233
+ Find and return the content of the given file as a pandas DataFrame object.
234
+
235
+ The file is loaded relative from the location of the configuration data
236
+ as defined by `get_conf_data_location()`.
231
237
 
232
238
  Args:
233
239
  - resource_name: Filename, preceded by "pandas//".
@@ -237,7 +243,7 @@ def _load_pandas(resource_name: str, separator: str):
237
243
 
238
244
  parts = resource_name[8:].rsplit("/", 1)
239
245
  [in_dir, fn] = parts if len(parts) > 1 else [None, parts[0]]
240
- conf_location = os.environ['PLATO_CONF_DATA_LOCATION']
246
+ conf_location = get_conf_data_location()
241
247
 
242
248
  try:
243
249
  pandas_file_location = Path(conf_location) / in_dir / fn
@@ -336,7 +342,7 @@ class NavigableDict(dict):
336
342
 
337
343
  """
338
344
 
339
- def __init__(self, head: dict = None):
345
+ def __init__(self, head: dict = None, label: str = None):
340
346
  """
341
347
  Args:
342
348
  head (dict): the original dictionary
@@ -344,6 +350,7 @@ class NavigableDict(dict):
344
350
  head = head or {}
345
351
  super().__init__(head)
346
352
  self.__dict__["_memoized"] = {}
353
+ self.__dict__["_label"] = label
347
354
 
348
355
  # By agreement, we only want the keys to be set as attributes if all keys are strings.
349
356
  # That way we enforce that always all keys are navigable, or none.
@@ -572,7 +579,7 @@ class NavigableDict(dict):
572
579
  return msg
573
580
 
574
581
  def __rich__(self) -> Tree:
575
- tree = Tree("NavigableDict", guide_style="dim")
582
+ tree = Tree(self.__dict__["_label"] or "NavigableDict", guide_style="dim")
576
583
  walk_dict_tree(self, tree, text_style="dark grey")
577
584
  return tree
578
585
 
@@ -636,8 +643,8 @@ class Setup(NavigableDict):
636
643
  """The Setup class represents a version of the configuration of the test facility, the
637
644
  test setup and the Camera Under Test (CUT)."""
638
645
 
639
- def __init__(self, nav_dict: NavigableDict | dict = None):
640
- super().__init__(nav_dict or {})
646
+ def __init__(self, nav_dict: NavigableDict | dict = None, label: str = None):
647
+ super().__init__(nav_dict or {}, label=label)
641
648
 
642
649
  @staticmethod
643
650
  def from_dict(my_dict):
@@ -651,7 +658,7 @@ class Setup(NavigableDict):
651
658
  >>> assert setup["ID"] == setup.ID == "my-setup-001"
652
659
 
653
660
  """
654
- return Setup(my_dict)
661
+ return Setup(my_dict, label="Setup")
655
662
 
656
663
  @staticmethod
657
664
  def from_yaml_string(yaml_content: str = None):
@@ -674,15 +681,16 @@ class Setup(NavigableDict):
674
681
  if "Setup" in setup_dict:
675
682
  setup_dict = setup_dict["Setup"]
676
683
 
677
- return Setup(setup_dict)
684
+ return Setup(setup_dict, label="Setup")
678
685
 
679
686
  @staticmethod
680
687
  @lru_cache
681
- def from_yaml_file(filename: Union[str, Path] = None):
688
+ def from_yaml_file(filename: Union[str, Path] = None, add_local_settings: bool = True):
682
689
  """Loads a Setup from the given YAML file.
683
690
 
684
691
  Args:
685
692
  filename (str): the path of the YAML file to be loaded
693
+ add_local_settings (bool): if local settings shall be loaded and override the settings from the YAML file.
686
694
 
687
695
  Returns:
688
696
  a Setup that was loaded from the given location.
@@ -692,9 +700,11 @@ class Setup(NavigableDict):
692
700
  if not filename:
693
701
  raise ValueError("Invalid argument to function: No filename or None given.")
694
702
 
695
- setup_dict = Settings.load("Setup", filename=filename, force=True)
703
+ # MODULE_LOGGER.info(f"Loading {filename}...")
696
704
 
697
- setup = Setup(setup_dict)
705
+ setup_dict = Settings.load("Setup", filename=filename, force=True, add_local_settings=add_local_settings)
706
+
707
+ setup = Setup(setup_dict, label="Setup")
698
708
  setup.set_private_attribute("_filename", filename)
699
709
  if setup_id := _parse_filename_for_setup_id(str(filename)):
700
710
  setup.set_private_attribute("_setup_id", setup_id)
@@ -911,18 +921,15 @@ def get_setup(setup_id: int = None):
911
921
 
912
922
  def _check_conditions_for_get_path_of_setup_file(site_id: str) -> Path:
913
923
  """
914
- Check some pre-conditions that need to be met before we try to determine the file path for
915
- the requested Setup file.
924
+ Check some pre-conditions that need to be met before we try to determine the
925
+ file path for the requested Setup file.
916
926
 
917
927
  The following checks are performed:
918
928
 
919
- * if the environment variable 'PLATO_CONF_REPO_LOCATION' is set
920
-
929
+ * if the environment variable '{PROJECT}_CONF_REPO_LOCATION' is set
921
930
  * if the directory specified in the env variable actually exists
922
-
923
931
  * if the folder with the Setups exists for the given site_id
924
932
 
925
-
926
933
  Args:
927
934
  site_id (str): the name of the test house
928
935
 
@@ -939,9 +946,12 @@ def _check_conditions_for_get_path_of_setup_file(site_id: str) -> Path:
939
946
 
940
947
  if not (repo_location := get_conf_repo_location()):
941
948
  raise LookupError(
942
- f"Environment variable doesn't exist, please define {repo_location_env} and try again."
949
+ f"Environment variable doesn't exist or points to an invalid location, please (re-)define"
950
+ f" {repo_location_env} and try again."
943
951
  )
944
952
 
953
+ print_env()
954
+
945
955
  repo_location = Path(repo_location)
946
956
  setup_location = repo_location / 'data' / site_id / 'conf'
947
957
 
@@ -962,11 +972,13 @@ def _check_conditions_for_get_path_of_setup_file(site_id: str) -> Path:
962
972
 
963
973
  def get_path_of_setup_file(setup_id: int, site_id: str) -> Path:
964
974
  """
965
- Returns the Path to the last Setup file for the given site_id. The last Setup file is the file
966
- with the largest setup_id number.
975
+ Returns the Path to the last Setup file for the given site_id. The last Setup
976
+ file is the file with the largest setup_id number.
967
977
 
968
- This function needs the environment variable <PROJECT>_CONF_REPO_LOCATION to be defined as the
969
- location of the repository with configuration data on your disk.
978
+ This function needs the environment variable <PROJECT>_CONF_REPO_LOCATION to
979
+ be defined as the location of the repository with configuration data on your
980
+ disk. If the repo is not defined, the configuration data location will be used
981
+ instead.
970
982
 
971
983
  Args:
972
984
  setup_id (int): the identifier for the requested Setup
@@ -984,7 +996,10 @@ def get_path_of_setup_file(setup_id: int, site_id: str) -> Path:
984
996
 
985
997
  """
986
998
 
987
- setup_location = _check_conditions_for_get_path_of_setup_file(site_id)
999
+ if not has_conf_repo_location():
1000
+ setup_location = Path(get_conf_data_location(site_id))
1001
+ else:
1002
+ setup_location = _check_conditions_for_get_path_of_setup_file(site_id)
988
1003
 
989
1004
  if setup_id:
990
1005
  files = list(setup_location.glob(f'SETUP_{site_id}_{setup_id:05d}_*.yaml'))
egse/system.py CHANGED
@@ -28,6 +28,7 @@ import socket
28
28
  import subprocess # For executing a shell command
29
29
  import sys
30
30
  import time
31
+ import warnings
31
32
  from collections import namedtuple
32
33
  from contextlib import contextmanager
33
34
  from pathlib import Path
@@ -512,6 +513,9 @@ class AttributeDict(dict):
512
513
  return self.__class__.__name__ + f"({{{sub_msg}{', ...' if len(self) > count else ''}}})"
513
514
 
514
515
 
516
+ attrdict = AttributeDict
517
+
518
+
515
519
  def walk_dict_tree(dictionary: dict, tree: Tree, text_style: str = "green"):
516
520
  for k, v in dictionary.items():
517
521
  if isinstance(v, dict):
@@ -931,22 +935,23 @@ def env_var(**kwargs):
931
935
 
932
936
  """
933
937
  saved_env = {}
934
- try:
935
- for k, v in kwargs.items():
936
- saved_env[k] = os.environ.get(k)
937
- if v is None:
938
- if k in os.environ:
939
- del os.environ[k]
940
- else:
941
- os.environ[k] = v
942
- yield
943
- finally:
944
- for k, v in saved_env.items():
945
- if v is None:
946
- if k in os.environ:
947
- del os.environ[k]
948
- else:
949
- os.environ[k] = v
938
+
939
+ for k, v in kwargs.items():
940
+ saved_env[k] = os.environ.get(k)
941
+ if v is None:
942
+ if k in os.environ:
943
+ del os.environ[k]
944
+ else:
945
+ os.environ[k] = v
946
+
947
+ yield
948
+
949
+ for k, v in saved_env.items():
950
+ if v is None:
951
+ if k in os.environ:
952
+ del os.environ[k]
953
+ else:
954
+ os.environ[k] = v
950
955
 
951
956
 
952
957
  def filter_by_attr(elements: Iterable, **attrs) -> List:
@@ -1120,7 +1125,7 @@ def is_namespace(module) -> bool:
1120
1125
  return False
1121
1126
 
1122
1127
 
1123
- def get_package_location(module) -> List[Path]:
1128
+ def get_package_location(module: str) -> List[Path]:
1124
1129
  """
1125
1130
  Retrieves the file system locations associated with a Python package.
1126
1131
 
@@ -1153,6 +1158,7 @@ def get_package_location(module) -> List[Path]:
1153
1158
  try:
1154
1159
  module = importlib.import_module(module)
1155
1160
  except TypeError:
1161
+ warnings.warn(f"The module is not found or is not valid: {module_name}.")
1156
1162
  return []
1157
1163
 
1158
1164
  if is_namespace(module):