apsbits 1.0.0__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 (47) 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 +51 -0
  5. apsbits/core/catalog_init.py +36 -0
  6. apsbits/core/run_engine_init.py +118 -0
  7. apsbits/demo_instrument/README.md +1 -0
  8. apsbits/demo_instrument/__init__.py +22 -0
  9. apsbits/demo_instrument/callbacks/__init__.py +1 -0
  10. apsbits/demo_instrument/callbacks/nexus_data_file_writer.py +58 -0
  11. apsbits/demo_instrument/callbacks/spec_data_file_writer.py +97 -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 +82 -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 +76 -0
  22. apsbits/demo_qserver/qs-config.yml +51 -0
  23. apsbits/demo_qserver/qs_host.sh +231 -0
  24. apsbits/demo_qserver/user_group_permissions.yaml +46 -0
  25. apsbits/tests/__init__.py +1 -0
  26. apsbits/tests/conftest.py +39 -0
  27. apsbits/tests/test_config.py +113 -0
  28. apsbits/tests/test_device_factories.py +44 -0
  29. apsbits/tests/test_general.py +98 -0
  30. apsbits/tests/test_stored_dict.py +139 -0
  31. apsbits/utils/__init__.py +1 -0
  32. apsbits/utils/aps_functions.py +67 -0
  33. apsbits/utils/config_loaders.py +169 -0
  34. apsbits/utils/controls_setup.py +107 -0
  35. apsbits/utils/create_new_instrument.py +129 -0
  36. apsbits/utils/helper_functions.py +123 -0
  37. apsbits/utils/logging_setup.py +211 -0
  38. apsbits/utils/make_devices.py +162 -0
  39. apsbits/utils/metadata.py +96 -0
  40. apsbits/utils/sim_creator.py +202 -0
  41. apsbits/utils/stored_dict.py +174 -0
  42. apsbits-1.0.0.dist-info/METADATA +195 -0
  43. apsbits-1.0.0.dist-info/RECORD +47 -0
  44. apsbits-1.0.0.dist-info/WHEEL +5 -0
  45. apsbits-1.0.0.dist-info/entry_points.txt +2 -0
  46. apsbits-1.0.0.dist-info/licenses/LICENSE +48 -0
  47. apsbits-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,111 @@
1
+ """
2
+ Plans in support of APS Data Management
3
+ =======================================
4
+
5
+ .. autosummary::
6
+
7
+ ~dm_kickoff_workflow
8
+ ~dm_list_processing_jobs
9
+ ~dm_submit_workflow_job
10
+ """
11
+
12
+ import logging
13
+
14
+ from apstools.devices import DM_WorkflowConnector
15
+ from apstools.utils import dm_api_proc
16
+ from apstools.utils import share_bluesky_metadata_with_dm
17
+ from bluesky import plan_stubs as bps
18
+
19
+ logger = logging.getLogger(__name__)
20
+ logger.bsdev(__file__)
21
+
22
+
23
+ def dm_kickoff_workflow(run, argsDict, timeout=None, wait=False):
24
+ """
25
+ Start a DM workflow for this bluesky run and share run's metadata with DM.
26
+
27
+ PARAMETERS:
28
+
29
+ run (*obj*): Bluesky run object (such as 'run = cat[uid]').
30
+
31
+ argsDict (*dict*): Dictionary of parameters needed by 'workflowName'.
32
+ At minimum, most workflows expect these keys: 'filePath' and
33
+ 'experimentName'. Consult the workflow for the expected
34
+ content of 'argsDict'.
35
+
36
+ timeout (*number*): When should bluesky stop reporting on this
37
+ DM workflow job (if it has not ended). Units are seconds.
38
+ Default is forever.
39
+
40
+ wait (*bool*): Should this plan stub wait for the job to end?
41
+ Default is 'False'.
42
+ """
43
+ dm_workflow = DM_WorkflowConnector(name="dm_workflow")
44
+
45
+ if timeout is None:
46
+ # Disable periodic reports, use a long time (s).
47
+ timeout = 999_999_999_999
48
+
49
+ yield from bps.mv(dm_workflow.concise_reporting, True)
50
+ yield from bps.mv(dm_workflow.reporting_period, timeout)
51
+
52
+ workflow_name = argsDict.pop["workflowName"]
53
+ yield from dm_workflow.run_as_plan(
54
+ workflow=workflow_name,
55
+ wait=wait,
56
+ timeout=timeout,
57
+ **argsDict,
58
+ )
59
+
60
+ # Upload bluesky run metadata to APS DM.
61
+ share_bluesky_metadata_with_dm(argsDict["experimentName"], workflow_name, run)
62
+
63
+ # Users requested the DM workflow job ID be printed to the console.
64
+ dm_workflow._update_processing_data()
65
+ job_id = dm_workflow.job_id.get()
66
+ job_stage = dm_workflow.stage_id.get()
67
+ job_status = dm_workflow.status.get()
68
+ print(f"DM workflow id: {job_id!r} status: {job_status} stage: {job_stage}")
69
+
70
+
71
+ def dm_list_processing_jobs(exclude=None):
72
+ """
73
+ Show all the DM jobs with status not excluded.
74
+
75
+ Excluded status (default): 'done', 'failed'
76
+ """
77
+ yield from bps.null() # make this a plan stub
78
+ api = dm_api_proc()
79
+ if exclude is None:
80
+ exclude = ("done", "failed")
81
+
82
+ for j in api.listProcessingJobs():
83
+ if j["status"] not in exclude:
84
+ print(
85
+ f"id={j['id']!r}"
86
+ f" submitted={j.get('submissionTimestamp')}"
87
+ f" status={j['status']!r}"
88
+ )
89
+
90
+
91
+ def dm_submit_workflow_job(workflowName, argsDict):
92
+ """
93
+ Low-level plan stub to submit a job to a DM workflow.
94
+
95
+ It is recommended to use dm_kickoff_workflow() instead.
96
+ This plan does not share run metadata with DM.
97
+
98
+ PARAMETERS:
99
+
100
+ workflowName (*str*): Name of the DM workflow to be run.
101
+
102
+ argsDict (*dict*): Dictionary of parameters needed by 'workflowName'.
103
+ At minimum, most workflows expect these keys: 'filePath' and
104
+ 'experimentName'. Consult the workflow for the expected
105
+ content of 'argsDict'.
106
+ """
107
+ yield from bps.null() # make this a plan stub
108
+ api = dm_api_proc()
109
+
110
+ job = api.startProcessingJob(api.username, workflowName, argsDict)
111
+ print(f"workflow={workflowName!r} id={job['id']!r}")
@@ -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,76 @@
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 init_bec_peaks
15
+ from apsbits.core.catalog_init import init_catalog
16
+ from apsbits.core.run_engine_init import init_RE
17
+ from apsbits.utils.aps_functions import aps_dm_setup
18
+ from apsbits.utils.config_loaders import get_config
19
+ from apsbits.utils.helper_functions import register_bluesky_magics
20
+ from apsbits.utils.helper_functions import running_in_queueserver
21
+ from apsbits.utils.make_devices import make_devices
22
+
23
+ logger = logging.getLogger(__name__)
24
+ logger.bsdev(__file__)
25
+
26
+ # Get the configuration
27
+ iconfig = get_config()
28
+
29
+ # Configure the session with callbacks, devices, and plans.
30
+ aps_dm_setup(iconfig.get("DM_SETUP_FILE"))
31
+
32
+ if iconfig.get("USE_BLUESKY_MAGICS", False):
33
+ register_bluesky_magics()
34
+
35
+ # Initialize core components
36
+ bec, peaks = init_bec_peaks(iconfig)
37
+ cat = init_catalog(iconfig)
38
+ RE, sd = init_RE(iconfig, bec_instance=bec, cat_instance=cat)
39
+
40
+ # Import optional components based on configuration
41
+ if iconfig.get("NEXUS_DATA_FILES", {}).get("ENABLE", False):
42
+ from .callbacks.nexus_data_file_writer import nxwriter_init
43
+
44
+ nxwriter = nxwriter_init(RE)
45
+
46
+ if iconfig.get("SPEC_DATA_FILES", {}).get("ENABLE", False):
47
+ from .callbacks.spec_data_file_writer import init_specwriter_with_RE
48
+ from .callbacks.spec_data_file_writer import newSpecFile # noqa: F401
49
+ from .callbacks.spec_data_file_writer import spec_comment # noqa: F401
50
+ from .callbacks.spec_data_file_writer import specwriter # noqa: F401
51
+
52
+ init_specwriter_with_RE(RE)
53
+
54
+ # Import all plans
55
+ from .plans import * # noqa
56
+
57
+ # These imports must come after the above setup.
58
+ if running_in_queueserver():
59
+ ### To make all the standard plans available in QS, import by '*', otherwise import
60
+ ### plan by plan.
61
+ from apstools.plans import lineup2 # noqa: F401
62
+ from bluesky.plans import * # noqa: F403
63
+
64
+ else:
65
+ # Import bluesky plans and stubs with prefixes set by common conventions.
66
+ # The apstools plans and utils are imported by '*'.
67
+ from apstools.plans import * # noqa: F403
68
+ from apstools.utils import * # noqa: F403
69
+ from bluesky import plan_stubs as bps # noqa: F401
70
+ from bluesky import plans as bp # noqa: F401
71
+
72
+ from apsbits.utils.controls_setup import oregistry
73
+
74
+ oregistry.clear()
75
+
76
+ RE(make_devices())
@@ -0,0 +1,51 @@
1
+ # Bluesky-Queueserver start-re-manager configuration
2
+ # use:
3
+ # queue-monitor &
4
+ # ./qserver/qs_host.sh start
5
+ #
6
+ # or
7
+ # cd ./qserver
8
+ # start-re-manager --config=./qs-config.yml
9
+ #
10
+ # https://blueskyproject.io/bluesky-queueserver/manager_config.html
11
+
12
+ network:
13
+ redis_addr: localhost:6379
14
+ redis_name_prefix: qs_default
15
+ zmq_control_addr: tcp://*:60615
16
+ zmq_info_addr: tcp://*:60625
17
+ zmq_publish_console: true
18
+
19
+ operation:
20
+ # choices: SILENT QUIET NORMAL VERBOSE
21
+ console_logging_level: NORMAL
22
+
23
+ # emergency_lock_key: custom_lock_key
24
+
25
+ print_console_output: true
26
+
27
+ # choices: NEVER ENVIRONMENT_OPEN ALWAYS
28
+ update_existing_plans_and_devices: ENVIRONMENT_OPEN
29
+
30
+ # choices: NEVER ON_REQUEST ON_STARTUP
31
+ user_group_permissions_reload: ON_STARTUP
32
+
33
+ run_engine:
34
+ # databroker_config: name_of_databroker_config_file
35
+
36
+ # kafka_server: 127.0.0.1:9092
37
+ # kafka_topic: custom_topic_name
38
+
39
+ use_persistent_metadata: true
40
+ zmq_data_proxy_addr: localhost:5567
41
+
42
+ startup:
43
+ keep_re: true
44
+ startup_module: demo_instrument.startup
45
+ existing_plans_and_devices_path: ./
46
+ user_group_permissions_path: ./
47
+
48
+ worker:
49
+ use_ipython_kernel: true
50
+ # ipython_kernel_ip: auto
51
+ ipython_matplotlib: qt5
@@ -0,0 +1,231 @@
1
+ #!/bin/bash
2
+ # file: qs_host.sh
3
+ # Manage the bluesky queueserver host process.
4
+ # Could be in a screen session or run as a direct process.
5
+
6
+ SHELL_SCRIPT_NAME=${BASH_SOURCE:-${0}}
7
+ SCRIPT_DIR="$(dirname $(readlink -f "${SHELL_SCRIPT_NAME}"))"
8
+ CONFIGS_DIR=$(readlink -f "${SCRIPT_DIR}/../demo_instrument/configs")
9
+
10
+ ###-----------------------------
11
+ ### Change program defaults here
12
+
13
+ # Instrument configuration YAML file with databroker catalog name.
14
+ ICONFIG_YML="${CONFIGS_DIR}"/iconfig.yml
15
+
16
+ # Bluesky queueserver configuration YAML file.
17
+ # This file contains the definition of 'redis_addr'. (default: localhost:6379)
18
+ QS_CONFIG_YML="${SCRIPT_DIR}/qs-config.yml"
19
+
20
+ # Host name (from $hostname) where the queueserver host process runs.
21
+ # QS_HOSTNAME=amber.xray.aps.anl.gov # if a specific host is required
22
+ QS_HOSTNAME="$(hostname)"
23
+
24
+ PROCESS=start-re-manager # from the conda environment
25
+ STARTUP_COMMAND="${PROCESS} --config=${QS_CONFIG_YML}"
26
+
27
+ #--------------------
28
+ # internal configuration below
29
+
30
+ # echo "PROCESS=${PROCESS}"
31
+ if [ ! -f $(which "${PROCESS}") ]; then
32
+ echo "PROCESS '${PROCESS}': file not found. CONDA_PREFIX='${CONDA_PREFIX}'"
33
+ exit 1
34
+ fi
35
+
36
+ if [ -z "$STARTUP_DIR" ] ; then
37
+ # If no startup dir is specified, use the directory with this script
38
+ STARTUP_DIR="${SCRIPT_DIR}"
39
+ fi
40
+
41
+ if [ "${DATABROKER_CATALOG}" == "" ]; then
42
+ if [ -f "${ICONFIG_YML}" ]; then
43
+ DATABROKER_CATALOG=$(grep DATABROKER_CATALOG "${ICONFIG_YML}" | awk '{print $NF}')
44
+ # echo "Using catalog ${DATABROKER_CATALOG}"
45
+ fi
46
+ fi
47
+ DEFAULT_SESSION_NAME="bluesky_queueserver-${DATABROKER_CATALOG}"
48
+
49
+ #--------------------
50
+
51
+ SELECTION=${1:-usage}
52
+ SESSION_NAME=${2:-"${DEFAULT_SESSION_NAME}"}
53
+
54
+ # But other management commands will fail if mismatch
55
+ if [ "$(hostname)" != "${QS_HOSTNAME}" ]; then
56
+ echo "Must manage queueserver process on ${QS_HOSTNAME}. This is $(hostname)."
57
+ exit 1
58
+ fi
59
+
60
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
61
+
62
+ # echo "SESSION_NAME = ${SESSION_NAME}"
63
+ # echo "SHELL_SCRIPT_NAME = ${SHELL_SCRIPT_NAME}"
64
+ # echo "STARTUP_COMMAND = ${STARTUP_COMMAND}"
65
+ # echo "STARTUP_DIR = ${STARTUP_DIR}"
66
+
67
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
68
+
69
+ function checkpid() {
70
+ # Assume the process is down until proven otherwise
71
+ PROCESS_DOWN=1
72
+
73
+ MY_UID=$(id -u)
74
+ # The '\$' is needed in the pgrep pattern to select vm7, but not vm7.sh
75
+ MY_PID=$(ps -u | grep "${PROCESS}")
76
+ #!echo "MY_PID=${MY_PID}"
77
+ SCREEN_SESSION="${MY_PID}.${SESSION_NAME}"
78
+
79
+ if [ "${MY_PID}" != "" ] ; then
80
+ SCREEN_PID="${MY_PID}"
81
+
82
+ # At least one instance of the process is running;
83
+ # Find the binary that is associated with this process
84
+ for pid in ${MY_PID}; do
85
+ # compare directories
86
+ BIN_CWD=$(readlink "/proc/${pid}/cwd")
87
+ START_CWD=$(readlink -f "${STARTUP_DIR}")
88
+
89
+ if [ "$BIN_CWD" = "$START_CWD" ] ; then
90
+ # The process is running with PID=$pid from $STARTUP_DIR
91
+ P_PID=$(ps -p "${pid}" -o ppid=)
92
+ # strip leading (and trailing) whitespace
93
+ arr=($P_PID)
94
+ P_PID=${arr[0]}
95
+ SCREEN_SESSION="${P_PID}.${SESSION_NAME}"
96
+ SCREEN_MATCH=$(screen -ls "${SCREEN_SESSION}" | grep "${SESSION_NAME}")
97
+ if [ "${SCREEN_MATCH}" != "" ] ; then
98
+ # process is running in screen
99
+ PROCESS_DOWN=0
100
+ MY_PID=${pid}
101
+ SCREEN_PID=${P_PID}
102
+ break
103
+ fi
104
+ fi
105
+ done
106
+ else
107
+ # process is not running
108
+ PROCESS_DOWN=1
109
+ fi
110
+
111
+ return ${PROCESS_DOWN}
112
+ }
113
+
114
+ function checkup () {
115
+ if ! checkpid; then
116
+ restart
117
+ fi
118
+ }
119
+
120
+ function console () {
121
+ if checkpid; then
122
+ echo "Connecting to ${SCREEN_SESSION}'s screen session"
123
+ # The -r flag will only connect if no one is attached to the session
124
+ #!screen -r "${SESSION_NAME}"
125
+ # The -x flag will connect even if someone is attached to the session
126
+ screen -x "${SCREEN_SESSION}"
127
+ else
128
+ echo "${SCREEN_NAME} is not running"
129
+ fi
130
+ }
131
+
132
+ function exit_if_running() {
133
+ # ensure that multiple, simultaneous processes are not started by this user ID
134
+ MY_UID=$(id -u)
135
+ MY_PID=$(pgrep "${SESSION_NAME}"\$ -u "${MY_UID}")
136
+
137
+ if [ "" != "${MY_PID}" ] ; then
138
+ echo "${SESSION_NAME} is already running (PID=${MY_PID}), won't start a new one"
139
+ exit 1
140
+ fi
141
+ }
142
+
143
+ function restart() {
144
+ stop
145
+ start
146
+ }
147
+
148
+ function run_process() {
149
+ # only use this for diagnostic purposes
150
+ exit_if_running
151
+ cd "${STARTUP_DIR}"
152
+ ${STARTUP_COMMAND}
153
+ }
154
+
155
+ function screenpid() {
156
+ if [ -z "${SCREEN_PID}" ] ; then
157
+ echo
158
+ else
159
+ echo " in a screen session (pid=${SCREEN_PID})"
160
+ fi
161
+ }
162
+
163
+ function start() {
164
+ if checkpid; then
165
+ echo -n "${SCREEN_SESSION} is already running (pid=${MY_PID})"
166
+ screenpid
167
+ else
168
+ if [ ! -f "${CONDA_EXE}" ]; then
169
+ echo "No 'conda' command available."
170
+ exit 1
171
+ fi
172
+ echo "Starting ${SESSION_NAME}"
173
+ cd "${STARTUP_DIR}"
174
+ # Run SESSION_NAME inside a screen session
175
+ CMD="screen -DmS ${SESSION_NAME} -h 5000 ${STARTUP_COMMAND}"
176
+ ${CMD} &
177
+ fi
178
+ }
179
+
180
+ function status() {
181
+ if checkpid; then
182
+ echo -n "${SCREEN_SESSION} is running (pid=${MY_PID})"
183
+ screenpid
184
+ else
185
+ echo "${SESSION_NAME} is not running"
186
+ fi
187
+ }
188
+
189
+ function stop() {
190
+ if checkpid; then
191
+ echo "Stopping ${SCREEN_SESSION} (pid=${MY_PID})"
192
+ kill "${MY_PID}"
193
+ else
194
+ echo "${SESSION_NAME} is not running"
195
+ fi
196
+ }
197
+
198
+ function usage() {
199
+ echo "Usage: $(basename "${SHELL_SCRIPT_NAME}") {start|stop|restart|status|checkup|console|run} [NAME]"
200
+ echo ""
201
+ echo " COMMANDS"
202
+ echo " console attach to process console if process is running in screen"
203
+ echo " checkup check that process is running, restart if not"
204
+ echo " restart restart process"
205
+ echo " run run process in console (not screen)"
206
+ echo " start start process"
207
+ echo " status report if process is running"
208
+ echo " stop stop process"
209
+ echo ""
210
+ echo " OPTIONAL TERMS"
211
+ echo " NAME name of process (default: ${DEFAULT_SESSION_NAME})"
212
+ }
213
+
214
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
215
+
216
+ case ${SELECTION} in
217
+ start) start ;;
218
+ stop | kill) stop ;;
219
+ restart) restart ;;
220
+ status) status ;;
221
+ checkup) checkup ;;
222
+ console) console ;;
223
+ run) run_process ;;
224
+ *) usage ;;
225
+ esac
226
+
227
+ # -----------------------------------------------------------------------------
228
+ # :author: BCDA
229
+ # :copyright: (c) 2017-2025, UChicago Argonne, LLC
230
+ # The full license is in the file LICENSE, distributed with this software.
231
+ # -----------------------------------------------------------------------------
@@ -0,0 +1,46 @@
1
+ user_groups:
2
+ root: # The group includes all available plan and devices
3
+ allowed_plans:
4
+ - null # Allow all
5
+ forbidden_plans:
6
+ - ":^_" # All plans with names starting with '_'
7
+ allowed_devices:
8
+ - null # Allow all
9
+ forbidden_devices:
10
+ - ":^_:?.*" # All devices with names starting with '_'
11
+ allowed_functions:
12
+ - null # Allow all
13
+ forbidden_functions:
14
+ - ":^_" # All functions with names starting with '_'
15
+ primary: # The group includes beamline staff, includes all or most of the plans and devices
16
+ allowed_plans:
17
+ - ":.*" # Different way to allow all plans.
18
+ forbidden_plans:
19
+ - null # Nothing is forbidden
20
+ allowed_devices:
21
+ - ":?.*:depth=5" # Allow all device and subdevices. Maximum deepth for subdevices is 5.
22
+ forbidden_devices:
23
+ - null # Nothing is forbidden
24
+ allowed_functions:
25
+ - "function_sleep" # Explicitly listed name
26
+ test_user: # Users with limited access capabilities
27
+ allowed_plans:
28
+ - ":^count" # Use regular expression patterns
29
+ - ":scan$"
30
+ forbidden_plans:
31
+ - ":^adaptive_scan$" # Use regular expression patterns
32
+ - ":^inner_product"
33
+ allowed_devices:
34
+ - ":^det:?.*" # Use regular expression patterns
35
+ - ":^motor:?.*"
36
+ - ":^sim_bundle_A:?.*"
37
+ forbidden_devices:
38
+ - ":^det[3-5]$:?.*" # Use regular expression patterns
39
+ - ":^motor\\d+$:?.*"
40
+ allowed_functions:
41
+ - ":element$"
42
+ - ":elements$"
43
+ - "function_sleep"
44
+ - "clear_buffer"
45
+ forbidden_functions:
46
+ - ":^_" # All functions with names starting with '_'
@@ -0,0 +1 @@
1
+ """Test code for minimal instrument package."""
@@ -0,0 +1,39 @@
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 pathlib import Path
13
+ from typing import Any
14
+
15
+ import pytest
16
+
17
+ from apsbits.demo_instrument.startup import RE
18
+ from apsbits.demo_instrument.startup import make_devices
19
+ from apsbits.utils.config_loaders import load_config
20
+
21
+
22
+ @pytest.fixture(scope="session")
23
+ def runengine_with_devices() -> Any:
24
+ """
25
+ Initialize the RunEngine with devices for testing.
26
+
27
+ This fixture calls RE with the `make_devices()` plan stub to mimic
28
+ the behavior previously performed in the startup module.
29
+
30
+ Returns:
31
+ Any: An instance of the RunEngine with devices configured.
32
+ """
33
+ # Load the configuration before testing
34
+ instrument_path = Path(__file__).parent.parent / "demo_instrument"
35
+ iconfig_path = instrument_path / "configs" / "iconfig.yml"
36
+ load_config(iconfig_path)
37
+
38
+ RE(make_devices())
39
+ return RE