cgse-common 2025.0.5__tar.gz → 2025.0.6__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 (38) hide show
  1. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/PKG-INFO +1 -1
  2. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/pyproject.toml +6 -3
  3. cgse_common-2025.0.6/src/cgse_common/__init__.py +0 -0
  4. {cgse_common-2025.0.5/src/egse → cgse_common-2025.0.6/src/cgse_common}/settings.yaml +4 -1
  5. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/decorators.py +13 -4
  6. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/env.py +21 -9
  7. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/plugin.py +68 -6
  8. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/settings.py +168 -154
  9. cgse_common-2025.0.6/src/egse/settings.yaml +5 -0
  10. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/setup.py +13 -5
  11. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/.gitignore +0 -0
  12. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/README.md +0 -0
  13. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/bits.py +0 -0
  14. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/calibration.py +0 -0
  15. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/command.py +0 -0
  16. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/config.py +0 -0
  17. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/control.py +0 -0
  18. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/device.py +0 -0
  19. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/exceptions.py +0 -0
  20. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/hk.py +0 -0
  21. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/metrics.py +0 -0
  22. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/mixin.py +0 -0
  23. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/monitoring.py +0 -0
  24. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/observer.py +0 -0
  25. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/obsid.py +0 -0
  26. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/persistence.py +0 -0
  27. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/process.py +0 -0
  28. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/protocol.py +0 -0
  29. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/proxy.py +0 -0
  30. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/reload.py +0 -0
  31. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/resource.py +0 -0
  32. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/response.py +0 -0
  33. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/services.py +0 -0
  34. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/services.yaml +0 -0
  35. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/state.py +0 -0
  36. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/system.py +0 -0
  37. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/version.py +0 -0
  38. {cgse_common-2025.0.5 → cgse_common-2025.0.6}/src/egse/zmq_ser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgse-common
3
- Version: 2025.0.5
3
+ Version: 2025.0.6
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 = "2025.0.5"
3
+ version = "2025.0.6"
4
4
  description = "Software framework to support hardware testing"
5
5
  authors = [
6
6
  {name = "IVS KU Leuven"}
@@ -36,10 +36,13 @@ dependencies = [
36
36
  [project.entry-points."cgse.version"]
37
37
  cgse-common = 'egse'
38
38
 
39
+ [project.entry-points."cgse.settings"]
40
+ cgse-common = "cgse_common:settings.yaml"
41
+
39
42
  [tool.pytest.ini_options]
40
43
  pythonpath = "src"
41
44
  testpaths = ["tests"]
42
- addopts = "-ra --cov --cov-report html"
45
+ addopts = "-ra --cov --cov-branch --cov-report html"
43
46
  filterwarnings = [
44
47
  "ignore::DeprecationWarning"
45
48
  ]
@@ -60,7 +63,7 @@ exclude = [
60
63
  ]
61
64
 
62
65
  [tool.hatch.build.targets.wheel]
63
- packages = ["src/egse"]
66
+ packages = ["src/egse", "src/cgse-common"]
64
67
 
65
68
  [tool.ruff]
66
69
  line-length = 120
File without changes
@@ -1,5 +1,8 @@
1
+ PACKAGES:
2
+ CGSE_COMMON: Common classes, functions, decorators, etc. for the CGSE
3
+
1
4
  SITE:
2
- ID: XXX
5
+ ID: XXXX # The SITE ID shall be filled in by the local settings file
3
6
  SSH_SERVER: localhost # The IP address of the SSH server on your site
4
7
  SSH_PORT: 22 # The TCP/IP port on which the SSH server is listening
5
8
 
@@ -10,6 +10,8 @@ import warnings
10
10
  from typing import Callable
11
11
  from typing import Optional
12
12
 
13
+ import rich
14
+
13
15
  from egse.settings import Settings
14
16
  from egse.system import get_caller_info
15
17
 
@@ -227,7 +229,14 @@ def profile_func(output_file=None, sort_by='cumulative', lines_to_print=None, st
227
229
 
228
230
 
229
231
  def profile(func):
230
- """Print the function signature and return value"""
232
+ """
233
+ Prints the function signature and return value to stdout.
234
+
235
+ This function checks the `Settings.profiling()` value and only prints out
236
+ profiling information if this returns True.
237
+
238
+ Profiling can be activated with `Settings.set_profiling(True)`.
239
+ """
231
240
  if not hasattr(profile, "counter"):
232
241
  profile.counter = 0
233
242
 
@@ -240,10 +249,10 @@ def profile(func):
240
249
  signature = ", ".join(args_repr + kwargs_repr)
241
250
  caller = get_caller_info(level=2)
242
251
  prefix = f"PROFILE[{profile.counter}]: "
243
- _LOGGER.info(f"{prefix}Calling {func.__name__}({signature})")
244
- _LOGGER.info(f"{prefix} from {caller.filename} at {caller.lineno}.")
252
+ rich.print(f"{prefix}Calling {func.__name__}({signature})")
253
+ rich.print(f"{prefix} from {caller.filename} at {caller.lineno}.")
245
254
  value = func(*args, **kwargs)
246
- _LOGGER.info(f"{prefix}{func.__name__!r} returned {value!r}")
255
+ rich.print(f"{prefix}{func.__name__!r} returned {value!r}")
247
256
  profile.counter -= 1
248
257
  else:
249
258
  value = func(*args, **kwargs)
@@ -47,7 +47,7 @@ __all__ = [
47
47
  "get_conf_repo_location_env_name",
48
48
  "get_data_storage_location",
49
49
  "get_data_storage_location_env_name",
50
- "get_local_settings",
50
+ "get_local_settings_path",
51
51
  "get_local_settings_env_name",
52
52
  "get_log_file_location",
53
53
  "get_log_file_location_env_name",
@@ -455,18 +455,30 @@ def set_local_settings(path: str | Path | None):
455
455
  _env.set('LOCAL_SETTINGS', path)
456
456
 
457
457
 
458
- def get_local_settings() -> str:
459
- """Returns the fully qualified filename of the local settings YAML file."""
458
+ def get_local_settings_path() -> str or None:
459
+ """
460
+ Returns the fully qualified filename of the local settings YAML file. When the local settings environment
461
+ variable is not defined or is an empty string, None is returned.
462
+
463
+ Warnings:
464
+ - When the local settings environment variable is not defined, or
465
+ - when the path defined by the environment variable doesn't exist.
466
+ """
460
467
 
461
468
  local_settings = _env.get("LOCAL_SETTINGS")
462
469
 
463
- if local_settings and not Path(local_settings).exists():
470
+ if not local_settings:
471
+ warnings.warn(f"The local settings environment variable '{get_local_settings_env_name()}' "
472
+ f"is not defined or is an empty string.")
473
+ return None
474
+
475
+ if not Path(local_settings).exists():
464
476
  warnings.warn(
465
- f"The local settings '{local_settings}' doesn't exist. As a result, "
477
+ f"The local settings path '{local_settings}' doesn't exist. As a result, "
466
478
  f"the local settings for your project will not be loaded."
467
479
  )
468
480
 
469
- return local_settings or None
481
+ return local_settings
470
482
 
471
483
 
472
484
  def has_conf_repo_location() -> bool:
@@ -552,7 +564,7 @@ def print_env():
552
564
  console.print(f" {get_log_file_location_env_name():{col_width}s}: {get_log_file_location()}")
553
565
  console.print(f" {get_conf_data_location_env_name():{col_width}s}: {get_conf_data_location()}")
554
566
  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()}")
567
+ console.print(f" {get_local_settings_env_name():{col_width}s}: {get_local_settings_path()}")
556
568
 
557
569
 
558
570
  @contextlib.contextmanager
@@ -726,8 +738,8 @@ def main(args: list | None = None): # pragma: no cover
726
738
  rich.print(f" get_log_file_location() = [red]{exc}[/]")
727
739
 
728
740
  try:
729
- rich.print(f" {get_local_settings() = }", flush=True, end="")
730
- location = get_local_settings()
741
+ rich.print(f" {get_local_settings_path() = }", flush=True, end="")
742
+ location = get_local_settings_path()
731
743
  if location is None or not Path(location).exists():
732
744
  rich.print(" [red]⟶ ERROR: The local settings file is not defined or doesn't exist![/]")
733
745
  else:
@@ -1,34 +1,96 @@
1
+ """
2
+ This module provides function to load plugins and settings from entry-points.
3
+ """
4
+ __all__ = [
5
+ "load_plugins",
6
+ "get_file_infos",
7
+ "entry_points",
8
+ ]
1
9
  import logging
2
10
  import os
3
11
  import sys
4
12
  import textwrap
5
13
  import traceback
14
+ from importlib.metadata import EntryPoint
15
+ from pathlib import Path
6
16
 
7
17
  import click
8
18
  import rich
9
19
 
10
- LOGGER = logging.getLogger(__name__)
20
+ _LOGGER = logging.getLogger(__name__)
11
21
 
12
22
 
13
- def entry_points(name: str):
23
+ def entry_points(name: str) -> set[EntryPoint]:
24
+ """
25
+ Returns a set with all entry-points for the given group name.
26
+
27
+ When the name is not known as an entry-point group, an empty set will be returned.
28
+ """
29
+
14
30
  import importlib.metadata
15
31
 
16
32
  try:
17
33
  x = importlib.metadata.entry_points()[name]
18
34
  return {ep for ep in x} # use of set here to remove duplicates
19
35
  except KeyError:
20
- return []
36
+ return set()
21
37
 
22
38
 
23
39
  def load_plugins(entry_point: str) -> dict:
24
-
40
+ """
41
+ Returns a dictionary with plugins loaded. The keys are the names of the entry-points,
42
+ the values are the loaded modules or objects.
43
+ """
25
44
  eps = {}
26
45
  for ep in entry_points(entry_point):
27
46
  try:
28
47
  eps[ep.name] = ep.load()
29
48
  except Exception as exc:
30
49
  eps[ep.name] = None
31
- LOGGER.error(f"Couldn't load entry point: {exc}")
50
+ _LOGGER.error(f"Couldn't load entry point: {exc}")
51
+
52
+ return eps
53
+
54
+
55
+ def get_file_infos(entry_point: str) -> dict[str, tuple[Path, str]]:
56
+ """
57
+ Returns a dictionary with location and filename of all the entries found for
58
+ the given entry-point name.
59
+
60
+ The entry-points are interpreted as follows: <name> = "<module>:<filename>" where
61
+
62
+ - <name> is the name of the entry-point given in the pyproject.toml file
63
+ - <module> is a valid module name that can be imported and from which the location can be determined.
64
+ - <filename> is the name of the target file, e.g. a YAML file
65
+
66
+ As an example, for the `cgse-common` settings, the following entry in the `pyproject.toml`:
67
+
68
+ [project.entry-points."cgse.settings"]
69
+ cgse-common = "cgse_common:settings.yaml"
70
+
71
+ Note that the module name for this entry point has an underscore instead of a dash.
72
+
73
+ Return:
74
+ A dictionary with the entry point name as the key and a tuple (location, filename) as the value.
75
+ """
76
+ from egse.system import get_module_location
77
+
78
+ eps = dict()
79
+
80
+ for ep in entry_points(entry_point):
81
+ try:
82
+ path = get_module_location(ep.module)
83
+
84
+ if path is None:
85
+ _LOGGER.error(
86
+ f"The entry-point '{ep.name}' is ill defined. The module part doesn't exist or is a "
87
+ f"namespace. No settings are loaded for this entry-point."
88
+ )
89
+ else:
90
+ eps[ep.name] = (path, ep.attr)
91
+
92
+ except Exception as exc:
93
+ _LOGGER.error(f"The entry point '{ep.name}' is ill defined: {exc}")
32
94
 
33
95
  return eps
34
96
 
@@ -77,7 +139,7 @@ class BrokenCommand(click.Command):
77
139
  self.help = textwrap.dedent(
78
140
  f"""\
79
141
  Warning: entry point could not be loaded. Contact its author for help.
80
-
142
+
81
143
  {traceback.format_exc()}
82
144
  """
83
145
  )
@@ -3,9 +3,16 @@ The Settings class handles user and configuration settings that are provided in
3
3
  a [`YAML`](http://yaml.org) file.
4
4
 
5
5
  The idea is that settings are grouped by components or any arbitrary grouping that makes sense for
6
- the application or for the user. The Settings class can read from different YAML files. By default,
7
- settings are loaded from a file called ``settings.yaml``. The default yaml configuration file is
8
- located in the same directory as this module.
6
+ the application or for the user. Settings are also modular and provided by each package by means
7
+ of entry-points.The Settings class can read from different YAML files.
8
+
9
+ By default, settings are loaded from a file called `settings.yaml`, but this can be changed in the entry-point
10
+ definition.
11
+
12
+ The yaml configuration files are provided as entry-points by the packages that provided an entry-point
13
+ group 'cgse.settings'. The Settings dictionary (attrdict) is constructed from the configuration YAML
14
+ files from each of the packages. Settings can be overwritten by the next package configuration file.
15
+ So, make sure the group names in each package configuration file are unique.
9
16
 
10
17
  The YAML file is read and the configuration parameters for the given group are
11
18
  available as instance variables of the returned class.
@@ -23,11 +30,10 @@ The intended use is as follows:
23
30
  else:
24
31
  raise RMAPError("Attempt to access outside the RMAP memory map.")
25
32
 
26
-
27
- The above code reads the settings from the default YAML file for a group called ``DSI``.
33
+ The above code reads the settings from the default YAML file for a group called `DSI`.
28
34
  The settings will then be available as variables of the returned class, in this case
29
- ``dsi_settings``. The returned class is and behaves also like a dictionary, so you can check
30
- if a configuration parameter is defined like this:
35
+ `dsi_settings`. The returned class is and behaves also like a dictionary, so you can
36
+ check if a configuration parameter is defined like this:
31
37
 
32
38
  if "DSI_FEE_IP_ADDRESS" not in dsi_settings:
33
39
  # define the IP address of the DSI
@@ -46,12 +52,12 @@ The YAML section for the above code looks like this:
46
52
  RMAP_BASE_ADDRESS: 0x00000000 # The start of the RMAP memory map managed by the FEE
47
53
  RMAP_MEMORY_SIZE: 4096 # The size of the RMAP memory map managed by the FEE
48
54
 
49
- When you want to read settings from another YAML file, specify the ``filename=`` keyword.
50
- If that file is located at a specific location, also use the ``location=`` keyword.
55
+ When you want to read settings from another YAML file, specify the `filename=` keyword.
56
+ If that file is located at a specific location, also use the `location=` keyword.
51
57
 
52
58
  my_settings = Settings.load(filename="user.yaml", location="/Users/JohnDoe")
53
59
 
54
- The above code will read the complete YAML file, i.e. all the groups into a dictionary.
60
+ The above code will read the YAML file from the given location and not from the entry-points.
55
61
 
56
62
  """
57
63
  from __future__ import annotations
@@ -63,12 +69,9 @@ from typing import Any
63
69
 
64
70
  import yaml # This module is provided by the pip package PyYaml - pip install pyyaml
65
71
 
66
- from egse.env import get_local_settings
67
72
  from egse.env import get_local_settings_env_name
68
- from egse.exceptions import FileIsEmptyError
73
+ from egse.env import get_local_settings_path
69
74
  from egse.system import attrdict
70
- from egse.system import get_package_location
71
- from egse.system import ignore_m_warning
72
75
  from egse.system import recursive_dict_update
73
76
 
74
77
  _LOGGER = logging.getLogger(__name__)
@@ -80,22 +83,6 @@ class SettingsError(Exception):
80
83
  pass
81
84
 
82
85
 
83
- def is_defined(cls, name):
84
- return hasattr(cls, name)
85
-
86
-
87
- def get_attr_value(cls, name, default=None):
88
- try:
89
- return getattr(cls, name)
90
- except AttributeError:
91
- return default
92
-
93
-
94
- def set_attr_value(cls, name, value):
95
- if hasattr(cls, name):
96
- raise KeyError(f"Overwriting setting {name} with {value}, was {hasattr(cls, name)}")
97
-
98
-
99
86
  # Fix the problem: YAML loads 5e-6 as string and not a number
100
87
  # https://stackoverflow.com/questions/30458977/yaml-loads-5e-6-as-string-and-not-a-number
101
88
 
@@ -112,91 +99,117 @@ SAFE_LOADER.add_implicit_resolver(
112
99
  list(u'-+0123456789.'))
113
100
 
114
101
 
115
- def get_settings_locations(location: str | Path = None, filename: str = "settings.yaml") -> list[Path]:
102
+ def load_settings_file(path: Path, filename: str, force: bool = False) -> attrdict:
103
+ """
104
+ Loads the YAML configuration file that is located at `path / filename`.
105
+
106
+ Args:
107
+ - path (PATH): the folder where the YAML file is located
108
+ - filename (str): the name of the YAML configuration file
109
+ - force (bool): force reloading, i.e. don't use the cached information
110
+
111
+ Raises:
112
+ A SettingsError when the configuration file doesn't exist or cannot be found.
116
113
 
117
- yaml_locations: set[Path] = set()
114
+ A SettingsError when there was an error reading the configuration file.
118
115
 
119
- if location is None:
120
- package_locations = get_package_location("egse") # `egse` is a namespace
116
+ Returns:
117
+ A dictionary (attrdict) with all the settings from the given file.
121
118
 
122
- for package_location in package_locations:
123
- if (package_location / filename).exists():
124
- yaml_locations.add(package_location)
119
+ Note that, in case of an empty configuration file, and empty dictionary
120
+ is returned and a warning message is issued.
121
+ """
122
+ try:
123
+ yaml_document = read_configuration_file(path / filename, force=force)
124
+ settings = attrdict(
125
+ {name: value for name, value in yaml_document.items()}
126
+ )
127
+ except FileNotFoundError as exc:
128
+ raise SettingsError(
129
+ f"The Settings YAML file '{filename}' is not found at {path!s}. "
130
+ ) from exc
125
131
 
126
- yaml_locations.add(_HERE)
127
- _LOGGER.debug(f"yaml_locations in Settings.load(): {yaml_locations}")
132
+ if not settings:
133
+ _LOGGER.warning(
134
+ f"The Settings YAML file '{filename}' at {path!s} is empty. "
135
+ f"No local settings were loaded, an empty dictionary is returned.")
128
136
 
129
- else:
137
+ return settings
130
138
 
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
139
 
137
- return list(yaml_locations)
140
+ def load_global_settings(entry_point: str = 'cgse.settings', force: bool = False) -> attrdict:
141
+ """
142
+ Loads the settings that are defined by the given entry_point. The entry-points are defined in the
143
+ `pyproject.toml` files of the packages that export their global settings.
138
144
 
145
+ Args:
146
+ - entry_point (str): the name of the entry-point group [default: 'cgse.settings']
147
+ - force (bool): force reloading the settings, i.e. ignore the cache
139
148
 
140
- def load_global_settings(locations, filename: str = "settings.yaml", force: bool = False) -> attrdict:
149
+ Returns:
150
+ A dictionary (attrdict) containing a collection of all the settings exported by the packages
151
+ through the given entry-point.
141
152
 
142
- global_settings = attrdict()
153
+ """
154
+ from egse.plugin import get_file_infos
143
155
 
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
156
+ ep_settings = get_file_infos(entry_point)
157
+
158
+ global_settings = attrdict(label="Settings")
159
+
160
+ for ep_name, (path, filename) in ep_settings.items():
161
+ settings = load_settings_file(path, filename, force)
162
+ recursive_dict_update(global_settings, settings)
152
163
 
153
164
  return global_settings
154
165
 
155
166
 
156
- def load_local_settings(force: bool = False):
167
+ def load_local_settings(force: bool = False) -> attrdict:
168
+ """
169
+ Loads the local settings file that is defined from the environment variable <PROJECT>_LOCAL_SETTINGS.
170
+
171
+ This function might return an empty dictionary when
157
172
 
158
- local_settings = {}
159
- try:
160
- local_settings_location = get_local_settings()
173
+ - the local settings YAML file is empty
174
+ - the local settings environment variable is not defined.
161
175
 
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})")
176
+ in both cases a warning message is logged.
177
+
178
+ Returns:
179
+ A dictionary (attrdict) with all local settings.
180
+
181
+ Raises:
182
+ A SettingsError when the local settings YAML file is not found. Check the <PROJECT>_LOCAL_SETTINGS
183
+ environment variable.
184
+ """
185
+ local_settings = attrdict()
186
+
187
+ local_settings_path = get_local_settings_path()
188
+
189
+ if local_settings_path:
190
+ path = Path(local_settings_path)
191
+ local_settings = load_settings_file(path.parent, path.name, force)
184
192
 
185
193
  return local_settings
186
194
 
187
195
 
188
- def read_configuration_file(filename: str, *, force=False):
196
+ def read_configuration_file(filename: Path, *, force=False):
189
197
  """
190
198
  Read the YAML input configuration file. The configuration file is only read
191
199
  once and memoized as load optimization.
192
200
 
193
201
  Args:
194
- filename (str): the fully qualified filename of the YAML file
195
- force (bool): force reloading the file
202
+ filename (Path): the fully qualified filename of the YAML file
203
+ force (bool): force reloading the file, even when it was memoized
204
+
205
+ Raises:
206
+ A SettingsError when there was an error reading the YAML file.
196
207
 
197
208
  Returns:
198
209
  a dictionary containing all the configuration settings from the YAML file.
199
210
  """
211
+ filename = str(filename)
212
+
200
213
  if force or not Settings.is_memoized(filename):
201
214
 
202
215
  _LOGGER.debug(f"Parsing YAML configuration file {filename}.")
@@ -261,94 +274,95 @@ class Settings:
261
274
  def profiling(cls):
262
275
  return cls.__profile
263
276
 
264
- @classmethod
265
- def load(cls, group_name=None, filename="settings.yaml", location=None, *, force=False, add_local_settings=True):
277
+ @staticmethod
278
+ def _load_all(
279
+ entry_point: str = 'cgse.settings',
280
+ add_local_settings: bool = False,
281
+ force: bool = False
282
+ ) -> attrdict:
266
283
  """
267
- Load the settings for the given group from YAML configuration file.
268
- When no group is provided, the complete configuration is returned.
269
-
270
- The default YAML file is 'settings.yaml' and is located in the same directory
271
- as the settings module.
272
-
273
- About the ``location`` keyword several options are available.
274
-
275
- * when no location is given, i.e. ``location=None``, the YAML settings file is searched for
276
- at the same location as the settings module.
277
-
278
- * when a relative location is given, the YAML settings file is searched for relative to the
279
- current working directory.
280
-
281
- * when an absolute location is given, that location is used 'as is'.
284
+ Loads all settings from all package with the entry point 'cgse.settings'
285
+ """
286
+ global_settings = load_global_settings(entry_point, force)
282
287
 
283
- Args:
284
- group_name (str): the name of one of the main groups from the YAML file
285
- filename (str): the name of the YAML file to read
286
- location (str, Path): the path to the location of the YAML file
287
- force (bool): force reloading the file
288
- add_local_settings (bool): update the Settings with site specific local settings
288
+ # Load the LOCAL settings YAML file
289
289
 
290
- Returns:
291
- a dynamically created class with the configuration parameters as instance variables.
290
+ if add_local_settings:
291
+ local_settings = load_local_settings(force)
292
+ recursive_dict_update(global_settings, local_settings)
292
293
 
293
- Raises:
294
- a SettingsError when the group is not defined in the YAML file.
295
- """
294
+ return global_settings
296
295
 
297
- # Load all detected YAML documents, these are considered global settings
296
+ @staticmethod
297
+ def _load_group(
298
+ group_name: str,
299
+ entry_point: str = 'cgse.settings',
300
+ add_local_settings: bool = False,
301
+ force: bool = False
302
+ ) -> attrdict:
298
303
 
299
- yaml_locations = get_settings_locations(location, filename)
300
- global_settings = load_global_settings(yaml_locations, filename, force)
304
+ global_settings = load_global_settings(entry_point, force)
301
305
 
302
- if not global_settings:
303
- raise SettingsError(f"There were no global settings defined for {filename} at {yaml_locations}.")
306
+ group_settings = attrdict(label=group_name)
304
307
 
305
- # Load the LOCAL settings YAML file
308
+ if group_name in global_settings:
309
+ group_settings = attrdict(
310
+ {name: value for name, value in global_settings[group_name].items()},
311
+ label=group_name
312
+ )
306
313
 
307
314
  if add_local_settings:
308
315
  local_settings = load_local_settings(force)
309
- else:
310
- local_settings = {}
311
-
312
- if group_name in (None, ""):
313
- global_settings = attrdict(
314
- {name: value for name, value in global_settings.items()},
315
- label="Settings"
316
- )
317
- if add_local_settings:
318
- recursive_dict_update(global_settings, local_settings)
319
- return global_settings
320
-
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
316
+ if group_name in local_settings:
317
+ recursive_dict_update(group_settings, local_settings[group_name])
329
318
 
330
- if not include_global_settings and not include_local_settings:
319
+ if not group_settings:
331
320
  raise SettingsError(
332
321
  f"Group name '{group_name}' is not defined in the global nor in the local settings."
333
322
  )
334
323
 
335
- # Check if the group has any settings
324
+ return group_settings
336
325
 
337
- if include_global_settings and not global_settings[group_name]:
338
- _LOGGER.warning(f"Empty group in YAML document {filename} for {group_name}.")
326
+ @staticmethod
327
+ def _load_one(location: str, filename: str, force=False) -> attrdict:
339
328
 
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)
329
+ return load_settings_file(Path(location).expanduser(), filename, force)
330
+
331
+ @classmethod
332
+ def load(
333
+ cls,
334
+ group_name=None, filename="settings.yaml", location=None,
335
+ *, add_local_settings=True, force=False
336
+ ):
337
+ """
338
+ Load the settings for the given group. When no group is provided, the
339
+ complete configuration is returned.
347
340
 
348
- if add_local_settings and include_local_settings:
349
- recursive_dict_update(group_settings, local_settings[group_name])
341
+ The Settings are loaded from entry-points that are defined in each of the
342
+ packages that provide a Settings file.
350
343
 
351
- return group_settings
344
+ If a location is explicitly provided, the Settings will be loaded from that
345
+ location, using the given filename or the default (which is settings.yaml).
346
+
347
+ Args:
348
+ group_name (str): the name of one of the main groups from the YAML file
349
+ filename (str): the name of the YAML file to read [default=settings.yaml]
350
+ location (str, Path): the path to the location of the YAML file
351
+ force (bool): force reloading the file
352
+ add_local_settings (bool): update the Settings with site specific local settings
353
+
354
+ Returns:
355
+ a dynamically created class with the configuration parameters as instance variables.
356
+
357
+ Raises:
358
+ a SettingsError when the group is not defined in the YAML file.
359
+ """
360
+ if group_name:
361
+ return cls._load_group(group_name, add_local_settings=add_local_settings, force=force)
362
+ elif location:
363
+ return cls._load_one(location=location, filename=filename, force=force)
364
+ else:
365
+ return cls._load_all(add_local_settings=add_local_settings, force=force)
352
366
 
353
367
  @classmethod
354
368
  def to_string(cls):
@@ -400,7 +414,7 @@ def main(args: list | None = None): # pragma: no cover
400
414
  from rich import print
401
415
 
402
416
  if args.local:
403
- location = get_local_settings()
417
+ location = get_local_settings_path()
404
418
  if location:
405
419
  location = str(Path(location).expanduser().resolve())
406
420
  settings = Settings.load(filename=location)
@@ -0,0 +1,5 @@
1
+ # This is a stub file. Don't add any settings to this file, use the settings.yaml file
2
+ # in your projects module, like e.g. `cgse_common/settings.yaml` for this module.
3
+
4
+ SITE:
5
+ ID: XXXX
@@ -120,6 +120,7 @@ import logging
120
120
  import os
121
121
  import re
122
122
  import textwrap
123
+ import warnings
123
124
  from functools import lru_cache
124
125
  from pathlib import Path
125
126
  from typing import Any
@@ -130,13 +131,14 @@ import rich
130
131
  import yaml
131
132
  from rich.tree import Tree
132
133
 
134
+ from egse.env import get_conf_data_location
133
135
  from egse.env import get_conf_repo_location
134
136
  from egse.env import get_conf_repo_location_env_name
135
137
  from egse.env import get_data_storage_location
136
138
  from egse.env import has_conf_repo_location
137
139
  from egse.env import print_env
138
140
  from egse.response import Failure
139
- from egse.env import get_conf_data_location
141
+ from egse.settings import read_configuration_file
140
142
  from egse.system import format_datetime
141
143
  from egse.system import sanity_check
142
144
  from egse.system import walk_dict_tree
@@ -220,8 +222,8 @@ def _load_yaml(resource_name: str):
220
222
  [in_dir, fn] = parts if len(parts) > 1 else [None, parts[0]]
221
223
  conf_location = get_conf_data_location()
222
224
  try:
223
- yaml_location = Path(conf_location) / in_dir / fn
224
- content = NavigableDict(Settings.load(filename=yaml_location, add_local_settings=False))
225
+ yaml_location = Path(conf_location) / in_dir
226
+ content = NavigableDict(Settings.load(location=yaml_location, filename=fn, add_local_settings=False))
225
227
  except (TypeError, SettingsError) as exc:
226
228
  raise ValueError(
227
229
  f"Couldn't load resource '{resource_name}' from default {conf_location=}") from exc
@@ -695,14 +697,20 @@ class Setup(NavigableDict):
695
697
  Returns:
696
698
  a Setup that was loaded from the given location.
697
699
  """
698
- from egse.settings import Settings
699
700
 
700
701
  if not filename:
701
702
  raise ValueError("Invalid argument to function: No filename or None given.")
702
703
 
703
704
  # MODULE_LOGGER.info(f"Loading {filename}...")
704
705
 
705
- setup_dict = Settings.load("Setup", filename=filename, force=True, add_local_settings=add_local_settings)
706
+ setup_dict = read_configuration_file(filename, force=True)
707
+ if setup_dict == {}:
708
+ warnings.warn(f"Empty Setup file: {filename!s}")
709
+
710
+ try:
711
+ setup_dict = setup_dict["Setup"]
712
+ except KeyError:
713
+ warnings.warn(f"Setup file doesn't have a 'Setup' group: {filename!s}")
706
714
 
707
715
  setup = Setup(setup_dict, label="Setup")
708
716
  setup.set_private_attribute("_filename", filename)
File without changes