cgse-common 0.16.14__tar.gz → 0.17.1__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.
Files changed (49) hide show
  1. {cgse_common-0.16.14 → cgse_common-0.17.1}/PKG-INFO +1 -1
  2. {cgse_common-0.16.14 → cgse_common-0.17.1}/pyproject.toml +5 -3
  3. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/cgse_common/cgse.py +5 -1
  4. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/config.py +3 -2
  5. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/decorators.py +2 -1
  6. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/device.py +8 -2
  7. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/env.py +117 -49
  8. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/heartbeat.py +1 -1
  9. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/log.py +9 -2
  10. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/plugins/metrics/influxdb.py +34 -2
  11. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/scpi.py +63 -47
  12. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/settings.py +11 -7
  13. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/setup.py +12 -6
  14. cgse_common-0.17.1/src/egse/socketdevice.py +379 -0
  15. cgse_common-0.16.14/src/egse/socketdevice.py +0 -205
  16. {cgse_common-0.16.14 → cgse_common-0.17.1}/.gitignore +0 -0
  17. {cgse_common-0.16.14 → cgse_common-0.17.1}/README.md +0 -0
  18. {cgse_common-0.16.14 → cgse_common-0.17.1}/justfile +0 -0
  19. {cgse_common-0.16.14 → cgse_common-0.17.1}/noxfile.py +0 -0
  20. {cgse_common-0.16.14 → cgse_common-0.17.1}/service_registry.db +0 -0
  21. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/cgse_common/__init__.py +0 -0
  22. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/cgse_common/settings.yaml +0 -0
  23. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/bits.py +0 -0
  24. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/calibration.py +0 -0
  25. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/counter.py +0 -0
  26. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/dicts.py +0 -0
  27. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/exceptions.py +0 -0
  28. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/hk.py +0 -0
  29. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/metrics.py +0 -0
  30. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/observer.py +0 -0
  31. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/obsid.py +0 -0
  32. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/persistence.py +0 -0
  33. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/plugin.py +0 -0
  34. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/plugins/metrics/duckdb.py +0 -0
  35. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/plugins/metrics/timescaledb.py +0 -0
  36. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/process.py +0 -0
  37. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/py.typed +0 -0
  38. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/randomwalk.py +0 -0
  39. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/ratelimit.py +0 -0
  40. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/reload.py +0 -0
  41. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/resource.py +0 -0
  42. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/response.py +0 -0
  43. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/settings.yaml +0 -0
  44. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/signal.py +0 -0
  45. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/state.py +0 -0
  46. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/system.py +0 -0
  47. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/task.py +0 -0
  48. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/version.py +0 -0
  49. {cgse_common-0.16.14 → cgse_common-0.17.1}/src/egse/zmq_ser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgse-common
3
- Version: 0.16.14
3
+ Version: 0.17.1
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "cgse-common"
3
- version = "0.16.14"
3
+ version = "0.17.1"
4
4
  description = "Software framework to support hardware testing"
5
5
  authors = [
6
6
  {name = "IvS KU Leuven"}
@@ -48,7 +48,6 @@ dependencies = [
48
48
 
49
49
  [project.scripts]
50
50
  cgse = 'cgse_common.cgse:app'
51
- monitor = "egse.monitoring:app"
52
51
 
53
52
  [project.entry-points."cgse.version"]
54
53
  cgse-common = 'egse.version:get_version_installed'
@@ -61,10 +60,13 @@ pythonpath = "src"
61
60
  testpaths = ["tests"]
62
61
  addopts = "-ra --cov --cov-branch --cov-report html"
63
62
  log_cli = true
64
- log_cli_level = "INFO"
63
+ log_cli_level = "DEBUG"
65
64
  filterwarnings = [
66
65
  "ignore::DeprecationWarning"
67
66
  ]
67
+ markers = [
68
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')"
69
+ ]
68
70
 
69
71
  [tool.coverage.run]
70
72
  omit = [
@@ -72,7 +72,7 @@ class SortedCommandGroup(TyperGroup):
72
72
  commands = super().list_commands(ctx)
73
73
 
74
74
  # Define priority commands in specific order
75
- priority_commands = ["init", "version", "show", "top", "core", "reg", "not", "log", "cm", "sm", "pm"]
75
+ priority_commands = ["init", "version", "show", "top", "core", "reg", "log", "nh", "cm", "sm", "pm"]
76
76
 
77
77
  # Custom sort:
78
78
  # First the priority commands in the given order (their index)
@@ -175,6 +175,10 @@ def main(ctx: typer.Context, verbose: bool = False):
175
175
  - inspect, configure, monitor the core services and device control servers.
176
176
  """
177
177
 
178
+ from egse.env import setup_env
179
+
180
+ setup_env()
181
+
178
182
  # This is more of a show-case for using application wide optional arguments and how to pass
179
183
  # them into (sub-)commands.
180
184
 
@@ -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
- exclude_dirs = ("venv", "venv38", ".venv", ".nox", ".git", ".idea", ".DS_Store")
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,7 +15,6 @@ from typing import Optional
15
15
 
16
16
  import rich
17
17
 
18
- from egse.settings import Settings
19
18
  from egse.system import get_caller_info
20
19
  from egse.log import logger
21
20
 
@@ -390,6 +389,8 @@ def profile(func):
390
389
  if not hasattr(profile, "counter"):
391
390
  profile.counter = 0
392
391
 
392
+ from egse.settings import Settings
393
+
393
394
  @functools.wraps(func)
394
395
  def wrapper_profile(*args, **kwargs):
395
396
  if Settings.profiling():
@@ -199,7 +199,7 @@ class DeviceConnectionInterface(DeviceConnectionObservable):
199
199
 
200
200
 
201
201
  class DeviceInterface(DeviceConnectionInterface):
202
- """Generic interface for all device classes."""
202
+ """A generic interface for all device classes."""
203
203
 
204
204
  @dynamic_interface
205
205
  def is_simulator(self) -> bool:
@@ -237,6 +237,7 @@ class DeviceTransport:
237
237
  raise NotImplementedError
238
238
 
239
239
  def read_string(self, encoding="utf-8") -> str:
240
+ """Reads a bytes object from the instrument and returns it converted into a stripped UTF-8 string."""
240
241
  return self.read().decode(encoding).strip()
241
242
 
242
243
  def trans(self, command: str) -> bytes:
@@ -297,6 +298,11 @@ class AsyncDeviceTransport:
297
298
 
298
299
  raise NotImplementedError
299
300
 
301
+ async def read_string(self, encoding="utf-8") -> str:
302
+ """Reads a bytes object from the instrument and returns it converted into a stripped UTF-8 string."""
303
+ b = await self.read()
304
+ return b.decode(encoding).strip()
305
+
300
306
  async def trans(self, command: str) -> bytes:
301
307
  """
302
308
  Send a single command to the device controller and block until a response from the
@@ -389,7 +395,7 @@ class AsyncDeviceConnectionInterface(DeviceConnectionObservable):
389
395
 
390
396
 
391
397
  class AsyncDeviceInterface(AsyncDeviceConnectionInterface):
392
- """Generic interface for all device classes."""
398
+ """A generic interface for all device classes."""
393
399
 
394
400
  def is_simulator(self) -> bool:
395
401
  """Checks whether the device is a simulator rather than a real hardware controller.
@@ -43,6 +43,10 @@ from __future__ import annotations
43
43
 
44
44
  __all__ = [
45
45
  "bool_env",
46
+ "int_env",
47
+ "str_env",
48
+ "load_dotenv",
49
+ "setup_env",
46
50
  "env_var",
47
51
  "get_conf_data_location",
48
52
  "get_conf_data_location_env_name",
@@ -68,14 +72,16 @@ import os
68
72
  import warnings
69
73
  from pathlib import Path
70
74
 
75
+ from dotenv import load_dotenv as _load_dotenv
76
+ from dotenv import find_dotenv
77
+ from rich.console import Console
78
+
79
+ from egse.decorators import static_vars
71
80
  from egse.log import logger
72
81
  from egse.system import all_logging_disabled
73
82
  from egse.system import get_caller_info
74
83
  from egse.system import ignore_m_warning
75
-
76
- from rich.console import Console
77
-
78
- console = Console(width=100)
84
+ from egse.system import type_name
79
85
 
80
86
  # Every project shall have a PROJECT and a SITE_ID environment variable set. This variable will be used to
81
87
  # create the other environment variables that are specific to the project.
@@ -99,7 +105,57 @@ KNOWN_PROJECT_ENVIRONMENT_VARIABLES = [
99
105
  ]
100
106
 
101
107
 
102
- def initialize():
108
+ def int_env(var_name: str, default: int, *, minimum: int | None = 1) -> int:
109
+ """Return an integer environment override, falling back to `default` on errors."""
110
+ raw = os.getenv(var_name)
111
+ if raw is None:
112
+ return default
113
+ try:
114
+ value = int(raw)
115
+ except ValueError:
116
+ logger.warning(f"Ignoring invalid integer for {var_name}: {raw!r}, returning {default=}")
117
+ return default
118
+ if minimum is not None and value < minimum:
119
+ logger.warning(f"Ignoring {var_name} because value {value} < {minimum}, returning {default=}")
120
+ return default
121
+ return value
122
+
123
+
124
+ def bool_env(var_name: str, default: bool = False) -> bool:
125
+ """Return True if the environment variable is set to 1, true, yes, or on. All case-insensitive."""
126
+
127
+ if value := os.getenv(var_name):
128
+ return value.strip().lower() in {"1", "true", "yes", "on"}
129
+
130
+ return default
131
+
132
+
133
+ def str_env(var_name: str, default: str | None = None) -> str:
134
+ """Return the value of the environment variable or default if variable is not defined."""
135
+ return os.getenv(var_name, default)
136
+
137
+
138
+ VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
139
+
140
+
141
+ def load_dotenv():
142
+ """Overwrites the load_dotenv function for CGSE specifics.
143
+
144
+ - set `CGSE_DOTENV_DISABLED=true` to disable loading the `.env` file
145
+ - the `.env` file is searched for relative to the current working directory,
146
+ unlike the default, which searches from the script location.
147
+
148
+ NOTE:
149
+ - don't use the `load_dotenv` function from the `dotenv` package
150
+ - don't use `load_dotenv` in any modules, only in entrypoints and apps.
151
+ """
152
+ if not bool_env("CGSE_DOTENV_DISABLED") and (dotenv_location := find_dotenv(usecwd=True)):
153
+ logger.debug(f"Loading environment variables from {dotenv_location}.")
154
+ _load_dotenv(dotenv_path=dotenv_location)
155
+
156
+
157
+ @static_vars(is_initialized=False)
158
+ def setup_env():
103
159
  """
104
160
  Initialize the environment variables that are required for the CGSE to function properly.
105
161
  This function will print a warning if any of the mandatory environment variables is not set.
@@ -110,6 +166,14 @@ def initialize():
110
166
 
111
167
  global _env
112
168
 
169
+ if setup_env.is_initialized:
170
+ return
171
+
172
+ if VERBOSE_DEBUG:
173
+ logger.debug(f"Initialising the environment...")
174
+
175
+ load_dotenv()
176
+
113
177
  for name in MANDATORY_ENVIRONMENT_VARIABLES:
114
178
  try:
115
179
  _env.set(name, os.environ[name])
@@ -123,8 +187,10 @@ def initialize():
123
187
  project = _env.get("PROJECT")
124
188
 
125
189
  for gen_var_name in KNOWN_PROJECT_ENVIRONMENT_VARIABLES:
126
- env_var = f"{project}_{gen_var_name}"
127
- _env.set(gen_var_name, os.environ.get(env_var, NoValue()))
190
+ _env_var = f"{project}_{gen_var_name}"
191
+ _env.set(gen_var_name, os.environ.get(_env_var, NoValue()))
192
+
193
+ setup_env.is_initialized = True
128
194
 
129
195
 
130
196
  class _Env:
@@ -136,8 +202,12 @@ class _Env:
136
202
  def set(self, key, value):
137
203
  if value is None:
138
204
  if key in self._env:
205
+ if VERBOSE_DEBUG:
206
+ logger.debug(f"Unsetting environment variable {key}")
139
207
  del self._env[key]
140
208
  else:
209
+ if VERBOSE_DEBUG:
210
+ logger.debug(f"Setting environment variable {key}={value}")
141
211
  self._env[key] = value
142
212
 
143
213
  def get(self, key) -> str:
@@ -166,11 +236,11 @@ class NoValue:
166
236
  return False
167
237
 
168
238
  def __repr__(self):
169
- return f"{self.__class__.__name__}"
239
+ return f"{type_name(self)}"
170
240
 
171
241
 
172
242
  # The module needs to be initialized before it can be used.
173
- initialize()
243
+ setup_env()
174
244
 
175
245
 
176
246
  def _check_no_value(var_name, value):
@@ -244,7 +314,7 @@ def set_data_storage_location(location: str | Path | None):
244
314
  _env.set("DATA_STORAGE_LOCATION", None)
245
315
  return
246
316
 
247
- if not Path(location).exists():
317
+ if not Path(location).expanduser().exists():
248
318
  warnings.warn(f"The location you provided for the environment variable {env_name} doesn't exist: {location}.")
249
319
 
250
320
  os.environ[env_name] = str(location)
@@ -312,7 +382,7 @@ def set_conf_data_location(location: str | Path | None):
312
382
  _env.set("CONF_DATA_LOCATION", None)
313
383
  return
314
384
 
315
- if not Path(location).exists():
385
+ if not Path(location).expanduser().exists():
316
386
  warnings.warn(f"The location you provided for the environment variable {env_name} doesn't exist: {location}.")
317
387
 
318
388
  os.environ[env_name] = location
@@ -378,7 +448,7 @@ def set_log_file_location(location: str | Path | None):
378
448
  _env.set("LOG_FILE_LOCATION", None)
379
449
  return
380
450
 
381
- if not Path(location).exists():
451
+ if not Path(location).expanduser().exists():
382
452
  warnings.warn(f"The location you provided for the environment variable {env_name} doesn't exist: {location}.")
383
453
 
384
454
  os.environ[env_name] = location
@@ -423,7 +493,7 @@ def get_log_file_location(site_id: str = None, check_exists: bool = False) -> st
423
493
  log_data_root = f"{data_root}/log"
424
494
 
425
495
  if check_exists:
426
- if not Path(log_data_root).is_dir():
496
+ if not Path(log_data_root).expanduser().is_dir():
427
497
  raise ValueError(f"The location that was constructed doesn't exist: {log_data_root}")
428
498
 
429
499
  return log_data_root
@@ -453,8 +523,11 @@ def set_local_settings(path: str | Path | None):
453
523
  _env.set("LOCAL_SETTINGS", None)
454
524
  return
455
525
 
456
- if not Path(path).exists():
457
- warnings.warn(f"The location you provided for the environment variable {env_name} doesn't exist: {path}.")
526
+ if not Path(path).expanduser().is_file():
527
+ warnings.warn(
528
+ f"The location you provided for the environment variable {env_name} doesn't exist or is not a "
529
+ f"regular file: {path}."
530
+ )
458
531
 
459
532
  os.environ[env_name] = path
460
533
  _env.set("LOCAL_SETTINGS", path)
@@ -481,9 +554,9 @@ def get_local_settings_path() -> str | None:
481
554
  )
482
555
  return None
483
556
 
484
- if not Path(local_settings).exists():
557
+ if not Path(local_settings).expanduser().is_file():
485
558
  warnings.warn(
486
- f"The local settings path '{local_settings}' doesn't exist. As a result, "
559
+ f"The local settings path '{local_settings}' doesn't exist or is not a regular file. As a result, "
487
560
  f"the local settings for your project will not be loaded."
488
561
  )
489
562
 
@@ -519,7 +592,7 @@ def get_conf_repo_location() -> str | None:
519
592
  )
520
593
  return None
521
594
 
522
- if not Path(location).exists():
595
+ if not Path(location).expanduser().exists():
523
596
  warnings.warn(f"The location of the configuration data repository doesn't exist: {location}.")
524
597
  return None
525
598
 
@@ -545,7 +618,7 @@ def set_conf_repo_location(location: str | Path | None):
545
618
  _env.set("CONF_REPO_LOCATION", None)
546
619
  return
547
620
 
548
- if not Path(location).exists():
621
+ if not Path(location).expanduser().exists():
549
622
  warnings.warn(f"The location you provided for the environment variable {env_name} doesn't exist: {location}.")
550
623
 
551
624
  os.environ[env_name] = location
@@ -574,20 +647,11 @@ def print_env():
574
647
  console.print(f" {get_local_settings_env_name():{col_width}s}: {get_local_settings_path()}")
575
648
 
576
649
 
577
- def bool_env(var_name: str) -> bool:
578
- """Return True if the environment variable is set to 1, true, yes, or on. All case-insensitive."""
579
-
580
- if value := os.getenv(var_name):
581
- return value.strip().lower() in {"1", "true", "yes", "on"}
582
-
583
- return False
584
-
585
-
586
650
  @contextlib.contextmanager
587
651
  def env_var(**kwargs: str | int | float | bool | None):
588
652
  """
589
- Context manager to run some code that need alternate settings for environment variables.
590
- This will automatically initialize the CGSE environment upon entry and re-initialize upon exit.
653
+ Context manager to run some code that need alternate settings for environment variables. This will automatically
654
+ initialize the CGSE environment upon entry and re-initialize when the context manager exits.
591
655
 
592
656
  Note:
593
657
  This context manager is different from the one in `egse.system` because of the CGSE environment changes.
@@ -614,7 +678,8 @@ def env_var(**kwargs: str | int | float | bool | None):
614
678
  else:
615
679
  os.environ[k] = v
616
680
 
617
- initialize()
681
+ setup_env.is_initialized = False
682
+ setup_env()
618
683
 
619
684
  yield
620
685
 
@@ -625,7 +690,8 @@ def env_var(**kwargs: str | int | float | bool | None):
625
690
  else:
626
691
  os.environ[k] = v
627
692
 
628
- initialize()
693
+ setup_env.is_initialized = False
694
+ setup_env()
629
695
 
630
696
 
631
697
  def main(args: list | None = None): # pragma: no cover
@@ -650,32 +716,34 @@ def main(args: list | None = None): # pragma: no cover
650
716
  "--mkdir",
651
717
  default=False,
652
718
  action="store_true",
653
- help="Create directory that doesn't exist.",
719
+ help="Create any directory that doesn't exist.",
654
720
  )
655
721
 
656
722
  args = parser.parse_args(args or [])
657
723
 
658
- def check_env_dir(env_var: str):
659
- value = _env.get(env_var)
724
+ setup_env()
725
+
726
+ def check_env_dir(_env_var: str):
727
+ value = _env.get(_env_var)
660
728
 
661
729
  if value == NoValue():
662
730
  value = "[bold red]not set"
663
- elif not value.startswith("/"):
731
+ elif not Path(value).expanduser().is_absolute():
664
732
  value = f"[default]{value} [bold orange3](this is a relative path!)"
665
- elif not os.path.exists(value):
733
+ elif not Path(value).expanduser().exists():
666
734
  value = f"[default]{value} [bold red](location doesn't exist!)"
667
- elif not os.path.isdir(value):
735
+ elif not Path(value).expanduser().is_dir():
668
736
  value = f"[default]{value} [bold red](location is not a directory!)"
669
737
  else:
670
738
  value = f"[default]{value}"
671
739
  return value
672
740
 
673
- def check_env_file(env_var: str):
674
- value = _env.get(env_var)
741
+ def check_env_file(_env_var: str):
742
+ value = _env.get(_env_var)
675
743
 
676
744
  if not value:
677
745
  value = "[bold red]not set"
678
- elif not os.path.exists(value):
746
+ elif not Path(value).expanduser().exists():
679
747
  value = f"[default]{value} [bold red](location doesn't exist!)"
680
748
  else:
681
749
  value = f"[default]{value}"
@@ -701,7 +769,7 @@ def main(args: list | None = None): # pragma: no cover
701
769
  try:
702
770
  rich.print(f" {get_data_storage_location() = }", flush=True, end="")
703
771
  location = get_data_storage_location()
704
- if not Path(location).exists():
772
+ if not Path(location).expanduser().exists():
705
773
  if args.mkdir:
706
774
  rich.print(f" [green]⟶ Creating data storage location: {location} (+ daily + obs)[/]")
707
775
  Path(location).mkdir(parents=True)
@@ -717,7 +785,7 @@ def main(args: list | None = None): # pragma: no cover
717
785
  try:
718
786
  rich.print(f" {get_conf_data_location() = }", flush=True, end="")
719
787
  location = get_conf_data_location()
720
- if not Path(location).exists():
788
+ if not Path(location).expanduser().exists():
721
789
  if args.mkdir:
722
790
  rich.print(f" [green]⟶ Creating configuration data location: {location}[/]")
723
791
  Path(location).mkdir(parents=True)
@@ -731,7 +799,7 @@ def main(args: list | None = None): # pragma: no cover
731
799
  try:
732
800
  rich.print(f" {get_conf_repo_location() = }", flush=True, end="")
733
801
  location = get_conf_repo_location()
734
- if location is None or not Path(location).exists():
802
+ if location is None or not Path(location).expanduser().exists():
735
803
  rich.print(" [red]⟶ ERROR: The configuration repository location doesn't exist![/]")
736
804
  else:
737
805
  rich.print()
@@ -741,7 +809,7 @@ def main(args: list | None = None): # pragma: no cover
741
809
  try:
742
810
  rich.print(f" {get_log_file_location() = }", flush=True, end="")
743
811
  location = get_log_file_location()
744
- if not Path(location).exists():
812
+ if not Path(location).expanduser().exists():
745
813
  if args.mkdir:
746
814
  rich.print(f" [green]⟶ Creating log files location: {location}[/]")
747
815
  Path(location).mkdir(parents=True)
@@ -755,7 +823,7 @@ def main(args: list | None = None): # pragma: no cover
755
823
  try:
756
824
  rich.print(f" {get_local_settings_path() = }", flush=True, end="")
757
825
  location = get_local_settings_path()
758
- if location is None or not Path(location).exists():
826
+ if location is None or not Path(location).expanduser().exists():
759
827
  rich.print(" [red]⟶ ERROR: The local settings file is not defined or doesn't exist![/]")
760
828
  else:
761
829
  rich.print()
@@ -782,8 +850,8 @@ def main(args: list | None = None): # pragma: no cover
782
850
  of the name. By default, this directory is located in the overall data storage folder.
783
851
 
784
852
  [bold]PROJECT_CONF_REPO_LOCATION[/bold]:
785
- This variable is the root of the working copy of the 'plato-cgse-conf' project.
786
- The value is usually set to `~/git/plato-cgse-conf`.
853
+ This variable is the root of the working copy of the repo for configuration files.
854
+ The value is usually set to `~/git/{project}-conf`.
787
855
 
788
856
  [bold]PROJECT_DATA_STORAGE_LOCATION[/bold]:
789
857
  This directory contains all the data files from the control servers and other
@@ -805,7 +873,7 @@ def main(args: list | None = None): # pragma: no cover
805
873
  [bold]PROJECT_LOCAL_SETTINGS[/bold]:
806
874
  This file is used for local site-specific settings. When the environment
807
875
  variable is not set, no local settings will be loaded. By default, this variable
808
- is assumed to be '/cgse/local_settings.yaml'.
876
+ assumes to be '/cgse/local_settings.yaml'.
809
877
  """
810
878
 
811
879
  if args.doc:
@@ -80,7 +80,7 @@ if __name__ == "__main__":
80
80
  broadcaster.start()
81
81
 
82
82
  # You will see that the timestamp of this custom message is drifting because
83
- # we do not take the execution time of the `set_message()` etc. intyo accout.
83
+ # we do not take the execution time of the `set_message()` etc. into account.
84
84
  # In the actual HeartbeatBroadcaster above, we do take that into account.
85
85
 
86
86
  try:
@@ -135,20 +135,27 @@ for handler in root_logger.handlers:
135
135
  logger = egse_logger
136
136
 
137
137
  if __name__ == "__main__":
138
+ from egse.env import str_env
139
+
138
140
  root_logger = logging.getLogger()
139
141
 
140
- rich.print(
142
+ print(
141
143
  textwrap.dedent(
142
144
  """
145
+ Environment variables:
146
+ - LOG_LEVEL=debug|info|warning|critical
147
+ - LOG_FORMAT=full|clean
148
+
143
149
  Example logging statements
144
150
  - logging level set to INFO
151
+ - logging format set to full
145
152
  - fields are separated by a colon ':'
146
153
  - fields: date & time: process name : level : logger name : lineno : filename : message
147
154
  """
148
155
  )
149
156
  )
150
157
 
151
- if os.getenv("LOG_FORMAT_FULL") == "true":
158
+ if str_env("LOG_FORMAT", "clean").strip().lower() == "full":
152
159
  rich.print(
153
160
  f"[b]{'Date & Time':^23s} : {'Process Name':20s} : {'Level':8s} : {'Logger Name':^25s} : {' Line '} : "
154
161
  f"{'Filename':20s} : {'Message'}[/]"
@@ -37,11 +37,14 @@ import pandas
37
37
  import pyarrow
38
38
  from influxdb_client_3 import InfluxDBClient3
39
39
  from influxdb_client_3 import Point
40
- from influxdb_client_3 import SYNCHRONOUS
40
+ from influxdb_client_3 import WriteType
41
41
  from influxdb_client_3 import write_client_options
42
42
  from influxdb_client_3.exceptions import InfluxDB3ClientError
43
43
  from influxdb_client_3.write_client.domain.write_precision import WritePrecision
44
44
 
45
+ from egse.env import bool_env
46
+ from egse.env import int_env
47
+ from egse.env import str_env
45
48
  from egse.metrics import TimeSeriesRepository
46
49
  from egse.system import type_name
47
50
 
@@ -63,9 +66,38 @@ class InfluxDBRepository(TimeSeriesRepository):
63
66
  def __exit__(self, exc_type, exc_val, exc_tb):
64
67
  self.close()
65
68
 
69
+ def _load_client_options(self):
70
+ self._batch_size = int_env("CGSE_INFLUX_BATCH_SIZE", 1_000)
71
+ self._flush_interval = int_env("CGSE_INFLUX_FLUSH_MS", 1_000)
72
+ self._retry_interval = int_env("CGSE_INFLUX_RETRY_MS", 3_000)
73
+ self._max_retry_delay = int_env("CGSE_INFLUX_RETRY_MAX_DELAY_MS", 3_000)
74
+ self._max_retry_time = int_env("CGSE_INFLUX_RETRY_MAX_TIME_MS", 6_000, minimum=0)
75
+ self._max_retries = int_env("CGSE_INFLUX_MAX_RETRIES", 5, minimum=0)
76
+ self._no_sync = bool_env("CGSE_INFLUX_NO_SYNC", default=True)
77
+ self._write_type_env = str_env("CGSE_INFLUX_WRITE_TYPE")
78
+ self._write_type = WriteType.asynchronous
79
+ if self._write_type_env:
80
+ match self._write_type_env.strip().lower():
81
+ case "batch":
82
+ self._write_type = WriteType.batching
83
+ case "async":
84
+ self._write_type = WriteType.asynchronous
85
+ case "sync":
86
+ self._write_type = WriteType.synchronous
87
+
66
88
  def connect(self):
89
+ self._load_client_options()
90
+
67
91
  wco = write_client_options(
68
- write_options=SYNCHRONOUS, write_precision=self.metrics_time_precision, flush_interval=200
92
+ write_type=self._write_type,
93
+ batch_size=self._batch_size,
94
+ flush_interval=self._flush_interval,
95
+ retry_interval=self._retry_interval,
96
+ max_retries=self._max_retries,
97
+ max_retry_delay=self._max_retry_delay,
98
+ max_retry_time=self._max_retry_time,
99
+ no_sync=self._no_sync,
100
+ write_precision=self.metrics_time_precision,
69
101
  )
70
102
  self.client = InfluxDBClient3(host=self.host, database=self.database, token=self.token, write_options=wco)
71
103