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.
- apsbits/__init__.py +18 -0
- apsbits/_version.py +21 -0
- apsbits/core/__init__.py +11 -0
- apsbits/core/best_effort_init.py +38 -0
- apsbits/core/catalog_init.py +29 -0
- apsbits/core/run_engine_init.py +71 -0
- apsbits/demo_instrument/README.md +1 -0
- apsbits/demo_instrument/__init__.py +3 -0
- apsbits/demo_instrument/callbacks/__init__.py +1 -0
- apsbits/demo_instrument/callbacks/nexus_data_file_writer.py +60 -0
- apsbits/demo_instrument/callbacks/spec_data_file_writer.py +93 -0
- apsbits/demo_instrument/configs/__init__.py +1 -0
- apsbits/demo_instrument/configs/devices.yml +52 -0
- apsbits/demo_instrument/configs/devices_aps_only.yml +6 -0
- apsbits/demo_instrument/configs/iconfig.yml +81 -0
- apsbits/demo_instrument/configs/logging.yml +41 -0
- apsbits/demo_instrument/devices/__init__.py +1 -0
- apsbits/demo_instrument/plans/__init__.py +8 -0
- apsbits/demo_instrument/plans/dm_plans.py +111 -0
- apsbits/demo_instrument/plans/sim_plans.py +69 -0
- apsbits/demo_instrument/startup.py +63 -0
- apsbits/tests/__init__.py +1 -0
- apsbits/tests/conftest.py +32 -0
- apsbits/tests/test_device_factories.py +44 -0
- apsbits/tests/test_general.py +83 -0
- apsbits/tests/test_stored_dict.py +139 -0
- apsbits/utils/__init__.py +1 -0
- apsbits/utils/aps_functions.py +67 -0
- apsbits/utils/config_loaders.py +61 -0
- apsbits/utils/context_aware.py +187 -0
- apsbits/utils/controls_setup.py +113 -0
- apsbits/utils/create_new_instrument.py +144 -0
- apsbits/utils/helper_functions.py +113 -0
- apsbits/utils/logging_setup.py +211 -0
- apsbits/utils/make_devices_yaml.py +127 -0
- apsbits/utils/metadata.py +101 -0
- apsbits/utils/sim_creator.py +199 -0
- apsbits/utils/stored_dict.py +192 -0
- apsbits-1.0.0rc2.dist-info/LICENSE +48 -0
- apsbits-1.0.0rc2.dist-info/METADATA +349 -0
- apsbits-1.0.0rc2.dist-info/RECORD +44 -0
- apsbits-1.0.0rc2.dist-info/WHEEL +5 -0
- apsbits-1.0.0rc2.dist-info/entry_points.txt +2 -0
- 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
|
+
# )
|