"""
nscan plan
+++++++++++++++++++++++++++++++++++++++
.. autosummary::
~CommandFileReadError
~command_list_as_table
~execute_command_list
~get_command_list
~parse_Excel_command_file
~parse_text_command_file
~register_command_handler
~run_command_file
~summarize_command_file
"""
import logging
import pathlib
import pyRestTable
from bluesky import plan_stubs as bps
from .. import utils
logger = logging.getLogger(__name__)
logger.info(__file__)
[docs]class CommandFileReadError(IOError):
"""
Exception when reading a command file.
.. index:: Bluesky Exception; CommandFileReadError
"""
[docs]def command_list_as_table(commands, show_raw=False):
"""
format a command list as a pyRestTable.Table object
"""
tbl = pyRestTable.Table()
tbl.addLabel("line #")
tbl.addLabel("action")
tbl.addLabel("parameters")
if show_raw: # only the developer might use this
tbl.addLabel("raw input")
for command in commands:
action, args, line_number, raw_command = command
row = [line_number, action, ", ".join(map(str, args))]
if show_raw:
row.append(str(raw_command))
tbl.addRow(row)
return tbl
[docs]def execute_command_list(filename, commands, md=None):
"""
plan: execute the command list
.. index:: Bluesky Plan; execute_command_list
The command list is a tuple described below.
* Only recognized commands will be executed.
* Unrecognized commands will be reported as comments.
See example implementation with APS USAXS instrument:
https://github.com/APS-USAXS/ipython-usaxs/blob/5db882c47d935c593968f1e2144d35bec7d0181e/profile_bluesky/startup/50-plans.py#L381-L469
PARAMETERS
filename
*str* :
Name of input text file. Can be relative or absolute path,
such as "actions.txt", "../sample.txt", or
"/path/to/overnight.txt".
commands
*[command]* :
List of command tuples for use in ``execute_command_list()``
where
command
*tuple* :
(action, OrderedDict, line_number, raw_command)
action
*str* :
names a known action to be handled
parameters
*list* :
List of parameters for the action.
The list is empty of there are no values
line_number
*int* :
line number (1-based) from the input text file
raw_command
*str* or *[str]* :
contents from input file, such as:
``SAXS 0 0 0 blank``
SEE ALSO
.. autosummary::
~execute_command_list
~register_command_handler
~run_command_file
~summarize_command_file
~parse_Excel_command_file
~parse_text_command_file
*new in apstools release 1.1.7*
"""
full_filename = pathlib.Path(filename).absolute()
if len(commands) == 0:
yield from bps.null()
return
text = f"Command file: {filename}\n"
text += str(command_list_as_table(commands))
print(text)
for command in commands:
action, args, i, raw_command = command
print(f"file line {i}: {raw_command}")
_md = {}
_md["full_filename"] = str(full_filename)
_md["filename"] = filename
_md["line_number"] = i
_md["action"] = action
# note: args is shorter than parameters, means the same thing here
_md["parameters"] = args
_md.update(md or {}) # overlay with user-supplied metadata
action = action.lower()
if action == "tune_optics":
# example: yield from tune_optics(md=_md)
emsg = (
f"This is an example. action={raw_command}."
" Must define your own execute_command_list() function"
)
logger.warn(emsg)
yield from bps.null()
else:
print(f"no handling for line {i}: {raw_command}")
[docs]def get_command_list(filename):
"""
return command list from either text or Excel file
SEE ALSO
.. autosummary::
~execute_command_list
~get_command_list
~register_command_handler
~run_command_file
~summarize_command_file
~parse_Excel_command_file
~parse_text_command_file
*new in apstools release 1.1.7*
"""
full_filename = pathlib.Path(filename)
if not full_filename.exists():
raise IOError(f"file not found: {filename}")
try:
commands = parse_Excel_command_file(filename)
except (ValueError, utils.ExcelReadError):
try:
commands = parse_text_command_file(filename)
except ValueError as exc:
raise CommandFileReadError(f"could not read {filename} as command list file: {exc}")
return commands
class _HandlerRegistrar:
"""
Internal use, allows redefinition of execute_command_list().
"""
command = None
[docs]def parse_Excel_command_file(filename):
"""
parse an Excel spreadsheet with commands, return as command list
TEXT view of spreadsheet (Excel file line numbers shown)::
[1] List of sample scans to be run
[2]
[3]
[4] scan sx sy thickness sample name
[5] FlyScan 0 0 0 blank
[6] FlyScan 5 2 0 blank
PARAMETERS
filename
*str* :
Name of input Excel spreadsheet file. Can be relative or absolute path,
such as "actions.xslx", "../sample.xslx", or
"/path/to/overnight.xslx".
RETURNS
list of commands
*[command]* :
List of command tuples for use in ``execute_command_list()``
RAISES
FileNotFoundError
if file cannot be found
SEE ALSO
.. autosummary::
~get_command_list
~register_command_handler
~run_command_file
~summarize_command_file
~parse_text_command_file
*new in apstools release 1.1.7*
"""
full_filename = pathlib.Path(filename)
if not full_filename.exists():
raise FileNotFoundError(full_filename)
xl = utils.ExcelDatabaseFileGeneric(full_filename)
commands = []
if len(xl.db) > 0:
for i, row in enumerate(xl.db.values()):
action, *values = list(row.values())
# trim off any None values from end
while len(values) > 0:
if values[-1] is not None:
break
values = values[:-1]
commands.append((action, values, i + 1, list(row.values())))
return commands
[docs]def parse_text_command_file(filename):
"""
parse a text file with commands, return as command list
* The text file is interpreted line-by-line.
* Blank lines are ignored.
* A pound sign (#) marks the rest of that line as a comment.
* All remaining lines are interpreted as commands with arguments.
Example of text file (no line numbers shown)::
#List of sample scans to be run
# pound sign starts a comment (through end of line)
# action value
mono_shutter open
# action x y width height
uslits 0 0 0.4 1.2
# action sx sy thickness sample name
FlyScan 0 0 0 blank
FlyScan 5 2 0 "empty container"
# action sx sy thickness sample name
SAXS 0 0 0 blank
# action value
mono_shutter close
PARAMETERS
filename
*str* :
Name of input text file. Can be relative or absolute path,
such as "actions.txt", "../sample.txt", or
"/path/to/overnight.txt".
RETURNS
list of commands
*[command]* :
List of command tuples for use in ``execute_command_list()``
RAISES
FileNotFoundError
if file cannot be found
SEE ALSO
.. autosummary::
~execute_command_list
~get_command_list
~register_command_handler
~run_command_file
~summarize_command_file
~parse_Excel_command_file
*new in apstools release 1.1.7*
"""
full_filename = pathlib.Path(filename)
if not full_filename.exists():
raise FileNotFoundError(full_filename)
with open(full_filename, "r") as fp:
buf = fp.readlines()
commands = []
for i, raw_command in enumerate(buf):
row = raw_command.strip()
if row == "" or row.startswith("#"):
continue # comment or blank
else: # command line
action, *values = utils.split_quoted_line(row)
commands.append((action, values, i + 1, raw_command.rstrip()))
return commands
[docs]def register_command_handler(handler=None):
"""
Define the function called to execute the command list
PARAMETERS
handler *obj* :
Reference of the ``execute_command_list`` function
to be used from :func:`~apstools.plans.run_command_file()`.
If ``None`` or not provided,
will reset to :func:`~apstools.plans.execute_command_list()`,
which is also the initial setting.
SEE ALSO
.. autosummary::
~execute_command_list
~get_command_list
~register_command_handler
~summarize_command_file
~parse_Excel_command_file
~parse_text_command_file
*new in apstools release 1.1.7*
"""
COMMAND_LIST_REGISTRY.command = handler or execute_command_list
[docs]def run_command_file(filename, md=None):
"""
plan: execute a list of commands from a text or Excel file
.. index:: Bluesky Plan; run_command_file
* Parse the file into a command list
* yield the command list to the RunEngine (or other)
SEE ALSO
.. autosummary::
~execute_command_list
~get_command_list
~register_command_handler
~summarize_command_file
~parse_Excel_command_file
~parse_text_command_file
*new in apstools release 1.1.7*
"""
_md = dict(command_file=filename)
_md.update(md or {})
commands = get_command_list(filename)
yield from COMMAND_LIST_REGISTRY.command(filename, commands, md=_md)
[docs]def summarize_command_file(filename):
"""
print the command list from a text or Excel file
SEE ALSO
.. autosummary::
~execute_command_list
~get_command_list
~run_command_file
~parse_Excel_command_file
~parse_text_command_file
*new in apstools release 1.1.7*
"""
commands = get_command_list(filename)
print(f"Command file: {filename}")
print(command_list_as_table(commands))
COMMAND_LIST_REGISTRY = _HandlerRegistrar()
COMMAND_LIST_REGISTRY.command = execute_command_list
# -----------------------------------------------------------------------------
# :author: Pete R. Jemian
# :email: jemian@anl.gov
# :copyright: (c) 2017-2023, UChicago Argonne, LLC
#
# Distributed under the terms of the Argonne National Laboratory Open Source License.
#
# The full license is in the file LICENSE.txt, distributed with this software.
# -----------------------------------------------------------------------------