apsbits 1.0.0rc2__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.
Files changed (44) hide show
  1. apsbits/__init__.py +18 -0
  2. apsbits/_version.py +21 -0
  3. apsbits/core/__init__.py +11 -0
  4. apsbits/core/best_effort_init.py +38 -0
  5. apsbits/core/catalog_init.py +29 -0
  6. apsbits/core/run_engine_init.py +71 -0
  7. apsbits/demo_instrument/README.md +1 -0
  8. apsbits/demo_instrument/__init__.py +3 -0
  9. apsbits/demo_instrument/callbacks/__init__.py +1 -0
  10. apsbits/demo_instrument/callbacks/nexus_data_file_writer.py +60 -0
  11. apsbits/demo_instrument/callbacks/spec_data_file_writer.py +93 -0
  12. apsbits/demo_instrument/configs/__init__.py +1 -0
  13. apsbits/demo_instrument/configs/devices.yml +52 -0
  14. apsbits/demo_instrument/configs/devices_aps_only.yml +6 -0
  15. apsbits/demo_instrument/configs/iconfig.yml +81 -0
  16. apsbits/demo_instrument/configs/logging.yml +41 -0
  17. apsbits/demo_instrument/devices/__init__.py +1 -0
  18. apsbits/demo_instrument/plans/__init__.py +8 -0
  19. apsbits/demo_instrument/plans/dm_plans.py +111 -0
  20. apsbits/demo_instrument/plans/sim_plans.py +69 -0
  21. apsbits/demo_instrument/startup.py +63 -0
  22. apsbits/tests/__init__.py +1 -0
  23. apsbits/tests/conftest.py +32 -0
  24. apsbits/tests/test_device_factories.py +44 -0
  25. apsbits/tests/test_general.py +83 -0
  26. apsbits/tests/test_stored_dict.py +139 -0
  27. apsbits/utils/__init__.py +1 -0
  28. apsbits/utils/aps_functions.py +67 -0
  29. apsbits/utils/config_loaders.py +61 -0
  30. apsbits/utils/context_aware.py +187 -0
  31. apsbits/utils/controls_setup.py +113 -0
  32. apsbits/utils/create_new_instrument.py +144 -0
  33. apsbits/utils/helper_functions.py +113 -0
  34. apsbits/utils/logging_setup.py +211 -0
  35. apsbits/utils/make_devices_yaml.py +127 -0
  36. apsbits/utils/metadata.py +101 -0
  37. apsbits/utils/sim_creator.py +199 -0
  38. apsbits/utils/stored_dict.py +192 -0
  39. apsbits-1.0.0rc2.dist-info/LICENSE +48 -0
  40. apsbits-1.0.0rc2.dist-info/METADATA +349 -0
  41. apsbits-1.0.0rc2.dist-info/RECORD +44 -0
  42. apsbits-1.0.0rc2.dist-info/WHEEL +5 -0
  43. apsbits-1.0.0rc2.dist-info/entry_points.txt +2 -0
  44. apsbits-1.0.0rc2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,69 @@
1
+ """
2
+ Simulators from ophyd
3
+ =====================
4
+
5
+ For development and testing only, provides plans.
6
+
7
+ .. autosummary::
8
+ ~sim_count_plan
9
+ ~sim_print_plan
10
+ ~sim_rel_scan_plan
11
+ """
12
+
13
+ import logging
14
+
15
+ from bluesky import plan_stubs as bps
16
+ from bluesky import plans as bp
17
+
18
+ from apsbits.utils.controls_setup import oregistry
19
+
20
+ logger = logging.getLogger(__name__)
21
+ logger.bsdev(__file__)
22
+
23
+ DEFAULT_MD = {"title": "test run with simulator(s)"}
24
+
25
+
26
+ def sim_count_plan(num: int = 1, imax: float = 10_000, md: dict = DEFAULT_MD):
27
+ """Demonstrate the ``count()`` plan."""
28
+ logger.debug("sim_count_plan()")
29
+ sim_det = oregistry["sim_det"]
30
+ yield from bps.mv(sim_det.Imax, imax)
31
+ yield from bp.count([sim_det], num=num, md=md)
32
+
33
+
34
+ def sim_print_plan():
35
+ """Demonstrate a ``print()`` plan stub (no data streams)."""
36
+ logger.debug("sim_print_plan()")
37
+ yield from bps.null()
38
+ sim_det = oregistry["sim_det"]
39
+ sim_motor = oregistry["sim_motor"]
40
+ print("sim_print_plan(): This is a test.")
41
+ print(f"sim_print_plan(): {sim_motor.position=} {sim_det.read()=}.")
42
+
43
+
44
+ def sim_rel_scan_plan(
45
+ span: float = 5,
46
+ num: int = 11,
47
+ imax: float = 10_000,
48
+ center: float = 0,
49
+ sigma: float = 1,
50
+ noise: str = "uniform", # none poisson uniform
51
+ md: dict = DEFAULT_MD,
52
+ ):
53
+ """Demonstrate the ``rel_scan()`` plan."""
54
+ logger.debug("sim_rel_scan_plan()")
55
+ sim_det = oregistry["sim_det"]
56
+ sim_motor = oregistry["sim_motor"]
57
+ # fmt: off
58
+ yield from bps.mv(
59
+ sim_det.Imax, imax,
60
+ sim_det.center, center,
61
+ sim_det.sigma, sigma,
62
+ sim_det.noise, noise,
63
+ )
64
+ # fmt: on
65
+ print(f"sim_rel_scan_plan(): {sim_motor.position=}.")
66
+ print(f"sim_rel_scan_plan(): {sim_det.read()=}.")
67
+ print(f"sim_rel_scan_plan(): {sim_det.read_configuration()=}.")
68
+ print(f"sim_rel_scan_plan(): {sim_det.noise._enum_strs=}.")
69
+ yield from bp.rel_scan([sim_det], sim_motor, -span / 2, span / 2, num=num, md=md)
@@ -0,0 +1,63 @@
1
+ """
2
+ Start Bluesky Data Acquisition sessions of all kinds.
3
+
4
+ Includes:
5
+
6
+ * Python script
7
+ * IPython console
8
+ * Jupyter notebook
9
+ * Bluesky queueserver
10
+ """
11
+
12
+ import logging
13
+
14
+ from apsbits.core.best_effort_init import bec # noqa: F401
15
+ from apsbits.core.best_effort_init import peaks # noqa: F401
16
+ from apsbits.core.catalog_init import cat # noqa: F401
17
+ from apsbits.core.run_engine_init import RE # noqa: F401
18
+ from apsbits.core.run_engine_init import sd # noqa: F401
19
+ from apsbits.utils.aps_functions import aps_dm_setup
20
+ from apsbits.utils.config_loaders import iconfig
21
+
22
+ # Bluesky data acquisition setup
23
+ # from apsbits.utils.config_loaders import iconfig
24
+ from apsbits.utils.helper_functions import register_bluesky_magics
25
+ from apsbits.utils.helper_functions import running_in_queueserver
26
+ from apsbits.utils.make_devices_yaml import make_devices # noqa: F401
27
+
28
+ # User specific imports
29
+ from .plans import * # noqa: F403
30
+
31
+ logger = logging.getLogger(__name__)
32
+ logger.bsdev(__file__)
33
+
34
+ aps_dm_setup(iconfig.get("DM_SETUP_FILE"))
35
+
36
+ if iconfig.get("USE_BLUESKY_MAGICS", False):
37
+ register_bluesky_magics()
38
+
39
+ # Configure the session with callbacks, devices, and plans.
40
+ if iconfig.get("NEXUS_DATA_FILES", {}).get("ENABLE", False):
41
+ from .callbacks.nexus_data_file_writer import nxwriter # noqa: F401
42
+
43
+ if iconfig.get("SPEC_DATA_FILES", {}).get("ENABLE", False):
44
+ from .callbacks.spec_data_file_writer import newSpecFile # noqa: F401
45
+ from .callbacks.spec_data_file_writer import spec_comment # noqa: F401
46
+ from .callbacks.spec_data_file_writer import specwriter # noqa: F401
47
+
48
+ # These imports must come after the above setup.
49
+ if running_in_queueserver():
50
+ ### To make all the standard plans available in QS, import by '*', otherwise import
51
+ ### plan by plan.
52
+ from apstools.plans import lineup2 # noqa: F401
53
+ from bluesky.plans import * # noqa: F403
54
+
55
+ else:
56
+ # Import bluesky plans and stubs with prefixes set by common conventions.
57
+ # The apstools plans and utils are imported by '*'.
58
+ from apstools.plans import * # noqa: F403
59
+ from apstools.utils import * # noqa: F403
60
+ from bluesky import plan_stubs as bps # noqa: F401
61
+ from bluesky import plans as bp # noqa: F401
62
+
63
+ from apsbits.utils.controls_setup import oregistry # noqa: F401
@@ -0,0 +1 @@
1
+ """Test code for minimal instrument package."""
@@ -0,0 +1,32 @@
1
+ """
2
+ Pytest fixtures for instrument tests.
3
+
4
+ This module provides fixtures for initializing the RunEngine with devices,
5
+ allowing tests to operate with device-dependent configurations without relying
6
+ on the production startup logic.
7
+
8
+ Fixtures:
9
+ runengine_with_devices: A RunEngine object in a session with devices configured.
10
+ """
11
+
12
+ from typing import Any
13
+
14
+ import pytest
15
+
16
+ from apsbits.demo_instrument.startup import RE
17
+ from apsbits.demo_instrument.startup import make_devices
18
+
19
+
20
+ @pytest.fixture(scope="session")
21
+ def runengine_with_devices() -> Any:
22
+ """
23
+ Initialize the RunEngine with devices for testing.
24
+
25
+ This fixture calls RE with the `make_devices()` plan stub to mimic
26
+ the behavior previously performed in the startup module.
27
+
28
+ Returns:
29
+ Any: An instance of the RunEngine with devices configured.
30
+ """
31
+ RE(make_devices())
32
+ return RE
@@ -0,0 +1,44 @@
1
+ """Test the device factories."""
2
+
3
+ import pytest
4
+
5
+ from apsbits.utils.sim_creator import motors
6
+ from apsbits.utils.sim_creator import predefined_device
7
+
8
+
9
+ @pytest.mark.parametrize(
10
+ "creator, name, klass",
11
+ [
12
+ ["ophyd.sim.motor", None, "SynAxis"],
13
+ ["ophyd.sim.motor", "sim_motor", "SynAxis"],
14
+ ["ophyd.sim.noisy_det", None, "SynGauss"],
15
+ ["ophyd.sim.noisy_det", "sim_det", "SynGauss"],
16
+ ],
17
+ )
18
+ def test_predefined(creator, name, klass):
19
+ """import predefined devices"""
20
+ for device in predefined_device(creator=creator, name=name):
21
+ assert device is not None
22
+ assert device.__class__.__name__ == klass
23
+ if name is not None:
24
+ assert device.name == name
25
+
26
+
27
+ @pytest.mark.parametrize(
28
+ "kwargs",
29
+ [
30
+ {"prefix": "ioc:m", "first": 1, "last": 4, "labels": ["motor"]},
31
+ {"prefix": "ioc:m", "names": "m", "first": 7, "last": 22, "labels": ["motor"]},
32
+ ],
33
+ )
34
+ def test_motors(kwargs):
35
+ """create a block of motors"""
36
+ count = 0
37
+ for device in motors(**kwargs):
38
+ count += 1
39
+ assert device is not None
40
+ assert device.__class__.__name__ == "EpicsMotor"
41
+ if kwargs.get("names") is None:
42
+ assert device.name.startswith("m")
43
+ assert isinstance(int(device.name[1:]), int)
44
+ assert count == (1 + kwargs["last"] - kwargs["first"])
@@ -0,0 +1,83 @@
1
+ """
2
+ Test that instrument can be started.
3
+
4
+ Here is just enough testing to get a CI workflow started. More are possible.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from apsbits.demo_instrument.plans.sim_plans import sim_count_plan
10
+ from apsbits.demo_instrument.plans.sim_plans import sim_print_plan
11
+ from apsbits.demo_instrument.plans.sim_plans import sim_rel_scan_plan
12
+ from apsbits.demo_instrument.startup import bec
13
+ from apsbits.demo_instrument.startup import cat
14
+ from apsbits.demo_instrument.startup import iconfig
15
+ from apsbits.demo_instrument.startup import peaks
16
+ from apsbits.demo_instrument.startup import running_in_queueserver
17
+ from apsbits.demo_instrument.startup import sd
18
+ from apsbits.demo_instrument.startup import specwriter
19
+
20
+
21
+ def test_startup(runengine_with_devices: object) -> None:
22
+ """
23
+ Test that standard startup works and the RunEngine has initialized the devices.
24
+ """
25
+ # The fixture ensures that runengine_with_devices is initialized.
26
+ assert runengine_with_devices is not None
27
+ assert cat is not None
28
+ assert bec is not None
29
+ assert peaks is not None
30
+ assert sd is not None
31
+ assert iconfig is not None
32
+ assert specwriter is not None
33
+
34
+ if iconfig.get("DATABROKER_CATALOG", "temp") == "temp":
35
+ assert len(cat) == 0
36
+ assert not running_in_queueserver()
37
+
38
+
39
+ @pytest.mark.parametrize(
40
+ "plan, n_uids",
41
+ [
42
+ [sim_print_plan, 0],
43
+ [sim_count_plan, 1],
44
+ [sim_rel_scan_plan, 1],
45
+ ],
46
+ )
47
+ def test_sim_plans(runengine_with_devices: object, plan: object, n_uids: int) -> None:
48
+ """
49
+ Test supplied simulator plans using the RunEngine with devices.
50
+ """
51
+ bec.disable_plots()
52
+ n_runs = len(cat)
53
+ # Use the fixture-provided run engine to run the plan.
54
+ uids = runengine_with_devices(plan())
55
+ assert len(uids) == n_uids
56
+ assert len(cat) == n_runs + len(uids)
57
+
58
+
59
+ def test_iconfig() -> None:
60
+ """
61
+ Test the instrument configuration.
62
+ """
63
+ version: str = iconfig.get(
64
+ "ICONFIG_VERSION", "0.0.0"
65
+ ) # TODO: Will anyone ever have a wrong catalog version?
66
+ assert version >= "2.0.0"
67
+
68
+ cat_name: str = iconfig.get("DATABROKER_CATALOG")
69
+ assert cat_name is not None
70
+ assert cat_name == cat.name
71
+
72
+ assert "RUN_ENGINE" in iconfig
73
+ assert "DEFAULT_METADATA" in iconfig["RUN_ENGINE"]
74
+
75
+ default_md = iconfig["RUN_ENGINE"]["DEFAULT_METADATA"]
76
+ assert "beamline_id" in default_md
77
+ assert "instrument_name" in default_md
78
+ assert "proposal_id" in default_md
79
+ assert "databroker_catalog" in default_md
80
+ assert default_md["databroker_catalog"] == cat.name
81
+
82
+ xmode = iconfig.get("XMODE_DEBUG_LEVEL")
83
+ assert xmode is not None
@@ -0,0 +1,139 @@
1
+ """
2
+ Test the utils.stored_dict module.
3
+ """
4
+
5
+ import pathlib
6
+ import tempfile
7
+ import time
8
+ from contextlib import nullcontext as does_not_raise
9
+
10
+ import pytest
11
+
12
+ from apsbits.utils.config_loaders import load_config_yaml
13
+ from apsbits.utils.stored_dict import StoredDict
14
+
15
+
16
+ def luftpause(delay=0.05):
17
+ """A brief wait for content to flush to storage."""
18
+ time.sleep(max(0, delay))
19
+
20
+
21
+ @pytest.fixture
22
+ def md_file():
23
+ """Provide a temporary file (deleted on close)."""
24
+ tfile = tempfile.NamedTemporaryFile(
25
+ prefix="re_md_",
26
+ suffix=".yml",
27
+ delete=False,
28
+ )
29
+ path = pathlib.Path(tfile.name)
30
+ yield pathlib.Path(tfile.name)
31
+
32
+ if path.exists():
33
+ path.unlink() # delete the file
34
+
35
+
36
+ def test_StoredDict(md_file):
37
+ """Test the StoredDict class."""
38
+ assert md_file.exists()
39
+ assert len(open(md_file).read().splitlines()) == 0 # empty
40
+
41
+ sdict = StoredDict(md_file, delay=0.2, title="unit testing")
42
+ assert sdict is not None
43
+ assert len(sdict) == 0
44
+ assert sdict._delay == 0.2
45
+ assert sdict._title == "unit testing"
46
+ assert len(open(md_file).read().splitlines()) == 0 # still empty
47
+ assert sdict._sync_key == f"sync_agent_{id(sdict):x}"
48
+ assert not sdict.sync_in_progress
49
+
50
+ # Write an empty dictionary.
51
+ sdict.flush()
52
+ luftpause()
53
+ buf = open(md_file).read().splitlines()
54
+ assert len(buf) == 4, f"{buf=}"
55
+ assert buf[-1] == "{}" # The empty dict.
56
+ assert buf[0].startswith("# ")
57
+ assert buf[1].startswith("# ")
58
+ assert "unit testing" in buf[0]
59
+
60
+ # Add a new {key: value} pair.
61
+ assert not sdict.sync_in_progress
62
+ sdict["a"] = 1
63
+ assert sdict.sync_in_progress
64
+ sdict.flush()
65
+ assert time.time() > sdict._sync_deadline
66
+ luftpause()
67
+ assert not sdict.sync_in_progress
68
+ assert len(open(md_file).read().splitlines()) == 4
69
+
70
+ # Change the only value.
71
+ sdict["a"] = 2
72
+ sdict.flush()
73
+ luftpause()
74
+ assert len(open(md_file).read().splitlines()) == 4 # Still.
75
+
76
+ # Add another key.
77
+ sdict["bee"] = "bumble"
78
+ sdict.flush()
79
+ print(f"\n\nthis is the md_file: {md_file}\n\n")
80
+ luftpause()
81
+ assert len(open(md_file).read().splitlines()) == 5
82
+
83
+ # Test _delayed_sync_to_storage.
84
+ sdict["bee"] = "queen"
85
+ md = load_config_yaml(md_file)
86
+ assert len(md) == 2 # a & bee
87
+ assert "a" in md
88
+ assert md["bee"] == "bumble" # The old value.
89
+
90
+ time.sleep(sdict._delay / 2)
91
+ # Still not written ...
92
+ assert load_config_yaml(md_file)["bee"] == "bumble"
93
+
94
+ time.sleep(sdict._delay)
95
+ # Should be written by now.
96
+ assert load_config_yaml(md_file)["bee"] == "queen"
97
+
98
+ del sdict["bee"] # __delitem__
99
+ assert "bee" not in sdict # __getitem__
100
+
101
+
102
+ @pytest.mark.parametrize(
103
+ "md, xcept, text",
104
+ [
105
+ [{"a": 1}, None, str(None)], # int value is ok
106
+ [{"a": 2.2}, None, str(None)], # float value is ok
107
+ [{"a": "3"}, None, str(None)], # str value is ok
108
+ [{"a": [4, 5, 6]}, None, str(None)], # list value is ok
109
+ [{"a": {"bb": [4, 5, 6]}}, None, str(None)], # nested value is ok
110
+ [{1: 1}, None, str(None)], # int key is ok
111
+ [{"a": object()}, TypeError, "not JSON serializable"],
112
+ [{object(): 1}, TypeError, "keys must be str, int, float, "],
113
+ [{"a": [4, object(), 6]}, TypeError, "not JSON serializable"],
114
+ [{"a": {object(): [4, 5, 6]}}, TypeError, "keys must be str, int, "],
115
+ ],
116
+ )
117
+ def test_set_exceptions(md, xcept, text, md_file):
118
+ """Cases that might raise an exception."""
119
+ sdict = StoredDict(md_file, delay=0.2, title="unit testing")
120
+ context = does_not_raise() if xcept is None else pytest.raises(xcept)
121
+ with context as reason:
122
+ sdict.update(md)
123
+ assert text in str(reason), f"{reason=}"
124
+
125
+
126
+ def test_popitem(md_file):
127
+ """Can't popitem from empty dict."""
128
+ sdict = StoredDict(md_file, delay=0.2, title="unit testing")
129
+ with pytest.raises(KeyError) as reason:
130
+ sdict.popitem()
131
+ assert "dictionary is empty" in str(reason), f"{reason=}"
132
+
133
+
134
+ def test_repr(md_file):
135
+ """__repr__"""
136
+ sdict = StoredDict(md_file, delay=0.1, title="unit testing")
137
+ sdict["a"] = 1
138
+ assert repr(sdict) == "<StoredDict {'a': 1}>"
139
+ assert str(sdict) == "<StoredDict {'a': 1}>"
@@ -0,0 +1 @@
1
+ """Local utilties and miscellaneous code."""
@@ -0,0 +1,67 @@
1
+ """
2
+ APS utility helper functions
3
+ ============================
4
+
5
+ .. autosummary::
6
+ ~host_on_aps_subnet
7
+ ~aps_dm_setup
8
+ """
9
+
10
+ import logging
11
+ import os
12
+ import pathlib
13
+ import socket
14
+
15
+ logger = logging.getLogger(__name__)
16
+ logger.bsdev(__file__)
17
+
18
+
19
+ def aps_dm_setup(dm_setup_file_path):
20
+ """
21
+ APS Data Management setup
22
+ =========================
23
+
24
+ Read the bash shell script file used by DM to setup the environment. Parse any
25
+ ``export`` lines and add their environment variables to this session. This is
26
+ done by brute force here since the APS DM environment setup requires different
27
+ Python code than bluesky and the two often clash.
28
+
29
+ This setup must be done before any of the DM package libraries are called.
30
+
31
+ """
32
+ if dm_setup_file_path is not None:
33
+ bash_script = pathlib.Path(dm_setup_file_path)
34
+ if bash_script.exists():
35
+ logger.info("APS DM environment file: %s", str(bash_script))
36
+ # parse environment variables from bash script
37
+ environment = {}
38
+ for line in open(bash_script).readlines():
39
+ if not line.startswith("export "):
40
+ continue
41
+ k, v = line.strip().split()[-1].split("=")
42
+ environment[k] = v
43
+ os.environ.update(environment)
44
+
45
+ workflow_owner = os.environ["DM_STATION_NAME"].lower()
46
+ logger.info("APS DM workflow owner: %s", workflow_owner)
47
+ else:
48
+ logger.warning("APS DM setup file does not exist: '%s'", bash_script)
49
+
50
+
51
+ def host_on_aps_subnet():
52
+ """Detect if this host is on an APS subnet."""
53
+ LOOPBACK_IP4 = "127.0.0.1"
54
+ PUBLIC_IP4_PREFIX = "164.54."
55
+ PRIVATE_IP4_PREFIX = "10.54."
56
+ TEST_IP = "10.254.254.254" # does not have to be reachable
57
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
58
+ sock.settimeout(0)
59
+ try:
60
+ sock.connect((TEST_IP, 1))
61
+ ip4 = sock.getsockname()[0]
62
+ except Exception:
63
+ ip4 = LOOPBACK_IP4
64
+ return True in [
65
+ ip4.startswith(PUBLIC_IP4_PREFIX),
66
+ ip4.startswith(PRIVATE_IP4_PREFIX),
67
+ ]
@@ -0,0 +1,61 @@
1
+ """
2
+ Load configuration files
3
+ ========================
4
+
5
+ Load supported configuration files, such as ``iconfig.yml``.
6
+
7
+ .. autosummary::
8
+ ~load_config_yaml
9
+ ~IConfigFileVersionError
10
+ """
11
+
12
+ import logging
13
+ import pathlib
14
+
15
+ import yaml
16
+
17
+ logger = logging.getLogger(__name__)
18
+ logger.bsdev(__file__)
19
+ instrument_path = pathlib.Path(__file__).parent.parent
20
+ DEFAULT_ICONFIG_YML_FILE = (
21
+ instrument_path / "demo_instrument" / "configs" / "iconfig.yml"
22
+ )
23
+ ICONFIG_MINIMUM_VERSION = "2.0.0"
24
+
25
+
26
+ def load_config_yaml(iconfig_yml=None) -> dict:
27
+ """
28
+ Load iconfig.yml (and other YAML) configuration files.
29
+
30
+ Parameters
31
+ ----------
32
+ iconfig_yml: str
33
+ Name of the YAML file to be loaded. The name can be
34
+ absolute or relative to the current working directory.
35
+ Default: ``INSTRUMENT/demo_instrument/configs/iconfig.yml``
36
+ """
37
+
38
+ if iconfig_yml is None:
39
+ path = DEFAULT_ICONFIG_YML_FILE
40
+ else:
41
+ path = pathlib.Path(iconfig_yml)
42
+ if not path.exists():
43
+ raise FileExistsError(f"Configuration file '{path}' does not exist.")
44
+ config = yaml.load(open(path, "r").read(), yaml.Loader)
45
+ return config
46
+
47
+
48
+ # class IConfigFileVersionError(ValueError):
49
+ # """Configuration file version too old."""
50
+
51
+
52
+ # # Validate the iconfig file has the minimum version.
53
+ # _version = iconfig.get("ICONFIG_VERSION")
54
+ # print(f"\n\n\niconfig version: {_version}\n\n\n")
55
+ # if _version is None or _version < ICONFIG_MINIMUM_VERSION:
56
+ # raise IConfigFileVersionError(
57
+ # "Configuration file version too old."
58
+ # f" Found {_version!r}."
59
+ # f" Expected minimum {ICONFIG_MINIMUM_VERSION!r}."
60
+ # f" Configuration file '{DEFAULT_ICONFIG_YML_FILE}'."
61
+ # )