Skip to content

Commit 374b4cb

Browse files
authored
Merge pull request #215 from BCDA-APS/214-replay
add replay(): replay previous scan(s)
2 parents 9445c23 + a4bba0a commit 374b4cb

File tree

5 files changed

+136
-40
lines changed

5 files changed

+136
-40
lines changed

CHANGES.rst

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Change History
66

77
:1.1.11: release *tba* :
88

9+
* `#214 <https://github.com/BCDA-APS/apstools/issues/214>`_
10+
new: ``apstools.utils.APS_utils.replay()``
911
* `#213 <https://github.com/BCDA-APS/apstools/issues/213>`_
1012
``list_recent_scans`` show ``exit_status``
1113
* `#212 <https://github.com/BCDA-APS/apstools/issues/212>`_
@@ -38,7 +40,7 @@ Change History
3840
* `#196 <https://github.com/BCDA-APS/apstools/issues/196>`_
3941
`spec2ophyd` handle MOTPAR:read_misc_1
4042
* `#194 <https://github.com/BCDA-APS/apstools/issues/194>`_
41-
show table of global ophyd `Signal`s and `Device`s
43+
new ``show_ophyd_symbols`` shows table of global ophyd `Signal`s and `Device`s
4244
* `#193 <https://github.com/BCDA-APS/apstools/issues/193>`_
4345
`spec2ophyd` ignore None items in SPEC config file
4446
* `#192 <https://github.com/BCDA-APS/apstools/issues/192>`_
@@ -52,20 +54,20 @@ Change History
5254

5355
* `DEPRECATION <https://github.com/BCDA-APS/apstools/issues/90#issuecomment-483405890>`_
5456
`apstools.plans.run_blocker_in_plan()` will be removed by 2019-12-31.
55-
`Do not write blocking code in bluesky plans.
57+
Do not write blocking code in bluesky plans.
5658
* Dropped python 3.5 from supported versions
5759
* `#175 <https://github.com/BCDA-APS/apstools/issues/175>`_
5860
move `plans.run_in_thread()` to `utils.run_in_thread()`
5961
* `#168 <https://github.com/BCDA-APS/apstools/issues/168>`_
60-
add module to migrate SPEC config file to ophyd setup
62+
new `spec2ophyd` migrates SPEC config file to ophyd setup
6163
* `#166 <https://github.com/BCDA-APS/apstools/issues/166>`_
6264
`device_read2table()`: format `device.read()` results in a pyRestTable.Table
6365
* `#161 <https://github.com/BCDA-APS/apstools/issues/161>`_
6466
`addDeviceDataAsStream()`: add Device as named document stream event
6567
* `#159 <https://github.com/BCDA-APS/apstools/issues/159>`_
6668
convert xlrd.XLRDError into apstools.utils.ExcelReadError
6769
* `#158 <https://github.com/BCDA-APS/apstools/issues/158>`_
68-
run a command list from text file or Excel spreadsheet
70+
new ``run_command_file()`` runs a command list from text file or Excel spreadsheet
6971

7072
:1.1.6: released *2019-05-26*
7173

apstools/migration/spec2ophyd.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
read SPEC config file and convert to ophyd setup commands
55
66
output of ophyd configuration to stdout
7+
8+
*new in apstools release 1.1.7*
79
"""
810

911

apstools/plans.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ def execute_command_list(filename, commands, md={}):
127127
~summarize_command_file
128128
~parse_Excel_command_file
129129
~parse_text_command_file
130+
131+
*new in apstools release 1.1.7*
130132
"""
131133
full_filename = os.path.abspath(filename)
132134

@@ -179,6 +181,7 @@ def get_command_list(filename):
179181
~parse_Excel_command_file
180182
~parse_text_command_file
181183
184+
*new in apstools release 1.1.7*
182185
"""
183186
full_filename = os.path.abspath(filename)
184187
if not os.path.exists(full_filename):
@@ -376,7 +379,7 @@ def parse_Excel_command_file(filename):
376379
~summarize_command_file
377380
~parse_text_command_file
378381
379-
382+
*new in apstools release 1.1.7*
380383
"""
381384
full_filename = os.path.abspath(filename)
382385
assert os.path.exists(full_filename)
@@ -457,6 +460,7 @@ def parse_text_command_file(filename):
457460
~summarize_command_file
458461
~parse_Excel_command_file
459462
463+
*new in apstools release 1.1.7*
460464
"""
461465
full_filename = os.path.abspath(filename)
462466
assert os.path.exists(full_filename)
@@ -504,6 +508,7 @@ def register_command_handler(handler=None):
504508
~parse_Excel_command_file
505509
~parse_text_command_file
506510
511+
*new in apstools release 1.1.7*
507512
"""
508513
global _COMMAND_HANDLER_
509514
_COMMAND_HANDLER_ = handler or execute_command_list
@@ -527,6 +532,7 @@ def run_command_file(filename, md={}):
527532
~parse_Excel_command_file
528533
~parse_text_command_file
529534
535+
*new in apstools release 1.1.7*
530536
"""
531537
commands = get_command_list(filename)
532538
yield from _COMMAND_HANDLER_(filename, commands)
@@ -617,6 +623,7 @@ def summarize_command_file(filename):
617623
~parse_Excel_command_file
618624
~parse_text_command_file
619625
626+
*new in apstools release 1.1.7*
620627
"""
621628
commands = get_command_list(filename)
622629
print(f"Command file: {filename}")

apstools/utils.py

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
~pairwise
2121
~print_snapshot_list
2222
~print_RE_md
23+
~replay
2324
~run_in_thread
2425
~show_ophyd_symbols
2526
~split_quoted_line
@@ -40,7 +41,9 @@
4041
# The full license is in the file LICENSE.txt, distributed with this software.
4142
#-----------------------------------------------------------------------------
4243

44+
from bluesky.callbacks.best_effort import BestEffortCallback
4345
from collections import OrderedDict
46+
import databroker
4447
import datetime
4548
from email.mime.text import MIMEText
4649
from event_model import NumpyEncoder
@@ -190,10 +193,12 @@ def list_recent_scans(num=20, keys=[], printing=True, show_command=False, db=Non
190193
191194
PARAMETERS
192195
193-
num : int (default: ``20``)
196+
num : int
194197
Make the table include the ``num`` most recent scans.
195-
keys : [str] (default: ``[]``)
198+
(default: ``20``)
199+
keys : [str]
196200
Include these additional keys from the start document.
201+
(default: ``[]``)
197202
198203
Two special keys are supported:
199204
@@ -203,13 +208,16 @@ def list_recent_scans(num=20, keys=[], printing=True, show_command=False, db=Non
203208
Also, it will be truncated so that it is no more than 40 characters.)
204209
* ``exit_status`` : from the stop document
205210
206-
printing : bool (default: ``True``)
211+
printing : bool
207212
If True, print the table to stdout
208-
show_command : bool (default: ``False``)
213+
(default: ``True``)
214+
show_command : bool
209215
If True, show the (reconstructed) full command,
210216
but truncate it to no more than 40 characters)
211-
db : object (default: ``db`` from the IPython shell)
217+
(default: ``False``)
218+
db : object
212219
Instance of ``databroker.Broker()``
220+
(default: ``db`` from the IPython shell)
213221
214222
RETURNS
215223
@@ -229,13 +237,9 @@ def list_recent_scans(num=20, keys=[], printing=True, show_command=False, db=Non
229237
f17f026 2019-07-25 16:19:04.929030 149 count testing 4845
230238
========= ========================== ======= ========= =========== =====
231239
240+
*new in apstools release 1.1.10*
232241
"""
233-
try:
234-
from IPython import get_ipython
235-
global_db = get_ipython().user_ns["db"]
236-
except AttributeError as _exc:
237-
global_db = None
238-
db = db or global_db
242+
db = db or ipython_shell_namespace()["db"]
239243

240244
if show_command:
241245
labels = "scan_id command".split() + keys
@@ -299,12 +303,7 @@ def print_RE_md(dictionary=None, fmt="simple", printing=True):
299303
======================== ===================================
300304
301305
"""
302-
try:
303-
from IPython import get_ipython
304-
RE = get_ipython().user_ns["RE"]
305-
except AttributeError as _exc:
306-
RE = None
307-
dictionary = dictionary or RE.md
306+
dictionary = dictionary or ipython_shell_namespace()["RE"].md
308307
md = dict(dictionary) # copy of input for editing
309308
v = dictionary_table(md["versions"], fmt=fmt) # sub-table
310309
md["versions"] = str(v).rstrip()
@@ -335,6 +334,42 @@ def pairwise(iterable):
335334
return zip(a, a)
336335

337336

337+
def replay(headers, callback=None):
338+
"""
339+
replay the document stream from one (or more) scans (headers)
340+
341+
PARAMETERS
342+
343+
headers: scan or [scan]
344+
Scan(s) to be replayed through callback.
345+
A *scan* is an instance of a Bluesky `databroker.Header`.
346+
see: https://nsls-ii.github.io/databroker/api.html?highlight=header#header-api
347+
348+
callback: scan or [scan]
349+
The Bluesky callback to handle the stream of documents from a scan.
350+
If `None`, then use the `bec` (BestEffortCallback) from the IPython shell.
351+
(default:`None`)
352+
353+
*new in apstools release 1.1.11*
354+
"""
355+
callback = callback or ipython_shell_namespace().get(
356+
"bec", # get from IPython shell
357+
BestEffortCallback(), # make one, if we must
358+
)
359+
if isinstance(headers, databroker.Header):
360+
headers = tuple(headers)
361+
for h in headers:
362+
if not isinstance(h, databroker.Header):
363+
emsg = f"Must be a databroker Header: received: {type(h)}: |{h}|"
364+
raise TypeError(emsg)
365+
cmd = _rebuild_scan_command(h.start)
366+
logger.debug(f"{cmd}")
367+
368+
# at last, this is where the real action happens
369+
for k, doc in h.documents(): # get the stream
370+
callback(k, doc) # play it through the callback
371+
372+
338373
def run_in_thread(func):
339374
"""
340375
(decorator) run ``func`` in thread
@@ -364,15 +399,19 @@ def show_ophyd_symbols(show_pv=True, printing=True, verbose=False, symbols=None)
364399
365400
PARAMETERS
366401
367-
show_pv: bool (default: True)
402+
show_pv: bool
368403
If True, also show relevant EPICS PV, if available.
369-
printing: bool (default: True)
404+
(default: True)
405+
printing: bool
370406
If True, print table to stdout.
371-
verbose: bool (default: False)
407+
(default: True)
408+
verbose: bool
372409
If True, also show ``str(obj``.
373-
symbols: dict (default: `globals()`)
410+
(default: False)
411+
symbols: dict
374412
If None, use global symbol table.
375413
If not None, use provided dictionary.
414+
(default: `globals()`)
376415
377416
RETURNS
378417
@@ -402,6 +441,8 @@ def show_ophyd_symbols(show_pv=True, printing=True, verbose=False, symbols=None)
402441
Out[1]: <pyRestTable.rest_table.Table at 0x7fa4398c7cf8>
403442
404443
In [2]:
444+
445+
*new in apstools release 1.1.8*
405446
"""
406447
table = pyRestTable.Table()
407448
table.labels = ["name", "ophyd structure"]
@@ -410,12 +451,14 @@ def show_ophyd_symbols(show_pv=True, printing=True, verbose=False, symbols=None)
410451
if verbose:
411452
table.addLabel("object representation")
412453
table.addLabel("label(s)")
413-
try:
414-
from IPython import get_ipython
415-
g = get_ipython().user_ns
416-
except AttributeError as _exc:
417-
g = globals()
418-
g = symbols or g
454+
if symbols is None:
455+
# the default choice
456+
g = ipython_shell_namespace()
457+
if len(g) == 0:
458+
# ultimate fallback
459+
g = globals()
460+
else:
461+
g = symbols
419462
for k, v in sorted(g.items()):
420463
if isinstance(v, (ophyd.Signal, ophyd.Device)):
421464
row = [k, v.__class__.__name__]
@@ -846,6 +889,18 @@ def ipython_profile_name():
846889
return get_ipython().profile
847890

848891

892+
def ipython_shell_namespace():
893+
"""
894+
get the IPython shell's namespace dictionary (or empty if not found)
895+
"""
896+
try:
897+
from IPython import get_ipython
898+
ns = get_ipython().user_ns
899+
except AttributeError as _exc:
900+
ns = {}
901+
return ns
902+
903+
849904
def print_snapshot_list(db, **search_criteria):
850905
"""
851906
print (stdout) a list of all snapshots in the databroker

tests/test_utils.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,29 +172,59 @@ def test_show_ophyd_symbols(self):
172172
if k not in wont_show:
173173
self.assertTrue(k in rr, msg)
174174
self.assertEqual(num, len(table.rows))
175-
176-
def test_list_recent_scans(self):
175+
176+
177+
class Test_With_Database(unittest.TestCase):
178+
179+
def setUp(self):
177180
from tests.test_export_json import get_db
178-
db = get_db()
179-
headers = db(plan_name="count")
181+
self.db = get_db()
182+
183+
def tearDown(self):
184+
pass
185+
186+
def test_list_recent_scans(self):
187+
headers = self.db(plan_name="count")
180188
headers = list(headers)[0:1]
181189
self.assertEqual(len(headers), 1)
182190
table = APS_utils.list_recent_scans(
183191
keys=["exit_status",],
184192
show_command=True,
185193
printing=False,
186194
num=10,
187-
db=db,
195+
db=self.db,
188196
)
189197
self.assertIsNotNone(table)
190-
self.assertEqual(len(table.labels), 5, "asked for 2 extra columns (total 5)")
191-
self.assertEqual(len(table.rows), 10, "asked for 10 rows")
192-
self.assertLessEqual(len(table.rows[1][3]), 40, "command row should be 40 char or less")
198+
self.assertEqual(
199+
len(table.labels),
200+
5,
201+
"asked for 2 extra columns (total 5)")
202+
self.assertEqual(
203+
len(table.rows),
204+
10,
205+
"asked for 10 rows")
206+
self.assertLessEqual(
207+
len(table.rows[1][3]),
208+
40,
209+
"command row should be 40 char or less")
210+
211+
def test_replay(self):
212+
replies = []
213+
def my_cb(key, doc):
214+
replies.append((key, len(doc)))
215+
APS_utils.replay(self.db(plan_name="count"), callback=my_cb)
216+
217+
self.assertGreater(len(replies), 0)
218+
keys = set([v[0] for v in replies])
219+
for item in "start stop event descriptor datum resource".split():
220+
msg = f"{item} not in {keys}"
221+
self.assertIn(item, keys, msg)
193222

194223

195224
def suite(*args, **kw):
196225
test_list = [
197226
Test_Utils,
227+
Test_With_Database,
198228
]
199229
test_suite = unittest.TestSuite()
200230
for test_case in test_list:

0 commit comments

Comments
 (0)