From 4e539326525d07680f34b54318459f59cf788fa4 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 26 Jun 2020 10:34:26 -0500 Subject: [PATCH 01/47] ENH #333 initial commit --- apstools/beamtime/__init__.py | 0 apstools/beamtime/bss_info.adl | 625 ++++++++++ apstools/beamtime/bss_info.db | 342 ++++++ apstools/beamtime/bss_info.py | 459 ++++++++ apstools/beamtime/bss_info.ui | 1645 +++++++++++++++++++++++++++ apstools/beamtime/bss_info_ophyd.py | 228 ++++ apstools/beamtime/make_database.py | 158 +++ 7 files changed, 3457 insertions(+) create mode 100644 apstools/beamtime/__init__.py create mode 100644 apstools/beamtime/bss_info.adl create mode 100755 apstools/beamtime/bss_info.db create mode 100755 apstools/beamtime/bss_info.py create mode 100644 apstools/beamtime/bss_info.ui create mode 100755 apstools/beamtime/bss_info_ophyd.py create mode 100755 apstools/beamtime/make_database.py diff --git a/apstools/beamtime/__init__.py b/apstools/beamtime/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apstools/beamtime/bss_info.adl b/apstools/beamtime/bss_info.adl new file mode 100644 index 000000000..56a624459 --- /dev/null +++ b/apstools/beamtime/bss_info.adl @@ -0,0 +1,625 @@ + +file { + name="bss_info.adl" + version=030111 +} +display { + object { + x=122 + y=83 + width=400 + height=300 + } + clr=14 + bclr=4 + cmap="" + gridSpacing=5 + gridOn=0 + snapToGrid=0 +} +"color map" { + ncolors=65 + colors { + ffffff, + ececec, + dadada, + c8c8c8, + bbbbbb, + aeaeae, + 9e9e9e, + 919191, + 858585, + 787878, + 696969, + 5a5a5a, + 464646, + 2d2d2d, + 000000, + 00d800, + 1ebb00, + 339900, + 2d7f00, + 216c00, + fd0000, + de1309, + be190b, + a01207, + 820400, + 5893ff, + 597ee1, + 4b6ec7, + 3a5eab, + 27548d, + fbf34a, + f9da3c, + eeb62b, + e19015, + cd6100, + ffb0ff, + d67fe2, + ae4ebc, + 8b1a96, + 610a75, + a4aaff, + 8793e2, + 6a73c1, + 4d52a4, + 343386, + c7bb6d, + b79d5c, + a47e3c, + 7d5627, + 58340f, + 99ffff, + 73dfff, + 4ea5f9, + 2a63e4, + 0a00b8, + ebf1b5, + d4db9d, + bbc187, + a6a462, + 8b8239, + 73ff6b, + 52da3b, + 3cb420, + 289315, + 1a7309, + } +} +text { + object { + x=10 + y=10 + width=380 + height=24 + } + "basic attribute" { + clr=14 + } + textix="APS ESAF & Proposal Info: $(P)" +} +composite { + object { + x=10 + y=39 + width=380 + height=130 + } + "composite name"="" + children { + text { + object { + x=226 + y=45 + width=70 + height=14 + } + "basic attribute" { + clr=14 + } + textix="APS run" + align="horiz. right" + } + "text entry" { + object { + x=301 + y=45 + width=80 + height=14 + } + control { + chan="$(P)esaf:cycle" + clr=14 + bclr=50 + } + limits { + } + } + text { + object { + x=17 + y=44 + width=120 + height=16 + } + "basic attribute" { + clr=14 + } + textix="ESAF" + } + rectangle { + object { + x=10 + y=39 + width=380 + height=130 + } + "basic attribute" { + clr=14 + fill="outline" + width=2 + } + } + "text entry" { + object { + x=75 + y=71 + width=100 + height=14 + } + control { + chan="$(P)esaf:id" + clr=14 + bclr=50 + } + limits { + } + } + text { + object { + x=40 + y=71 + width=30 + height=14 + } + "basic attribute" { + clr=14 + } + textix="ID" + align="horiz. right" + } + "text entry" { + object { + x=247 + y=71 + width=100 + height=12 + } + control { + chan="$(P)esaf:status" + clr=14 + bclr=55 + } + limits { + } + } + text { + object { + x=180 + y=71 + width=61 + height=12 + } + "basic attribute" { + clr=14 + } + textix="status" + align="horiz. right" + } + text { + object { + x=20 + y=90 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="dates" + align="horiz. right" + } + "text entry" { + object { + x=75 + y=90 + width=101 + height=12 + } + control { + chan="$(P)esaf:startDate" + clr=14 + bclr=55 + } + limits { + } + } + text { + object { + x=181 + y=90 + width=20 + height=12 + } + "basic attribute" { + clr=14 + } + textix="to" + align="horiz. right" + } + "text entry" { + object { + x=206 + y=90 + width=101 + height=12 + } + control { + chan="$(P)esaf:endDate" + clr=14 + bclr=55 + } + limits { + } + } + text { + object { + x=20 + y=107 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="title" + align="horiz. right" + } + "text update" { + object { + x=77 + y=107 + width=303 + height=12 + } + monitor { + chan="$(P)esaf:title" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + text { + object { + x=20 + y=124 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="users" + align="horiz. right" + } + "text update" { + object { + x=77 + y=124 + width=303 + height=12 + } + monitor { + chan="$(P)esaf:users" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + "text update" { + object { + x=77 + y=141 + width=303 + height=12 + } + monitor { + chan="$(P)esaf:userBadges" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + text { + object { + x=20 + y=141 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="badges" + align="horiz. right" + } + } +} +composite { + object { + x=10 + y=174 + width=380 + height=120 + } + "composite name"="" + children { + text { + object { + x=17 + y=179 + width=120 + height=16 + } + "basic attribute" { + clr=14 + } + textix="Proposal" + } + rectangle { + object { + x=10 + y=174 + width=380 + height=120 + } + "basic attribute" { + clr=14 + fill="outline" + width=2 + } + } + text { + object { + x=183 + y=181 + width=110 + height=14 + } + "basic attribute" { + clr=14 + } + textix="beamline name" + align="horiz. right" + } + "text entry" { + object { + x=298 + y=181 + width=80 + height=14 + } + control { + chan="$(P)proposal:beamline" + clr=14 + bclr=50 + } + limits { + } + } + text { + object { + x=20 + y=223 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="mail in" + align="horiz. right" + } + text { + object { + x=20 + y=240 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="title" + align="horiz. right" + } + "text update" { + object { + x=77 + y=240 + width=303 + height=12 + } + monitor { + chan="$(P)proposal:title" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + text { + object { + x=20 + y=257 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="users" + align="horiz. right" + } + "text update" { + object { + x=77 + y=257 + width=303 + height=12 + } + monitor { + chan="$(P)proposal:users" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + "text update" { + object { + x=77 + y=274 + width=303 + height=12 + } + monitor { + chan="$(P)proposal:userBadges" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + text { + object { + x=20 + y=274 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="badges" + align="horiz. right" + } + "choice button" { + object { + x=75 + y=224 + width=80 + height=12 + } + control { + chan="$(P)proposal:mailInFlag" + clr=14 + bclr=55 + } + stacking="column" + } + text { + object { + x=160 + y=224 + width=100 + height=12 + } + "basic attribute" { + clr=14 + } + textix="proprietary" + align="horiz. right" + } + "choice button" { + object { + x=265 + y=225 + width=80 + height=12 + } + control { + chan="$(P)proposal:proprietaryFlag" + clr=14 + bclr=55 + } + stacking="column" + } + text { + object { + x=39 + y=204 + width=30 + height=14 + } + "basic attribute" { + clr=14 + } + textix="ID" + align="horiz. right" + } + "text entry" { + object { + x=74 + y=204 + width=100 + height=14 + } + control { + chan="$(P)proposal:id" + clr=14 + bclr=50 + } + limits { + } + } + text { + object { + x=192 + y=205 + width=80 + height=12 + } + "basic attribute" { + clr=14 + } + textix="submitted" + align="horiz. right" + } + "text entry" { + object { + x=277 + y=205 + width=100 + height=12 + } + control { + chan="$(P)proposal:submittedDate" + clr=14 + bclr=55 + } + limits { + } + } + } +} diff --git a/apstools/beamtime/bss_info.db b/apstools/beamtime/bss_info.db new file mode 100755 index 000000000..7ee1d2af0 --- /dev/null +++ b/apstools/beamtime/bss_info.db @@ -0,0 +1,342 @@ +# +# file: bss_info.db +# EPICS database for information from APS ESAF & Proposal databases +# +# BSS: Beamline Scheduling System + +# softIoc -m P=ioc:bss: -d bss_info.db + +# note: this file autogenerated by make_database.py + +record(stringout, "$(P)esaf:cycle") + +record(waveform, "$(P)esaf:description") { + field(FTVL, "CHAR") + field(NELM, 4096) +} + +record(stringout, "$(P)esaf:endDate") + +record(stringout, "$(P)esaf:id") + +record(stringout, "$(P)esaf:status") + +record(stringout, "$(P)esaf:sector") + +record(stringout, "$(P)esaf:startDate") + +record(waveform, "$(P)esaf:title") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(waveform, "$(P)esaf:userBadges") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(waveform, "$(P)esaf:users") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)esaf:user1:badgeNumber") + +record(stringout, "$(P)esaf:user1:email") + +record(stringout, "$(P)esaf:user1:firstName") + +record(stringout, "$(P)esaf:user1:lastName") + +record(stringout, "$(P)esaf:user2:badgeNumber") + +record(stringout, "$(P)esaf:user2:email") + +record(stringout, "$(P)esaf:user2:firstName") + +record(stringout, "$(P)esaf:user2:lastName") + +record(stringout, "$(P)esaf:user3:badgeNumber") + +record(stringout, "$(P)esaf:user3:email") + +record(stringout, "$(P)esaf:user3:firstName") + +record(stringout, "$(P)esaf:user3:lastName") + +record(stringout, "$(P)esaf:user4:badgeNumber") + +record(stringout, "$(P)esaf:user4:email") + +record(stringout, "$(P)esaf:user4:firstName") + +record(stringout, "$(P)esaf:user4:lastName") + +record(stringout, "$(P)esaf:user5:badgeNumber") + +record(stringout, "$(P)esaf:user5:email") + +record(stringout, "$(P)esaf:user5:firstName") + +record(stringout, "$(P)esaf:user5:lastName") + +record(stringout, "$(P)esaf:user6:badgeNumber") + +record(stringout, "$(P)esaf:user6:email") + +record(stringout, "$(P)esaf:user6:firstName") + +record(stringout, "$(P)esaf:user6:lastName") + +record(stringout, "$(P)esaf:user7:badgeNumber") + +record(stringout, "$(P)esaf:user7:email") + +record(stringout, "$(P)esaf:user7:firstName") + +record(stringout, "$(P)esaf:user7:lastName") + +record(stringout, "$(P)esaf:user8:badgeNumber") + +record(stringout, "$(P)esaf:user8:email") + +record(stringout, "$(P)esaf:user8:firstName") + +record(stringout, "$(P)esaf:user8:lastName") + +record(stringout, "$(P)esaf:user9:badgeNumber") + +record(stringout, "$(P)esaf:user9:email") + +record(stringout, "$(P)esaf:user9:firstName") + +record(stringout, "$(P)esaf:user9:lastName") + +record(stringout, "$(P)proposal:beamline") + +record(bo, "$(P)proposal:mailInFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:id") + +record(bo, "$(P)proposal:proprietaryFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:submittedDate") + +record(waveform, "$(P)proposal:title") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(waveform, "$(P)proposal:userBadges") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(waveform, "$(P)proposal:users") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user1:badgeNumber") + +record(stringout, "$(P)proposal:user1:email") + +record(stringout, "$(P)proposal:user1:firstName") + +record(waveform, "$(P)proposal:user1:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user1:instId") + +record(stringout, "$(P)proposal:user1:lastName") + +record(bo, "$(P)proposal:user1:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user1:userId") + +record(stringout, "$(P)proposal:user2:badgeNumber") + +record(stringout, "$(P)proposal:user2:email") + +record(stringout, "$(P)proposal:user2:firstName") + +record(waveform, "$(P)proposal:user2:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user2:instId") + +record(stringout, "$(P)proposal:user2:lastName") + +record(bo, "$(P)proposal:user2:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user2:userId") + +record(stringout, "$(P)proposal:user3:badgeNumber") + +record(stringout, "$(P)proposal:user3:email") + +record(stringout, "$(P)proposal:user3:firstName") + +record(waveform, "$(P)proposal:user3:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user3:instId") + +record(stringout, "$(P)proposal:user3:lastName") + +record(bo, "$(P)proposal:user3:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user3:userId") + +record(stringout, "$(P)proposal:user4:badgeNumber") + +record(stringout, "$(P)proposal:user4:email") + +record(stringout, "$(P)proposal:user4:firstName") + +record(waveform, "$(P)proposal:user4:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user4:instId") + +record(stringout, "$(P)proposal:user4:lastName") + +record(bo, "$(P)proposal:user4:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user4:userId") + +record(stringout, "$(P)proposal:user5:badgeNumber") + +record(stringout, "$(P)proposal:user5:email") + +record(stringout, "$(P)proposal:user5:firstName") + +record(waveform, "$(P)proposal:user5:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user5:instId") + +record(stringout, "$(P)proposal:user5:lastName") + +record(bo, "$(P)proposal:user5:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user5:userId") + +record(stringout, "$(P)proposal:user6:badgeNumber") + +record(stringout, "$(P)proposal:user6:email") + +record(stringout, "$(P)proposal:user6:firstName") + +record(waveform, "$(P)proposal:user6:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user6:instId") + +record(stringout, "$(P)proposal:user6:lastName") + +record(bo, "$(P)proposal:user6:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user6:userId") + +record(stringout, "$(P)proposal:user7:badgeNumber") + +record(stringout, "$(P)proposal:user7:email") + +record(stringout, "$(P)proposal:user7:firstName") + +record(waveform, "$(P)proposal:user7:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user7:instId") + +record(stringout, "$(P)proposal:user7:lastName") + +record(bo, "$(P)proposal:user7:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user7:userId") + +record(stringout, "$(P)proposal:user8:badgeNumber") + +record(stringout, "$(P)proposal:user8:email") + +record(stringout, "$(P)proposal:user8:firstName") + +record(waveform, "$(P)proposal:user8:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user8:instId") + +record(stringout, "$(P)proposal:user8:lastName") + +record(bo, "$(P)proposal:user8:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user8:userId") + +record(stringout, "$(P)proposal:user9:badgeNumber") + +record(stringout, "$(P)proposal:user9:email") + +record(stringout, "$(P)proposal:user9:firstName") + +record(waveform, "$(P)proposal:user9:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user9:instId") + +record(stringout, "$(P)proposal:user9:lastName") + +record(bo, "$(P)proposal:user9:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user9:userId") diff --git a/apstools/beamtime/bss_info.py b/apstools/beamtime/bss_info.py new file mode 100755 index 000000000..025906c15 --- /dev/null +++ b/apstools/beamtime/bss_info.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python + +""" +Retrieve specific records from the APS Proposal and ESAF databases + +BSS: Beamline Scheduling System + +EXAMPLES:: + + bss_info current + bss_info esaf 226319 + bss_info proposal 66083 2020-2 9-ID-B,C +""" + +import argparse +import datetime +import dm # APS data management library +from bss_info_ophyd import EpicsBssDevice +import os +import pyRestTable +import sys +import time +import yaml + +DM_APS_DB_WEB_SERVICE_URL = "https://xraydtn01.xray.aps.anl.gov:11236" +CONNECT_TIMEOUT = 5 +POLL_INTERVAL = 0.01 + +api_bss = dm.BssApsDbApi(DM_APS_DB_WEB_SERVICE_URL) +api_esaf = dm.EsafApsDbApi(DM_APS_DB_WEB_SERVICE_URL) + +_cache_ = {} + + +class EpicsNotConnected(Exception): ... + + +def connect_epics(prefix): + t0 = time.time() + t_timeout = t0 + CONNECT_TIMEOUT + bss = EpicsBssDevice(prefix, name="bss") + while not bss.connected and time.time() < t_timeout: + time.sleep(POLL_INTERVAL) + # printColumns(bss.read_attrs, width=40, numColumns=2) + if not bss.connected: + raise EpicsNotConnected( + f"Did not connect with EPICS {prefix} in {CONNECT_TIMEOUT}s") + t_connect = time.time() - t0 + print(f"connected in {t_connect:.03f}s") + return bss + + +def epicsClear(prefix): + """ + clear the EPICS database + """ + print(f"clear EPICS {prefix}") + bss = connect_epics(prefix) + + t0 = time.time() + bss.clear() + t_clear = time.time() - t0 + print(f"cleared in {t_clear:.03f}s") + + +def epicsUpdate(prefix): + """ + update an EPICS database with current ESAF & proposal information + """ + print(f"update EPICS {prefix}") + bss = connect_epics(prefix) + + bss.clear() + cycle = bss.esaf.aps_cycle.get() + beamline = bss.proposal.beamline_name.get() + sector = bss.esaf.sector.get() + esaf_id = bss.esaf.esaf_id.get() + proposal_id = bss.proposal.proposal_id.get() + + if len(beamline) == 0: + raise ValueError( + f"must set beamline name in {bss.proposal.beamline_name.pvname}") + elif beamline not in listAllBeamlines(): + raise ValueError(f"{beamline} is not known") + if len(cycle) == 0: + raise ValueError( + f"must set APS cycle name in {bss.esaf.aps_cycle.pvname}") + elif cycle not in listAllRuns(): + raise ValueError(f"{cycle} is not known") + + bss.clear() + + if len(esaf_id) > 0: + esaf = getEsaf(esaf_id) + bss.esaf.description.put(esaf["description"]) + bss.esaf.title.put(esaf["esafTitle"]) + bss.esaf.esaf_status.put(esaf["esafStatus"]) + bss.esaf.end_date.put(esaf["experimentEndDate"]) + bss.esaf.start_date.put(esaf["experimentStartDate"]) + + bss.esaf.user_last_names.put( + ",".join([user["lastName"] for user in esaf["experimentUsers"]]) + ) + bss.esaf.user_badges.put( + ",".join([user["badge"] for user in esaf["experimentUsers"]]) + ) + for i, user in enumerate(esaf["experimentUsers"]): + obj = getattr(bss.esaf, f"user{i+1}") + obj.badge_number.put(user["badge"]) + obj.email.put(user["email"]) + obj.first_name.put(user["firstName"]) + obj.last_name.put(user["lastName"]) + if i == 9: + break + + if len(proposal_id) > 0: + proposal = getProposal(proposal_id, cycle, beamline) + bss.proposal.mail_in_flag.put(proposal["mailInFlag"] in ("Y", "y")) + bss.proposal.proprietary_flag.put(proposal["proprietaryFlag"] in ("Y", "y")) + bss.proposal.submitted_date.put(proposal["submittedDate"]) + bss.proposal.title.put(proposal["title"]) + + bss.proposal.user_last_names.put( + ",".join([user["lastName"] for user in proposal["experimenters"]]) + ) + bss.proposal.user_badges.put( + ",".join([user["badge"] for user in proposal["experimenters"]]) + ) + for i, user in enumerate(proposal["experimenters"]): + obj = getattr(bss.proposal, f"user{i+1}") + obj.badge_number.put(user["badge"]) + obj.email.put(user["email"]) + obj.first_name.put(user["firstName"]) + obj.last_name.put(user["lastName"]) + obj.institution.put(user["institution"]) + obj.institution_id.put(str(user["instId"])) + obj.user_id.put(str(user["id"])) + obj.pi_flag.put(user.get("piFlag") in ("Y", "y")) + if i == 9: + break + + +def epicsSetup(prefix, beamline, cycle=None): + if beamline not in listAllBeamlines(): + raise ValueError(f"{beamline} is not known") + if cycle not in listAllRuns(): + raise ValueError(f"{cycle} is not known") + + bss = connect_epics(prefix) + + cycle = cycle or getCurrentCycle() + sector = int(beamline.split("-")[0]) + print(f"setup EPICS {prefix} {beamline} cycle={cycle} sector={sector}") + + bss.clear() + bss.esaf.aps_cycle.put(cycle) + bss.proposal.beamline_name.put(beamline) + bss.esaf.sector.put(str(sector)) + + +def getCurrentCycle(): + return api_bss.getCurrentRun()["name"] + + +def getCurrentEsafs(sector): + if isinstance(sector, int): + sector = f"{sector:02d}" + if len(sector) == 1: + sector = "0" + sector + tNow = datetime.datetime.now() + esafs = api_esaf.listEsafs(sector=sector, year=tNow.year) + results = [] + for esaf in esafs: + if tNow < iso2datetime(esaf["experimentStartDate"]): + continue + if tNow > iso2datetime(esaf["experimentEndDate"]): + continue + results.append(esaf) + return results + + +def getCurrentInfo(beamline): + sector = beamline.split("-")[0] + tNow = datetime.datetime.now() + + matches = [] + for esaf in api_esaf.listEsafs(sector=sector, year=tNow.year): + print(f"ESAF {esaf['esafId']}: {esaf['esafTitle']}") + esaf_badges = [user["badge"] for user in esaf["experimentUsers"]] + for run in listRecentRuns(): + for proposal in api_bss.listProposals(beamlineName=beamline, runName=run): + print(f"proposal {proposal['id']}: {proposal['title']}") + proposal["experimenters"] + user["badge"] + count = 0 + for user in proposal["experimenters"]: + if user["badge"] in esaf_badges: + count += 1 + if count > 0: + matches.append( + dict( + esaf=esaf, + proposal=proposal, + num_true=count, + num_esaf_badges=len(esaf_badges), + num_proposal_badges=len(findings), + ) + ) + return matches + + +def getCurrentProposals(beamline): + proposals = [] + for cycle in listRecentRuns(): + for prop in api_bss.listProposals(beamlineName=beamline, runName=cycle): + prop = dict(prop) + prop["cycle"] = cycle + proposals.append(prop) + return proposals + + +def getEsaf(esafId): + try: + record = api_esaf.getEsaf(int(esafId)) + except dm.ObjectNotFound: + raise EsafNotFound(esafId) + return dict(record.data) + + +def getProposal(proposalId, runName, beamlineName): + # avoid possible dm.DmException + if runName not in listAllRuns(): + raise DmRecordNotFound(f"run '{runName}' not found") + + if beamlineName not in listAllBeamlines(): + raise DmRecordNotFound(f"beamline '{beamlineName}' not found") + + try: + record = api_bss.getProposal(str(proposalId), runName, beamlineName) + except dm.ObjectNotFound: + raise ProposalNotFound( + f"id={proposalId}" + f" run={runName}" + f" beamline={beamlineName}" + ) + return dict(record.data) + + +def iso2datetime(isodate): + return datetime.datetime.fromisoformat(isodate) + + +def listAllBeamlines(): + if "beamlines" not in _cache_: + _cache_["beamlines"] = [ + entry["name"] + for entry in api_bss.listBeamlines() + ] + return _cache_["beamlines"] + + +def listAllRuns(): + if "cycles" not in _cache_: + _cache_["cycles"] = sorted([ + entry["name"] + for entry in api_bss.listRuns() + ]) + return _cache_["cycles"] + + +def listRecentRuns(quantity=6): + # 6 runs is the duration of a user proposal + tNow = datetime.datetime.now() + runs = [ + run["name"] + for run in api_bss.listRuns() + if iso2datetime(run["startTime"]) <= tNow + ] + return sorted(runs, reverse=True)[:quantity] + + +def printColumns(items, numColumns=5, width=10): + n = len(items) + rows = n // numColumns + if n % numColumns > 0: + rows += 1 + for base in range(0, rows): + row = [ + items[base+k*rows] + for k in range(numColumns) + if base+k*rows < n] + print("".join([f"{s:{width}s}" for s in row])) + + +def trim(text, length=40): + if len(text) > length: + text = text[:length-3] + "..." + return text + + +class DmRecordNotFound(Exception): ... +class EsafNotFound(DmRecordNotFound): ... +class ProposalNotFound(DmRecordNotFound): ... + + +class EsafInfo: + + def __init__(self, esaf): + self.title = esaf["esafTitle"] + self.id = esaf["esafId"] + self.status = esaf["esafStatus"] + self.startDate = esaf["experimentStartDate"] + self.endDate = esaf["experimentEndDate"] + self.sector = esaf["sector"] + users = [u["lastName"] for u in esaf["experimentUsers"]] + self.users = ",".join(sorted(users)) + + +class ScheduleInfo: + + def __init__(self, proposal): + self.others = [] + self.contact = None + self.title = proposal["title"] + self.id = proposal["id"] + for exp in proposal["experimenters"]: + if exp.get("piFlag", "N") == "Y": + self.badge = exp["badge"] + self.email = exp["email"] + self.institution = exp["institution"] + self.contact = f'{exp["firstName"]} {exp["lastName"]}' + else: + self.others.append(exp["lastName"]) + self.others = ",".join(sorted(self.others)) + + +def get_options(): + parser = argparse.ArgumentParser( + prog=os.path.split(sys.argv[0])[-1], + description=__doc__.strip().splitlines()[0], + ) + + subcommand = parser.add_subparsers(dest='subcommand', title='subcommand') + + p_sub = subcommand.add_parser('beamlines', help="print list of beamlines") + + p_sub = subcommand.add_parser('current', help="print current ESAF(s) and proposal(s)") + p_sub.add_argument('beamlineName', type=str, help="Beamline name") + + p_sub = subcommand.add_parser('cycles', help="print APS cycle names") + + p_sub = subcommand.add_parser('esaf', help="print specific ESAF") + p_sub.add_argument('esafId', type=int, help="ESAF ID number") + + p_sub = subcommand.add_parser('proposal', help="print specific proposal") + p_sub.add_argument('proposalId', type=str, help="ESAF ID number") + p_sub.add_argument('cycle', type=str, help="APS run (cycle) name") + p_sub.add_argument('beamlineName', type=str, help="Beamline name") + + p_sub = subcommand.add_parser('clear', help="EPICS PVs: clear") + p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + + p_sub = subcommand.add_parser('setup', help="EPICS PVs: setup") + p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + p_sub.add_argument('beamlineName', type=str, help="Beamline name") + p_sub.add_argument('cycle', type=str, help="APS run (cycle) name") + + p_sub = subcommand.add_parser('update', help="EPICS PVs: update from BSS") + p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + + return parser.parse_args() + + +def main(): + args = get_options() + if args.subcommand == "beamlines": + printColumns(listAllBeamlines(), numColumns=4, width=15) + + elif args.subcommand == "clear": + epicsClear(args.prefix) + + elif args.subcommand == "current": + records = getCurrentProposals(args.beamlineName) + if len(records) == 0: + print(f"No current proposals for {args.beamlineName}") + else: + table = pyRestTable.Table() + table.labels = "id cycle date user(s) title".split() + for item in records: + users = trim(",".join([ + user["lastName"] + for user in item["experimenters"] + ]), 20) + table.addRow(( + item["id"], + item["cycle"], + item["submittedDate"], + users, + trim(item["title"]),)) + print(f"Current Proposal(s) on {args.beamlineName}") + print() + print(table) + + sector = args.beamlineName.split("-")[0] + records = getCurrentEsafs(sector) + if len(records) == 0: + print(f"No current ESAFs for sector {sector}") + else: + table = pyRestTable.Table() + table.labels = "id status start end user(s) title".split() + for item in records: + users = trim( + ",".join([ + user["lastName"] + for user in item["experimentUsers"] + ]), + 20) + table.addRow(( + item["esafId"], + item["esafStatus"], + item["experimentStartDate"].split()[0], + item["experimentEndDate"].split()[0], + users, + trim(item["esafTitle"], 40), + )) + print(f"Current ESAF(s) on sector {sector}") + print() + print(table) + + elif args.subcommand == "cycles": + printColumns(listAllRuns()) + + elif args.subcommand == "esaf": + try: + esaf = getEsaf(args.esafId) + print(yaml.dump(esaf)) + except DmRecordNotFound as exc: + print(exc) + except dm.DmException as exc: + print(f"dm reported: {exc}") + + elif args.subcommand == "proposal": + try: + proposal = getProposal(args.proposalId, args.cycle, args.beamlineName) + print(yaml.dump(proposal)) + except DmRecordNotFound as exc: + print(exc) + except dm.DmException as exc: + print(f"dm reported: {exc}") + + elif args.subcommand == "setup": + epicsSetup(args.prefix, args.beamlineName, args.cycle) + + elif args.subcommand == "update": + epicsUpdate(args.prefix) + + +if __name__ == "__main__": + main() diff --git a/apstools/beamtime/bss_info.ui b/apstools/beamtime/bss_info.ui new file mode 100644 index 000000000..9a46a68e8 --- /dev/null +++ b/apstools/beamtime/bss_info.ui @@ -0,0 +1,1645 @@ + + +MainWindow + + + + 122 + 83 + 400 + 300 + + + + + +QWidget#centralWidget {background: rgba(187, 187, 187, 255);} +QPushButton::menu-indicator {image: url(none.png); width: 0} + + + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + APS ESAF & Proposal Info: $(P) + + + ESimpleLabel::WidthAndHeight + + + + 10 + 10 + 380 + 24 + + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 10 + 39 + 382 + 132 + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + APS run + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 216 + 6 + 70 + 14 + + + + + + + 291 + 6 + 80 + 14 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:cycle + + + + 0 + 0 + 0 + + + + + 153 + 255 + 255 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + ESAF + + + ESimpleLabel::WidthAndHeight + + + + 7 + 5 + 120 + 16 + + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + + + caGraphics::Rectangle + + + + 0 + 0 + 380 + 130 + + + + 2 + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + Solid + + + + + + 65 + 32 + 100 + 14 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:id + + + + 0 + 0 + 0 + + + + + 153 + 255 + 255 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + ID + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 30 + 32 + 30 + 14 + + + + + + + 237 + 32 + 100 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:status + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + status + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 170 + 32 + 61 + 12 + + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + dates + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 51 + 50 + 12 + + + + + + + 65 + 51 + 101 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:startDate + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + to + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 171 + 51 + 20 + 12 + + + + + + + 196 + 51 + 101 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:endDate + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + title + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 68 + 50 + 12 + + + + + + + 67 + 68 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:title + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + users + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 85 + 50 + 12 + + + + + + + 67 + 85 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:users + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + + 67 + 102 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:userBadges + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + badges + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 102 + 50 + 12 + + + + + + + + 10 + 174 + 382 + 122 + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + Proposal + + + ESimpleLabel::WidthAndHeight + + + + 7 + 5 + 120 + 16 + + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + + + caGraphics::Rectangle + + + + 0 + 0 + 380 + 120 + + + + 2 + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + Solid + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + beamline name + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 173 + 7 + 110 + 14 + + + + + + + 288 + 7 + 80 + 14 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:beamline + + + + 0 + 0 + 0 + + + + + 153 + 255 + 255 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + mail in + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 49 + 50 + 12 + + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + title + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 66 + 50 + 12 + + + + + + + 67 + 66 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:title + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + users + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 83 + 50 + 12 + + + + + + + 67 + 83 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:users + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + + 67 + 100 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:userBadges + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + badges + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 100 + 50 + 12 + + + + + + + 65 + 50 + 80 + 12 + + + + $(P)proposal:mailInFlag + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + Column + + + caChoice::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + proprietary + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 150 + 50 + 100 + 12 + + + + + + + 255 + 51 + 80 + 12 + + + + $(P)proposal:proprietaryFlag + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + Column + + + caChoice::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + ID + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 29 + 30 + 30 + 14 + + + + + + + 64 + 30 + 100 + 14 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:id + + + + 0 + 0 + 0 + + + + + 153 + 255 + 255 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + submitted + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 182 + 31 + 80 + 12 + + + + + + + 267 + 31 + 100 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:submittedDate + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + caLabel_0 + caLabel_1 + caLabel_2 + caRectangle_0 + caLabel_3 + caLabel_4 + caLabel_5 + caLabel_6 + caLabel_7 + caLabel_8 + caLabel_9 + caFrame_0 + caLabel_10 + caRectangle_1 + caLabel_11 + caLabel_12 + caLabel_13 + caLabel_14 + caLabel_15 + caLabel_16 + caLabel_17 + caLabel_18 + caFrame_1 + caTextEntry_0 + caTextEntry_1 + caTextEntry_2 + caTextEntry_3 + caTextEntry_4 + caLineEdit_0 + caLineEdit_1 + caLineEdit_2 + caTextEntry_5 + caLineEdit_3 + caLineEdit_4 + caLineEdit_5 + caChoice_0 + caChoice_1 + caTextEntry_6 + caTextEntry_7 + + + \ No newline at end of file diff --git a/apstools/beamtime/bss_info_ophyd.py b/apstools/beamtime/bss_info_ophyd.py new file mode 100755 index 000000000..403306c4e --- /dev/null +++ b/apstools/beamtime/bss_info_ophyd.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python + +""" +ophyd support for bss_info + +EXAMPLE:: + + bss_info = EpicsBssDevice("ioc:bss:", name="bss_info") +""" + +__all__ = ["EpicsBssDevice",] + +from ophyd import Component, Device, EpicsSignal + + +class EpicsEsafExperimenterDevice(Device): + """ + - badge: '64065' + badgeNumber: '64065' + email: kuzmenko@aps.anl.gov + firstName: Ivan + lastName: Kuzmenko + """ + badge_number = Component(EpicsSignal, "badgeNumber", string=True) + email = Component(EpicsSignal, "email", string=True) + first_name = Component(EpicsSignal, "firstName", string=True) + last_name = Component(EpicsSignal, "lastName", string=True) + + def clear(self): + self.badge_number.put("") + self.email.put("") + self.first_name.put("") + self.last_name.put("") + + +class EpicsEsafDevice(Device): + """ + description: We will commission beamline and USAXS instrument. We will perform experiments + with safe beamline standards and test samples (all located at beamline and used + for this purpose routinely) to evaluate performance of beamline and instrument. + We will perform hardware and software development as needed. + esafId: 226319 + esafStatus: Approved + esafTitle: Commission 9ID and USAXS + experimentEndDate: '2020-09-28 08:00:00' + experimentStartDate: '2020-05-26 08:00:00' + experimentUsers: + - badge: '86312' + badgeNumber: '86312' + email: ilavsky@aps.anl.gov + firstName: Jan + lastName: Ilavsky + - badge: '53748' + badgeNumber: '53748' + email: emaxey@aps.anl.gov + firstName: Evan + lastName: Maxey + - badge: '64065' + badgeNumber: '64065' + email: kuzmenko@aps.anl.gov + firstName: Ivan + lastName: Kuzmenko + sector: 09 + """ + + aps_cycle = Component(EpicsSignal, "cycle", string=True) + description = Component(EpicsSignal, "description", string=True) + end_date = Component(EpicsSignal, "endDate", string=True) + esaf_id = Component(EpicsSignal, "id", string=True) + esaf_status = Component(EpicsSignal, "status", string=True) + sector = Component(EpicsSignal, "sector", string=True) + start_date = Component(EpicsSignal, "startDate", string=True) + title = Component(EpicsSignal, "title", string=True) + user_last_names = Component(EpicsSignal, "users", string=True) + user_badges = Component(EpicsSignal, "userBadges", string=True) + + user1 = Component(EpicsEsafExperimenterDevice, "user1:") + user2 = Component(EpicsEsafExperimenterDevice, "user2:") + user3 = Component(EpicsEsafExperimenterDevice, "user3:") + user4 = Component(EpicsEsafExperimenterDevice, "user4:") + user5 = Component(EpicsEsafExperimenterDevice, "user5:") + user6 = Component(EpicsEsafExperimenterDevice, "user6:") + user7 = Component(EpicsEsafExperimenterDevice, "user7:") + user8 = Component(EpicsEsafExperimenterDevice, "user8:") + user9 = Component(EpicsEsafExperimenterDevice, "user9:") + + def clear(self): + # self.aps_cycle.put("") # user controls this + self.description.put("") + self.end_date.put("") + # self.esaf_id.put("") # user controls this + self.esaf_status.put("") + # self.sector.put("") + self.start_date.put("") + self.title.put("") + self.user_last_names.put("") + self.user_badges.put("") + + self.clear_users() + + def clear_users(self): + self.user1.clear() + self.user2.clear() + self.user3.clear() + self.user4.clear() + self.user5.clear() + self.user6.clear() + self.user7.clear() + self.user8.clear() + self.user9.clear() + + +class EpicsProposalExperimenterDevice(Device): + """ + badge: '300679' + email: yilianglin@uchicago.edu + firstName: Yiliang + id: 433796 + instId: 3435 + institution: The University of Chicago + lastName: Lin + piFlag: Y + """ + badge_number = Component(EpicsSignal, "badgeNumber", string=True) + email = Component(EpicsSignal, "email", string=True) + first_name = Component(EpicsSignal, "firstName", string=True) + institution = Component(EpicsSignal, "institution", string=True) + institution_id = Component(EpicsSignal, "instId", string=True) + last_name = Component(EpicsSignal, "lastName", string=True) + pi_flag = Component(EpicsSignal, "piFlag", string=True) + user_id = Component(EpicsSignal, "userId", string=True) + + def clear(self): + self.badge_number.put("") + self.email.put("") + self.first_name.put("") + self.last_name.put("") + self.user_id.put("") + self.institution_id.put("") + self.institution.put("") + self.pi_flag.put(0) + + +class EpicsProposalDevice(Device): + """ + experimenters: + - badge: '292588' + email: fangyin123@uchicago.edu + firstName: Yin + id: 433774 + instId: 3435 + institution: The University of Chicago + lastName: Fang + - badge: '304975' + email: sjiuyun@uchicago.edu + firstName: Jiuyun + id: 433775 + instId: 3435 + institution: The University of Chicago + lastName: Shi + - badge: '300679' + email: yilianglin@uchicago.edu + firstName: Yiliang + id: 433796 + instId: 3435 + institution: The University of Chicago + lastName: Lin + piFlag: Y + id: 66083 + mailInFlag: N + proprietaryFlag: N + submittedDate: '2019-07-04 13:42:32' + title: Mapping the mechanical-responsive conductive network in bioinspired composite + materials with 3D correlative x-ray fluorescence and ptychographic tomography + totalShiftsRequested: 27 + """ + + beamline_name = Component(EpicsSignal, "beamline", string=True) + + mail_in_flag = Component(EpicsSignal, "mailInFlag", string=True) + proposal_id = Component(EpicsSignal, "id", string=True) + proprietary_flag = Component(EpicsSignal, "proprietaryFlag", string=True) + submitted_date = Component(EpicsSignal, "submittedDate", string=True) + title = Component(EpicsSignal, "title", string=True) + user_last_names = Component(EpicsSignal, "users", string=True) + user_badges = Component(EpicsSignal, "userBadges", string=True) + + user1 = Component(EpicsProposalExperimenterDevice, "user1:") + user2 = Component(EpicsProposalExperimenterDevice, "user2:") + user3 = Component(EpicsProposalExperimenterDevice, "user3:") + user4 = Component(EpicsProposalExperimenterDevice, "user4:") + user5 = Component(EpicsProposalExperimenterDevice, "user5:") + user6 = Component(EpicsProposalExperimenterDevice, "user6:") + user7 = Component(EpicsProposalExperimenterDevice, "user7:") + user8 = Component(EpicsProposalExperimenterDevice, "user8:") + user9 = Component(EpicsProposalExperimenterDevice, "user9:") + + def clear(self): + # self.beamline_name.put("") # user controls this + self.mail_in_flag.put(0) + # self.proposal_id.put(-1) # user controls this + self.proprietary_flag.put(0) + self.submitted_date.put("") + self.title.put("") + self.user_last_names.put("") + self.user_badges.put("") + + self.clear_users() + + def clear_users(self): + self.user1.clear() + self.user2.clear() + self.user3.clear() + self.user4.clear() + self.user5.clear() + self.user6.clear() + self.user7.clear() + self.user8.clear() + self.user9.clear() + + +class EpicsBssDevice(Device): + esaf = Component(EpicsEsafDevice, "esaf:") + proposal = Component(EpicsProposalDevice, "proposal:") + + def clear(self): + self.esaf.clear() + self.proposal.clear() diff --git a/apstools/beamtime/make_database.py b/apstools/beamtime/make_database.py new file mode 100755 index 000000000..6c61c076d --- /dev/null +++ b/apstools/beamtime/make_database.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python + +""" +Create the EPICS database +""" + +import os +import sys + +#name RTYP length + +raw_data = """ +esaf:cycle stringout +esaf:description waveform 4096 +esaf:endDate stringout +esaf:id stringout +esaf:status stringout +esaf:sector stringout +esaf:startDate stringout +esaf:title waveform 1024 +esaf:userBadges waveform 1024 +esaf:users waveform 1024 +esaf:user1:badgeNumber stringout +esaf:user1:email stringout +esaf:user1:firstName stringout +esaf:user1:lastName stringout +esaf:user2:badgeNumber stringout +esaf:user2:email stringout +esaf:user2:firstName stringout +esaf:user2:lastName stringout +esaf:user3:badgeNumber stringout +esaf:user3:email stringout +esaf:user3:firstName stringout +esaf:user3:lastName stringout +esaf:user4:badgeNumber stringout +esaf:user4:email stringout +esaf:user4:firstName stringout +esaf:user4:lastName stringout +esaf:user5:badgeNumber stringout +esaf:user5:email stringout +esaf:user5:firstName stringout +esaf:user5:lastName stringout +esaf:user6:badgeNumber stringout +esaf:user6:email stringout +esaf:user6:firstName stringout +esaf:user6:lastName stringout +esaf:user7:badgeNumber stringout +esaf:user7:email stringout +esaf:user7:firstName stringout +esaf:user7:lastName stringout +esaf:user8:badgeNumber stringout +esaf:user8:email stringout +esaf:user8:firstName stringout +esaf:user8:lastName stringout +esaf:user9:badgeNumber stringout +esaf:user9:email stringout +esaf:user9:firstName stringout +esaf:user9:lastName stringout +proposal:beamline stringout +proposal:mailInFlag bo +proposal:id stringout +proposal:proprietaryFlag bo +proposal:submittedDate stringout +proposal:title waveform 1024 +proposal:userBadges waveform 1024 +proposal:users waveform 1024 +proposal:user1:badgeNumber stringout +proposal:user1:email stringout +proposal:user1:firstName stringout +proposal:user1:institution waveform 1024 +proposal:user1:instId stringout +proposal:user1:lastName stringout +proposal:user1:piFlag bo +proposal:user1:userId stringout +proposal:user2:badgeNumber stringout +proposal:user2:email stringout +proposal:user2:firstName stringout +proposal:user2:institution waveform 1024 +proposal:user2:instId stringout +proposal:user2:lastName stringout +proposal:user2:piFlag bo +proposal:user2:userId stringout +proposal:user3:badgeNumber stringout +proposal:user3:email stringout +proposal:user3:firstName stringout +proposal:user3:institution waveform 1024 +proposal:user3:instId stringout +proposal:user3:lastName stringout +proposal:user3:piFlag bo +proposal:user3:userId stringout +proposal:user4:badgeNumber stringout +proposal:user4:email stringout +proposal:user4:firstName stringout +proposal:user4:institution waveform 1024 +proposal:user4:instId stringout +proposal:user4:lastName stringout +proposal:user4:piFlag bo +proposal:user4:userId stringout +proposal:user5:badgeNumber stringout +proposal:user5:email stringout +proposal:user5:firstName stringout +proposal:user5:institution waveform 1024 +proposal:user5:instId stringout +proposal:user5:lastName stringout +proposal:user5:piFlag bo +proposal:user5:userId stringout +proposal:user6:badgeNumber stringout +proposal:user6:email stringout +proposal:user6:firstName stringout +proposal:user6:institution waveform 1024 +proposal:user6:instId stringout +proposal:user6:lastName stringout +proposal:user6:piFlag bo +proposal:user6:userId stringout +proposal:user7:badgeNumber stringout +proposal:user7:email stringout +proposal:user7:firstName stringout +proposal:user7:institution waveform 1024 +proposal:user7:instId stringout +proposal:user7:lastName stringout +proposal:user7:piFlag bo +proposal:user7:userId stringout +proposal:user8:badgeNumber stringout +proposal:user8:email stringout +proposal:user8:firstName stringout +proposal:user8:institution waveform 1024 +proposal:user8:instId stringout +proposal:user8:lastName stringout +proposal:user8:piFlag bo +proposal:user8:userId stringout +proposal:user9:badgeNumber stringout +proposal:user9:email stringout +proposal:user9:firstName stringout +proposal:user9:institution waveform 1024 +proposal:user9:instId stringout +proposal:user9:lastName stringout +proposal:user9:piFlag bo +proposal:user9:userId stringout +""".strip().splitlines() + +for row in raw_data: + parts = row.split() + pvname, rtyp = parts[:2] + head = f'record({rtyp}, "$(P){pvname}")' + fields = [] + if rtyp == "waveform" and len(parts) == 3: + length = parts[-1] + fields.append(f'field(FTVL, "CHAR")') + fields.append(f'field(NELM, {length})') + elif rtyp == "bo": + fields.append(f'field(ZNAM, "OFF")') + fields.append(f'field(ONAM, "ON")') + record = head + newline = "\n" + if len(fields) > 0: + field_specs = "\n".join([f" {field}" for field in fields]) + record += " {\n" + f"{field_specs}" + "\n}" + print(record + "\n") From 606bfb016263ba2c729c18fbce29f51713d1abbf Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 26 Jun 2020 10:39:01 -0500 Subject: [PATCH 02/47] MNT #333 permissions --- apstools/beamtime/bss_info.db | 0 apstools/beamtime/bss_info_ophyd.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 apstools/beamtime/bss_info.db mode change 100755 => 100644 apstools/beamtime/bss_info_ophyd.py diff --git a/apstools/beamtime/bss_info.db b/apstools/beamtime/bss_info.db old mode 100755 new mode 100644 diff --git a/apstools/beamtime/bss_info_ophyd.py b/apstools/beamtime/bss_info_ophyd.py old mode 100755 new mode 100644 From 509a6994afb1338280f358c29d123c42627af448 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 26 Jun 2020 10:47:20 -0500 Subject: [PATCH 03/47] DOC #333 --- apstools/beamtime/bss_info.db | 6 +++--- apstools/beamtime/make_database.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apstools/beamtime/bss_info.db b/apstools/beamtime/bss_info.db index 7ee1d2af0..6f47e742c 100644 --- a/apstools/beamtime/bss_info.db +++ b/apstools/beamtime/bss_info.db @@ -2,12 +2,12 @@ # file: bss_info.db # EPICS database for information from APS ESAF & Proposal databases # -# BSS: Beamline Scheduling System +# BSS: Beamtime Scheduling System +# +# note: this file autogenerated by make_database.py # softIoc -m P=ioc:bss: -d bss_info.db -# note: this file autogenerated by make_database.py - record(stringout, "$(P)esaf:cycle") record(waveform, "$(P)esaf:description") { diff --git a/apstools/beamtime/make_database.py b/apstools/beamtime/make_database.py index 6c61c076d..65de59693 100755 --- a/apstools/beamtime/make_database.py +++ b/apstools/beamtime/make_database.py @@ -138,6 +138,20 @@ proposal:user9:userId stringout """.strip().splitlines() +print("""\ +# +# file: bss_info.db +# EPICS database for information from APS ESAF & Proposal databases +# +# BSS: Beamtime Scheduling System +# +# note: this file autogenerated by make_database.py +# +# Start the EPICS IOC with a command such as: +# softIoc -m P=ioc:bss: -d bss_info.db + +""") + for row in raw_data: parts = row.split() pvname, rtyp = parts[:2] From b73e1a51fb82fbf8fa0fa6c3b87ffd9afa7800e1 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 26 Jun 2020 10:51:41 -0500 Subject: [PATCH 04/47] MNT #333 --- apstools/beamtime/bss_info.db | 5 ++++- apstools/beamtime/{make_database.py => bss_info_makedb.py} | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) rename apstools/beamtime/{make_database.py => bss_info_makedb.py} (97%) diff --git a/apstools/beamtime/bss_info.db b/apstools/beamtime/bss_info.db index 6f47e742c..1d1599f81 100644 --- a/apstools/beamtime/bss_info.db +++ b/apstools/beamtime/bss_info.db @@ -5,8 +5,11 @@ # BSS: Beamtime Scheduling System # # note: this file autogenerated by make_database.py +# ./bss_info_makedb.py | tee bss_info.db +# +# Start the EPICS IOC with a command (from EPICS base): +# softIoc -m P=ioc:bss: -d bss_info.db -# softIoc -m P=ioc:bss: -d bss_info.db record(stringout, "$(P)esaf:cycle") diff --git a/apstools/beamtime/make_database.py b/apstools/beamtime/bss_info_makedb.py similarity index 97% rename from apstools/beamtime/make_database.py rename to apstools/beamtime/bss_info_makedb.py index 65de59693..43f6428d8 100755 --- a/apstools/beamtime/make_database.py +++ b/apstools/beamtime/bss_info_makedb.py @@ -146,10 +146,10 @@ # BSS: Beamtime Scheduling System # # note: this file autogenerated by make_database.py +# ./bss_info_makedb.py | tee bss_info.db # -# Start the EPICS IOC with a command such as: +# Start the EPICS IOC with a command (from EPICS base): # softIoc -m P=ioc:bss: -d bss_info.db - """) for row in raw_data: @@ -169,4 +169,4 @@ if len(fields) > 0: field_specs = "\n".join([f" {field}" for field in fields]) record += " {\n" + f"{field_specs}" + "\n}" - print(record + "\n") + print("\n" + record) From b803527a2d2c5c29d1f69ce21e743092d1875bc8 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 6 Jul 2020 12:26:48 -0500 Subject: [PATCH 05/47] MNT #334 Codacy --- apstools/beamtime/bss_info.py | 157 ++++++++++++++------------- apstools/beamtime/bss_info_makedb.py | 3 - 2 files changed, 84 insertions(+), 76 deletions(-) diff --git a/apstools/beamtime/bss_info.py b/apstools/beamtime/bss_info.py index 025906c15..24af9d6de 100755 --- a/apstools/beamtime/bss_info.py +++ b/apstools/beamtime/bss_info.py @@ -73,7 +73,7 @@ def epicsUpdate(prefix): bss.clear() cycle = bss.esaf.aps_cycle.get() beamline = bss.proposal.beamline_name.get() - sector = bss.esaf.sector.get() + # sector = bss.esaf.sector.get() esaf_id = bss.esaf.esaf_id.get() proposal_id = bss.proposal.proposal_id.get() @@ -188,10 +188,9 @@ def getCurrentInfo(beamline): print(f"ESAF {esaf['esafId']}: {esaf['esafTitle']}") esaf_badges = [user["badge"] for user in esaf["experimentUsers"]] for run in listRecentRuns(): - for proposal in api_bss.listProposals(beamlineName=beamline, runName=run): + for proposal in api_bss.listProposals(beamlineName=beamline, + runName=run): print(f"proposal {proposal['id']}: {proposal['title']}") - proposal["experimenters"] - user["badge"] count = 0 for user in proposal["experimenters"]: if user["badge"] in esaf_badges: @@ -199,11 +198,11 @@ def getCurrentInfo(beamline): if count > 0: matches.append( dict( - esaf=esaf, - proposal=proposal, - num_true=count, - num_esaf_badges=len(esaf_badges), - num_proposal_badges=len(findings), + esaf=esaf, + proposal=proposal, + num_true=count, + num_esaf_badges=len(esaf_badges), + num_proposal_badges=len(proposal["experimenters"]), ) ) return matches @@ -287,7 +286,7 @@ def printColumns(items, numColumns=5, width=10): for base in range(0, rows): row = [ items[base+k*rows] - for k in range(numColumns) + for k in range(numColumns) if base+k*rows < n] print("".join([f"{s:{width}s}" for s in row])) @@ -304,7 +303,7 @@ class ProposalNotFound(DmRecordNotFound): ... class EsafInfo: - + def __init__(self, esaf): self.title = esaf["esafTitle"] self.id = esaf["esafId"] @@ -317,7 +316,7 @@ def __init__(self, esaf): class ScheduleInfo: - + def __init__(self, proposal): self.others = [] self.contact = None @@ -336,7 +335,7 @@ def __init__(self, proposal): def get_options(): parser = argparse.ArgumentParser( - prog=os.path.split(sys.argv[0])[-1], + prog=os.path.split(sys.argv[0])[-1], description=__doc__.strip().splitlines()[0], ) @@ -371,6 +370,75 @@ def get_options(): return parser.parse_args() +def cmd_current(args): + records = getCurrentProposals(args.beamlineName) + if len(records) == 0: + print(f"No current proposals for {args.beamlineName}") + else: + table = pyRestTable.Table() + table.labels = "id cycle date user(s) title".split() + for item in records: + users = trim(",".join([ + user["lastName"] + for user in item["experimenters"] + ]), 20) + table.addRow(( + item["id"], + item["cycle"], + item["submittedDate"], + users, + trim(item["title"]),)) + print(f"Current Proposal(s) on {args.beamlineName}") + print() + print(table) + + sector = args.beamlineName.split("-")[0] + records = getCurrentEsafs(sector) + if len(records) == 0: + print(f"No current ESAFs for sector {sector}") + else: + table = pyRestTable.Table() + table.labels = "id status start end user(s) title".split() + for item in records: + users = trim( + ",".join([ + user["lastName"] + for user in item["experimentUsers"] + ]), + 20) + table.addRow(( + item["esafId"], + item["esafStatus"], + item["experimentStartDate"].split()[0], + item["experimentEndDate"].split()[0], + users, + trim(item["esafTitle"], 40), + )) + print(f"Current ESAF(s) on sector {sector}") + print() + print(table) + + +def cmd_esaf(args): + try: + esaf = getEsaf(args.esafId) + print(yaml.dump(esaf)) + except DmRecordNotFound as exc: + print(exc) + except dm.DmException as exc: + print(f"dm reported: {exc}") + + +def cmd_proposal(args): + try: + proposal = getProposal(args.proposalId, args.cycle, args.beamlineName) + print(yaml.dump(proposal)) + except DmRecordNotFound as exc: + print(exc) + except dm.DmException as exc: + print(f"dm reported: {exc}") + + def main(): args = get_options() if args.subcommand == "beamlines": @@ -380,73 +448,16 @@ def main(): epicsClear(args.prefix) elif args.subcommand == "current": - records = getCurrentProposals(args.beamlineName) - if len(records) == 0: - print(f"No current proposals for {args.beamlineName}") - else: - table = pyRestTable.Table() - table.labels = "id cycle date user(s) title".split() - for item in records: - users = trim(",".join([ - user["lastName"] - for user in item["experimenters"] - ]), 20) - table.addRow(( - item["id"], - item["cycle"], - item["submittedDate"], - users, - trim(item["title"]),)) - print(f"Current Proposal(s) on {args.beamlineName}") - print() - print(table) - - sector = args.beamlineName.split("-")[0] - records = getCurrentEsafs(sector) - if len(records) == 0: - print(f"No current ESAFs for sector {sector}") - else: - table = pyRestTable.Table() - table.labels = "id status start end user(s) title".split() - for item in records: - users = trim( - ",".join([ - user["lastName"] - for user in item["experimentUsers"] - ]), - 20) - table.addRow(( - item["esafId"], - item["esafStatus"], - item["experimentStartDate"].split()[0], - item["experimentEndDate"].split()[0], - users, - trim(item["esafTitle"], 40), - )) - print(f"Current ESAF(s) on sector {sector}") - print() - print(table) + cmd_current(args) elif args.subcommand == "cycles": printColumns(listAllRuns()) elif args.subcommand == "esaf": - try: - esaf = getEsaf(args.esafId) - print(yaml.dump(esaf)) - except DmRecordNotFound as exc: - print(exc) - except dm.DmException as exc: - print(f"dm reported: {exc}") + cmd_esaf(args) elif args.subcommand == "proposal": - try: - proposal = getProposal(args.proposalId, args.cycle, args.beamlineName) - print(yaml.dump(proposal)) - except DmRecordNotFound as exc: - print(exc) - except dm.DmException as exc: - print(f"dm reported: {exc}") + cmd_proposal(args) elif args.subcommand == "setup": epicsSetup(args.prefix, args.beamlineName, args.cycle) diff --git a/apstools/beamtime/bss_info_makedb.py b/apstools/beamtime/bss_info_makedb.py index 43f6428d8..c8a7665d7 100755 --- a/apstools/beamtime/bss_info_makedb.py +++ b/apstools/beamtime/bss_info_makedb.py @@ -4,9 +4,6 @@ Create the EPICS database """ -import os -import sys - #name RTYP length raw_data = """ From f544e8d697144e38be04049ccfb56b54c26d32c0 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 6 Jul 2020 12:51:30 -0500 Subject: [PATCH 06/47] MNT #334 Codacy --- apstools/devices.py | 1 + apstools/filewriters.py | 3 ++- apstools/plans.py | 27 +++++++++++++++------------ apstools/utils.py | 3 ++- create_release_notes.py | 2 +- tests/test_filewriter.py | 2 +- 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/apstools/devices.py b/apstools/devices.py index 71efdfbd5..3f3751265 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -1856,6 +1856,7 @@ def generate_datum(self, key, timestamp, datum_kwargs): datum_kwargs["HDF5_file_name"] = hdf5_file_name logger.debug("make_filename: %s", hdf5_file_name) + logger.debug("write_path: %s", write_path) return super().generate_datum(key, timestamp, datum_kwargs) def get_frames_per_point(self): diff --git a/apstools/filewriters.py b/apstools/filewriters.py index 95eaf8509..33a27bd7c 100644 --- a/apstools/filewriters.py +++ b/apstools/filewriters.py @@ -46,6 +46,7 @@ SPEC_TIME_FORMAT = "%a %b %d %H:%M:%S %Y" SCAN_ID_RESET_VALUE = 0 + def _rebuild_scan_command(doc): """ reconstruct the scan command for SPEC data file #S line @@ -657,7 +658,7 @@ def myPlan(): #C Mon Jan 28 12:48:14 2019. exit_status = success """ - # global specwriter # such as: specwriter = SpecWriterCallback() + global specwriter # such as: specwriter = SpecWriterCallback() writer = writer or specwriter if doc is None: if writer.scanning: diff --git a/apstools/plans.py b/apstools/plans.py index 96a073c7f..5eedb9045 100644 --- a/apstools/plans.py +++ b/apstools/plans.py @@ -87,7 +87,7 @@ def addDeviceDataAsStream(devices, label): yield from bps.save() -def execute_command_list(filename, commands, md={}): +def execute_command_list(filename, commands, md=None): """ plan: execute the command list @@ -206,7 +206,7 @@ def get_command_list(filename): def lineup( counter, axis, minus, plus, npts, time_s=0.1, peak_factor=4, width_factor=0.8, - _md={}): + md=None): """ lineup and center a given axis, relative to current position @@ -312,16 +312,16 @@ def peak_analysis(): bec.peaks.aligned = aligned bec.peaks.ATTRS = ('com', 'cen', 'max', 'min', 'fwhm') - md = dict(_md) - md["purpose"] = "alignment" - yield from bp.rel_scan([counter], axis, minus, plus, npts, md=md) + _md = dict(purpose="alignment") + _md.update(md or {}) + yield from bp.rel_scan([counter], axis, minus, plus, npts, md=_md) yield from peak_analysis() if bec.peaks.aligned: # again, tweak axis to maximize - md["purpose"] = "alignment - fine" + _md["purpose"] = "alignment - fine" fwhm = bec.peaks["fwhm"][counter.name] - yield from bp.rel_scan([counter], axis, -fwhm, fwhm, npts, md=md) + yield from bp.rel_scan([counter], axis, -fwhm, fwhm, npts, md=_md) yield from peak_analysis() if scaler is not None: @@ -594,7 +594,7 @@ def register_command_handler(handler=None): _COMMAND_HANDLER_ = handler or execute_command_list -def run_command_file(filename, md={}): +def run_command_file(filename, md=None): """ plan: execute a list of commands from a text or Excel file @@ -614,8 +614,10 @@ def run_command_file(filename, md={}): *new in apstools release 1.1.7* """ + _md = dict(command_file=filename) + _md.update(md or {}) commands = get_command_list(filename) - yield from _COMMAND_HANDLER_(filename, commands) + yield from _COMMAND_HANDLER_(filename, commands, md=_md) def snapshot(obj_list, stream="primary", md=None): @@ -738,7 +740,7 @@ def sscan_1D( running_stream="primary", final_array_stream=None, device_settings_stream="settings", - md={}): + md=None): """ simple 1-D scan using EPICS synApps sscan record @@ -836,9 +838,10 @@ def phase_cb(value, timestamp, **kwargs): # watch for new data to be read out sscan.scan_phase.subscribe(phase_cb) - md["plan_name"] = "sscan_1D" + _md = dict(plan_name="sscan_1D") + _md.update(md or {}) - yield from bps.open_run(md) # start data collection + yield from bps.open_run(_md) # start data collection yield from bps.mv(sscan.execute_scan, 1) # start sscan started = True diff --git a/apstools/utils.py b/apstools/utils.py index 3493b7892..08dd4f2bd 100644 --- a/apstools/utils.py +++ b/apstools/utils.py @@ -237,7 +237,7 @@ def itemizer(fmt, items): def listruns( - num=20, keys=[], printing=True, + num=20, keys=None, printing=True, show_command=True, db=None, exit_status=None, **db_search_terms): @@ -308,6 +308,7 @@ def listruns( *new in apstools release 1.1.10* """ db = db or ipython_shell_namespace()["db"] + keys = keys or [] if show_command: labels = "scan_id command".split() + keys diff --git a/create_release_notes.py b/create_release_notes.py index f4dda3445..1c135a190 100755 --- a/create_release_notes.py +++ b/create_release_notes.py @@ -37,7 +37,7 @@ def findGitConfigFile(): Needs testing for when things are wrong. """ path = os.getcwd() - for i in range(99): + for _ in range(99): config_file = os.path.join(path, ".git", "config") if os.path.exists(config_file): return config_file # found it! diff --git a/tests/test_filewriter.py b/tests/test_filewriter.py index bff08d121..9170573bd 100644 --- a/tests/test_filewriter.py +++ b/tests/test_filewriter.py @@ -242,7 +242,7 @@ def test_receiver_battery(self): callback.file_extension, apstools.filewriters.NEXUS_FILE_EXTENSION) - for plan_name, document_set in self.db.items(): + for plan_name in self.db: callback.clear() callback.file_path = self.tempdir self.assertIsNone(callback.uid, plan_name) From 62b25fafb267a6d9366ef05925e720f352b690a4 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 6 Jul 2020 15:30:55 -0500 Subject: [PATCH 07/47] TST #333 start on unit tests --- apstools/beamtime/bss_info.py | 5 +++- tests/test_beamtime.py | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/test_beamtime.py diff --git a/apstools/beamtime/bss_info.py b/apstools/beamtime/bss_info.py index 24af9d6de..ba5d127f2 100755 --- a/apstools/beamtime/bss_info.py +++ b/apstools/beamtime/bss_info.py @@ -15,17 +15,20 @@ import argparse import datetime import dm # APS data management library -from bss_info_ophyd import EpicsBssDevice import os import pyRestTable import sys import time import yaml +from .bss_info_ophyd import EpicsBssDevice + + DM_APS_DB_WEB_SERVICE_URL = "https://xraydtn01.xray.aps.anl.gov:11236" CONNECT_TIMEOUT = 5 POLL_INTERVAL = 0.01 +# TODO: set to None and provide singleton initializers, not at global api_bss = dm.BssApsDbApi(DM_APS_DB_WEB_SERVICE_URL) api_esaf = dm.EsafApsDbApi(DM_APS_DB_WEB_SERVICE_URL) diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py new file mode 100644 index 000000000..db5ec40d6 --- /dev/null +++ b/tests/test_beamtime.py @@ -0,0 +1,50 @@ + +""" +unit tests for beamtime info +""" + +import os +import socket +import sys +import unittest + +_test_path = os.path.dirname(__file__) +_path = os.path.join(_test_path, '..') +if _path not in sys.path: + sys.path.insert(0, _path) + +from tests.common import Capture_stdout, Capture_stderr + +import apstools.beamtime.bss_info + + +def using_APS_workstation(): + hostname = socket.gethostname() + return hostname.lower().endswith(".aps.anl.gov") + + +class Test_Beamtime(unittest.TestCase): + + def test_general(self): + self.assertEqual(apstools.beamtime.bss_info.CONNECT_TIMEOUT, 5) + self.assertEqual(apstools.beamtime.bss_info.POLL_INTERVAL, 0.01) + + def test_aps_only(self): + if not using_APS_workstation(): + self.assertTrue(True) # test something + return + + +def suite(*args, **kw): + test_list = [ + Test_Beamtime, + ] + test_suite = unittest.TestSuite() + for test_case in test_list: + test_suite.addTest(unittest.makeSuite(test_case)) + return test_suite + + +if __name__ == "__main__": + runner=unittest.TextTestRunner() + runner.run(suite()) From aebaac452feadd6c981bbdc2647b73691c037313 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 6 Jul 2020 16:24:05 -0500 Subject: [PATCH 08/47] TST #333 tests not at APS --- apstools/beamtime/bss_info.py | 1 - conda-recipe/meta.yaml | 2 ++ requirements.txt | 1 + tests/test_beamtime.py | 42 ++++++++++++++++++++++++++++++----- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apstools/beamtime/bss_info.py b/apstools/beamtime/bss_info.py index ba5d127f2..1e081f3ac 100755 --- a/apstools/beamtime/bss_info.py +++ b/apstools/beamtime/bss_info.py @@ -28,7 +28,6 @@ CONNECT_TIMEOUT = 5 POLL_INTERVAL = 0.01 -# TODO: set to None and provide singleton initializers, not at global api_bss = dm.BssApsDbApi(DM_APS_DB_WEB_SERVICE_URL) api_esaf = dm.EsafApsDbApi(DM_APS_DB_WEB_SERVICE_URL) diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 7a0368df9..c5e87440a 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -36,8 +36,10 @@ requirements: - pip run: - python + - aps-dm-api - bluesky>=1.6.2 - databroker>=1.0.6 + - h5py - ophyd>=1.5.1 - pandas - pyEpics>=3.4.2 diff --git a/requirements.txt b/requirements.txt index 7fc047391..e30128a0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +aps-dm-api bluesky>=1.6.2 databroker>=1.0.6 h5py diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index db5ec40d6..9fc89b35a 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -3,6 +3,7 @@ unit tests for beamtime info """ +import datetime import os import socket import sys @@ -15,7 +16,7 @@ from tests.common import Capture_stdout, Capture_stderr -import apstools.beamtime.bss_info +from apstools.beamtime import bss_info def using_APS_workstation(): @@ -26,13 +27,44 @@ def using_APS_workstation(): class Test_Beamtime(unittest.TestCase): def test_general(self): - self.assertEqual(apstools.beamtime.bss_info.CONNECT_TIMEOUT, 5) - self.assertEqual(apstools.beamtime.bss_info.POLL_INTERVAL, 0.01) + self.assertEqual(bss_info.CONNECT_TIMEOUT, 5) + self.assertEqual(bss_info.POLL_INTERVAL, 0.01) + self.assertEqual( + bss_info.DM_APS_DB_WEB_SERVICE_URL, + "https://xraydtn01.xray.aps.anl.gov:11236") + self.assertIsNotNone(bss_info.api_bss) + self.assertIsNotNone(bss_info.api_esaf) - def test_aps_only(self): + def test_iso2datetime(self): + self.assertEqual( + bss_info.iso2datetime("2020-06-30 12:31:45.067890"), + datetime.datetime(2020, 6, 30, 12, 31, 45, 67890) + ) + + def test_not_at_aps(self): + self.assertTrue(True) # test something + if using_APS_workstation(): + return + + # do not try to test for fails using dm package, it has no timeout + + def test_only_at_aps(self): + self.assertTrue(True) # test something if not using_APS_workstation(): - self.assertTrue(True) # test something return + runs = bss_info.api_bss.listRuns() + + def test_printColumns(self): + with Capture_stdout() as received: + bss_info.printColumns("1 2 3 4 5 6".split(), numColumns=3, width=3) + self.assertEqual(len(received), 2) + self.assertEqual(received[0], "1 3 5 ") + self.assertEqual(received[1], "2 4 6 ") + + source = "0123456789" + self.assertEqual(bss_info.trim(source), source) + self.assertNotEqual(bss_info.trim(source, length=8), source) + self.assertEqual(bss_info.trim(source, length=8), "01234...") def suite(*args, **kw): From 27e7595c12061e5627f5e6f9d1108c80456bc678 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 6 Jul 2020 16:29:30 -0500 Subject: [PATCH 09/47] TST #333 --- tests/__main__.py | 2 ++ tests/test_beamtime.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/__main__.py b/tests/__main__.py index 154cd4429..65406de17 100644 --- a/tests/__main__.py +++ b/tests/__main__.py @@ -12,6 +12,7 @@ def suite(*args, **kw): + from tests import test_beamtime from tests import test_simple from tests import test_filewriter from tests import test_export_json @@ -25,6 +26,7 @@ def suite(*args, **kw): test_exceltable, test_commandlist, test_utils, + test_beamtime, ] test_suite = unittest.TestSuite() diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 9fc89b35a..c78a4b427 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -14,8 +14,6 @@ if _path not in sys.path: sys.path.insert(0, _path) -from tests.common import Capture_stdout, Capture_stderr - from apstools.beamtime import bss_info @@ -55,6 +53,7 @@ def test_only_at_aps(self): runs = bss_info.api_bss.listRuns() def test_printColumns(self): + from tests.common import Capture_stdout with Capture_stdout() as received: bss_info.printColumns("1 2 3 4 5 6".split(), numColumns=3, width=3) self.assertEqual(len(received), 2) From 705084415ba70dfe3e315b51a4c3a71455222928 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 6 Jul 2020 17:24:12 -0500 Subject: [PATCH 10/47] TST #333 WIP --- tests/test_beamtime.py | 96 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index c78a4b427..86aa20cd0 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -22,8 +22,95 @@ def using_APS_workstation(): return hostname.lower().endswith(".aps.anl.gov") +class Test_ProgramCommands(unittest.TestCase): + + def setUp(self): + self.sys_argv0 = sys.argv[0] + sys.argv = [self.sys_argv0,] + + def tearDown(self): + sys.argv = [self.sys_argv0,] + + def test_no_options(self): + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertIsNone(args.subcommand) + + def test_beamlines(self): + sys.argv.append("beamlines") + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "beamlines") + + def test_current(self): + sys.argv.append("current") + sys.argv.append("9-ID-B,C") + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "current") + self.assertEqual(args.beamlineName, "9-ID-B,C") + + def test_cycles(self): + sys.argv.append("cycles") + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "cycles") + + def test_esaf(self): + sys.argv.append("esaf") + sys.argv.append("12345") + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "esaf") + self.assertEqual(args.esafId, 12345) + + def test_proposal(self): + sys.argv.append("proposal") + sys.argv.append("proposal_number_here") + sys.argv.append("1995-1") + sys.argv.append("my_beamline") + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "proposal") + self.assertEqual(args.proposalId, "proposal_number_here") + self.assertEqual(args.cycle, "1995-1") + self.assertEqual(args.beamlineName, "my_beamline") + + def test_EPICS_clear(self): + sys.argv.append("clear") + sys.argv.append("bss:") + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "clear") + self.assertEqual(args.prefix, "bss:") + + def test_EPICS_setup(self): + sys.argv.append("setup") + sys.argv.append("bss:") + sys.argv.append("my_beamline") + sys.argv.append("1995-1") + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "setup") + self.assertEqual(args.prefix, "bss:") + self.assertEqual(args.beamlineName, "my_beamline") + self.assertEqual(args.cycle, "1995-1") + + def test_EPICS_update(self): + sys.argv.append("update") + sys.argv.append("bss:") + args = bss_info.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "update") + self.assertEqual(args.prefix, "bss:") + + class Test_Beamtime(unittest.TestCase): + def test_command_options(self): + self.assertTrue(True) # test something + # TODO: + def test_general(self): self.assertEqual(bss_info.CONNECT_TIMEOUT, 5) self.assertEqual(bss_info.POLL_INTERVAL, 0.01) @@ -50,7 +137,13 @@ def test_only_at_aps(self): self.assertTrue(True) # test something if not using_APS_workstation(): return - runs = bss_info.api_bss.listRuns() + + runs = bss_info.listAllRuns() + self.assertGreater(len(runs), 1) + self.assertEqual(bss_info.getCurrentCycle(), runs[-1]) + self.assertEqual(bss_info.listRecentRuns()[0], runs[-1]) + + self.assertGreater(len(bss_info.listAllBeamlines()), 1) def test_printColumns(self): from tests.common import Capture_stdout @@ -69,6 +162,7 @@ def test_printColumns(self): def suite(*args, **kw): test_list = [ Test_Beamtime, + Test_ProgramCommands, ] test_suite = unittest.TestSuite() for test_case in test_list: From b25fdc3a9d43b8c907d76e02a17c411ceae63a41 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 6 Jul 2020 17:29:58 -0500 Subject: [PATCH 11/47] TST #333 --- tests/test_beamtime.py | 108 ++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 86aa20cd0..02caeaee3 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -22,6 +22,60 @@ def using_APS_workstation(): return hostname.lower().endswith(".aps.anl.gov") +class Test_Beamtime(unittest.TestCase): + + def test_command_options(self): + self.assertTrue(True) # test something + # TODO: + + def test_general(self): + self.assertEqual(bss_info.CONNECT_TIMEOUT, 5) + self.assertEqual(bss_info.POLL_INTERVAL, 0.01) + self.assertEqual( + bss_info.DM_APS_DB_WEB_SERVICE_URL, + "https://xraydtn01.xray.aps.anl.gov:11236") + self.assertIsNotNone(bss_info.api_bss) + self.assertIsNotNone(bss_info.api_esaf) + + def test_iso2datetime(self): + self.assertEqual( + bss_info.iso2datetime("2020-06-30 12:31:45.067890"), + datetime.datetime(2020, 6, 30, 12, 31, 45, 67890) + ) + + def test_not_at_aps(self): + self.assertTrue(True) # test something + if using_APS_workstation(): + return + + # do not try to test for fails using dm package, it has no timeout + + def test_only_at_aps(self): + self.assertTrue(True) # test something + if not using_APS_workstation(): + return + + runs = bss_info.listAllRuns() + self.assertGreater(len(runs), 1) + self.assertEqual(bss_info.getCurrentCycle(), runs[-1]) + self.assertEqual(bss_info.listRecentRuns()[0], runs[-1]) + + self.assertGreater(len(bss_info.listAllBeamlines()), 1) + + def test_printColumns(self): + from tests.common import Capture_stdout + with Capture_stdout() as received: + bss_info.printColumns("1 2 3 4 5 6".split(), numColumns=3, width=3) + self.assertEqual(len(received), 2) + self.assertEqual(received[0], "1 3 5 ") + self.assertEqual(received[1], "2 4 6 ") + + source = "0123456789" + self.assertEqual(bss_info.trim(source), source) + self.assertNotEqual(bss_info.trim(source, length=8), source) + self.assertEqual(bss_info.trim(source, length=8), "01234...") + + class Test_ProgramCommands(unittest.TestCase): def setUp(self): @@ -105,60 +159,6 @@ def test_EPICS_update(self): self.assertEqual(args.prefix, "bss:") -class Test_Beamtime(unittest.TestCase): - - def test_command_options(self): - self.assertTrue(True) # test something - # TODO: - - def test_general(self): - self.assertEqual(bss_info.CONNECT_TIMEOUT, 5) - self.assertEqual(bss_info.POLL_INTERVAL, 0.01) - self.assertEqual( - bss_info.DM_APS_DB_WEB_SERVICE_URL, - "https://xraydtn01.xray.aps.anl.gov:11236") - self.assertIsNotNone(bss_info.api_bss) - self.assertIsNotNone(bss_info.api_esaf) - - def test_iso2datetime(self): - self.assertEqual( - bss_info.iso2datetime("2020-06-30 12:31:45.067890"), - datetime.datetime(2020, 6, 30, 12, 31, 45, 67890) - ) - - def test_not_at_aps(self): - self.assertTrue(True) # test something - if using_APS_workstation(): - return - - # do not try to test for fails using dm package, it has no timeout - - def test_only_at_aps(self): - self.assertTrue(True) # test something - if not using_APS_workstation(): - return - - runs = bss_info.listAllRuns() - self.assertGreater(len(runs), 1) - self.assertEqual(bss_info.getCurrentCycle(), runs[-1]) - self.assertEqual(bss_info.listRecentRuns()[0], runs[-1]) - - self.assertGreater(len(bss_info.listAllBeamlines()), 1) - - def test_printColumns(self): - from tests.common import Capture_stdout - with Capture_stdout() as received: - bss_info.printColumns("1 2 3 4 5 6".split(), numColumns=3, width=3) - self.assertEqual(len(received), 2) - self.assertEqual(received[0], "1 3 5 ") - self.assertEqual(received[1], "2 4 6 ") - - source = "0123456789" - self.assertEqual(bss_info.trim(source), source) - self.assertNotEqual(bss_info.trim(source, length=8), source) - self.assertEqual(bss_info.trim(source, length=8), "01234...") - - def suite(*args, **kw): test_list = [ Test_Beamtime, From 4d257314a80b3e98e2dab63870f40b34bfb64b46 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Mon, 6 Jul 2020 17:30:11 -0500 Subject: [PATCH 12/47] TST #333 LGTM review --- apstools/beamtime/bss_info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apstools/beamtime/bss_info.py b/apstools/beamtime/bss_info.py index 1e081f3ac..c1769bd95 100755 --- a/apstools/beamtime/bss_info.py +++ b/apstools/beamtime/bss_info.py @@ -343,12 +343,12 @@ def get_options(): subcommand = parser.add_subparsers(dest='subcommand', title='subcommand') - p_sub = subcommand.add_parser('beamlines', help="print list of beamlines") + subcommand.add_parser('beamlines', help="print list of beamlines") p_sub = subcommand.add_parser('current', help="print current ESAF(s) and proposal(s)") p_sub.add_argument('beamlineName', type=str, help="Beamline name") - p_sub = subcommand.add_parser('cycles', help="print APS cycle names") + subcommand.add_parser('cycles', help="print APS cycle names") p_sub = subcommand.add_parser('esaf', help="print specific ESAF") p_sub.add_argument('esafId', type=int, help="ESAF ID number") From 09f9d2d6a38be05a6b3c1cb3cbd96579aa39df05 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 01:24:42 -0500 Subject: [PATCH 13/47] TST #333 refactor so testable --- apstools/beamtime/bss_info_makedb.py | 70 ++++++++++++++++------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/apstools/beamtime/bss_info_makedb.py b/apstools/beamtime/bss_info_makedb.py index c8a7665d7..45d3c76eb 100755 --- a/apstools/beamtime/bss_info_makedb.py +++ b/apstools/beamtime/bss_info_makedb.py @@ -135,35 +135,43 @@ proposal:user9:userId stringout """.strip().splitlines() -print("""\ -# -# file: bss_info.db -# EPICS database for information from APS ESAF & Proposal databases -# -# BSS: Beamtime Scheduling System -# -# note: this file autogenerated by make_database.py -# ./bss_info_makedb.py | tee bss_info.db -# -# Start the EPICS IOC with a command (from EPICS base): -# softIoc -m P=ioc:bss: -d bss_info.db -""") -for row in raw_data: - parts = row.split() - pvname, rtyp = parts[:2] - head = f'record({rtyp}, "$(P){pvname}")' - fields = [] - if rtyp == "waveform" and len(parts) == 3: - length = parts[-1] - fields.append(f'field(FTVL, "CHAR")') - fields.append(f'field(NELM, {length})') - elif rtyp == "bo": - fields.append(f'field(ZNAM, "OFF")') - fields.append(f'field(ONAM, "ON")') - record = head - newline = "\n" - if len(fields) > 0: - field_specs = "\n".join([f" {field}" for field in fields]) - record += " {\n" + f"{field_specs}" + "\n}" - print("\n" + record) +def main(): + header = """\ + # + # file: bss_info.db + # EPICS database for information from APS ESAF & Proposal databases + # + # BSS: Beamtime Scheduling System + # + # note: this file autogenerated by make_database.py + # ./bss_info_makedb.py | tee bss_info.db + # + # Start the EPICS IOC with a command (from EPICS base): + # softIoc -m P=ioc:bss: -d bss_info.db + """ + for line in header.splitlines(): + print(line.strip()) + + for row in raw_data: + parts = row.split() + pvname, rtyp = parts[:2] + head = f'record({rtyp}, "$(P){pvname}")' + fields = [] + if rtyp == "waveform" and len(parts) == 3: + length = parts[-1] + fields.append(f'field(FTVL, "CHAR")') + fields.append(f'field(NELM, {length})') + elif rtyp == "bo": + fields.append(f'field(ZNAM, "OFF")') + fields.append(f'field(ONAM, "ON")') + record = head + newline = "\n" + if len(fields) > 0: + field_specs = "\n".join([f" {field}" for field in fields]) + record += " {\n" + f"{field_specs}" + "\n}" + print("\n" + record) + + +if __name__ == "__main__": + main() From d745be5d5fbbf5172229bffc4d00487ebb2289d2 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 01:24:54 -0500 Subject: [PATCH 14/47] TST #333 --- tests/test_beamtime.py | 157 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 5 deletions(-) diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 02caeaee3..3683ce8ae 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -6,6 +6,7 @@ import datetime import os import socket +import subprocess import sys import unittest @@ -14,7 +15,10 @@ if _path not in sys.path: sys.path.insert(0, _path) -from apstools.beamtime import bss_info +from apstools.beamtime import bss_info, bss_info_makedb + + +BSS_TEST_IOC_PREFIX = "ioc:bss:" def using_APS_workstation(): @@ -22,11 +26,16 @@ def using_APS_workstation(): return hostname.lower().endswith(".aps.anl.gov") -class Test_Beamtime(unittest.TestCase): +def bss_IOC_available(): + import epics + # try connecting with one of the PVs in the database + cycle = epics.PV(f"{BSS_TEST_IOC_PREFIX}esaf:cycle") + cycle.wait_for_connection(timeout=2) + conn = cycle.connected + return cycle.connected - def test_command_options(self): - self.assertTrue(True) # test something - # TODO: + +class Test_Beamtime(unittest.TestCase): def test_general(self): self.assertEqual(bss_info.CONNECT_TIMEOUT, 5) @@ -62,6 +71,18 @@ def test_only_at_aps(self): self.assertGreater(len(bss_info.listAllBeamlines()), 1) + # TODO: test the other functions + # getCurrentEsafs + # getCurrentInfo + # getCurrentProposals + # getEsaf + # getProposal + # class DmRecordNotFound(Exception): ... + # class EsafNotFound(DmRecordNotFound): ... + # class ProposalNotFound(DmRecordNotFound): ... + # EsafInfo + # ScheduleInfo + def test_printColumns(self): from tests.common import Capture_stdout with Capture_stdout() as received: @@ -76,6 +97,130 @@ def test_printColumns(self): self.assertEqual(bss_info.trim(source, length=8), "01234...") +class Test_EPICS(unittest.TestCase): + + def start_ioc_subprocess(self, db_file): + softIoc = os.path.abspath(os.path.join( + os.environ.get("CONDA_PREFIX"), + "epics", + "bin", + os.environ.get("EPICS_HOST_ARCH", "linux-x86_64"), + "softIoc" + )) + if not os.path.exists(softIoc): + return + db_file = os.path.abspath(os.path.join( + os.path.dirname(bss_info.__file__), + db_file + )) + if not os.path.exists(db_file): + return + cmd = f"{softIoc} -m P={BSS_TEST_IOC_PREFIX} -d {db_file}".split() + return subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False) + + def setUp(self): + self.bss = None + self.ioc_process = self.start_ioc_subprocess("bss_info.db") + + def tearDown(self): + if self.bss is not None: + self.bss.destroy() + self.bss = None + if self.ioc_process is not None: + self.ioc_process.communicate('exit\n'.encode()) + self.ioc_process = None + + def test_ioc(self): + if not bss_IOC_available(): + return + + from apstools.beamtime import bss_info_ophyd as bio + self.bss = bio.EpicsBssDevice(BSS_TEST_IOC_PREFIX, name="bss") + self.bss.wait_for_connection(timeout=2) + self.assertTrue(self.bss.connected) + + self.assertEqual(self.bss.esaf.aps_cycle.get(), "") + + def test_EPICS(self): + from tests.common import Capture_stdout + + if not bss_IOC_available(): + return + + with Capture_stdout(): + self.bss = bss_info.connect_epics(BSS_TEST_IOC_PREFIX) + self.assertTrue(self.bss.connected) + self.assertEqual(self.bss.esaf.aps_cycle.get(), "") + + self.bss.esaf.aps_cycle.put(bss_info.getCurrentCycle()) + self.assertNotEqual(self.bss.esaf.aps_cycle.get(), "") + + beamline = "9-ID-B,C" + cycle = "2019-3" + with Capture_stdout(): + bss_info.epicsSetup(BSS_TEST_IOC_PREFIX, beamline, cycle) + self.assertNotEqual(self.bss.proposal.beamline_name.get(), "harpo") + self.assertEqual(self.bss.proposal.beamline_name.get(), beamline) + self.assertEqual(self.bss.esaf.aps_cycle.get(), cycle) + self.assertEqual(self.bss.esaf.sector.get(), beamline.split("-")[0]) + + # epicsUpdate + """ + Example ESAF on sector 9 + + ====== ======== ========== ========== ==================== ================================= + id status start end user(s) title + ====== ======== ========== ========== ==================== ================================= + 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS + ====== ======== ========== ========== ==================== ================================= + + ===== ====== =================== ==================== ======================================== + id cycle date user(s) title + ===== ====== =================== ==================== ======================================== + 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r... + ===== ====== =================== ==================== ======================================== + """ + esaf_id = "226319" + proposal_id = "64629" + self.bss.esaf.aps_cycle.put("2019-2") + self.bss.esaf.esaf_id.put(esaf_id) + self.bss.proposal.proposal_id.put(proposal_id) + with Capture_stdout(): + bss_info.epicsUpdate(BSS_TEST_IOC_PREFIX) + self.assertEqual( + self.bss.esaf.title.get(), + "Commission 9ID and USAXS") + self.assertTrue( + self.bss.proposal.title.get().startswith( + "2019 National School on Neutron & X-r")) + + with Capture_stdout(): + bss_info.epicsClear(BSS_TEST_IOC_PREFIX) + self.assertNotEqual(self.bss.esaf.aps_cycle.get(), "") + self.assertEqual(self.bss.esaf.title.get(), "") + self.assertEqual(self.bss.proposal.title.get(), "") + + +class Test_MakeDatabase(unittest.TestCase): + + def test_general(self): + from tests.common import Capture_stdout + with Capture_stdout() as db: + bss_info_makedb.main() + self.assertEqual(len(db), 345) + self.assertEqual(db[0], "#") + self.assertEqual(db[1], "# file: bss_info.db") + # randomly-selected spot checks + self.assertEqual(db[22], 'record(stringout, "$(P)esaf:id")') + self.assertEqual(db[128], ' field(ONAM, "ON")') + self.assertEqual(db[265], ' field(FTVL, "CHAR")') + + class Test_ProgramCommands(unittest.TestCase): def setUp(self): @@ -162,6 +307,8 @@ def test_EPICS_update(self): def suite(*args, **kw): test_list = [ Test_Beamtime, + Test_EPICS, + Test_MakeDatabase, Test_ProgramCommands, ] test_suite = unittest.TestSuite() From 1c07e03d922c221822bc61a53c87404b7bdc017f Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 01:29:28 -0500 Subject: [PATCH 15/47] MNT #333 Codacy --- apstools/beamtime/bss_info_makedb.py | 1 - tests/test_beamtime.py | 29 +++++++++++++--------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/apstools/beamtime/bss_info_makedb.py b/apstools/beamtime/bss_info_makedb.py index 45d3c76eb..59e7d7663 100755 --- a/apstools/beamtime/bss_info_makedb.py +++ b/apstools/beamtime/bss_info_makedb.py @@ -166,7 +166,6 @@ def main(): fields.append(f'field(ZNAM, "OFF")') fields.append(f'field(ONAM, "ON")') record = head - newline = "\n" if len(fields) > 0: field_specs = "\n".join([f" {field}" for field in fields]) record += " {\n" + f"{field_specs}" + "\n}" diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 3683ce8ae..66965758b 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -31,7 +31,6 @@ def bss_IOC_available(): # try connecting with one of the PVs in the database cycle = epics.PV(f"{BSS_TEST_IOC_PREFIX}esaf:cycle") cycle.wait_for_connection(timeout=2) - conn = cycle.connected return cycle.connected @@ -170,21 +169,19 @@ def test_EPICS(self): self.assertEqual(self.bss.esaf.sector.get(), beamline.split("-")[0]) # epicsUpdate - """ - Example ESAF on sector 9 - - ====== ======== ========== ========== ==================== ================================= - id status start end user(s) title - ====== ======== ========== ========== ==================== ================================= - 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS - ====== ======== ========== ========== ==================== ================================= - - ===== ====== =================== ==================== ======================================== - id cycle date user(s) title - ===== ====== =================== ==================== ======================================== - 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r... - ===== ====== =================== ==================== ======================================== - """ + # Example ESAF on sector 9 + + # ====== ======== ========== ========== ==================== ================================= + # id status start end user(s) title + # ====== ======== ========== ========== ==================== ================================= + # 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS + # ====== ======== ========== ========== ==================== ================================= + + # ===== ====== =================== ==================== ======================================== + # id cycle date user(s) title + # ===== ====== =================== ==================== ======================================== + # 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r... + # ===== ====== =================== ==================== ======================================== esaf_id = "226319" proposal_id = "64629" self.bss.esaf.aps_cycle.put("2019-2") From 83cacfd3b776a818dbe701c6ca80c5a2496d011c Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 01:37:01 -0500 Subject: [PATCH 16/47] TST #333 Codacy --- tests/test_beamtime.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 66965758b..1c89209ea 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -151,16 +151,21 @@ def test_EPICS(self): if not bss_IOC_available(): return + beamline = "9-ID-B,C" + cycle = "2019-3" + with Capture_stdout(): self.bss = bss_info.connect_epics(BSS_TEST_IOC_PREFIX) self.assertTrue(self.bss.connected) self.assertEqual(self.bss.esaf.aps_cycle.get(), "") - self.bss.esaf.aps_cycle.put(bss_info.getCurrentCycle()) + self.bss.esaf.aps_cycle.put(cycle) self.assertNotEqual(self.bss.esaf.aps_cycle.get(), "") - beamline = "9-ID-B,C" - cycle = "2019-3" + if not using_APS_workstation(): + return + + # setup with Capture_stdout(): bss_info.epicsSetup(BSS_TEST_IOC_PREFIX, beamline, cycle) self.assertNotEqual(self.bss.proposal.beamline_name.get(), "harpo") From b3de52445d8b4a5fa15bdc91800a281e40ac8e94 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 10:15:36 -0500 Subject: [PATCH 17/47] MNT #333 randmize the IOC prefix for testing --- tests/test_beamtime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 1c89209ea..34d1f3266 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -9,6 +9,7 @@ import subprocess import sys import unittest +import uuid _test_path = os.path.dirname(__file__) _path = os.path.join(_test_path, '..') @@ -17,8 +18,7 @@ from apstools.beamtime import bss_info, bss_info_makedb - -BSS_TEST_IOC_PREFIX = "ioc:bss:" +BSS_TEST_IOC_PREFIX = f"tst{uuid.uuid4().hex[:7]}:bss:" def using_APS_workstation(): From 3020fff0c5d87e01e84060cea6fc0dfe5c874ce1 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 10:45:53 -0500 Subject: [PATCH 18/47] MNT #333 remove unused code --- apstools/beamtime/bss_info.py | 31 ------------------------------- tests/test_beamtime.py | 2 -- 2 files changed, 33 deletions(-) diff --git a/apstools/beamtime/bss_info.py b/apstools/beamtime/bss_info.py index c1769bd95..488ca2002 100755 --- a/apstools/beamtime/bss_info.py +++ b/apstools/beamtime/bss_info.py @@ -304,37 +304,6 @@ class EsafNotFound(DmRecordNotFound): ... class ProposalNotFound(DmRecordNotFound): ... -class EsafInfo: - - def __init__(self, esaf): - self.title = esaf["esafTitle"] - self.id = esaf["esafId"] - self.status = esaf["esafStatus"] - self.startDate = esaf["experimentStartDate"] - self.endDate = esaf["experimentEndDate"] - self.sector = esaf["sector"] - users = [u["lastName"] for u in esaf["experimentUsers"]] - self.users = ",".join(sorted(users)) - - -class ScheduleInfo: - - def __init__(self, proposal): - self.others = [] - self.contact = None - self.title = proposal["title"] - self.id = proposal["id"] - for exp in proposal["experimenters"]: - if exp.get("piFlag", "N") == "Y": - self.badge = exp["badge"] - self.email = exp["email"] - self.institution = exp["institution"] - self.contact = f'{exp["firstName"]} {exp["lastName"]}' - else: - self.others.append(exp["lastName"]) - self.others = ",".join(sorted(self.others)) - - def get_options(): parser = argparse.ArgumentParser( prog=os.path.split(sys.argv[0])[-1], diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 34d1f3266..53721fee2 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -79,8 +79,6 @@ def test_only_at_aps(self): # class DmRecordNotFound(Exception): ... # class EsafNotFound(DmRecordNotFound): ... # class ProposalNotFound(DmRecordNotFound): ... - # EsafInfo - # ScheduleInfo def test_printColumns(self): from tests.common import Capture_stdout From 7e37769c5d2fa051361eabb02174ec6952b0d42a Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 10:46:42 -0500 Subject: [PATCH 19/47] MNT #333 remove eclipse config --- .project | 17 ----------------- .pydevproject | 8 -------- 2 files changed, 25 deletions(-) delete mode 100644 .project delete mode 100644 .pydevproject diff --git a/.project b/.project deleted file mode 100644 index 5334c032e..000000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - apstools - - - - - - org.python.pydev.PyDevBuilder - - - - - - org.python.pydev.pythonNature - - diff --git a/.pydevproject b/.pydevproject deleted file mode 100644 index de3b0a124..000000000 --- a/.pydevproject +++ /dev/null @@ -1,8 +0,0 @@ - - - - /${PROJECT_DIR_NAME} - - python interpreter - bluesky - From a402c755e0487b9c7bd88bf8e6cbc998eb19ddf1 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 10:53:29 -0500 Subject: [PATCH 20/47] TST #333 let travis-ci find aps-dm-api --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5ef97e2b5..72fd1e5fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ before_script: - bash miniconda.sh -b -p $HOME/miniconda - export PATH=$HOME/miniconda/bin:$PATH - export PY_ENV=travis-$TRAVIS_PYTHON_VERSION - - conda create -y -n $PY_ENV -c apsu -c conda-forge -c nsls2forge -c defaults python=$TRAVIS_PYTHON_VERSION bluesky coverage databroker docopt epics-base numpy "ophyd>=1.4.0rc3" "pyepics>=3.4.1" pandas requests xlrd + - conda create -y -n $PY_ENV -c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag python=$TRAVIS_PYTHON_VERSION bluesky coverage databroker docopt epics-base numpy "ophyd>=1.4.0rc3" "pyepics>=3.4.1" pandas requests xlrd - source activate $PY_ENV - which pip - which python From 14716e62d419d235f0d9d80fe3673996429307c2 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 11:05:54 -0500 Subject: [PATCH 21/47] TST #333 aps-dm-api not on PyPI can't put aps-dm-api in requirements.txt yet --- .travis.yml | 2 +- requirements.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 72fd1e5fd..7852ab266 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ before_script: - bash miniconda.sh -b -p $HOME/miniconda - export PATH=$HOME/miniconda/bin:$PATH - export PY_ENV=travis-$TRAVIS_PYTHON_VERSION - - conda create -y -n $PY_ENV -c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag python=$TRAVIS_PYTHON_VERSION bluesky coverage databroker docopt epics-base numpy "ophyd>=1.4.0rc3" "pyepics>=3.4.1" pandas requests xlrd + - conda create -y -n $PY_ENV -c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag python=$TRAVIS_PYTHON_VERSION aps-dm-api bluesky coverage databroker docopt epics-base numpy "ophyd>=1.4.0rc3" "pyepics>=3.4.1" pandas requests xlrd - source activate $PY_ENV - which pip - which python diff --git a/requirements.txt b/requirements.txt index e30128a0e..7fc047391 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -aps-dm-api bluesky>=1.6.2 databroker>=1.0.6 h5py From 5ab3aa93f0f23b5aaa7dcd8e23cf5276965dfde1 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 11:17:15 -0500 Subject: [PATCH 22/47] CI #333 for travis-ci --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7852ab266..bf534ed17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,9 @@ before_script: - bash miniconda.sh -b -p $HOME/miniconda - export PATH=$HOME/miniconda/bin:$PATH - export PY_ENV=travis-$TRAVIS_PYTHON_VERSION - - conda create -y -n $PY_ENV -c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag python=$TRAVIS_PYTHON_VERSION aps-dm-api bluesky coverage databroker docopt epics-base numpy "ophyd>=1.4.0rc3" "pyepics>=3.4.1" pandas requests xlrd + - export MY_CONDA_CHANNELS="-c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag" + - export MY_CONDA_PACKAGES="aps-dm-api bluesky coverage databroker docopt epics-base numpy ophyd>=1.4.0rc3 pyepics>=3.4.1 pandas requests xlrd" + - conda create -y -n $PY_ENV $MY_CONDA_CHANNELS python=$TRAVIS_PYTHON_VERSION $MY_CONDA_PACKAGES - source activate $PY_ENV - which pip - which python From 88dcf3e7c40e22f3b529067402c3f087ad83d6c5 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 11:43:22 -0500 Subject: [PATCH 23/47] CI #333 adjust travis-ci requirements --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bf534ed17..272789102 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ before_script: - export PATH=$HOME/miniconda/bin:$PATH - export PY_ENV=travis-$TRAVIS_PYTHON_VERSION - export MY_CONDA_CHANNELS="-c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag" - - export MY_CONDA_PACKAGES="aps-dm-api bluesky coverage databroker docopt epics-base numpy ophyd>=1.4.0rc3 pyepics>=3.4.1 pandas requests xlrd" + - export MY_CONDA_PACKAGES="aps-dm-api bluesky>=1.6.2 coverage databroker docopt epics-base numpy ophyd>=1.5.1 pyepics>=3.4.2 pandas requests xlrd" - conda create -y -n $PY_ENV $MY_CONDA_CHANNELS python=$TRAVIS_PYTHON_VERSION $MY_CONDA_PACKAGES - source activate $PY_ENV - which pip From 41233221082e1d2ab76ea73970e7f4fa79aeab6e Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 15:08:39 -0500 Subject: [PATCH 24/47] TST #333 for py38 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 272789102..d862e038c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ python: - "3.8" # - "nightly" + # see https://github.com/BCDA-APS/apstools/issues/137#ref-commit-5c7baa2 # install: # - pip install -r requirements.txt From 872e391715a019b5157297ed933f315b43161eaf Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 15:23:03 -0500 Subject: [PATCH 25/47] MNT #333 Codacy --- create_release_notes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/create_release_notes.py b/create_release_notes.py index 1c135a190..b368171fa 100755 --- a/create_release_notes.py +++ b/create_release_notes.py @@ -37,13 +37,16 @@ def findGitConfigFile(): Needs testing for when things are wrong. """ path = os.getcwd() - for _ in range(99): + while len(path.split(os.path.sep)): config_file = os.path.join(path, ".git", "config") if os.path.exists(config_file): return config_file # found it! # next, look in the parent directory - path = os.path.abspath(os.path.join(path, "..")) + parent = os.path.dirname(path) + if parent == path: + break + path = parent msg = "Could not find .git/config file in any parent directory." logger.error(msg) From 4b502758fad55782cf1e30db2a59fb25794e3a13 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 17:06:03 -0500 Subject: [PATCH 26/47] MNT #333 refactor name --- .../beamtime/{bss_info.adl => apsbss.adl} | 2 +- apstools/beamtime/{bss_info.db => apsbss.db} | 6 +- apstools/beamtime/{bss_info.py => apsbss.py} | 8 +-- apstools/beamtime/{bss_info.ui => apsbss.ui} | 0 .../{bss_info_makedb.py => apsbss_makedb.py} | 6 +- .../{bss_info_ophyd.py => apsbss_ophyd.py} | 4 +- tests/test_beamtime.py | 66 +++++++++---------- 7 files changed, 46 insertions(+), 46 deletions(-) rename apstools/beamtime/{bss_info.adl => apsbss.adl} (99%) rename apstools/beamtime/{bss_info.db => apsbss.db} (98%) rename apstools/beamtime/{bss_info.py => apsbss.py} (99%) rename apstools/beamtime/{bss_info.ui => apsbss.ui} (100%) rename apstools/beamtime/{bss_info_makedb.py => apsbss_makedb.py} (97%) rename apstools/beamtime/{bss_info_ophyd.py => apsbss_ophyd.py} (98%) diff --git a/apstools/beamtime/bss_info.adl b/apstools/beamtime/apsbss.adl similarity index 99% rename from apstools/beamtime/bss_info.adl rename to apstools/beamtime/apsbss.adl index 56a624459..e70b59426 100644 --- a/apstools/beamtime/bss_info.adl +++ b/apstools/beamtime/apsbss.adl @@ -1,6 +1,6 @@ file { - name="bss_info.adl" + name="apsbss.adl" version=030111 } display { diff --git a/apstools/beamtime/bss_info.db b/apstools/beamtime/apsbss.db similarity index 98% rename from apstools/beamtime/bss_info.db rename to apstools/beamtime/apsbss.db index 1d1599f81..d82feb753 100644 --- a/apstools/beamtime/bss_info.db +++ b/apstools/beamtime/apsbss.db @@ -1,14 +1,14 @@ # -# file: bss_info.db +# file: apsbss.db # EPICS database for information from APS ESAF & Proposal databases # # BSS: Beamtime Scheduling System # # note: this file autogenerated by make_database.py -# ./bss_info_makedb.py | tee bss_info.db +# ./apsbss_makedb.py | tee apsbss.db # # Start the EPICS IOC with a command (from EPICS base): -# softIoc -m P=ioc:bss: -d bss_info.db +# softIoc -m P=ioc:bss: -d apsbss.db record(stringout, "$(P)esaf:cycle") diff --git a/apstools/beamtime/bss_info.py b/apstools/beamtime/apsbss.py similarity index 99% rename from apstools/beamtime/bss_info.py rename to apstools/beamtime/apsbss.py index 488ca2002..59ff23520 100755 --- a/apstools/beamtime/bss_info.py +++ b/apstools/beamtime/apsbss.py @@ -7,9 +7,9 @@ EXAMPLES:: - bss_info current - bss_info esaf 226319 - bss_info proposal 66083 2020-2 9-ID-B,C + apsbss current + apsbss esaf 226319 + apsbss proposal 66083 2020-2 9-ID-B,C """ import argparse @@ -21,7 +21,7 @@ import time import yaml -from .bss_info_ophyd import EpicsBssDevice +from .apsbss_ophyd import EpicsBssDevice DM_APS_DB_WEB_SERVICE_URL = "https://xraydtn01.xray.aps.anl.gov:11236" diff --git a/apstools/beamtime/bss_info.ui b/apstools/beamtime/apsbss.ui similarity index 100% rename from apstools/beamtime/bss_info.ui rename to apstools/beamtime/apsbss.ui diff --git a/apstools/beamtime/bss_info_makedb.py b/apstools/beamtime/apsbss_makedb.py similarity index 97% rename from apstools/beamtime/bss_info_makedb.py rename to apstools/beamtime/apsbss_makedb.py index 59e7d7663..0a1a0b7ad 100755 --- a/apstools/beamtime/bss_info_makedb.py +++ b/apstools/beamtime/apsbss_makedb.py @@ -139,16 +139,16 @@ def main(): header = """\ # - # file: bss_info.db + # file: apsbss.db # EPICS database for information from APS ESAF & Proposal databases # # BSS: Beamtime Scheduling System # # note: this file autogenerated by make_database.py - # ./bss_info_makedb.py | tee bss_info.db + # ./apsbss_makedb.py | tee apsbss.db # # Start the EPICS IOC with a command (from EPICS base): - # softIoc -m P=ioc:bss: -d bss_info.db + # softIoc -m P=ioc:bss: -d apsbss.db """ for line in header.splitlines(): print(line.strip()) diff --git a/apstools/beamtime/bss_info_ophyd.py b/apstools/beamtime/apsbss_ophyd.py similarity index 98% rename from apstools/beamtime/bss_info_ophyd.py rename to apstools/beamtime/apsbss_ophyd.py index 403306c4e..26b223eca 100644 --- a/apstools/beamtime/bss_info_ophyd.py +++ b/apstools/beamtime/apsbss_ophyd.py @@ -1,11 +1,11 @@ #!/usr/bin/env python """ -ophyd support for bss_info +ophyd support for apsbss EXAMPLE:: - bss_info = EpicsBssDevice("ioc:bss:", name="bss_info") + apsbss = EpicsBssDevice("ioc:bss:", name="apsbss") """ __all__ = ["EpicsBssDevice",] diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 53721fee2..c3f4b503a 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -16,7 +16,7 @@ if _path not in sys.path: sys.path.insert(0, _path) -from apstools.beamtime import bss_info, bss_info_makedb +from apstools.beamtime import apsbss, apsbss_makedb BSS_TEST_IOC_PREFIX = f"tst{uuid.uuid4().hex[:7]}:bss:" @@ -37,17 +37,17 @@ def bss_IOC_available(): class Test_Beamtime(unittest.TestCase): def test_general(self): - self.assertEqual(bss_info.CONNECT_TIMEOUT, 5) - self.assertEqual(bss_info.POLL_INTERVAL, 0.01) + self.assertEqual(apsbss.CONNECT_TIMEOUT, 5) + self.assertEqual(apsbss.POLL_INTERVAL, 0.01) self.assertEqual( - bss_info.DM_APS_DB_WEB_SERVICE_URL, + apsbss.DM_APS_DB_WEB_SERVICE_URL, "https://xraydtn01.xray.aps.anl.gov:11236") - self.assertIsNotNone(bss_info.api_bss) - self.assertIsNotNone(bss_info.api_esaf) + self.assertIsNotNone(apsbss.api_bss) + self.assertIsNotNone(apsbss.api_esaf) def test_iso2datetime(self): self.assertEqual( - bss_info.iso2datetime("2020-06-30 12:31:45.067890"), + apsbss.iso2datetime("2020-06-30 12:31:45.067890"), datetime.datetime(2020, 6, 30, 12, 31, 45, 67890) ) @@ -63,12 +63,12 @@ def test_only_at_aps(self): if not using_APS_workstation(): return - runs = bss_info.listAllRuns() + runs = apsbss.listAllRuns() self.assertGreater(len(runs), 1) - self.assertEqual(bss_info.getCurrentCycle(), runs[-1]) - self.assertEqual(bss_info.listRecentRuns()[0], runs[-1]) + self.assertEqual(apsbss.getCurrentCycle(), runs[-1]) + self.assertEqual(apsbss.listRecentRuns()[0], runs[-1]) - self.assertGreater(len(bss_info.listAllBeamlines()), 1) + self.assertGreater(len(apsbss.listAllBeamlines()), 1) # TODO: test the other functions # getCurrentEsafs @@ -83,15 +83,15 @@ def test_only_at_aps(self): def test_printColumns(self): from tests.common import Capture_stdout with Capture_stdout() as received: - bss_info.printColumns("1 2 3 4 5 6".split(), numColumns=3, width=3) + apsbss.printColumns("1 2 3 4 5 6".split(), numColumns=3, width=3) self.assertEqual(len(received), 2) self.assertEqual(received[0], "1 3 5 ") self.assertEqual(received[1], "2 4 6 ") source = "0123456789" - self.assertEqual(bss_info.trim(source), source) - self.assertNotEqual(bss_info.trim(source, length=8), source) - self.assertEqual(bss_info.trim(source, length=8), "01234...") + self.assertEqual(apsbss.trim(source), source) + self.assertNotEqual(apsbss.trim(source, length=8), source) + self.assertEqual(apsbss.trim(source, length=8), "01234...") class Test_EPICS(unittest.TestCase): @@ -107,7 +107,7 @@ def start_ioc_subprocess(self, db_file): if not os.path.exists(softIoc): return db_file = os.path.abspath(os.path.join( - os.path.dirname(bss_info.__file__), + os.path.dirname(apsbss.__file__), db_file )) if not os.path.exists(db_file): @@ -122,7 +122,7 @@ def start_ioc_subprocess(self, db_file): def setUp(self): self.bss = None - self.ioc_process = self.start_ioc_subprocess("bss_info.db") + self.ioc_process = self.start_ioc_subprocess("apsbss.db") def tearDown(self): if self.bss is not None: @@ -136,7 +136,7 @@ def test_ioc(self): if not bss_IOC_available(): return - from apstools.beamtime import bss_info_ophyd as bio + from apstools.beamtime import apsbss_ophyd as bio self.bss = bio.EpicsBssDevice(BSS_TEST_IOC_PREFIX, name="bss") self.bss.wait_for_connection(timeout=2) self.assertTrue(self.bss.connected) @@ -153,7 +153,7 @@ def test_EPICS(self): cycle = "2019-3" with Capture_stdout(): - self.bss = bss_info.connect_epics(BSS_TEST_IOC_PREFIX) + self.bss = apsbss.connect_epics(BSS_TEST_IOC_PREFIX) self.assertTrue(self.bss.connected) self.assertEqual(self.bss.esaf.aps_cycle.get(), "") @@ -165,7 +165,7 @@ def test_EPICS(self): # setup with Capture_stdout(): - bss_info.epicsSetup(BSS_TEST_IOC_PREFIX, beamline, cycle) + apsbss.epicsSetup(BSS_TEST_IOC_PREFIX, beamline, cycle) self.assertNotEqual(self.bss.proposal.beamline_name.get(), "harpo") self.assertEqual(self.bss.proposal.beamline_name.get(), beamline) self.assertEqual(self.bss.esaf.aps_cycle.get(), cycle) @@ -191,7 +191,7 @@ def test_EPICS(self): self.bss.esaf.esaf_id.put(esaf_id) self.bss.proposal.proposal_id.put(proposal_id) with Capture_stdout(): - bss_info.epicsUpdate(BSS_TEST_IOC_PREFIX) + apsbss.epicsUpdate(BSS_TEST_IOC_PREFIX) self.assertEqual( self.bss.esaf.title.get(), "Commission 9ID and USAXS") @@ -200,7 +200,7 @@ def test_EPICS(self): "2019 National School on Neutron & X-r")) with Capture_stdout(): - bss_info.epicsClear(BSS_TEST_IOC_PREFIX) + apsbss.epicsClear(BSS_TEST_IOC_PREFIX) self.assertNotEqual(self.bss.esaf.aps_cycle.get(), "") self.assertEqual(self.bss.esaf.title.get(), "") self.assertEqual(self.bss.proposal.title.get(), "") @@ -211,10 +211,10 @@ class Test_MakeDatabase(unittest.TestCase): def test_general(self): from tests.common import Capture_stdout with Capture_stdout() as db: - bss_info_makedb.main() + apsbss_makedb.main() self.assertEqual(len(db), 345) self.assertEqual(db[0], "#") - self.assertEqual(db[1], "# file: bss_info.db") + self.assertEqual(db[1], "# file: apsbss.db") # randomly-selected spot checks self.assertEqual(db[22], 'record(stringout, "$(P)esaf:id")') self.assertEqual(db[128], ' field(ONAM, "ON")') @@ -231,34 +231,34 @@ def tearDown(self): sys.argv = [self.sys_argv0,] def test_no_options(self): - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertIsNone(args.subcommand) def test_beamlines(self): sys.argv.append("beamlines") - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertEqual(args.subcommand, "beamlines") def test_current(self): sys.argv.append("current") sys.argv.append("9-ID-B,C") - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertEqual(args.subcommand, "current") self.assertEqual(args.beamlineName, "9-ID-B,C") def test_cycles(self): sys.argv.append("cycles") - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertEqual(args.subcommand, "cycles") def test_esaf(self): sys.argv.append("esaf") sys.argv.append("12345") - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertEqual(args.subcommand, "esaf") self.assertEqual(args.esafId, 12345) @@ -268,7 +268,7 @@ def test_proposal(self): sys.argv.append("proposal_number_here") sys.argv.append("1995-1") sys.argv.append("my_beamline") - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertEqual(args.subcommand, "proposal") self.assertEqual(args.proposalId, "proposal_number_here") @@ -278,7 +278,7 @@ def test_proposal(self): def test_EPICS_clear(self): sys.argv.append("clear") sys.argv.append("bss:") - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertEqual(args.subcommand, "clear") self.assertEqual(args.prefix, "bss:") @@ -288,7 +288,7 @@ def test_EPICS_setup(self): sys.argv.append("bss:") sys.argv.append("my_beamline") sys.argv.append("1995-1") - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertEqual(args.subcommand, "setup") self.assertEqual(args.prefix, "bss:") @@ -298,7 +298,7 @@ def test_EPICS_setup(self): def test_EPICS_update(self): sys.argv.append("update") sys.argv.append("bss:") - args = bss_info.get_options() + args = apsbss.get_options() self.assertIsNotNone(args) self.assertEqual(args.subcommand, "update") self.assertEqual(args.prefix, "bss:") From a4868d2598d797b17ffb0ea993389a408d1501be Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Tue, 7 Jul 2020 23:54:03 -0500 Subject: [PATCH 27/47] ENH #333 shell script --- apstools/beamtime/manage_ioc.sh | 174 ++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100755 apstools/beamtime/manage_ioc.sh diff --git a/apstools/beamtime/manage_ioc.sh b/apstools/beamtime/manage_ioc.sh new file mode 100755 index 000000000..540692224 --- /dev/null +++ b/apstools/beamtime/manage_ioc.sh @@ -0,0 +1,174 @@ +#!/bin/bash + +# manage the IOC for apsbss + +DEFAULT_SESSION_NAME=apsbss +DEFAULT_IOC_PREFIX=ioc:bss: + +SHELL_SCRIPT_NAME=${BASH_SOURCE:-${0}} +SELECTION=${1:-usage} +SESSION_NAME=${2:-"${DEFAULT_SESSION_NAME}"} +IOC_PREFIX=${3:-"${DEFAULT_IOC_PREFIX}"} + +IOC_BINARY=softIoc +EPICS_DATABASE=apsbss.db +START_IOC_COMMAND="${IOC_BINARY} -m P=${IOC_PREFIX} -d ${EPICS_DATABASE}" + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +if [ -z "$IOC_STARTUP_DIR" ] ; then + # If no startup dir is specified, use the directory with this script + IOC_STARTUP_DIR=$(dirname "${SHELL_SCRIPT_NAME}") +fi + +# echo "SESSION_NAME = ${SESSION_NAME}" +# echo "IOC_PREFIX = ${IOC_PREFIX}" +# echo "START_IOC_COMMAND = ${START_IOC_COMMAND}" +# echo "SHELL_SCRIPT_NAME = ${SHELL_SCRIPT_NAME}" +# echo "IOC_STARTUP_DIR = ${IOC_STARTUP_DIR}" + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +function checkpid() { + MY_UID=$(id -u) + # # The '\$' is needed in the pgrep pattern to select vm7, but not vm7.sh + IOC_PID=$(pgrep ${IOC_BINARY}\$ -u ${MY_UID}) + # #!echo "IOC_PID=${IOC_PID}" + + if [ "${IOC_PID}" != "" ] ; then + # Assume the IOC is down until proven otherwise + IOC_DOWN=1 + SCREEN_PID="" + + # At least one instance of the IOC binary is running; + # Find the binary that is associated with this script/IOC + for pid in ${IOC_PID}; do + # compare directories + BIN_CWD=`readlink /proc/${pid}/cwd` + IOC_CWD=`readlink -f ${IOC_STARTUP_DIR}` + + if [ "$BIN_CWD" = "$IOC_CWD" ] ; then + # The IOC is running; + # the process with PID=$pid is the + # IOC that was run from $IOC_STARTUP_DIR + P_PID=$(ps -p ${pid} -o ppid=) + # strip leading (and trailing) whitespace + arr=($P_PID) + P_PID=${arr[0]} + SCREEN_SESSION="${P_PID}.${SESSION_NAME}" + SCREEN_MATCH=$(screen -ls "${SCREEN_SESSION}" | grep "${SESSION_NAME}") + if [ "${SCREEN_MATCH}" != "" ] ; then + # IOC is running in screen + IOC_DOWN=0 + IOC_PID=${pid} + SCREEN_PID=${P_PID} + break + fi + fi + done + else + # IOC is not running + IOC_DOWN=1 + fi + + return ${IOC_DOWN} +} + +function console () { + if checkpid; then + echo "Connecting to ${SCREEN_SESSION}'s screen session" + # The -r flag will only connect if no one is attached to the session + #!screen -r ${SESSION_NAME} + # The -x flag will connect even if someone is attached to the session + screen -x ${SCREEN_SESSION} + else + echo "${SCREEN_NAME} is not running" + fi +} + +function exit_if_running() { + # ensure that multiple, simultaneous IOCs are not started by this user ID + MY_UID=`id -u` + IOC_PID="`pgrep ${SESSION_NAME}\$ -u ${MY_UID}`" + + if [ "" != "${IOC_PID}" ] ; then + echo "${SESSION_NAME} IOC is already running (PID=${IOC_PID}), won't start a new one" + exit 1 + fi +} + +function restart() { + stop + start +} + +function run_ioc() { + exit_if_running + ${START_IOC_COMMAND} +} + +function screenpid() { + if [ -z ${SCREEN_PID} ] ; then + echo + else + echo " in a screen session (pid=${SCREEN_PID})" + fi +} + +function start() { + if checkpid; then + echo -n "${SCREEN_SESSION} is already running (pid=${IOC_PID})" + screenpid + else + echo "Starting ${SESSION_NAME}" + cd ${IOC_STARTUP_DIR} + # Run SESSION_NAME inside a screen session + screen -dm -S ${SESSION_NAME} -h 5000 ${START_IOC_COMMAND} + fi +} + +function status() { + if checkpid; then + echo -n "${SCREEN_SESSION} is running (pid=${IOC_PID})" + screenpid + else + echo "${SESSION_NAME} is not running" + fi +} + +function stop() { + if checkpid; then + echo "Stopping ${SCREEN_SESSION} (pid=${IOC_PID})" + kill ${IOC_PID} + else + echo "${SESSION_NAME} is not running" + fi +} + +function usage() { + echo "Usage: `basename ${SHELL_SCRIPT_NAME}` {start|stop|restart|status|console|run} [NAME [PREFIX]]" + echo "" + echo " COMMANDS" + echo " console attach to IOC console if IOC is running in screen" + echo " restart restart IOC" + echo " run run IOC in console (not screen)" + echo " start start IOC" + echo " status report if IOC is running" + echo " stop stop IOC" + echo "" + echo " OPTIONAL TERMS" + echo " NAME name of IOC session (default: ${DEFAULT_SESSION_NAME})" + echo " PREFIX IOC prefix (default: ${DEFAULT_IOC_PREFIX})" +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +case ${SELECTION} in + start) start ;; + stop | kill) stop ;; + restart) restart ;; + status) status ;; + console) console ;; + run) run_ioc ;; + *) usage ;; +esac From 34852385a70d87ae2bae2ecdfcf165bcd90d7757 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 8 Jul 2020 00:41:26 -0500 Subject: [PATCH 28/47] TST #333 refactor --- tests/test_beamtime.py | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index c3f4b503a..e509c3353 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -8,6 +8,7 @@ import socket import subprocess import sys +import time import unittest import uuid @@ -96,52 +97,51 @@ def test_printColumns(self): class Test_EPICS(unittest.TestCase): - def start_ioc_subprocess(self, db_file): - softIoc = os.path.abspath(os.path.join( - os.environ.get("CONDA_PREFIX"), - "epics", - "bin", - os.environ.get("EPICS_HOST_ARCH", "linux-x86_64"), - "softIoc" - )) - if not os.path.exists(softIoc): - return - db_file = os.path.abspath(os.path.join( + def setUp(self): + self.bss = None + self.manager = os.path.abspath(os.path.join( os.path.dirname(apsbss.__file__), - db_file + "manage_ioc.sh" )) - if not os.path.exists(db_file): - return - cmd = f"{softIoc} -m P={BSS_TEST_IOC_PREFIX} -d {db_file}".split() - return subprocess.Popen( - cmd, + self.ioc_name = "test_apsbss" + cmd = f"{self.manager} restart {self.ioc_name} {BSS_TEST_IOC_PREFIX}" + self.ioc_process = subprocess.Popen( + cmd.encode().split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - - def setUp(self): - self.bss = None - self.ioc_process = self.start_ioc_subprocess("apsbss.db") + time.sleep(.5) # allow the IOC to start def tearDown(self): if self.bss is not None: self.bss.destroy() self.bss = None if self.ioc_process is not None: - self.ioc_process.communicate('exit\n'.encode()) self.ioc_process = None + cmd = f"{self.manager} stop {self.ioc_name} {BSS_TEST_IOC_PREFIX}" + session = subprocess.Popen( + cmd.encode().split(), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False) + # self.ioc_process.communicate(cmd.encode().split()) + self.manager = None def test_ioc(self): if not bss_IOC_available(): return from apstools.beamtime import apsbss_ophyd as bio + from ophyd import EpicsSignal + self.bss = bio.EpicsBssDevice(BSS_TEST_IOC_PREFIX, name="bss") self.bss.wait_for_connection(timeout=2) self.assertTrue(self.bss.connected) - - self.assertEqual(self.bss.esaf.aps_cycle.get(), "") + self.assertEqual(self.bss.esaf.title.get(), "") + self.assertEqual(self.bss.esaf.description.get(), "") + # self.assertEqual(self.bss.esaf.aps_cycle.get(), "") def test_EPICS(self): from tests.common import Capture_stdout From e42003489e3a16bf6e1f1fa2e4ddc871b1e687b5 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 8 Jul 2020 00:59:44 -0500 Subject: [PATCH 29/47] TST #333 Codacy --- apstools/beamtime/manage_ioc.sh | 26 +++++++++++++------------- tests/test_beamtime.py | 3 +-- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/apstools/beamtime/manage_ioc.sh b/apstools/beamtime/manage_ioc.sh index 540692224..02a0b5b90 100755 --- a/apstools/beamtime/manage_ioc.sh +++ b/apstools/beamtime/manage_ioc.sh @@ -32,7 +32,7 @@ fi function checkpid() { MY_UID=$(id -u) # # The '\$' is needed in the pgrep pattern to select vm7, but not vm7.sh - IOC_PID=$(pgrep ${IOC_BINARY}\$ -u ${MY_UID}) + IOC_PID=$(pgrep "${IOC_BINARY}"\$ -u "${MY_UID}") # #!echo "IOC_PID=${IOC_PID}" if [ "${IOC_PID}" != "" ] ; then @@ -44,14 +44,14 @@ function checkpid() { # Find the binary that is associated with this script/IOC for pid in ${IOC_PID}; do # compare directories - BIN_CWD=`readlink /proc/${pid}/cwd` - IOC_CWD=`readlink -f ${IOC_STARTUP_DIR}` + BIN_CWD=$(readlink "/proc/${pid}/cwd") + IOC_CWD=$(readlink -f "${IOC_STARTUP_DIR}") if [ "$BIN_CWD" = "$IOC_CWD" ] ; then # The IOC is running; # the process with PID=$pid is the # IOC that was run from $IOC_STARTUP_DIR - P_PID=$(ps -p ${pid} -o ppid=) + P_PID=$(ps -p "${pid}" -o ppid=) # strip leading (and trailing) whitespace arr=($P_PID) P_PID=${arr[0]} @@ -78,9 +78,9 @@ function console () { if checkpid; then echo "Connecting to ${SCREEN_SESSION}'s screen session" # The -r flag will only connect if no one is attached to the session - #!screen -r ${SESSION_NAME} + #!screen -r "${SESSION_NAME}" # The -x flag will connect even if someone is attached to the session - screen -x ${SCREEN_SESSION} + screen -x "${SCREEN_SESSION}" else echo "${SCREEN_NAME} is not running" fi @@ -88,8 +88,8 @@ function console () { function exit_if_running() { # ensure that multiple, simultaneous IOCs are not started by this user ID - MY_UID=`id -u` - IOC_PID="`pgrep ${SESSION_NAME}\$ -u ${MY_UID}`" + MY_UID=$(id -u) + IOC_PID=$(pgrep "${SESSION_NAME}"\$ -u "${MY_UID}") if [ "" != "${IOC_PID}" ] ; then echo "${SESSION_NAME} IOC is already running (PID=${IOC_PID}), won't start a new one" @@ -108,7 +108,7 @@ function run_ioc() { } function screenpid() { - if [ -z ${SCREEN_PID} ] ; then + if [ -z "${SCREEN_PID}" ] ; then echo else echo " in a screen session (pid=${SCREEN_PID})" @@ -121,9 +121,9 @@ function start() { screenpid else echo "Starting ${SESSION_NAME}" - cd ${IOC_STARTUP_DIR} + cd "${IOC_STARTUP_DIR}" # Run SESSION_NAME inside a screen session - screen -dm -S ${SESSION_NAME} -h 5000 ${START_IOC_COMMAND} + screen -dm -S "${SESSION_NAME}" -h 5000 "${START_IOC_COMMAND}" fi } @@ -139,14 +139,14 @@ function status() { function stop() { if checkpid; then echo "Stopping ${SCREEN_SESSION} (pid=${IOC_PID})" - kill ${IOC_PID} + kill "${IOC_PID}" else echo "${SESSION_NAME} is not running" fi } function usage() { - echo "Usage: `basename ${SHELL_SCRIPT_NAME}` {start|stop|restart|status|console|run} [NAME [PREFIX]]" + echo "Usage: $(basename ${SHELL_SCRIPT_NAME}) {start|stop|restart|status|console|run} [NAME [PREFIX]]" echo "" echo " COMMANDS" echo " console attach to IOC console if IOC is running in screen" diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index e509c3353..facc9d0e2 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -120,7 +120,7 @@ def tearDown(self): if self.ioc_process is not None: self.ioc_process = None cmd = f"{self.manager} stop {self.ioc_name} {BSS_TEST_IOC_PREFIX}" - session = subprocess.Popen( + subprocess.Popen( cmd.encode().split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -134,7 +134,6 @@ def test_ioc(self): return from apstools.beamtime import apsbss_ophyd as bio - from ophyd import EpicsSignal self.bss = bio.EpicsBssDevice(BSS_TEST_IOC_PREFIX, name="bss") self.bss.wait_for_connection(timeout=2) From c8ba0564dcadd9988e6c619bcc8f2e8fba3a29d4 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 8 Jul 2020 01:11:07 -0500 Subject: [PATCH 30/47] TST #333 Codacy --- apstools/beamtime/manage_ioc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apstools/beamtime/manage_ioc.sh b/apstools/beamtime/manage_ioc.sh index 02a0b5b90..52dc5c2eb 100755 --- a/apstools/beamtime/manage_ioc.sh +++ b/apstools/beamtime/manage_ioc.sh @@ -146,7 +146,7 @@ function stop() { } function usage() { - echo "Usage: $(basename ${SHELL_SCRIPT_NAME}) {start|stop|restart|status|console|run} [NAME [PREFIX]]" + echo "Usage: $(basename "${SHELL_SCRIPT_NAME}") {start|stop|restart|status|console|run} [NAME [PREFIX]]" echo "" echo " COMMANDS" echo " console attach to IOC console if IOC is running in screen" From fc33050f6b73bf3b99ea30ec9d04f1f52a981aeb Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 8 Jul 2020 10:43:18 -0500 Subject: [PATCH 31/47] TST #333 --- tests/test_beamtime.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index facc9d0e2..89152102f 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -91,8 +91,11 @@ def test_printColumns(self): source = "0123456789" self.assertEqual(apsbss.trim(source), source) - self.assertNotEqual(apsbss.trim(source, length=8), source) - self.assertEqual(apsbss.trim(source, length=8), "01234...") + got = apsbss.trim(source, length=8) + self.assertNotEqual(got, source) + self.assertTrue(got.endswith("...")) + self.assertEqual(len(got), 8) + self.assertEqual(got, "01234...") class Test_EPICS(unittest.TestCase): From 8ffa1647d3ea5b881602533af021371f4dd259f8 Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Wed, 8 Jul 2020 14:29:32 -0500 Subject: [PATCH 32/47] DOC #333 --- docs/source/applications/apsbss.rst | 43 +++++++++++++++++++ docs/source/applications/index.rst | 4 +- .../source/source/beamtime/_apsbss makedb.rst | 7 +++ docs/source/source/beamtime/_apsbss.rst | 7 +++ docs/source/source/beamtime/_apsbss_ophyd.rst | 7 +++ docs/source/source/beamtime/index.rst | 12 ++++++ docs/source/source/index.rst | 1 + 7 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 docs/source/applications/apsbss.rst create mode 100644 docs/source/source/beamtime/_apsbss makedb.rst create mode 100644 docs/source/source/beamtime/_apsbss.rst create mode 100644 docs/source/source/beamtime/_apsbss_ophyd.rst create mode 100644 docs/source/source/beamtime/index.rst diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst new file mode 100644 index 000000000..6cca16856 --- /dev/null +++ b/docs/source/applications/apsbss.rst @@ -0,0 +1,43 @@ + + +.. index:: apsbss_application + +.. _apsbss: + +apsbss +------ + +Information from the APS Proposal and ESAF (experiment safety approval form) databases. + +Example - command line +++++++++++++++++++++++ + +Before using the command-line interface, find out what +the *apsbss* application expects:: + + $ apsbss -h + usage: apsbss.py [-h] + {beamlines,current,cycles,esaf,proposal,clear,setup,update} + ... + + Retrieve specific records from the APS Proposal and ESAF databases + + optional arguments: + -h, --help show this help message and exit + + subcommand: + {beamlines,current,cycles,esaf,proposal,clear,setup,update} + beamlines print list of beamlines + current print current ESAF(s) and proposal(s) + cycles print APS cycle names + esaf print specific ESAF + proposal print specific proposal + clear EPICS PVs: clear + setup EPICS PVs: setup + update EPICS PVs: update from BSS + + +Source code documentation ++++++++++++++++++++++++++ + +See :ref:`beamtime_source_docs` for the source code documentation. diff --git a/docs/source/applications/index.rst b/docs/source/applications/index.rst index 64f2d05aa..c865c9813 100644 --- a/docs/source/applications/index.rst +++ b/docs/source/applications/index.rst @@ -8,14 +8,16 @@ Applications :maxdepth: 2 :glob: + apsbss snapshot spec2ophyd -There are two command-line applications provided by apstools: +There are the applications provided by apstools: ===================================================== ================================= application purpose ===================================================== ================================= +:ref:`apsbss_application` Information from the APS Proposal and ESAF databases. :ref:`apstools_plan_catalog ` summary list of all scans in the databroker :ref:`bluesky_snapshot` Take a snapshot of a list of EPICS PVs and record it in the databroker. :ref:`spec2ophyd` read SPEC config file and convert to ophyd setup commands diff --git a/docs/source/source/beamtime/_apsbss makedb.rst b/docs/source/source/beamtime/_apsbss makedb.rst new file mode 100644 index 000000000..014bd50d1 --- /dev/null +++ b/docs/source/source/beamtime/_apsbss makedb.rst @@ -0,0 +1,7 @@ + +apsbss: Create EPICS database +----------------------------- + +.. automodule:: apstools.beamtime.apsbss_makedb + :members: + diff --git a/docs/source/source/beamtime/_apsbss.rst b/docs/source/source/beamtime/_apsbss.rst new file mode 100644 index 000000000..fea8495cc --- /dev/null +++ b/docs/source/source/beamtime/_apsbss.rst @@ -0,0 +1,7 @@ + +apsbss: Command line application +-------------------------------- + +.. automodule:: apstools.beamtime.apsbss + :members: + diff --git a/docs/source/source/beamtime/_apsbss_ophyd.rst b/docs/source/source/beamtime/_apsbss_ophyd.rst new file mode 100644 index 000000000..df97ab423 --- /dev/null +++ b/docs/source/source/beamtime/_apsbss_ophyd.rst @@ -0,0 +1,7 @@ + +apsbss: Ophyd Device +-------------------- + +.. automodule:: apstools.beamtime.apsbss_ophyd + :members: + diff --git a/docs/source/source/beamtime/index.rst b/docs/source/source/beamtime/index.rst new file mode 100644 index 000000000..51929c86c --- /dev/null +++ b/docs/source/source/beamtime/index.rst @@ -0,0 +1,12 @@ +.. _beamtime_source_docs: + +APS Proposal and ESAF Support; Source Code Documentation +======================================================== + +Support for the APS Proposal and ESAF (experiment safety approval form) databases. + +.. toctree:: + :maxdepth: 2 + :glob: + + _* diff --git a/docs/source/source/index.rst b/docs/source/source/index.rst index ffdf3e690..48e0d254c 100644 --- a/docs/source/source/index.rst +++ b/docs/source/source/index.rst @@ -8,4 +8,5 @@ API Documentation :glob: _* + beamtime/index synApps/index From 94442993d02bac710efc1065434a15c51d2130e3 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 8 Jul 2020 22:00:26 -0500 Subject: [PATCH 33/47] DOC #333 WIP --- apstools/beamtime/apsbss.py | 229 +++++++++++++++++++++++--- apstools/beamtime/apsbss_makedb.py | 7 + apstools/beamtime/apsbss_ophyd.py | 87 ++-------- docs/source/applications/apsbss.rst | 4 +- docs/source/source/beamtime/index.rst | 8 +- setup.py | 3 +- 6 files changed, 244 insertions(+), 94 deletions(-) diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py index 59ff23520..664e20c2e 100755 --- a/apstools/beamtime/apsbss.py +++ b/apstools/beamtime/apsbss.py @@ -1,15 +1,53 @@ #!/usr/bin/env python """ -Retrieve specific records from the APS Proposal and ESAF databases +Retrieve specific records from the APS Proposal and ESAF databases. -BSS: Beamline Scheduling System +This code provides the command-line application: ``apsbss`` + +.. note:: BSS: APS Beamline Scheduling System EXAMPLES:: apsbss current apsbss esaf 226319 apsbss proposal 66083 2020-2 9-ID-B,C + +EPICS SUPPORT + +.. autosummary:: + + ~connect_epics + ~epicsClear + ~epicsSetup + ~epicsUpdate + +APS ESAF & PROPOSAL ACCESS + +.. autosummary:: + + ~getCurrentCycle + ~getCurrentEsafs + ~getCurrentInfo + ~getCurrentProposals + ~getEsaf + ~getProposal + ~iso2datetime + ~listAllBeamlines + ~listAllRuns + ~listRecentRuns + ~printColumns + ~trim + +APPLICATION + +.. autosummary:: + + ~cmd_current + ~cmd_esaf + ~cmd_proposal + ~get_options + ~main """ import argparse @@ -36,8 +74,20 @@ class EpicsNotConnected(Exception): ... +class DmRecordNotFound(Exception): ... +class EsafNotFound(DmRecordNotFound): ... +class ProposalNotFound(DmRecordNotFound): ... + def connect_epics(prefix): + """ + Connect with the EPICS database instance. + + PARAMETERS + + prefix (str): + EPICS PV prefix + """ t0 = time.time() t_timeout = t0 + CONNECT_TIMEOUT bss = EpicsBssDevice(prefix, name="bss") @@ -54,7 +104,13 @@ def connect_epics(prefix): def epicsClear(prefix): """ - clear the EPICS database + Clear the EPICS database. + Connect with the EPICS database instance. + + PARAMETERS + + prefix (str): + EPICS PV prefix """ print(f"clear EPICS {prefix}") bss = connect_epics(prefix) @@ -67,7 +123,13 @@ def epicsClear(prefix): def epicsUpdate(prefix): """ - update an EPICS database with current ESAF & proposal information + Update EPICS database instance with current ESAF & proposal info. + Connect with the EPICS database instance. + + PARAMETERS + + prefix (str): + EPICS PV prefix """ print(f"update EPICS {prefix}") bss = connect_epics(prefix) @@ -143,9 +205,23 @@ def epicsUpdate(prefix): def epicsSetup(prefix, beamline, cycle=None): + """ + Define the beamline name and APS cycle in the EPICS database. + Connect with the EPICS database instance. + + PARAMETERS + + prefix (str): + EPICS PV prefix + beamline (str): + Name of beam line (as defined by the BSS) + cycle (str): + Name of APS run cycle (as defined by the BSS). + optional: default is current APS run cycle name. + """ if beamline not in listAllBeamlines(): raise ValueError(f"{beamline} is not known") - if cycle not in listAllRuns(): + if cycle is not None and cycle not in listAllRuns(): raise ValueError(f"{cycle} is not known") bss = connect_epics(prefix) @@ -161,10 +237,19 @@ def epicsSetup(prefix, beamline, cycle=None): def getCurrentCycle(): + """Return the name of the current APS run cycle.""" return api_bss.getCurrentRun()["name"] def getCurrentEsafs(sector): + """ + Return list of ESAFs for the current year. + + PARAMETERS + + sector (str or int): + Name of sector. If ``str``, must be in ``%02d`` format (``02``, not ``2``). + """ if isinstance(sector, int): sector = f"{sector:02d}" if len(sector) == 1: @@ -182,6 +267,14 @@ def getCurrentEsafs(sector): def getCurrentInfo(beamline): + """ + From current year ESAFS, return list of ESAFs & proposals with same people. + + PARAMETERS + + beamline (str): + Name of beam line (as defined by the BSS). + """ sector = beamline.split("-")[0] tNow = datetime.datetime.now() @@ -211,6 +304,14 @@ def getCurrentInfo(beamline): def getCurrentProposals(beamline): + """ + Return a list of proposal ID numbers that are current. + + PARAMETERS + + beamline (str): + Name of beam line (as defined by the BSS). + """ proposals = [] for cycle in listRecentRuns(): for prop in api_bss.listProposals(beamlineName=beamline, runName=cycle): @@ -221,6 +322,14 @@ def getCurrentProposals(beamline): def getEsaf(esafId): + """ + Return ESAF as a dictionary. + + PARAMETERS + + esafId (int): + ESAF number + """ try: record = api_esaf.getEsaf(int(esafId)) except dm.ObjectNotFound: @@ -228,30 +337,51 @@ def getEsaf(esafId): return dict(record.data) -def getProposal(proposalId, runName, beamlineName): +def getProposal(proposalId, cycle, beamline): + """ + Return proposal as a dictionary. + + PARAMETERS + + proposalId (str): + Proposal identification number + cycle (str): + Name of APS run cycle (as defined by the BSS) + beamline (str): + Name of beam line (as defined by the BSS) + """ # avoid possible dm.DmException - if runName not in listAllRuns(): - raise DmRecordNotFound(f"run '{runName}' not found") + if cycle not in listAllRuns(): + raise DmRecordNotFound(f"cycle '{cycle}' not found") - if beamlineName not in listAllBeamlines(): - raise DmRecordNotFound(f"beamline '{beamlineName}' not found") + if beamline not in listAllBeamlines(): + raise DmRecordNotFound(f"beamline '{beamline}' not found") try: - record = api_bss.getProposal(str(proposalId), runName, beamlineName) + record = api_bss.getProposal(str(proposalId), cycle, beamline) except dm.ObjectNotFound: raise ProposalNotFound( f"id={proposalId}" - f" run={runName}" - f" beamline={beamlineName}" + f" cycle={cycle}" + f" beamline={beamline}" ) return dict(record.data) def iso2datetime(isodate): + """ + Convert a text ISO8601 date into a ``datetime`` object. + + PARAMETERS + + isodate (str): + Date and time in ISO8601 format. (e.g.: ``2020-07-01T12:34:56.789012``) + """ return datetime.datetime.fromisoformat(isodate) def listAllBeamlines(): + """Return list (from ``dm``) of known beam line names.""" if "beamlines" not in _cache_: _cache_["beamlines"] = [ entry["name"] @@ -261,6 +391,7 @@ def listAllBeamlines(): def listAllRuns(): + """Return a list of all known cycles. Cache for repeated use.""" if "cycles" not in _cache_: _cache_["cycles"] = sorted([ entry["name"] @@ -270,6 +401,14 @@ def listAllRuns(): def listRecentRuns(quantity=6): + """ + Return a list of the 6 most recent runs (2-year period). + + PARAMETERS + + quantity (int): + number of APS run cycles to include, optional (default: 6) + """ # 6 runs is the duration of a user proposal tNow = datetime.datetime.now() runs = [ @@ -281,6 +420,18 @@ def listRecentRuns(quantity=6): def printColumns(items, numColumns=5, width=10): + """ + Print a list of ``items`` in column order. + + PARAMETERS + + items (list(str)): + List of items to report + numColumns (int): + number of columns, optional (default: 5) + width (int): + width of each column, optional (default: 10) + """ n = len(items) rows = n // numColumns if n % numColumns > 0: @@ -294,17 +445,32 @@ def printColumns(items, numColumns=5, width=10): def trim(text, length=40): - if len(text) > length: - text = text[:length-3] + "..." - return text + """ + Return a string that is no longer than ``length``. + If a string is longer than ``length``, it is shortened + to the ``length-3`` characters, then, ``...`` is appended. + For very short length, the string is shortened to ``length`` + (and no ``...`` is appended). -class DmRecordNotFound(Exception): ... -class EsafNotFound(DmRecordNotFound): ... -class ProposalNotFound(DmRecordNotFound): ... + PARAMETERS + + text (str): + String, potentially longer than ``length`` + length (int): + maximum length, optional (default: 40) + """ + if length < 1: + raise ValueError(f"length must be positive, received {length}") + if length < 5: + text = text[:length] + elif len(text) > length: + text = text[:length-3] + "..." + return text def get_options(): + """Handle command line arguments.""" parser = argparse.ArgumentParser( prog=os.path.split(sys.argv[0])[-1], description=__doc__.strip().splitlines()[0], @@ -342,6 +508,14 @@ def get_options(): def cmd_current(args): + """ + Handle ``current`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ records = getCurrentProposals(args.beamlineName) if len(records) == 0: print(f"No current proposals for {args.beamlineName}") @@ -391,6 +565,14 @@ def cmd_current(args): def cmd_esaf(args): + """ + Handle ``esaf`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ try: esaf = getEsaf(args.esafId) print(yaml.dump(esaf)) @@ -401,6 +583,14 @@ def cmd_esaf(args): def cmd_proposal(args): + """ + Handle ``proposal`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ try: proposal = getProposal(args.proposalId, args.cycle, args.beamlineName) print(yaml.dump(proposal)) @@ -411,6 +601,7 @@ def cmd_proposal(args): def main(): + """Command-line interface for ``apsbss`` program.""" args = get_options() if args.subcommand == "beamlines": printColumns(listAllBeamlines(), numColumns=4, width=15) diff --git a/apstools/beamtime/apsbss_makedb.py b/apstools/beamtime/apsbss_makedb.py index 0a1a0b7ad..d470b6aa8 100755 --- a/apstools/beamtime/apsbss_makedb.py +++ b/apstools/beamtime/apsbss_makedb.py @@ -2,6 +2,12 @@ """ Create the EPICS database + +This is just a service program to make the ``.db`` file. + +.. autosummary:: + + ~main """ #name RTYP length @@ -137,6 +143,7 @@ def main(): + """Make an EPICS ``.db`` file from the PVs listed above.""" header = """\ # # file: apsbss.db diff --git a/apstools/beamtime/apsbss_ophyd.py b/apstools/beamtime/apsbss_ophyd.py index 26b223eca..29f9af068 100644 --- a/apstools/beamtime/apsbss_ophyd.py +++ b/apstools/beamtime/apsbss_ophyd.py @@ -6,6 +6,14 @@ EXAMPLE:: apsbss = EpicsBssDevice("ioc:bss:", name="apsbss") + +.. autosummary:: + + ~EpicsBssDevice + ~EpicsEsafDevice + ~EpicsEsafExperimenterDevice + ~EpicsProposalDevice + ~EpicsProposalExperimenterDevice """ __all__ = ["EpicsBssDevice",] @@ -15,11 +23,7 @@ class EpicsEsafExperimenterDevice(Device): """ - - badge: '64065' - badgeNumber: '64065' - email: kuzmenko@aps.anl.gov - firstName: Ivan - lastName: Kuzmenko + Ophyd device for experimenter info from APS ESAF. """ badge_number = Component(EpicsSignal, "badgeNumber", string=True) email = Component(EpicsSignal, "email", string=True) @@ -35,32 +39,7 @@ def clear(self): class EpicsEsafDevice(Device): """ - description: We will commission beamline and USAXS instrument. We will perform experiments - with safe beamline standards and test samples (all located at beamline and used - for this purpose routinely) to evaluate performance of beamline and instrument. - We will perform hardware and software development as needed. - esafId: 226319 - esafStatus: Approved - esafTitle: Commission 9ID and USAXS - experimentEndDate: '2020-09-28 08:00:00' - experimentStartDate: '2020-05-26 08:00:00' - experimentUsers: - - badge: '86312' - badgeNumber: '86312' - email: ilavsky@aps.anl.gov - firstName: Jan - lastName: Ilavsky - - badge: '53748' - badgeNumber: '53748' - email: emaxey@aps.anl.gov - firstName: Evan - lastName: Maxey - - badge: '64065' - badgeNumber: '64065' - email: kuzmenko@aps.anl.gov - firstName: Ivan - lastName: Kuzmenko - sector: 09 + Ophyd device for info from APS ESAF. """ aps_cycle = Component(EpicsSignal, "cycle", string=True) @@ -74,6 +53,7 @@ class EpicsEsafDevice(Device): user_last_names = Component(EpicsSignal, "users", string=True) user_badges = Component(EpicsSignal, "userBadges", string=True) + _max_users = 9 # 9 users at most? user1 = Component(EpicsEsafExperimenterDevice, "user1:") user2 = Component(EpicsEsafExperimenterDevice, "user2:") user3 = Component(EpicsEsafExperimenterDevice, "user3:") @@ -112,14 +92,7 @@ def clear_users(self): class EpicsProposalExperimenterDevice(Device): """ - badge: '300679' - email: yilianglin@uchicago.edu - firstName: Yiliang - id: 433796 - instId: 3435 - institution: The University of Chicago - lastName: Lin - piFlag: Y + Ophyd device for experimenter info from APS Proposal. """ badge_number = Component(EpicsSignal, "badgeNumber", string=True) email = Component(EpicsSignal, "email", string=True) @@ -143,36 +116,7 @@ def clear(self): class EpicsProposalDevice(Device): """ - experimenters: - - badge: '292588' - email: fangyin123@uchicago.edu - firstName: Yin - id: 433774 - instId: 3435 - institution: The University of Chicago - lastName: Fang - - badge: '304975' - email: sjiuyun@uchicago.edu - firstName: Jiuyun - id: 433775 - instId: 3435 - institution: The University of Chicago - lastName: Shi - - badge: '300679' - email: yilianglin@uchicago.edu - firstName: Yiliang - id: 433796 - instId: 3435 - institution: The University of Chicago - lastName: Lin - piFlag: Y - id: 66083 - mailInFlag: N - proprietaryFlag: N - submittedDate: '2019-07-04 13:42:32' - title: Mapping the mechanical-responsive conductive network in bioinspired composite - materials with 3D correlative x-ray fluorescence and ptychographic tomography - totalShiftsRequested: 27 + Ophyd device for info from APS Proposal. """ beamline_name = Component(EpicsSignal, "beamline", string=True) @@ -185,6 +129,7 @@ class EpicsProposalDevice(Device): user_last_names = Component(EpicsSignal, "users", string=True) user_badges = Component(EpicsSignal, "userBadges", string=True) + _max_users = 9 # 9 users at most? user1 = Component(EpicsProposalExperimenterDevice, "user1:") user2 = Component(EpicsProposalExperimenterDevice, "user2:") user3 = Component(EpicsProposalExperimenterDevice, "user3:") @@ -220,6 +165,10 @@ def clear_users(self): class EpicsBssDevice(Device): + """ + Ophyd device for info from APS Proposal and ESAF databases. + """ + esaf = Component(EpicsEsafDevice, "esaf:") proposal = Component(EpicsProposalDevice, "proposal:") diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index 6cca16856..43110d382 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -1,8 +1,8 @@ -.. index:: apsbss_application +.. index:: apsbss -.. _apsbss: +.. _apsbss_application: apsbss ------ diff --git a/docs/source/source/beamtime/index.rst b/docs/source/source/beamtime/index.rst index 51929c86c..784b74b92 100644 --- a/docs/source/source/beamtime/index.rst +++ b/docs/source/source/beamtime/index.rst @@ -1,9 +1,11 @@ .. _beamtime_source_docs: -APS Proposal and ESAF Support; Source Code Documentation -======================================================== +APS Proposal and ESAF Support +============================= -Support for the APS Proposal and ESAF (experiment safety approval form) databases. +Source code documentation of the +support for the APS Proposal and ESAF +(experiment safety approval form) databases. .. toctree:: :maxdepth: 2 diff --git a/setup.py b/setup.py index 810d1576e..f5541e1e7 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ """ -packaging setup for apstools +Packaging setup for apstools. """ # Always prefer setuptools over distutils @@ -16,6 +16,7 @@ __entry_points__ = { 'console_scripts': [ + 'apsbss = apstools.beamtime.apsbss:main', 'apstools_plan_catalog = apstools.examples:main', 'bluesky_snapshot = apstools.snapshot:snapshot_cli', 'bluesky_snapshot_viewer = apstools.snapshot:snapshot_gui', From 53d516d1c32326457e688faeebd52d576ed00a8e Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 8 Jul 2020 22:44:21 -0500 Subject: [PATCH 34/47] DOC #333 WIP --- docs/source/applications/apsbss.rst | 75 ++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index 43110d382..6ed43d2c7 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -7,7 +7,41 @@ apsbss ------ -Information from the APS Proposal and ESAF (experiment safety approval form) databases. +Provide information from APS Proposal and ESAF (experiment safety approval +form) databases as PVs at each beam line so that this information +(metadata) may be added to new data files. The ``aps-dm-api`` +(``dm`` for short) package [#]_ +is used to access the APS databases as read-only. + +*No information is written back to the APS databases.* + +.. [#] ``dm``: https://anaconda.org/search?q=aps-dm-api + +.. sidebar:: Info written to local PVs + + This information retreived from the APS databases is stored in PVs + on the beam line subnet. These PVs are available to *any* EPICS + client as metadata (SPEC, area detector, Bluesky, GUI screens, logging, other). + This design allows the local instrument team to override + any values read from the APS databases, if that is needed. + +Given a beam line name (such as ``9-ID-B,C``), +APS run cycle name (such as ``2020-2``), +proposal ID number (such as ``66083``), and +ESAF ID number (such as ``226319``), +the typical information obtained includes: + +* ESAF & proposal titles +* user names +* user institutional affiliations +* user emails +* applicable dates, reported in ISO8601 time format +* is proposal propietary? +* is experiment mail-in? + +These PVs are loaded on demand by the local instrument team at the beam line. + +.. TODO: describe how this is done Example - command line ++++++++++++++++++++++ @@ -37,6 +71,45 @@ the *apsbss* application expects:: update EPICS PVs: update from BSS +.. _apsbss_ioc_management: + +IOC Management +++++++++++++++ + +The EPICS PVs are provided by running an instance of ``apsbss.db`` +either in an existing EPICS IOC or using the ``softIoc`` application +from EPICS base. A shell script (``manage_ioc.sh``) is included +for management of the IOC:: + + $ ./manage_ioc.sh + Usage: manage_ioc.sh {start|stop|restart|status|console|run} [NAME [PREFIX]] + + COMMANDS + console attach to IOC console if IOC is running in screen + restart restart IOC + run run IOC in console (not screen) + start start IOC + status report if IOC is running + stop stop IOC + + OPTIONAL TERMS + NAME name of IOC session (default: apsbss) + PREFIX IOC prefix (default: ioc:bss:) + +.. TODO: download for ``apsbss.db`` + + +.. _apsbss_epics_gui_screens: + +Displays for MEDM & caQtDM +++++++++++++++++++++++++++ + +Display screen files are provided for viewing the EPICS PVs +using either MEDM (``apsbss.adl``) or caQtDM (``apsbss.ui``). + +.. TODO: downloads for each screen file +.. TODO: screen images here + Source code documentation +++++++++++++++++++++++++ From b2a203468c2aa1e03834704d9f3f10c6b6efa22c Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Wed, 8 Jul 2020 22:57:27 -0500 Subject: [PATCH 35/47] DOC #333 WIP --- apstools/beamtime/apsbss_ophyd.py | 45 +++++++++++++++++++++++++++++ docs/source/applications/apsbss.rst | 1 + 2 files changed, 46 insertions(+) diff --git a/apstools/beamtime/apsbss_ophyd.py b/apstools/beamtime/apsbss_ophyd.py index 29f9af068..60272113f 100644 --- a/apstools/beamtime/apsbss_ophyd.py +++ b/apstools/beamtime/apsbss_ophyd.py @@ -14,6 +14,7 @@ ~EpicsEsafExperimenterDevice ~EpicsProposalDevice ~EpicsProposalExperimenterDevice + """ __all__ = ["EpicsBssDevice",] @@ -24,6 +25,10 @@ class EpicsEsafExperimenterDevice(Device): """ Ophyd device for experimenter info from APS ESAF. + + .. autosummary:: + + ~clear """ badge_number = Component(EpicsSignal, "badgeNumber", string=True) email = Component(EpicsSignal, "email", string=True) @@ -31,6 +36,7 @@ class EpicsEsafExperimenterDevice(Device): last_name = Component(EpicsSignal, "lastName", string=True) def clear(self): + """Clear the fields for this user.""" self.badge_number.put("") self.email.put("") self.first_name.put("") @@ -40,6 +46,11 @@ def clear(self): class EpicsEsafDevice(Device): """ Ophyd device for info from APS ESAF. + + .. autosummary:: + + ~clear + ~clear_users """ aps_cycle = Component(EpicsSignal, "cycle", string=True) @@ -65,6 +76,15 @@ class EpicsEsafDevice(Device): user9 = Component(EpicsEsafExperimenterDevice, "user9:") def clear(self): + """ + Clear the most of the ESAF info. + + Do not clear these items: + + * ``aps_cycle`` + * ``esaf_id`` + * ``sector`` + """ # self.aps_cycle.put("") # user controls this self.description.put("") self.end_date.put("") @@ -79,6 +99,7 @@ def clear(self): self.clear_users() def clear_users(self): + """Clear the info for all users.""" self.user1.clear() self.user2.clear() self.user3.clear() @@ -93,6 +114,10 @@ def clear_users(self): class EpicsProposalExperimenterDevice(Device): """ Ophyd device for experimenter info from APS Proposal. + + .. autosummary:: + + ~clear """ badge_number = Component(EpicsSignal, "badgeNumber", string=True) email = Component(EpicsSignal, "email", string=True) @@ -104,6 +129,7 @@ class EpicsProposalExperimenterDevice(Device): user_id = Component(EpicsSignal, "userId", string=True) def clear(self): + """Clear the info for this user.""" self.badge_number.put("") self.email.put("") self.first_name.put("") @@ -117,6 +143,11 @@ def clear(self): class EpicsProposalDevice(Device): """ Ophyd device for info from APS Proposal. + + .. autosummary:: + + ~clear + ~clear_users """ beamline_name = Component(EpicsSignal, "beamline", string=True) @@ -141,6 +172,14 @@ class EpicsProposalDevice(Device): user9 = Component(EpicsProposalExperimenterDevice, "user9:") def clear(self): + """ + Clear the most of the proposal info. + + Do not clear these items: + + * ``beamline_name`` + * ``proposal_id`` + """ # self.beamline_name.put("") # user controls this self.mail_in_flag.put(0) # self.proposal_id.put(-1) # user controls this @@ -153,6 +192,7 @@ def clear(self): self.clear_users() def clear_users(self): + """Clear the info for all users.""" self.user1.clear() self.user2.clear() self.user3.clear() @@ -167,11 +207,16 @@ def clear_users(self): class EpicsBssDevice(Device): """ Ophyd device for info from APS Proposal and ESAF databases. + + .. autosummary:: + + ~clear """ esaf = Component(EpicsEsafDevice, "esaf:") proposal = Component(EpicsProposalDevice, "proposal:") def clear(self): + """Clear the proposal and ESAF info.""" self.esaf.clear() self.proposal.clear() diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index 6ed43d2c7..92905a9c6 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -70,6 +70,7 @@ the *apsbss* application expects:: setup EPICS PVs: setup update EPICS PVs: update from BSS +.. TODO: describe each of these subcommands .. _apsbss_ioc_management: From d6a4f5d0b2f59a1ede7b5353fa0d2b6e2a38f301 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 10 Jul 2020 00:40:16 -0500 Subject: [PATCH 36/47] fixes #341 --- apstools/utils.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/apstools/utils.py b/apstools/utils.py index 08dd4f2bd..65cd3e3dd 100644 --- a/apstools/utils.py +++ b/apstools/utils.py @@ -30,6 +30,7 @@ ~safe_ophyd_name ~show_ophyd_symbols ~split_quoted_line + ~summarize_runs ~text_encode ~to_unicode_or_bust ~trim_string_for_EPICS @@ -49,8 +50,9 @@ from bluesky.callbacks.best_effort import BestEffortCallback from bluesky import plan_stubs as bps -from collections import OrderedDict +from collections import defaultdict, OrderedDict import databroker +import databroker.queries import datetime from email.mime.text import MIMEText from event_model import NumpyEncoder @@ -727,6 +729,60 @@ def split_quoted_line(line): return parts +def summarize_runs(since=None): + """ + Report bluesky run metrics from the databroker. + + * How many different plans? + * How many runs? + * How many times each run was used? + * How frequently? (TODO:) + + PARAMETERS + + since (str) : + Report all runs since this ISO8601 date & time (default: ``1995``) + """ + since = since or "1995" + cat = db.v2.search(databroker.queries.TimeRange(since=since)) + plans = defaultdict(list) + t0 = time.time() + for n, uid in enumerate(cat): + t1 = time.time() + run = cat[uid] # this step is very slow (0.01 - 0.5 seconds each!) + t2 = time.time() + plan_name = run.metadata["start"].get("plan_name", "unknown") + dt = datetime.datetime.fromtimestamp(run.metadata["start"]["time"]).isoformat() + scan_id = run.metadata["start"].get("scan_id", "unknown") + plans[plan_name].append( + dict( + plan_name=plan_name, + dt=dt, + time_start=dt, + uid=uid, + scan_id=scan_id, + ) + ) + # print( + # f"{scan_id}" + # f" {dt}" + # f" dt1={(t1-t0)*1e6:4.01f}us" + # f" dt2={(t2-t1)*1e3:5.01f}ms" + # f" {plan_name}" + # ) + t0 = time.time() + + def sorter(plan_name): + return len(plans[plan_name]) + + table = pyRestTable.Table() + table.labels = "plan quantity".split() + for k in sorted(plans.keys(), key=sorter, reverse=True): + table.addRow((k, sorter(k))) + table.addRow(("TOTAL", n+1)) + print(table) + + def text_encode(source): """Encode ``source`` using the default codepoint.""" return source.encode(errors='ignore') From 45e56a262ba5658fa0ff50d2d8cf5ff937b894f6 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 10 Jul 2020 00:50:02 -0500 Subject: [PATCH 37/47] MNT #341 Codacy --- apstools/utils.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apstools/utils.py b/apstools/utils.py index 65cd3e3dd..2692dfcda 100644 --- a/apstools/utils.py +++ b/apstools/utils.py @@ -729,7 +729,7 @@ def split_quoted_line(line): return parts -def summarize_runs(since=None): +def summarize_runs(since=None, db=None): """ Report bluesky run metrics from the databroker. @@ -742,7 +742,11 @@ def summarize_runs(since=None): since (str) : Report all runs since this ISO8601 date & time (default: ``1995``) + db (object) : + Instance of ``databroker.Broker()`` + (default: ``db`` from the IPython shell) """ + db = db or ipython_shell_namespace()["db"] since = since or "1995" cat = db.v2.search(databroker.queries.TimeRange(since=since)) plans = defaultdict(list) @@ -763,13 +767,14 @@ def summarize_runs(since=None): scan_id=scan_id, ) ) - # print( - # f"{scan_id}" - # f" {dt}" - # f" dt1={(t1-t0)*1e6:4.01f}us" - # f" dt2={(t2-t1)*1e3:5.01f}ms" - # f" {plan_name}" - # ) + logger.debug( + "%s %s dt1=%4.01fus dt2=%5.01fms", + scan_id, + dt, + (t1-t0)*1e6, + (t2-t1)*1e3, + plan_name, + ) t0 = time.time() def sorter(plan_name): From e13e955c568758e2f42f6cc70543e3db17ca0c01 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 10 Jul 2020 00:52:25 -0500 Subject: [PATCH 38/47] MNT #341 Codacy --- apstools/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apstools/utils.py b/apstools/utils.py index 2692dfcda..305602911 100644 --- a/apstools/utils.py +++ b/apstools/utils.py @@ -747,7 +747,7 @@ def summarize_runs(since=None, db=None): (default: ``db`` from the IPython shell) """ db = db or ipython_shell_namespace()["db"] - since = since or "1995" + since = since or "1995" # no APS X-ray experiment data before 1995! cat = db.v2.search(databroker.queries.TimeRange(since=since)) plans = defaultdict(list) t0 = time.time() @@ -768,7 +768,7 @@ def summarize_runs(since=None, db=None): ) ) logger.debug( - "%s %s dt1=%4.01fus dt2=%5.01fms", + "%s %s dt1=%4.01fus dt2=%5.01fms %s", scan_id, dt, (t1-t0)*1e6, From 288476234cfab7676ec89a05f72f184b5cc076be Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 10 Jul 2020 17:20:30 -0500 Subject: [PATCH 39/47] DOC #333 --- .gitignore | 4 + apstools/beamtime/apsbss.py | 7 +- .../beamtime/{manage_ioc.sh => apsbss_ioc.sh} | 8 +- docs/source/applications/apsbss.rst | 48 +-- docs/source/applications/apsbss_ioc.rst | 394 ++++++++++++++++++ docs/source/applications/index.rst | 1 + docs/source/resources/bsv1.jpg | Bin .../resources/excel_plan_spreadsheet.jpg | Bin docs/source/resources/excel_simple.jpg | Bin docs/source/resources/sample_example.jpg | Bin docs/source/resources/sample_example.xlsx | Bin docs/source/resources/ui_id_entered.png | Bin 0 -> 18154 bytes docs/source/resources/ui_initialized.png | Bin 0 -> 17987 bytes docs/source/resources/ui_updated.png | Bin 0 -> 28293 bytes tests/test_beamtime.py | 2 +- 15 files changed, 434 insertions(+), 30 deletions(-) rename apstools/beamtime/{manage_ioc.sh => apsbss_ioc.sh} (95%) create mode 100644 docs/source/applications/apsbss_ioc.rst mode change 100755 => 100644 docs/source/resources/bsv1.jpg mode change 100755 => 100644 docs/source/resources/excel_plan_spreadsheet.jpg mode change 100755 => 100644 docs/source/resources/excel_simple.jpg mode change 100755 => 100644 docs/source/resources/sample_example.jpg mode change 100755 => 100644 docs/source/resources/sample_example.xlsx create mode 100644 docs/source/resources/ui_id_entered.png create mode 100644 docs/source/resources/ui_initialized.png create mode 100644 docs/source/resources/ui_updated.png diff --git a/.gitignore b/.gitignore index 30511684f..d49200bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ build_log.txt .logs/ .vscode/ 20*.dat + +# bird droppings from IT +.log +.loglogin diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py index 664e20c2e..62691f46d 100755 --- a/apstools/beamtime/apsbss.py +++ b/apstools/beamtime/apsbss.py @@ -483,13 +483,13 @@ def get_options(): p_sub = subcommand.add_parser('current', help="print current ESAF(s) and proposal(s)") p_sub.add_argument('beamlineName', type=str, help="Beamline name") - subcommand.add_parser('cycles', help="print APS cycle names") + subcommand.add_parser('cycles', help="print APS run cycle names") p_sub = subcommand.add_parser('esaf', help="print specific ESAF") p_sub.add_argument('esafId', type=int, help="ESAF ID number") p_sub = subcommand.add_parser('proposal', help="print specific proposal") - p_sub.add_argument('proposalId', type=str, help="ESAF ID number") + p_sub.add_argument('proposalId', type=str, help="proposal ID number") p_sub.add_argument('cycle', type=str, help="APS run (cycle) name") p_sub.add_argument('beamlineName', type=str, help="Beamline name") @@ -627,6 +627,9 @@ def main(): elif args.subcommand == "update": epicsUpdate(args.prefix) + else: + print("subcommand not recognized. Use -h for usage information.") + if __name__ == "__main__": main() diff --git a/apstools/beamtime/manage_ioc.sh b/apstools/beamtime/apsbss_ioc.sh similarity index 95% rename from apstools/beamtime/manage_ioc.sh rename to apstools/beamtime/apsbss_ioc.sh index 52dc5c2eb..96403e467 100755 --- a/apstools/beamtime/manage_ioc.sh +++ b/apstools/beamtime/apsbss_ioc.sh @@ -2,8 +2,11 @@ # manage the IOC for apsbss +#-------------------- +# change the program defaults here DEFAULT_SESSION_NAME=apsbss DEFAULT_IOC_PREFIX=ioc:bss: +#-------------------- SHELL_SCRIPT_NAME=${BASH_SOURCE:-${0}} SELECTION=${1:-usage} @@ -120,10 +123,11 @@ function start() { echo -n "${SCREEN_SESSION} is already running (pid=${IOC_PID})" screenpid else - echo "Starting ${SESSION_NAME}" + echo "Starting ${SESSION_NAME} with IOC prefix ${IOC_PREFIX}" cd "${IOC_STARTUP_DIR}" # Run SESSION_NAME inside a screen session - screen -dm -S "${SESSION_NAME}" -h 5000 "${START_IOC_COMMAND}" + CMD="screen -dm -S ${SESSION_NAME} -h 5000 ${START_IOC_COMMAND}" + $CMD fi } diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index 92905a9c6..bce5889f8 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -13,7 +13,8 @@ form) databases as PVs at each beam line so that this information (``dm`` for short) package [#]_ is used to access the APS databases as read-only. -*No information is written back to the APS databases.* +*No information is written back to the APS +databases from this software.* .. [#] ``dm``: https://anaconda.org/search?q=aps-dm-api @@ -40,8 +41,8 @@ the typical information obtained includes: * is experiment mail-in? These PVs are loaded on demand by the local instrument team at the beam line. - -.. TODO: describe how this is done +See the :ref:`apsbss_startup` section for details about +managing the EPICS PVs. Example - command line ++++++++++++++++++++++ @@ -70,35 +71,22 @@ the *apsbss* application expects:: setup EPICS PVs: setup update EPICS PVs: update from BSS -.. TODO: describe each of these subcommands - -.. _apsbss_ioc_management: +See :ref:`beamtime_source_docs` for the source code documentation +of each of these subcommands. IOC Management ++++++++++++++ The EPICS PVs are provided by running an instance of ``apsbss.db`` either in an existing EPICS IOC or using the ``softIoc`` application -from EPICS base. A shell script (``manage_ioc.sh``) is included -for management of the IOC:: - - $ ./manage_ioc.sh - Usage: manage_ioc.sh {start|stop|restart|status|console|run} [NAME [PREFIX]] - - COMMANDS - console attach to IOC console if IOC is running in screen - restart restart IOC - run run IOC in console (not screen) - start start IOC - status report if IOC is running - stop stop IOC +from EPICS base. A shell script (``apsbss_ioc.sh``) is included +for loading Proposal and ESAF information from the +APS databases into the IOC. - OPTIONAL TERMS - NAME name of IOC session (default: apsbss) - PREFIX IOC prefix (default: ioc:bss:) - -.. TODO: download for ``apsbss.db`` +* :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>` +See the section titled ":ref:`apsbss_startup`" +for the management of the EPICS IOC. .. _apsbss_epics_gui_screens: @@ -108,9 +96,19 @@ Displays for MEDM & caQtDM Display screen files are provided for viewing the EPICS PVs using either MEDM (``apsbss.adl``) or caQtDM (``apsbss.ui``). -.. TODO: downloads for each screen file +* MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>` +* caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>` + .. TODO: screen images here +Downloads ++++++++++ + +* EPICS database: :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>` +* EPICS IOC shell script :download:`apsbss_ioc.sh <../../../apstools/beamtime/apsbss_ioc.sh>` +* MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>` +* caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>` + Source code documentation +++++++++++++++++++++++++ diff --git a/docs/source/applications/apsbss_ioc.rst b/docs/source/applications/apsbss_ioc.rst new file mode 100644 index 000000000..beae11e42 --- /dev/null +++ b/docs/source/applications/apsbss_ioc.rst @@ -0,0 +1,394 @@ +.. _apsbss_startup: + +apsbss: IOC Startup and Management +================================== + +The :ref:`apsbss_application` software +provides information from the APS Proposal +and ESAF (experiment safety approval +form) databases as PVs to the local controls network. +The ``aps-dm-api`` (``dm`` for short) package [#]_ +is used to access the APS databases as read-only. +The information in the PVs can be used as metadata +for inclusion in data files produced + +*No information is written back to the APS +databases from this software.* + +.. [#] ``dm``: https://anaconda.org/search?q=aps-dm-api + + +Overview +-------- + +#. Create the PVs in an EPICS IOC +#. Initialize PVs with beam line name and APS run cycle number +#. Set PV with the Proposal ID number +#. Set PV with the ESAF ID number +#. Retrieve information from APS databases + + +.. _apsbss_ioc_management: + +Start EPICS IOC +--------------- + +The EPICS Process Variables (PVs) that support this software +are provided by an EPICS PV server (IOC). The PVs are defined +by including the ``apsbss.db`` database file in the startup +of the IOC. The database can be add to an existing IOC +or run as a stand-alone IOC using the ``softIoc`` application +from EPICS base. + +To ensure that we create PVs with unique names, decide what +prefix to use with the EPICS database. Such as, for APS beam +line 9-ID, you might pick: ``9id:bss:`` (making sure to end +the prefix with the customary ``:``). + +Add EPICS database to existing IOC +++++++++++++++++++++++++++++++++++ + +To include ``apsbss.db`` in an existing IOC, copy the file +(see Downloads below) into the IOC's startup directory +(typically the directory with ``st.cmd``). Edit the ``st.cmd`` +file and a line like this just before the call to ``iocInit``:: + + dbLoadRecords("apsbss.db", "P=9id:bss:") + +Once the IOC is started, these PVs should be available to any +EPICS client on the local network. + +Run EPICS database in softIoc from EPICS Base ++++++++++++++++++++++++++++++++++++++++++++++ + +For this example, we pick a unique name for this process, +(``ioc9idbss``) based on the PV prefix (``9id:bss:``). + +.. sidebar:: TIP + + If you customize your copy of ``apsbss_ioc.sh`` + and pre-define the two lines:: + + # change the program defaults here + DEFAULT_SESSION_NAME=apsbss + DEFAULT_IOC_PREFIX=ioc:bss: + + then you do not need to supply these terms as + command-line arguments. The usage command + will show these the default names you provided. + +**Start** the IOC using the ``apsbss_ioc.sh`` tool +(as described below), with this command:: + + $ apsbss_ioc.sh start ioc9idbss 9id:bss: + Starting ioc9idbss with IOC prefix 9id:bss: + +**Stop** the IOC with this command:: + + $ apsbss_ioc.sh stop ioc9idbss 9id:bss: + ioc9idbss is not running + +The IOC **restart** command, will first stop the IOC (if +it is running), then start it. + +Report whether or not the IOC is running with this command:: + + $ apsbss_ioc.sh status ioc9idbss 9id:bss: + ioc9idbss is not running + +To interact with the **console** of an IOC running in +a ``screen`` session:: + + $ apsbss_ioc.sh console ioc9idbss 9id:bss: + +To end this interaction, type ``^A`` then ``D`` which will +leave the IOC running. Type ``exit`` to stop the IOC from +the console. + +For diagnostic (or other) purposes, you can also run the IOC +without using a screen session. This is the command:: + + $ apsbss_ioc.sh run ioc9idbss 9id:bss: + dbLoadDatabase("/home/beams1/JEMIAN/.conda/envs/bluesky_2020_5/epics/bin/linux-x86_64/../../dbd/softIoc.dbd") + softIoc_registerRecordDeviceDriver(pdbbase) + dbLoadRecords("apsbss.db", "P=9id:bss:") + iocInit() + Starting iocInit + ############################################################################ + ## EPICS R7.0.4 + ## Rev. 2020-05-29T13:39+0000 + ############################################################################ + cas warning: Configured TCP port was unavailable. + cas warning: Using dynamically assigned TCP port 39809, + cas warning: but now two or more servers share the same UDP port. + cas warning: Depending on your IP kernel this server may not be + cas warning: reachable with UDP unicast (a host's IP in EPICS_CA_ADDR_LIST) + iocRun: All initialization complete + epics> + +You should see the IOC shell prompt (``epics> ``). If you type ``exit`` +or otherwise close the session, the IOC will exit. + + +Shell Script to manage softIoc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To run a stand-alone IOC using the ``softIoc`` application +from EPICS base, use the supplied IOC management shell script +``apsbss_ioc.sh``:: + + $ apsbss_ioc.sh + Usage: apsbss_ioc.sh {start|stop|restart|status|console|run} [NAME [PREFIX]] + + COMMANDS + console attach to IOC console if IOC is running in screen + restart restart IOC + run run IOC in console (not screen) + start start IOC + status report if IOC is running + stop stop IOC + + OPTIONAL TERMS + NAME name of IOC session (default: apsbss) + PREFIX IOC prefix (default: ioc:bss:) + +* :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>` +* :download:`apsbss_ioc.sh <../../../apstools/beamtime/apsbss_ioc.sh>` + +.. note:: The shell script assumes that a working ``softIoc`` application + (from EPICS base) is in your executable ``$PATH``. You should confirm + this first before trying to start the IOC. + +.. note:: The ``softIoc`` application is run within a ``screen`` + session so that it remains running even if you close the + console session. Confirm that you have the ``screen`` application + first before trying to start the IOC. + + +Initialize PVs for beam line and APC run cycle +---------------------------------------------- + +After creating the PVs in an IOC, the next step is to +initialize them with the beam line name and the APS +run cycle name. Both of these must match exactly +with values known in the data management (``dm``) system. + +For any of these commands, you must know the EPICS +PV prefix to be used. The examples above are for +beam line 9-ID. The PV prefix in these examples +is ``9id:bss:``. + +What beam line name to use? ++++++++++++++++++++++++++++ + +To learn the beam line names accepted by the system, use this command:: + + $ apsbss beamlines + 1-BM-B,C 8-ID-I 15-ID-B,C,D 23-BM-B + 1-ID-B,C,E 9-BM-B,C 16-BM-B 23-ID-B + 2-BM-A,B 9-ID-B,C 16-BM-D 23-ID-D + 2-ID-D 10-BM-A,B 16-ID-B 24-ID-C + 2-ID-E 10-ID-B 16-ID-D 24-ID-E + 3-ID-B,C,D 11-BM-B 17-BM-B 26-ID-C + 4-ID-C 11-ID-B 17-ID-B 27-ID-B + 4-ID-D 11-ID-C 18-ID-D 29-ID-C,D + 5-BM-C 11-ID-D 19-BM-D 30-ID-B,C + 5-BM-D 12-BM-B 19-ID-D 31-ID-D + 5-ID-B,C,D 12-ID-B 20-BM-B 32-ID-B,C + 6-BM-A,B 12-ID-C,D 20-ID-B,C 33-BM-C + 6-ID-B,C 13-BM-C 21-ID-D 33-ID-D,E + 6-ID-D 13-BM-D 21-ID-E 34-ID-C + 7-BM-B 13-ID-C,D 21-ID-F 34-ID-E + 7-ID-B,C,D 13-ID-E 21-ID-G 35-ID-B,C,D,E + 8-BM-B 14-BM-C 22-BM-D + 8-ID-E 14-ID-B 22-ID-D + +For either station at 9-ID, use ``9-ID-B,C``. + + +What APS run cycle to use? ++++++++++++++++++++++++++++ + +To learn the APS run cycle names accepted by the system, use this command:: + + $ apsbss cycles + 2008-3 2011-2 2014-1 2016-3 2019-2 + 2009-1 2011-3 2014-2 2017-1 2019-3 + 2009-2 2012-1 2014-3 2017-2 2020-1 + 2009-3 2012-2 2015-1 2017-3 2020-2 + 2010-1 2012-3 2015-2 2018-1 + 2010-2 2013-1 2015-3 2018-2 + 2010-3 2013-2 2016-1 2018-3 + 2011-1 2013-3 2016-2 2019-1 + +Pick the cycle of interest. Here, we pick ``2020-2``. + +Write the beam line name and cycle to the PVs ++++++++++++++++++++++++++++++++++++++++++++++ + +To configure ``9id:bss:`` PVs for beam line +``9-ID-B,C`` and cycle ``2020-2``, +use this command:: + + $ apsbss setup 9id:bss: 9-ID-B,C 2020-2 + connected in 0.143s + setup EPICS 9id:bss: 9-ID-B,C cycle=2020-2 sector=9 + +Or you could enter them into the appropriate boxes on the GUI. + +.. figure:: ../resources/ui_initialized.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM showing PV prefix + (``9id:bss:``), APS run cycle ``2020-2`` and beam line ``9-ID-B,C``. + +What Proposal and ESAF ID numbers to use? +----------------------------------------- + +Proposals are usually valid for two years. To learn what +proposals are valid for your beam line, use this command +with your own beam line's name. The report will provide +two tables, one for ESAFs for the current cycle and the +other for proposals +within the last two years (6 APS cycles):: + + $ apsbss current 9id:bss: 9-ID-B,C + Current Proposal(s) on 9-ID-B,C + + ===== ====== =================== ==================== ======================================== + id cycle date user(s) title + ===== ====== =================== ==================== ======================================== + 57504 2019-3 2017-10-27 15:31:46 Zhang,Levine,Long... Towards USAXS/SAXS/WAXS Characterizat... + 55236 2019-2 2017-07-07 12:32:39 Du,Vacek,Syed,Hon... Developing 3D cryo ptychography at th... + 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r... + 62490 2019-1 2018-10-25 11:10:49 Ilavsky,Frith,Sun Dissolution of nano-precipitates in m... + ===== ====== =================== ==================== ======================================== + + Current ESAF(s) on sector 9 + + ====== ======== ========== ========== ==================== ======================================== + id status start end user(s) title + ====== ======== ========== ========== ==================== ======================================== + 221805 Approved 2020-02-18 2020-12-25 Chen,Deng,Yao,Jia... Bionanoprobe commissioning + 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS + 226572 Approved 2020-06-10 2020-09-28 Sterbinsky,Heald,... 9BM Beamline Commissioning 2020-2 + 226612 Approved 2020-06-10 2020-09-28 Chen,Deng,Yao,Jia... Bionanoprobe commissioning + ====== ======== ========== ========== ==================== ======================================== + +Note that some of the information in the tables above has been removed for brevity. + +View Proposal Information +------------------------- + +To view information about a specific proposal, you +must be able to provide the proposal's ID number and +the APS run cycle name. + +:: + + $ apsbss proposal 64629 2019-2 9-ID-B,C + duration: 36000 + endTime: '2019-06-25 17:00:00' + experimenters: + - badge: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Jan + id: number_here + instId: 3927 + institution: Argonne National Laboratory + lastName: Ilavsky + - badge: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: John + id: number_here + instId: 3927 + institution: Argonne National Laboratory + lastName: Okasinski + piFlag: Y + id: 64629 + mailInFlag: N + proprietaryFlag: N + startTime: '2019-06-25 07:00:00' + submittedDate: '2019-03-01 18:35:02' + title: 2019 National School on Neutron & X-ray Scattering Beamline Practicals - CMS + totalShiftsRequested: 12 + + +Get ESAF Information +-------------------- + +To view information about a specific ESAF, you +must be able to provide the ESAF ID number. + +:: + + $ apsbss esaf 226319 + description: We will commission beamline and USAXS instrument. We will perform experiments + with safe beamline standards and test samples (all located at beamline and used + for this purpose routinely) to evaluate performance of beamline and instrument. + We will perform hardware and software development as needed. + esafId: 226319 + esafStatus: Approved + esafTitle: Commission 9ID and USAXS + experimentEndDate: '2020-09-28 08:00:00' + experimentStartDate: '2020-05-26 08:00:00' + experimentUsers: + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Jan + lastName: Ilavsky + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Evan + lastName: Maxey + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Ivan + lastName: Kuzmenko + sector: 09 + + +Update EPICS PVs with Proposal and ESAF +--------------------------------------- + +To update the PVs with Proposal and Information from the APS +database, first enter the proposal and ESAF ID numbers into +the GUI (or set the ``9id:bss:proposal:id``, and respectively). +Note that for this ESAF ID, we had to change the cycle to `2019-2`. + + +.. figure:: ../resources/ui_id_entered.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM with Proposal + and ESAF ID numbers added. + +Then, use this command to retrieve the information and update +the PVs:: + + $ apsbss update 9id:bss: + update EPICS 9id:bss: + connected in 0.105s + +Here's a view of the GUI after running the update. The +information shown in the GUI is only part of the PVs, +presented in a compact format. + +.. figure:: ../resources/ui_updated.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM showing Proposal + and ESAF information. + +Clear the EPICS PVs +------------------- + +To clear the information from the PVs, use this command:: + + $ apsbss clear 9id:bss: + clear EPICS 9id:bss: + connected in 0.104s + cleared in 0.011s diff --git a/docs/source/applications/index.rst b/docs/source/applications/index.rst index c865c9813..affabe88f 100644 --- a/docs/source/applications/index.rst +++ b/docs/source/applications/index.rst @@ -9,6 +9,7 @@ Applications :glob: apsbss + apsbss_ioc snapshot spec2ophyd diff --git a/docs/source/resources/bsv1.jpg b/docs/source/resources/bsv1.jpg old mode 100755 new mode 100644 diff --git a/docs/source/resources/excel_plan_spreadsheet.jpg b/docs/source/resources/excel_plan_spreadsheet.jpg old mode 100755 new mode 100644 diff --git a/docs/source/resources/excel_simple.jpg b/docs/source/resources/excel_simple.jpg old mode 100755 new mode 100644 diff --git a/docs/source/resources/sample_example.jpg b/docs/source/resources/sample_example.jpg old mode 100755 new mode 100644 diff --git a/docs/source/resources/sample_example.xlsx b/docs/source/resources/sample_example.xlsx old mode 100755 new mode 100644 diff --git a/docs/source/resources/ui_id_entered.png b/docs/source/resources/ui_id_entered.png new file mode 100644 index 0000000000000000000000000000000000000000..14769923f71a4e72ca7073eb6f33832e52f2b9b9 GIT binary patch literal 18154 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 literal 0 HcmV?d00001 diff --git a/docs/source/resources/ui_initialized.png b/docs/source/resources/ui_initialized.png new file mode 100644 index 0000000000000000000000000000000000000000..48aff4fe059b83c151cf550697d97edf819b973c GIT binary patch literal 17987 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 XKPrsTcn`n)0}&UI7S0sXa`}G%w2fte literal 0 HcmV?d00001 diff --git a/docs/source/resources/ui_updated.png b/docs/source/resources/ui_updated.png new file mode 100644 index 0000000000000000000000000000000000000000..58110764442a2afe4a30f9821d5376df7efc392f GIT binary patch literal 28293 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 literal 0 HcmV?d00001 diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py index 89152102f..5a9a9a7ae 100644 --- a/tests/test_beamtime.py +++ b/tests/test_beamtime.py @@ -104,7 +104,7 @@ def setUp(self): self.bss = None self.manager = os.path.abspath(os.path.join( os.path.dirname(apsbss.__file__), - "manage_ioc.sh" + "apsbss_ioc.sh" )) self.ioc_name = "test_apsbss" cmd = f"{self.manager} restart {self.ioc_name} {BSS_TEST_IOC_PREFIX}" From 94368b786c1dfe53248a128c1eac207705d8e679 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 10 Jul 2020 17:57:43 -0500 Subject: [PATCH 40/47] DOC #333 --- docs/source/applications/apsbss.rst | 262 +++++++++++++++++++++++- docs/source/applications/apsbss_ioc.rst | 233 +-------------------- 2 files changed, 259 insertions(+), 236 deletions(-) diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index bce5889f8..9ab9cbc3b 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -44,8 +44,262 @@ These PVs are loaded on demand by the local instrument team at the beam line. See the :ref:`apsbss_startup` section for details about managing the EPICS PVs. -Example - command line -++++++++++++++++++++++ + +Overview +++++++++ + +#. Create the PVs in an EPICS IOC +#. Initialize PVs with beam line name and APS run cycle number +#. Set PVs with the Proposal and ESAF ID numbers +#. Retrieve (& update PVs) information from APS databases + +**Enter beam line and APS run (cycle) info** + +.. figure:: ../resources/ui_initialized.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM showing PV prefix + (``9id:bss:``), APS run cycle ``2020-2`` and beam line ``9-ID-B,C``. + + +**Enter Proposal and ESAF ID numbers** + +Note that for this ESAF ID, we had to change the cycle to `2019-2`. + +.. figure:: ../resources/ui_id_entered.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM with Proposal + and ESAF ID numbers added. + +**Update PVs from APS databases** + +Command: ``apsbss update 9id:bss:`` + +Here's a view of the GUI after running the update. The +information shown in the GUI is only part of the PVs, +presented in a compact format. + +.. figure:: ../resources/ui_updated.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM showing Proposal + and ESAF information. + + +Initialize PVs for beam line and APC run cycle +++++++++++++++++++++++++++++++++++++++++++++++ + +After creating the PVs in an IOC, the next step is to +initialize them with the beam line name and the APS +run cycle name. Both of these must match exactly +with values known in the data management (``dm``) system. + +For any of these commands, you must know the EPICS +PV prefix to be used. The examples above are for +beam line 9-ID. The PV prefix in these examples +is ``9id:bss:``. + + +What beam line name to use? +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To learn the beam line names accepted by the system, use this command:: + + $ apsbss beamlines + 1-BM-B,C 8-ID-I 15-ID-B,C,D 23-BM-B + 1-ID-B,C,E 9-BM-B,C 16-BM-B 23-ID-B + 2-BM-A,B 9-ID-B,C 16-BM-D 23-ID-D + 2-ID-D 10-BM-A,B 16-ID-B 24-ID-C + 2-ID-E 10-ID-B 16-ID-D 24-ID-E + 3-ID-B,C,D 11-BM-B 17-BM-B 26-ID-C + 4-ID-C 11-ID-B 17-ID-B 27-ID-B + 4-ID-D 11-ID-C 18-ID-D 29-ID-C,D + 5-BM-C 11-ID-D 19-BM-D 30-ID-B,C + 5-BM-D 12-BM-B 19-ID-D 31-ID-D + 5-ID-B,C,D 12-ID-B 20-BM-B 32-ID-B,C + 6-BM-A,B 12-ID-C,D 20-ID-B,C 33-BM-C + 6-ID-B,C 13-BM-C 21-ID-D 33-ID-D,E + 6-ID-D 13-BM-D 21-ID-E 34-ID-C + 7-BM-B 13-ID-C,D 21-ID-F 34-ID-E + 7-ID-B,C,D 13-ID-E 21-ID-G 35-ID-B,C,D,E + 8-BM-B 14-BM-C 22-BM-D + 8-ID-E 14-ID-B 22-ID-D + +For either station at 9-ID, use ``9-ID-B,C``. + + +What APS run cycle to use? +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To learn the APS run cycle names accepted by the system, use this command:: + + $ apsbss cycles + 2008-3 2011-2 2014-1 2016-3 2019-2 + 2009-1 2011-3 2014-2 2017-1 2019-3 + 2009-2 2012-1 2014-3 2017-2 2020-1 + 2009-3 2012-2 2015-1 2017-3 2020-2 + 2010-1 2012-3 2015-2 2018-1 + 2010-2 2013-1 2015-3 2018-2 + 2010-3 2013-2 2016-1 2018-3 + 2011-1 2013-3 2016-2 2019-1 + +Pick the cycle of interest. Here, we pick ``2020-2``. + + +Write the beam line name and cycle to the PVs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To configure ``9id:bss:`` PVs for beam line +``9-ID-B,C`` and cycle ``2020-2``, +use this command:: + + $ apsbss setup 9id:bss: 9-ID-B,C 2020-2 + connected in 0.143s + setup EPICS 9id:bss: 9-ID-B,C cycle=2020-2 sector=9 + +Or you could enter them into the appropriate boxes on the GUI. + + +What Proposal and ESAF ID numbers to use? ++++++++++++++++++++++++++++++++++++++++++ + +Proposals are usually valid for two years. To learn what +proposals are valid for your beam line, use this command +with your own beam line's name. The report will provide +two tables, one for ESAFs for the current cycle and the +other for proposals +within the last two years (6 APS cycles):: + + $ apsbss current 9id:bss: 9-ID-B,C + Current Proposal(s) on 9-ID-B,C + + ===== ====== =================== ==================== ======================================== + id cycle date user(s) title + ===== ====== =================== ==================== ======================================== + 57504 2019-3 2017-10-27 15:31:46 Zhang,Levine,Long... Towards USAXS/SAXS/WAXS Characterizat... + 55236 2019-2 2017-07-07 12:32:39 Du,Vacek,Syed,Hon... Developing 3D cryo ptychography at th... + 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r... + 62490 2019-1 2018-10-25 11:10:49 Ilavsky,Frith,Sun Dissolution of nano-precipitates in m... + ===== ====== =================== ==================== ======================================== + + Current ESAF(s) on sector 9 + + ====== ======== ========== ========== ==================== ======================================== + id status start end user(s) title + ====== ======== ========== ========== ==================== ======================================== + 221805 Approved 2020-02-18 2020-12-25 Chen,Deng,Yao,Jia... Bionanoprobe commissioning + 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS + 226572 Approved 2020-06-10 2020-09-28 Sterbinsky,Heald,... 9BM Beamline Commissioning 2020-2 + 226612 Approved 2020-06-10 2020-09-28 Chen,Deng,Yao,Jia... Bionanoprobe commissioning + ====== ======== ========== ========== ==================== ======================================== + +Note that some of the information in the tables above has been removed for brevity. + + +View Proposal Information ++++++++++++++++++++++++++ + +To view information about a specific proposal, you +must be able to provide the proposal's ID number and +the APS run cycle name. + +:: + + $ apsbss proposal 64629 2019-2 9-ID-B,C + duration: 36000 + endTime: '2019-06-25 17:00:00' + experimenters: + - badge: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Jan + id: number_here + instId: 3927 + institution: Argonne National Laboratory + lastName: Ilavsky + - badge: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: John + id: number_here + instId: 3927 + institution: Argonne National Laboratory + lastName: Okasinski + piFlag: Y + id: 64629 + mailInFlag: N + proprietaryFlag: N + startTime: '2019-06-25 07:00:00' + submittedDate: '2019-03-01 18:35:02' + title: 2019 National School on Neutron & X-ray Scattering Beamline Practicals - CMS + totalShiftsRequested: 12 + + +Get ESAF Information +++++++++++++++++++++ + +To view information about a specific ESAF, you +must be able to provide the ESAF ID number. + +:: + + $ apsbss esaf 226319 + description: We will commission beamline and USAXS instrument. We will perform experiments + with safe beamline standards and test samples (all located at beamline and used + for this purpose routinely) to evaluate performance of beamline and instrument. + We will perform hardware and software development as needed. + esafId: 226319 + esafStatus: Approved + esafTitle: Commission 9ID and USAXS + experimentEndDate: '2020-09-28 08:00:00' + experimentStartDate: '2020-05-26 08:00:00' + experimentUsers: + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Jan + lastName: Ilavsky + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Evan + lastName: Maxey + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Ivan + lastName: Kuzmenko + sector: 09 + + +Update EPICS PVs with Proposal and ESAF ++++++++++++++++++++++++++++++++++++++++ + +To update the PVs with Proposal and Information from the APS +database, first enter the proposal and ESAF ID numbers into +the GUI (or set the ``9id:bss:proposal:id``, and respectively). +Note that for this ESAF ID, we had to change the cycle to `2019-2`. + +Then, use this command to retrieve the information and update +the PVs:: + + $ apsbss update 9id:bss: + update EPICS 9id:bss: + connected in 0.105s + + +Clear the EPICS PVs ++++++++++++++++++++ + +To clear the information from the PVs, use this command:: + + $ apsbss clear 9id:bss: + clear EPICS 9id:bss: + connected in 0.104s + cleared in 0.011s + + +Example - ``apsbss`` command line ++++++++++++++++++++++++++++++++++ Before using the command-line interface, find out what the *apsbss* application expects:: @@ -93,14 +347,12 @@ for the management of the EPICS IOC. Displays for MEDM & caQtDM ++++++++++++++++++++++++++ -Display screen files are provided for viewing the EPICS PVs +Display screen files are provided for viewing some of the EPICS PVs using either MEDM (``apsbss.adl``) or caQtDM (``apsbss.ui``). * MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>` * caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>` -.. TODO: screen images here - Downloads +++++++++ diff --git a/docs/source/applications/apsbss_ioc.rst b/docs/source/applications/apsbss_ioc.rst index beae11e42..a5efe41b6 100644 --- a/docs/source/applications/apsbss_ioc.rst +++ b/docs/source/applications/apsbss_ioc.rst @@ -25,7 +25,7 @@ Overview #. Initialize PVs with beam line name and APS run cycle number #. Set PV with the Proposal ID number #. Set PV with the ESAF ID number -#. Retrieve information from APS databases +#. Retrieve (& update PVs) information from APS databases .. _apsbss_ioc_management: @@ -162,233 +162,4 @@ from EPICS base, use the supplied IOC management shell script .. note:: The ``softIoc`` application is run within a ``screen`` session so that it remains running even if you close the console session. Confirm that you have the ``screen`` application - first before trying to start the IOC. - - -Initialize PVs for beam line and APC run cycle ----------------------------------------------- - -After creating the PVs in an IOC, the next step is to -initialize them with the beam line name and the APS -run cycle name. Both of these must match exactly -with values known in the data management (``dm``) system. - -For any of these commands, you must know the EPICS -PV prefix to be used. The examples above are for -beam line 9-ID. The PV prefix in these examples -is ``9id:bss:``. - -What beam line name to use? -+++++++++++++++++++++++++++ - -To learn the beam line names accepted by the system, use this command:: - - $ apsbss beamlines - 1-BM-B,C 8-ID-I 15-ID-B,C,D 23-BM-B - 1-ID-B,C,E 9-BM-B,C 16-BM-B 23-ID-B - 2-BM-A,B 9-ID-B,C 16-BM-D 23-ID-D - 2-ID-D 10-BM-A,B 16-ID-B 24-ID-C - 2-ID-E 10-ID-B 16-ID-D 24-ID-E - 3-ID-B,C,D 11-BM-B 17-BM-B 26-ID-C - 4-ID-C 11-ID-B 17-ID-B 27-ID-B - 4-ID-D 11-ID-C 18-ID-D 29-ID-C,D - 5-BM-C 11-ID-D 19-BM-D 30-ID-B,C - 5-BM-D 12-BM-B 19-ID-D 31-ID-D - 5-ID-B,C,D 12-ID-B 20-BM-B 32-ID-B,C - 6-BM-A,B 12-ID-C,D 20-ID-B,C 33-BM-C - 6-ID-B,C 13-BM-C 21-ID-D 33-ID-D,E - 6-ID-D 13-BM-D 21-ID-E 34-ID-C - 7-BM-B 13-ID-C,D 21-ID-F 34-ID-E - 7-ID-B,C,D 13-ID-E 21-ID-G 35-ID-B,C,D,E - 8-BM-B 14-BM-C 22-BM-D - 8-ID-E 14-ID-B 22-ID-D - -For either station at 9-ID, use ``9-ID-B,C``. - - -What APS run cycle to use? -+++++++++++++++++++++++++++ - -To learn the APS run cycle names accepted by the system, use this command:: - - $ apsbss cycles - 2008-3 2011-2 2014-1 2016-3 2019-2 - 2009-1 2011-3 2014-2 2017-1 2019-3 - 2009-2 2012-1 2014-3 2017-2 2020-1 - 2009-3 2012-2 2015-1 2017-3 2020-2 - 2010-1 2012-3 2015-2 2018-1 - 2010-2 2013-1 2015-3 2018-2 - 2010-3 2013-2 2016-1 2018-3 - 2011-1 2013-3 2016-2 2019-1 - -Pick the cycle of interest. Here, we pick ``2020-2``. - -Write the beam line name and cycle to the PVs -+++++++++++++++++++++++++++++++++++++++++++++ - -To configure ``9id:bss:`` PVs for beam line -``9-ID-B,C`` and cycle ``2020-2``, -use this command:: - - $ apsbss setup 9id:bss: 9-ID-B,C 2020-2 - connected in 0.143s - setup EPICS 9id:bss: 9-ID-B,C cycle=2020-2 sector=9 - -Or you could enter them into the appropriate boxes on the GUI. - -.. figure:: ../resources/ui_initialized.png - :width: 95% - - Image of ``apsbss.ui`` screen GUI in caQtDM showing PV prefix - (``9id:bss:``), APS run cycle ``2020-2`` and beam line ``9-ID-B,C``. - -What Proposal and ESAF ID numbers to use? ------------------------------------------ - -Proposals are usually valid for two years. To learn what -proposals are valid for your beam line, use this command -with your own beam line's name. The report will provide -two tables, one for ESAFs for the current cycle and the -other for proposals -within the last two years (6 APS cycles):: - - $ apsbss current 9id:bss: 9-ID-B,C - Current Proposal(s) on 9-ID-B,C - - ===== ====== =================== ==================== ======================================== - id cycle date user(s) title - ===== ====== =================== ==================== ======================================== - 57504 2019-3 2017-10-27 15:31:46 Zhang,Levine,Long... Towards USAXS/SAXS/WAXS Characterizat... - 55236 2019-2 2017-07-07 12:32:39 Du,Vacek,Syed,Hon... Developing 3D cryo ptychography at th... - 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r... - 62490 2019-1 2018-10-25 11:10:49 Ilavsky,Frith,Sun Dissolution of nano-precipitates in m... - ===== ====== =================== ==================== ======================================== - - Current ESAF(s) on sector 9 - - ====== ======== ========== ========== ==================== ======================================== - id status start end user(s) title - ====== ======== ========== ========== ==================== ======================================== - 221805 Approved 2020-02-18 2020-12-25 Chen,Deng,Yao,Jia... Bionanoprobe commissioning - 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS - 226572 Approved 2020-06-10 2020-09-28 Sterbinsky,Heald,... 9BM Beamline Commissioning 2020-2 - 226612 Approved 2020-06-10 2020-09-28 Chen,Deng,Yao,Jia... Bionanoprobe commissioning - ====== ======== ========== ========== ==================== ======================================== - -Note that some of the information in the tables above has been removed for brevity. - -View Proposal Information -------------------------- - -To view information about a specific proposal, you -must be able to provide the proposal's ID number and -the APS run cycle name. - -:: - - $ apsbss proposal 64629 2019-2 9-ID-B,C - duration: 36000 - endTime: '2019-06-25 17:00:00' - experimenters: - - badge: 'text_number_here' - email: uuuuuuuuuu@email.fqdn - firstName: Jan - id: number_here - instId: 3927 - institution: Argonne National Laboratory - lastName: Ilavsky - - badge: 'text_number_here' - email: uuuuuuuuuu@email.fqdn - firstName: John - id: number_here - instId: 3927 - institution: Argonne National Laboratory - lastName: Okasinski - piFlag: Y - id: 64629 - mailInFlag: N - proprietaryFlag: N - startTime: '2019-06-25 07:00:00' - submittedDate: '2019-03-01 18:35:02' - title: 2019 National School on Neutron & X-ray Scattering Beamline Practicals - CMS - totalShiftsRequested: 12 - - -Get ESAF Information --------------------- - -To view information about a specific ESAF, you -must be able to provide the ESAF ID number. - -:: - - $ apsbss esaf 226319 - description: We will commission beamline and USAXS instrument. We will perform experiments - with safe beamline standards and test samples (all located at beamline and used - for this purpose routinely) to evaluate performance of beamline and instrument. - We will perform hardware and software development as needed. - esafId: 226319 - esafStatus: Approved - esafTitle: Commission 9ID and USAXS - experimentEndDate: '2020-09-28 08:00:00' - experimentStartDate: '2020-05-26 08:00:00' - experimentUsers: - - badge: 'text_number_here' - badgeNumber: 'text_number_here' - email: uuuuuuuuuu@email.fqdn - firstName: Jan - lastName: Ilavsky - - badge: 'text_number_here' - badgeNumber: 'text_number_here' - email: uuuuuuuuuu@email.fqdn - firstName: Evan - lastName: Maxey - - badge: 'text_number_here' - badgeNumber: 'text_number_here' - email: uuuuuuuuuu@email.fqdn - firstName: Ivan - lastName: Kuzmenko - sector: 09 - - -Update EPICS PVs with Proposal and ESAF ---------------------------------------- - -To update the PVs with Proposal and Information from the APS -database, first enter the proposal and ESAF ID numbers into -the GUI (or set the ``9id:bss:proposal:id``, and respectively). -Note that for this ESAF ID, we had to change the cycle to `2019-2`. - - -.. figure:: ../resources/ui_id_entered.png - :width: 95% - - Image of ``apsbss.ui`` screen GUI in caQtDM with Proposal - and ESAF ID numbers added. - -Then, use this command to retrieve the information and update -the PVs:: - - $ apsbss update 9id:bss: - update EPICS 9id:bss: - connected in 0.105s - -Here's a view of the GUI after running the update. The -information shown in the GUI is only part of the PVs, -presented in a compact format. - -.. figure:: ../resources/ui_updated.png - :width: 95% - - Image of ``apsbss.ui`` screen GUI in caQtDM showing Proposal - and ESAF information. - -Clear the EPICS PVs -------------------- - -To clear the information from the PVs, use this command:: - - $ apsbss clear 9id:bss: - clear EPICS 9id:bss: - connected in 0.104s - cleared in 0.011s + first before trying to start the IOC. \ No newline at end of file From 16a3331eb9d2d1922b8ea1516dc5361e4beac5b9 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 10 Jul 2020 18:20:01 -0500 Subject: [PATCH 41/47] ENH #333 add PV report --- apstools/beamtime/apsbss.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py index 62691f46d..d68b4cb1a 100755 --- a/apstools/beamtime/apsbss.py +++ b/apstools/beamtime/apsbss.py @@ -504,6 +504,9 @@ def get_options(): p_sub = subcommand.add_parser('update', help="EPICS PVs: update from BSS") p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + p_sub = subcommand.add_parser('report', help="EPICS PVs: report what is in the PVs") + p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + return parser.parse_args() @@ -600,6 +603,21 @@ def cmd_proposal(args): print(f"dm reported: {exc}") +def cmd_report(args): + """ + Handle ``report`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ + from ..utils import object_explorer + + bss = connect_epics(args.prefix) + object_explorer(bss) + + def main(): """Command-line interface for ``apsbss`` program.""" args = get_options() @@ -627,6 +645,9 @@ def main(): elif args.subcommand == "update": epicsUpdate(args.prefix) + elif args.subcommand == "report": + cmd_report(args) + else: print("subcommand not recognized. Use -h for usage information.") From b560f0f71801ed1fbb06aa25f554829b4dcb4f66 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Fri, 10 Jul 2020 18:20:12 -0500 Subject: [PATCH 42/47] DOC #333 PV report --- docs/source/applications/apsbss.rst | 13 +++ docs/source/resources/apsbss_report.txt | 130 ++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 docs/source/resources/apsbss_report.txt diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index 9ab9cbc3b..9046502fc 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -298,6 +298,18 @@ To clear the information from the PVs, use this command:: cleared in 0.011s +Report information in the EPICS PVs ++++++++++++++++++++++++++++++++++++ + +To view all the information in the PVs, use this command:: + + $ apsbss report 9id:bss: + clear EPICS 9id:bss: + +Since this content is rather large, it is available +for download: :download:`apsbss report <../resources/apsbss_report.txt>` + + Example - ``apsbss`` command line +++++++++++++++++++++++++++++++++ @@ -324,6 +336,7 @@ the *apsbss* application expects:: clear EPICS PVs: clear setup EPICS PVs: setup update EPICS PVs: update from BSS + report EPICS PVs: report what is in the PVs See :ref:`beamtime_source_docs` for the source code documentation of each of these subcommands. diff --git a/docs/source/resources/apsbss_report.txt b/docs/source/resources/apsbss_report.txt new file mode 100644 index 000000000..a39782612 --- /dev/null +++ b/docs/source/resources/apsbss_report.txt @@ -0,0 +1,130 @@ +============================= ================================== ============================== +name PV reference value +============================= ================================== ============================== +esaf.aps_cycle 9id:bss:esaf:cycle 2019-2 +esaf.description 9id:bss:esaf:description We will commission beamline an +esaf.end_date 9id:bss:esaf:endDate 2020-09-28 08:00:00 +esaf.esaf_id 9id:bss:esaf:id 226319 +esaf.esaf_status 9id:bss:esaf:status Approved +esaf.sector 9id:bss:esaf:sector 9 +esaf.start_date 9id:bss:esaf:startDate 2020-05-26 08:00:00 +esaf.title 9id:bss:esaf:title Commission 9ID and USAXS +esaf.user1.badge_number 9id:bss:esaf:user1:badgeNumber 86312 +esaf.user1.email 9id:bss:esaf:user1:email ilavsky@aps.anl.gov +esaf.user1.first_name 9id:bss:esaf:user1:firstName Jan +esaf.user1.last_name 9id:bss:esaf:user1:lastName Ilavsky +esaf.user2.badge_number 9id:bss:esaf:user2:badgeNumber 53748 +esaf.user2.email 9id:bss:esaf:user2:email emaxey@aps.anl.gov +esaf.user2.first_name 9id:bss:esaf:user2:firstName Evan +esaf.user2.last_name 9id:bss:esaf:user2:lastName Maxey +esaf.user3.badge_number 9id:bss:esaf:user3:badgeNumber 64065 +esaf.user3.email 9id:bss:esaf:user3:email kuzmenko@aps.anl.gov +esaf.user3.first_name 9id:bss:esaf:user3:firstName Ivan +esaf.user3.last_name 9id:bss:esaf:user3:lastName Kuzmenko +esaf.user4.badge_number 9id:bss:esaf:user4:badgeNumber +esaf.user4.email 9id:bss:esaf:user4:email +esaf.user4.first_name 9id:bss:esaf:user4:firstName +esaf.user4.last_name 9id:bss:esaf:user4:lastName +esaf.user5.badge_number 9id:bss:esaf:user5:badgeNumber +esaf.user5.email 9id:bss:esaf:user5:email +esaf.user5.first_name 9id:bss:esaf:user5:firstName +esaf.user5.last_name 9id:bss:esaf:user5:lastName +esaf.user6.badge_number 9id:bss:esaf:user6:badgeNumber +esaf.user6.email 9id:bss:esaf:user6:email +esaf.user6.first_name 9id:bss:esaf:user6:firstName +esaf.user6.last_name 9id:bss:esaf:user6:lastName +esaf.user7.badge_number 9id:bss:esaf:user7:badgeNumber +esaf.user7.email 9id:bss:esaf:user7:email +esaf.user7.first_name 9id:bss:esaf:user7:firstName +esaf.user7.last_name 9id:bss:esaf:user7:lastName +esaf.user8.badge_number 9id:bss:esaf:user8:badgeNumber +esaf.user8.email 9id:bss:esaf:user8:email +esaf.user8.first_name 9id:bss:esaf:user8:firstName +esaf.user8.last_name 9id:bss:esaf:user8:lastName +esaf.user9.badge_number 9id:bss:esaf:user9:badgeNumber +esaf.user9.email 9id:bss:esaf:user9:email +esaf.user9.first_name 9id:bss:esaf:user9:firstName +esaf.user9.last_name 9id:bss:esaf:user9:lastName +esaf.user_badges 9id:bss:esaf:userBadges 86312,53748,64065 +esaf.user_last_names 9id:bss:esaf:users Ilavsky,Maxey,Kuzmenko +proposal.beamline_name 9id:bss:proposal:beamline 9-ID-B,C +proposal.mail_in_flag 9id:bss:proposal:mailInFlag OFF +proposal.proposal_id 9id:bss:proposal:id 64629 +proposal.proprietary_flag 9id:bss:proposal:proprietaryFlag OFF +proposal.submitted_date 9id:bss:proposal:submittedDate 2019-03-01 18:35:02 +proposal.title 9id:bss:proposal:title 2019 National School on Neutro +proposal.user1.badge_number 9id:bss:proposal:user1:badgeNumber 86312 +proposal.user1.email 9id:bss:proposal:user1:email ilavsky@aps.anl.gov +proposal.user1.first_name 9id:bss:proposal:user1:firstName Jan +proposal.user1.institution 9id:bss:proposal:user1:institution Argonne National Laboratory +proposal.user1.institution_id 9id:bss:proposal:user1:instId 3927 +proposal.user1.last_name 9id:bss:proposal:user1:lastName Ilavsky +proposal.user1.pi_flag 9id:bss:proposal:user1:piFlag OFF +proposal.user1.user_id 9id:bss:proposal:user1:userId 424292 +proposal.user2.badge_number 9id:bss:proposal:user2:badgeNumber 85283 +proposal.user2.email 9id:bss:proposal:user2:email okasinski@aps.anl.gov +proposal.user2.first_name 9id:bss:proposal:user2:firstName John +proposal.user2.institution 9id:bss:proposal:user2:institution Argonne National Laboratory +proposal.user2.institution_id 9id:bss:proposal:user2:instId 3927 +proposal.user2.last_name 9id:bss:proposal:user2:lastName Okasinski +proposal.user2.pi_flag 9id:bss:proposal:user2:piFlag ON +proposal.user2.user_id 9id:bss:proposal:user2:userId 424308 +proposal.user3.badge_number 9id:bss:proposal:user3:badgeNumber +proposal.user3.email 9id:bss:proposal:user3:email +proposal.user3.first_name 9id:bss:proposal:user3:firstName +proposal.user3.institution 9id:bss:proposal:user3:institution +proposal.user3.institution_id 9id:bss:proposal:user3:instId +proposal.user3.last_name 9id:bss:proposal:user3:lastName +proposal.user3.pi_flag 9id:bss:proposal:user3:piFlag OFF +proposal.user3.user_id 9id:bss:proposal:user3:userId +proposal.user4.badge_number 9id:bss:proposal:user4:badgeNumber +proposal.user4.email 9id:bss:proposal:user4:email +proposal.user4.first_name 9id:bss:proposal:user4:firstName +proposal.user4.institution 9id:bss:proposal:user4:institution +proposal.user4.institution_id 9id:bss:proposal:user4:instId +proposal.user4.last_name 9id:bss:proposal:user4:lastName +proposal.user4.pi_flag 9id:bss:proposal:user4:piFlag OFF +proposal.user4.user_id 9id:bss:proposal:user4:userId +proposal.user5.badge_number 9id:bss:proposal:user5:badgeNumber +proposal.user5.email 9id:bss:proposal:user5:email +proposal.user5.first_name 9id:bss:proposal:user5:firstName +proposal.user5.institution 9id:bss:proposal:user5:institution +proposal.user5.institution_id 9id:bss:proposal:user5:instId +proposal.user5.last_name 9id:bss:proposal:user5:lastName +proposal.user5.pi_flag 9id:bss:proposal:user5:piFlag OFF +proposal.user5.user_id 9id:bss:proposal:user5:userId +proposal.user6.badge_number 9id:bss:proposal:user6:badgeNumber +proposal.user6.email 9id:bss:proposal:user6:email +proposal.user6.first_name 9id:bss:proposal:user6:firstName +proposal.user6.institution 9id:bss:proposal:user6:institution +proposal.user6.institution_id 9id:bss:proposal:user6:instId +proposal.user6.last_name 9id:bss:proposal:user6:lastName +proposal.user6.pi_flag 9id:bss:proposal:user6:piFlag OFF +proposal.user6.user_id 9id:bss:proposal:user6:userId +proposal.user7.badge_number 9id:bss:proposal:user7:badgeNumber +proposal.user7.email 9id:bss:proposal:user7:email +proposal.user7.first_name 9id:bss:proposal:user7:firstName +proposal.user7.institution 9id:bss:proposal:user7:institution +proposal.user7.institution_id 9id:bss:proposal:user7:instId +proposal.user7.last_name 9id:bss:proposal:user7:lastName +proposal.user7.pi_flag 9id:bss:proposal:user7:piFlag OFF +proposal.user7.user_id 9id:bss:proposal:user7:userId +proposal.user8.badge_number 9id:bss:proposal:user8:badgeNumber +proposal.user8.email 9id:bss:proposal:user8:email +proposal.user8.first_name 9id:bss:proposal:user8:firstName +proposal.user8.institution 9id:bss:proposal:user8:institution +proposal.user8.institution_id 9id:bss:proposal:user8:instId +proposal.user8.last_name 9id:bss:proposal:user8:lastName +proposal.user8.pi_flag 9id:bss:proposal:user8:piFlag OFF +proposal.user8.user_id 9id:bss:proposal:user8:userId +proposal.user9.badge_number 9id:bss:proposal:user9:badgeNumber +proposal.user9.email 9id:bss:proposal:user9:email +proposal.user9.first_name 9id:bss:proposal:user9:firstName +proposal.user9.institution 9id:bss:proposal:user9:institution +proposal.user9.institution_id 9id:bss:proposal:user9:instId +proposal.user9.last_name 9id:bss:proposal:user9:lastName +proposal.user9.pi_flag 9id:bss:proposal:user9:piFlag OFF +proposal.user9.user_id 9id:bss:proposal:user9:userId +proposal.user_badges 9id:bss:proposal:userBadges 86312,85283 +proposal.user_last_names 9id:bss:proposal:users Ilavsky,Okasinski +============================= ================================== ============================== From 179647931861d7286cda5097c3f7cc05c9786fe6 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Sat, 11 Jul 2020 15:06:38 -0500 Subject: [PATCH 43/47] ENH #333 commands on GUI --- apstools/beamtime/apsbss.adl | 491 ++++++----- apstools/beamtime/apsbss.py | 53 +- apstools/beamtime/apsbss.ui | 1001 ++++++++++++---------- docs/source/applications/apsbss.rst | 115 ++- docs/source/resources/ui_id_entered.png | Bin 18154 -> 19826 bytes docs/source/resources/ui_initialized.png | Bin 17987 -> 19998 bytes docs/source/resources/ui_updated.png | Bin 28293 -> 29821 bytes 7 files changed, 949 insertions(+), 711 deletions(-) diff --git a/apstools/beamtime/apsbss.adl b/apstools/beamtime/apsbss.adl index e70b59426..e86f81951 100644 --- a/apstools/beamtime/apsbss.adl +++ b/apstools/beamtime/apsbss.adl @@ -1,6 +1,6 @@ file { - name="apsbss.adl" + name="/home/beams1/JEMIAN/Documents/projects/apstools/apstools/beamtime/apsbss.adl" version=030111 } display { @@ -8,7 +8,7 @@ display { x=122 y=83 width=400 - height=300 + height=366 } clr=14 bclr=4 @@ -101,33 +101,61 @@ text { } composite { object { - x=10 + x=54 y=39 - width=380 - height=130 + width=260 + height=36 } "composite name"="" children { text { object { - x=226 - y=45 - width=70 + x=54 + y=39 + width=120 height=14 } "basic attribute" { clr=14 } - textix="APS run" + textix="beam line name" align="horiz. right" } "text entry" { object { - x=301 - y=45 - width=80 + x=179 + y=39 + width=135 + height=16 + } + control { + chan="$(P)proposal:beamline" + clr=14 + bclr=50 + } + limits { + } + } + text { + object { + x=54 + y=59 + width=120 height=14 } + "basic attribute" { + clr=14 + } + textix="APS run cycle" + align="horiz. right" + } + "text entry" { + object { + x=179 + y=59 + width=135 + height=16 + } control { chan="$(P)esaf:cycle" clr=14 @@ -136,24 +164,67 @@ composite { limits { } } + } +} +"shell command" { + object { + x=19 + y=80 + width=200 + height=16 + } + command[0] { + label="update" + name="apsbss" + args="update $(P)" + } + clr=14 + bclr=51 + label=" get Proposal and ESAF info" +} +"shell command" { + object { + x=259 + y=80 + width=100 + height=16 + } + command[0] { + label="clear" + name="apsbss" + args="clear $(P)" + } + clr=14 + bclr=51 + label=" clear PVs" +} +composite { + object { + x=10 + y=101 + width=380 + height=120 + } + "composite name"="" + children { text { object { x=17 - y=44 + y=106 width=120 height=16 } "basic attribute" { clr=14 } - textix="ESAF" + textix="Proposal" } rectangle { object { x=10 - y=39 + y=101 width=380 - height=130 + height=120 } "basic attribute" { clr=14 @@ -161,234 +232,232 @@ composite { width=2 } } - "text entry" { + text { object { - x=75 - y=71 - width=100 - height=14 + x=20 + y=150 + width=50 + height=12 } - control { - chan="$(P)esaf:id" + "basic attribute" { clr=14 - bclr=50 - } - limits { } + textix="mail in" + align="horiz. right" } text { object { - x=40 - y=71 - width=30 - height=14 + x=20 + y=167 + width=50 + height=12 } "basic attribute" { clr=14 } - textix="ID" + textix="title" align="horiz. right" } - "text entry" { + "text update" { object { - x=247 - y=71 - width=100 + x=77 + y=167 + width=303 height=12 } - control { - chan="$(P)esaf:status" + monitor { + chan="$(P)proposal:title" clr=14 bclr=55 } + format="string" limits { } } text { object { - x=180 - y=71 - width=61 + x=20 + y=184 + width=50 height=12 } "basic attribute" { clr=14 } - textix="status" + textix="users" align="horiz. right" } - text { + "text update" { object { - x=20 - y=90 - width=50 + x=77 + y=184 + width=303 height=12 } - "basic attribute" { + monitor { + chan="$(P)proposal:users" clr=14 + bclr=55 + } + format="string" + limits { } - textix="dates" - align="horiz. right" } - "text entry" { + "text update" { object { - x=75 - y=90 - width=101 + x=77 + y=201 + width=303 height=12 } - control { - chan="$(P)esaf:startDate" + monitor { + chan="$(P)proposal:userBadges" clr=14 bclr=55 } + format="string" limits { } } text { object { - x=181 - y=90 - width=20 + x=20 + y=201 + width=50 height=12 } "basic attribute" { clr=14 } - textix="to" + textix="badges" align="horiz. right" } - "text entry" { + "choice button" { object { - x=206 - y=90 - width=101 + x=75 + y=151 + width=80 height=12 } control { - chan="$(P)esaf:endDate" + chan="$(P)proposal:mailInFlag" clr=14 bclr=55 } - limits { - } + stacking="column" } text { object { - x=20 - y=107 - width=50 + x=160 + y=151 + width=100 height=12 } "basic attribute" { clr=14 } - textix="title" + textix="proprietary" align="horiz. right" } - "text update" { + "choice button" { object { - x=77 - y=107 - width=303 + x=265 + y=152 + width=80 height=12 } - monitor { - chan="$(P)esaf:title" + control { + chan="$(P)proposal:proprietaryFlag" clr=14 bclr=55 } - format="string" - limits { - } + stacking="column" } text { object { - x=20 - y=124 - width=50 - height=12 + x=39 + y=131 + width=30 + height=14 } "basic attribute" { clr=14 } - textix="users" + textix="ID" align="horiz. right" } - "text update" { + "text entry" { object { - x=77 - y=124 - width=303 - height=12 + x=74 + y=131 + width=100 + height=14 } - monitor { - chan="$(P)esaf:users" + control { + chan="$(P)proposal:id" clr=14 - bclr=55 + bclr=50 } - format="string" limits { } } - "text update" { + text { object { - x=77 - y=141 - width=303 + x=192 + y=132 + width=80 height=12 } - monitor { - chan="$(P)esaf:userBadges" + "basic attribute" { clr=14 - bclr=55 - } - format="string" - limits { } + textix="submitted" + align="horiz. right" } - text { + "text entry" { object { - x=20 - y=141 - width=50 + x=277 + y=132 + width=100 height=12 } - "basic attribute" { + control { + chan="$(P)proposal:submittedDate" clr=14 + bclr=55 + } + limits { } - textix="badges" - align="horiz. right" } } } composite { object { x=10 - y=174 + y=226 width=380 - height=120 + height=130 } "composite name"="" children { text { object { x=17 - y=179 + y=231 width=120 height=16 } "basic attribute" { clr=14 } - textix="Proposal" + textix="ESAF" } rectangle { object { x=10 - y=174 + y=226 width=380 - height=120 + height=130 } "basic attribute" { clr=14 @@ -396,230 +465,204 @@ composite { width=2 } } + "text entry" { + object { + x=75 + y=258 + width=100 + height=14 + } + control { + chan="$(P)esaf:id" + clr=14 + bclr=50 + } + limits { + } + } text { object { - x=183 - y=181 - width=110 + x=40 + y=258 + width=30 height=14 } "basic attribute" { clr=14 } - textix="beamline name" + textix="ID" align="horiz. right" } "text entry" { object { - x=298 - y=181 - width=80 - height=14 + x=247 + y=258 + width=100 + height=12 } control { - chan="$(P)proposal:beamline" + chan="$(P)esaf:status" clr=14 - bclr=50 + bclr=55 } limits { } } text { object { - x=20 - y=223 - width=50 + x=180 + y=258 + width=61 height=12 } "basic attribute" { clr=14 } - textix="mail in" + textix="status" align="horiz. right" } text { object { x=20 - y=240 + y=277 width=50 height=12 } "basic attribute" { clr=14 } - textix="title" + textix="dates" align="horiz. right" } - "text update" { + "text entry" { object { - x=77 - y=240 - width=303 + x=75 + y=277 + width=101 height=12 } - monitor { - chan="$(P)proposal:title" + control { + chan="$(P)esaf:startDate" clr=14 bclr=55 } - format="string" limits { } } text { object { - x=20 - y=257 - width=50 + x=181 + y=277 + width=20 height=12 } "basic attribute" { clr=14 } - textix="users" + textix="to" align="horiz. right" } - "text update" { - object { - x=77 - y=257 - width=303 - height=12 - } - monitor { - chan="$(P)proposal:users" - clr=14 - bclr=55 - } - format="string" - limits { - } - } - "text update" { + "text entry" { object { - x=77 - y=274 - width=303 + x=206 + y=277 + width=101 height=12 } - monitor { - chan="$(P)proposal:userBadges" + control { + chan="$(P)esaf:endDate" clr=14 bclr=55 } - format="string" limits { } } text { object { x=20 - y=274 + y=294 width=50 height=12 } "basic attribute" { clr=14 } - textix="badges" + textix="title" align="horiz. right" } - "choice button" { + "text update" { object { - x=75 - y=224 - width=80 + x=77 + y=294 + width=303 height=12 } - control { - chan="$(P)proposal:mailInFlag" + monitor { + chan="$(P)esaf:title" clr=14 bclr=55 } - stacking="column" + format="string" + limits { + } } text { object { - x=160 - y=224 - width=100 + x=20 + y=311 + width=50 height=12 } "basic attribute" { clr=14 } - textix="proprietary" + textix="users" align="horiz. right" } - "choice button" { + "text update" { object { - x=265 - y=225 - width=80 + x=77 + y=311 + width=303 height=12 } - control { - chan="$(P)proposal:proprietaryFlag" + monitor { + chan="$(P)esaf:users" clr=14 bclr=55 } - stacking="column" - } - text { - object { - x=39 - y=204 - width=30 - height=14 - } - "basic attribute" { - clr=14 + format="string" + limits { } - textix="ID" - align="horiz. right" } - "text entry" { + "text update" { object { - x=74 - y=204 - width=100 - height=14 + x=77 + y=328 + width=303 + height=12 } - control { - chan="$(P)proposal:id" + monitor { + chan="$(P)esaf:userBadges" clr=14 - bclr=50 + bclr=55 } + format="string" limits { } } text { object { - x=192 - y=205 - width=80 + x=20 + y=328 + width=50 height=12 } "basic attribute" { clr=14 } - textix="submitted" + textix="badges" align="horiz. right" } - "text entry" { - object { - x=277 - y=205 - width=100 - height=12 - } - control { - chan="$(P)proposal:submittedDate" - clr=14 - bclr=55 - } - limits { - } - } } } diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py index d68b4cb1a..037ccd6af 100755 --- a/apstools/beamtime/apsbss.py +++ b/apstools/beamtime/apsbss.py @@ -480,10 +480,23 @@ def get_options(): subcommand.add_parser('beamlines', help="print list of beamlines") - p_sub = subcommand.add_parser('current', help="print current ESAF(s) and proposal(s)") - p_sub.add_argument('beamlineName', type=str, help="Beamline name") - - subcommand.add_parser('cycles', help="print APS run cycle names") + p_sub = subcommand.add_parser( + 'current', + help="print current ESAF(s) and proposal(s)") + p_sub.add_argument( + 'beamlineName', + type=str, + help="Beamline name") + + p_sub = subcommand.add_parser('cycles', help="print APS run cycle names") + p_sub.add_argument( + '-f', '--full', action="store_true", + default=False, + help="full report including dates (default is compact)") + p_sub.add_argument( + '-a', '--ascending', action="store_false", + default=True, + help="full report by ascending names (default is descending)") p_sub = subcommand.add_parser('esaf', help="print specific ESAF") p_sub.add_argument('esafId', type=int, help="ESAF ID number") @@ -504,11 +517,39 @@ def get_options(): p_sub = subcommand.add_parser('update', help="EPICS PVs: update from BSS") p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") - p_sub = subcommand.add_parser('report', help="EPICS PVs: report what is in the PVs") + p_sub = subcommand.add_parser( + 'report', + help="EPICS PVs: report what is in the PVs") p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") return parser.parse_args() +def cmd_cycles(args): + """ + Handle ``cycles`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ + if args.full: + table = pyRestTable.Table() + table.labels = "cycle start end".split() + + def sorter(entry): + return entry["startTime"] + + for entry in sorted(api_bss.listRuns(), + key=sorter, + reverse=args.ascending): + table.addRow(( + entry["name"], + entry["startTime"], + entry["endTime"], )) + print(table) + else: + printColumns(listAllRuns()) def cmd_current(args): """ @@ -631,7 +672,7 @@ def main(): cmd_current(args) elif args.subcommand == "cycles": - printColumns(listAllRuns()) + cmd_cycles(args) elif args.subcommand == "esaf": cmd_esaf(args) diff --git a/apstools/beamtime/apsbss.ui b/apstools/beamtime/apsbss.ui index 9a46a68e8..89b5fb501 100644 --- a/apstools/beamtime/apsbss.ui +++ b/apstools/beamtime/apsbss.ui @@ -7,7 +7,7 @@ 122 83 400 - 300 + 366 @@ -58,10 +58,10 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 10 + 54 39 - 382 - 132 + 262 + 38 @@ -83,7 +83,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - APS run + beam line name ESimpleLabel::WidthAndHeight @@ -93,9 +93,9 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 216 - 6 - 70 + 0 + 0 + 120 14 @@ -103,17 +103,17 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 291 - 6 - 80 - 14 + 125 + 0 + 135 + 16 caLineEdit::WidthAndHeight - $(P)esaf:cycle + $(P)proposal:beamline @@ -170,77 +170,37 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - ESAF + APS run cycle ESimpleLabel::WidthAndHeight - - - 7 - 5 - 120 - 16 - - - Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - - - - - caGraphics::Rectangle + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter 0 - 0 - 380 - 130 + 20 + 120 + 14 - - 2 - - - - 0 - 0 - 0 - - - - - 0 - 0 - 0 - - - - - 0 - 0 - 0 - - - - Solid - - 65 - 32 - 100 - 14 + 125 + 20 + 135 + 16 caLineEdit::WidthAndHeight - $(P)esaf:id + $(P)esaf:cycle @@ -278,6 +238,88 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} decimal + + + + + 19 + 80 + 200 + 16 + + + + + 0 + 0 + 0 + + + + + 115 + 223 + 255 + + + + get Proposal and ESAF info + + + update + + + apsbss + + + update $(P) + + + + + + 259 + 80 + 100 + 16 + + + + + 0 + 0 + 0 + + + + + 115 + 223 + 255 + + + + clear PVs + + + clear + + + apsbss + + + clear $(P) + + + + + + 10 + 101 + 382 + 122 + + QFrame::NoFrame @@ -297,37 +339,37 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - ID + Proposal ESimpleLabel::WidthAndHeight - - Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter - - 30 - 32 - 30 - 14 + 7 + 5 + 120 + 16 + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + - + + + caGraphics::Rectangle + - 237 - 32 - 100 - 12 + 0 + 0 + 380 + 120 - - caLineEdit::WidthAndHeight - - - $(P)esaf:status + + 2 @@ -337,32 +379,21 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - - 235 - 241 - 181 + + 0 + 0 + 0 - - caLineEdit::Channel - - - caLineEdit::Channel - - - caLineEdit::Channel - - - 0.0 - - - 1.0 - - - caLineEdit::Static + + + 0 + 0 + 0 + - - decimal + + Solid @@ -384,7 +415,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - status + mail in ESimpleLabel::WidthAndHeight @@ -394,9 +425,9 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 170 - 32 - 61 + 10 + 49 + 50 12 @@ -420,7 +451,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - dates + title ESimpleLabel::WidthAndHeight @@ -431,18 +462,18 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 10 - 51 + 66 50 12 - + - 65 - 51 - 101 + 67 + 66 + 303 12 @@ -450,7 +481,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} caLineEdit::WidthAndHeight - $(P)esaf:startDate + $(P)proposal:title @@ -481,11 +512,14 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 1.0 - - caLineEdit::Static + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - decimal + string + + + caLineEdit::Static @@ -507,7 +541,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - to + users ESimpleLabel::WidthAndHeight @@ -517,19 +551,19 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 171 - 51 - 20 + 10 + 83 + 50 12 - + - 196 - 51 - 101 + 67 + 83 + 303 12 @@ -537,7 +571,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} caLineEdit::WidthAndHeight - $(P)esaf:endDate + $(P)proposal:users @@ -568,16 +602,73 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 1.0 - - caLineEdit::Static + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - decimal + string + + + caLineEdit::Static - - - QFrame::NoFrame + + + + 67 + 100 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:userBadges + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame @@ -594,7 +685,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - title + badges ESimpleLabel::WidthAndHeight @@ -605,26 +696,23 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 10 - 68 + 100 50 12 - + - 67 - 68 - 303 + 65 + 50 + 80 12 - - caLineEdit::WidthAndHeight - - $(P)esaf:title + $(P)proposal:mailInFlag @@ -640,29 +728,11 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 181 - - caLineEdit::Channel - - - caLineEdit::Channel - - - caLineEdit::Channel - - - 0.0 - - - 1.0 - - - Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - - - string + + Column - caLineEdit::Static + caChoice::Static @@ -684,7 +754,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - users + proprietary ESimpleLabel::WidthAndHeight @@ -694,27 +764,24 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 10 - 85 - 50 + 150 + 50 + 100 12 - + - 67 - 85 - 303 + 255 + 51 + 80 12 - - caLineEdit::WidthAndHeight - - $(P)esaf:users + $(P)proposal:proprietaryFlag @@ -730,45 +797,63 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 181 - - caLineEdit::Channel + + Column - - caLineEdit::Channel + + caChoice::Static - - caLineEdit::Channel + + + + QFrame::NoFrame - - 0.0 + + + 0 + 0 + 0 + - - 1.0 + + + 0 + 0 + 0 + - - Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + ID - - string + + ESimpleLabel::WidthAndHeight - - caLineEdit::Static + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 29 + 30 + 30 + 14 + - + - 67 - 102 - 303 - 12 + 64 + 30 + 100 + 14 caLineEdit::WidthAndHeight - $(P)esaf:userBadges + $(P)proposal:id @@ -779,9 +864,9 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 235 - 241 - 181 + 153 + 255 + 255 @@ -799,17 +884,14 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 1.0 - - Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - - - string - caLineEdit::Static + + decimal + - + QFrame::NoFrame @@ -828,7 +910,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - badges + submitted ESimpleLabel::WidthAndHeight @@ -838,24 +920,75 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 10 - 102 - 50 + 182 + 31 + 80 12 + + + + 267 + 31 + 100 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:submittedDate + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + - + 10 - 174 + 226 382 - 122 + 132 - + QFrame::NoFrame @@ -874,7 +1007,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - Proposal + ESAF ESimpleLabel::WidthAndHeight @@ -900,7 +1033,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 0 0 380 - 120 + 130 @@ -931,48 +1064,12 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} Solid - - - QFrame::NoFrame - - - - 0 - 0 - 0 - - - - - 0 - 0 - 0 - - - - beamline name - - - ESimpleLabel::WidthAndHeight - - - Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter - - - - 173 - 7 - 110 - 14 - - - - + - 288 - 7 - 80 + 65 + 32 + 100 14 @@ -980,7 +1077,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} caLineEdit::WidthAndHeight - $(P)proposal:beamline + $(P)esaf:id @@ -1037,43 +1134,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - mail in - - - ESimpleLabel::WidthAndHeight - - - Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter - - - - 10 - 49 - 50 - 12 - - - - - - QFrame::NoFrame - - - - 0 - 0 - 0 - - - - - 0 - 0 - 0 - - - - title + ID ESimpleLabel::WidthAndHeight @@ -1083,19 +1144,19 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 10 - 66 - 50 - 12 + 30 + 32 + 30 + 14 - + - 67 - 66 - 303 + 237 + 32 + 100 12 @@ -1103,7 +1164,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} caLineEdit::WidthAndHeight - $(P)proposal:title + $(P)esaf:status @@ -1134,17 +1195,14 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 1.0 - - Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - - - string - caLineEdit::Static + + decimal + - + QFrame::NoFrame @@ -1163,7 +1221,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - users + status ESimpleLabel::WidthAndHeight @@ -1173,27 +1231,16 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 10 - 83 - 50 + 170 + 32 + 61 12 - - - - 67 - 83 - 303 - 12 - - - - caLineEdit::WidthAndHeight - - - $(P)proposal:users + + + QFrame::NoFrame @@ -1203,43 +1250,36 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - - 235 - 241 - 181 + + 0 + 0 + 0 - - caLineEdit::Channel - - - caLineEdit::Channel - - - caLineEdit::Channel - - - 0.0 + + dates - - 1.0 + + ESimpleLabel::WidthAndHeight - Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - - - string + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter - - caLineEdit::Static + + + 10 + 51 + 50 + 12 + - + - 67 - 100 - 303 + 65 + 51 + 101 12 @@ -1247,7 +1287,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} caLineEdit::WidthAndHeight - $(P)proposal:userBadges + $(P)esaf:startDate @@ -1278,15 +1318,12 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 1.0 - - Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - - - string - caLineEdit::Static + + decimal + @@ -1307,7 +1344,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - badges + to ESimpleLabel::WidthAndHeight @@ -1317,24 +1354,27 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 10 - 100 - 50 + 171 + 51 + 20 12 - + - 65 - 50 - 80 + 196 + 51 + 101 12 + + caLineEdit::WidthAndHeight + - $(P)proposal:mailInFlag + $(P)esaf:endDate @@ -1350,11 +1390,26 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 181 - - Column + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 - caChoice::Static + caLineEdit::Static + + + decimal @@ -1376,7 +1431,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - proprietary + title ESimpleLabel::WidthAndHeight @@ -1386,24 +1441,27 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 150 - 50 - 100 + 10 + 68 + 50 12 - + - 255 - 51 - 80 + 67 + 68 + 303 12 + + caLineEdit::WidthAndHeight + - $(P)proposal:proprietaryFlag + $(P)esaf:title @@ -1419,11 +1477,29 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 181 - - Column + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string - caChoice::Static + caLineEdit::Static @@ -1445,7 +1521,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - ID + users ESimpleLabel::WidthAndHeight @@ -1455,27 +1531,27 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 29 - 30 - 30 - 14 + 10 + 85 + 50 + 12 - + - 64 - 30 - 100 - 14 + 67 + 85 + 303 + 12 caLineEdit::WidthAndHeight - $(P)proposal:id + $(P)esaf:users @@ -1486,9 +1562,9 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} - 153 - 255 - 255 + 235 + 241 + 181 @@ -1506,55 +1582,22 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 1.0 - - caLineEdit::Static + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter - decimal - - - - - QFrame::NoFrame - - - - 0 - 0 - 0 - - - - - 0 - 0 - 0 - - - - submitted - - - ESimpleLabel::WidthAndHeight - - - Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + string - - - 182 - 31 - 80 - 12 - + + caLineEdit::Static - + - 267 - 31 - 100 + 67 + 102 + 303 12 @@ -1562,7 +1605,7 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} caLineEdit::WidthAndHeight - $(P)proposal:submittedDate + $(P)esaf:userBadges @@ -1593,29 +1636,69 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} 1.0 + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + caLineEdit::Static - - decimal + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + badges + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 102 + 50 + 12 + caLabel_0 caLabel_1 caLabel_2 - caRectangle_0 + caFrame_0 caLabel_3 + caRectangle_0 caLabel_4 caLabel_5 caLabel_6 caLabel_7 caLabel_8 caLabel_9 - caFrame_0 caLabel_10 - caRectangle_1 + caFrame_1 caLabel_11 + caRectangle_1 caLabel_12 caLabel_13 caLabel_14 @@ -1623,23 +1706,25 @@ QPushButton::menu-indicator {image: url(none.png); width: 0} caLabel_16 caLabel_17 caLabel_18 - caFrame_1 + caFrame_2 caTextEntry_0 caTextEntry_1 - caTextEntry_2 - caTextEntry_3 - caTextEntry_4 + caShellCommand_0 + caShellCommand_1 caLineEdit_0 caLineEdit_1 caLineEdit_2 - caTextEntry_5 - caLineEdit_3 - caLineEdit_4 - caLineEdit_5 caChoice_0 caChoice_1 + caTextEntry_2 + caTextEntry_3 + caTextEntry_4 + caTextEntry_5 caTextEntry_6 caTextEntry_7 + caLineEdit_3 + caLineEdit_4 + caLineEdit_5 \ No newline at end of file diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index 9046502fc..dc00d1475 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -18,7 +18,7 @@ databases from this software.* .. [#] ``dm``: https://anaconda.org/search?q=aps-dm-api -.. sidebar:: Info written to local PVs +.. sidebar:: PVs for experiment metadata This information retreived from the APS databases is stored in PVs on the beam line subnet. These PVs are available to *any* EPICS @@ -26,11 +26,14 @@ databases from this software.* This design allows the local instrument team to override any values read from the APS databases, if that is needed. -Given a beam line name (such as ``9-ID-B,C``), -APS run cycle name (such as ``2020-2``), -proposal ID number (such as ``66083``), and -ESAF ID number (such as ``226319``), -the typical information obtained includes: +Given: + +* a beam line name (such as ``9-ID-B,C``) +* APS run cycle name (such as ``2019-2``) to locate a specific proposal ID +* proposal ID number (such as ``66083``) +* ESAF ID number (such as ``226319``) + +The typical information obtained includes: * ESAF & proposal titles * user names @@ -48,6 +51,9 @@ managing the EPICS PVs. Overview ++++++++ +We'll demonstrate ``apsbss`` with information for APS beam +line 9-ID, using PV prefix ``9id:bss:``. + #. Create the PVs in an EPICS IOC #. Initialize PVs with beam line name and APS run cycle number #. Set PVs with the Proposal and ESAF ID numbers @@ -59,12 +65,16 @@ Overview :width: 95% Image of ``apsbss.ui`` screen GUI in caQtDM showing PV prefix - (``9id:bss:``), APS run cycle ``2020-2`` and beam line ``9-ID-B,C``. + (``9id:bss:``), APS run cycle ``2019-2`` and beam line ``9-ID-B,C``. + + * beam line name PV: ``9id:bss:proposal:beamline`` + * APS run cycle PV: ``9id:bss:esaf:cycle`` **Enter Proposal and ESAF ID numbers** -Note that for this ESAF ID, we had to change the cycle to `2019-2`. +Note we had to use the APS run cycle of `2019-2` +to match what is in the proposal's information. .. figure:: ../resources/ui_id_entered.png :width: 95% @@ -72,13 +82,19 @@ Note that for this ESAF ID, we had to change the cycle to `2019-2`. Image of ``apsbss.ui`` screen GUI in caQtDM with Proposal and ESAF ID numbers added. + * proposal ID number PV: ``9id:bss:proposal:id`` + * ESAF ID number PV: ``9id:bss:esaf:id`` + **Update PVs from APS databases** -Command: ``apsbss update 9id:bss:`` +In the GUI, press the button labeled ``get Proposal and ESAF info``. +This button executes the command line: ``apsbss update 9id:bss:`` Here's a view of the GUI after running the update. The information shown in the GUI is only part of the PVs, -presented in a compact format. +presented in a compact format. A full report of the +information received, including PV names, is available for +:download:`download <../resources/apsbss_report.txt>`. .. figure:: ../resources/ui_updated.png :width: 95% @@ -86,6 +102,9 @@ presented in a compact format. Image of ``apsbss.ui`` screen GUI in caQtDM showing Proposal and ESAF information. +To clear the PVs, in the GUI, press the button labeled ``clear PVs``. +This button executes the command line: ``apsbss clear 9id:bss:`` + Initialize PVs for beam line and APC run cycle ++++++++++++++++++++++++++++++++++++++++++++++ @@ -104,7 +123,8 @@ is ``9id:bss:``. What beam line name to use? ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To learn the beam line names accepted by the system, use this command:: +To learn the beam line names accepted by the system, use this command +(showing names defined on 2020-07-10):: $ apsbss beamlines 1-BM-B,C 8-ID-I 15-ID-B,C,D 23-BM-B @@ -132,7 +152,8 @@ For either station at 9-ID, use ``9-ID-B,C``. What APS run cycle to use? ^^^^^^^^^^^^^^^^^^^^^^^^^^ -To learn the APS run cycle names accepted by the system, use this command:: +To learn the APS run cycle names accepted by the system, use this command +(showing APS run cycle names defined on 2020-07-10):: $ apsbss cycles 2008-3 2011-2 2014-1 2016-3 2019-2 @@ -146,6 +167,50 @@ To learn the APS run cycle names accepted by the system, use this command:: Pick the cycle of interest. Here, we pick ``2020-2``. +To print the full report (including start and end of each cycle):: + + $ apsbss cycles --full + ====== =================== =================== + cycle start end + ====== =================== =================== + 2020-2 2020-06-09 07:00:00 2020-10-01 07:00:00 + 2020-1 2020-01-28 08:00:00 2020-06-09 07:00:00 + 2019-3 2019-09-24 07:00:00 2020-01-28 08:00:00 + 2019-2 2019-05-21 07:00:00 2019-09-24 07:00:00 + 2019-1 2019-01-22 08:00:00 2019-05-21 07:00:00 + 2018-3 2018-09-25 07:00:00 2019-01-22 08:00:00 + 2018-2 2018-05-22 07:00:00 2018-09-25 07:00:00 + 2018-1 2018-01-23 08:00:00 2018-05-22 07:00:00 + 2017-3 2017-09-26 07:00:00 2018-01-23 08:00:00 + 2017-2 2017-05-23 07:00:00 2017-09-26 07:00:00 + 2017-1 2017-01-24 08:00:00 2017-05-23 07:00:00 + 2016-3 2016-09-27 07:00:00 2017-01-24 08:00:00 + 2016-2 2016-05-24 07:00:00 2016-09-27 07:00:00 + 2016-1 2016-01-26 08:00:00 2016-05-24 07:00:00 + 2015-3 2015-09-29 07:00:00 2016-01-26 08:00:00 + 2015-2 2015-05-26 07:00:00 2015-09-29 07:00:00 + 2015-1 2015-01-27 08:00:00 2015-05-26 07:00:00 + 2014-3 2014-09-25 07:00:00 2015-01-27 08:00:00 + 2014-2 2014-05-20 07:00:00 2014-09-25 07:00:00 + 2014-1 2014-01-21 08:00:00 2014-05-20 07:00:00 + 2013-3 2013-09-24 07:00:00 2014-01-21 08:00:00 + 2013-2 2013-05-22 07:00:00 2013-09-24 07:00:00 + 2013-1 2013-01-22 08:00:00 2013-05-22 07:00:00 + 2012-3 2012-09-25 07:00:00 2013-01-22 08:00:00 + 2012-2 2012-05-23 07:00:00 2012-09-25 07:00:00 + 2012-1 2012-01-24 08:00:00 2012-05-23 07:00:00 + 2011-3 2011-09-27 07:00:00 2012-01-24 08:00:00 + 2011-2 2011-05-25 07:00:00 2011-09-27 07:00:00 + 2011-1 2011-01-25 08:00:00 2011-05-25 07:00:00 + 2010-3 2010-09-27 23:00:00 2011-01-25 08:00:00 + 2010-2 2010-05-26 07:00:00 2010-09-28 07:00:00 + 2010-1 2010-01-26 08:00:00 2010-05-26 07:00:00 + 2009-3 2009-09-29 07:00:00 2010-01-26 08:00:00 + 2009-2 2009-05-20 07:00:00 2009-09-29 07:00:00 + 2009-1 2009-01-21 08:00:00 2009-05-20 07:00:00 + 2008-3 2008-09-24 07:00:00 2009-01-21 08:00:00 + ====== =================== =================== + Write the beam line name and cycle to the PVs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -341,6 +406,21 @@ the *apsbss* application expects:: See :ref:`beamtime_source_docs` for the source code documentation of each of these subcommands. +.. _apsbss_epics_gui_screens: + +Displays for MEDM & caQtDM +++++++++++++++++++++++++++ + +Display screen files are provided for viewing some of the EPICS PVs +using either MEDM (``apsbss.adl``) or caQtDM (``apsbss.ui``). + +* caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>` +* MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>` + +Start caQtDM with this command: ``caQtDM -macro "P=9id:bss:" apsbss.ui &`` + +Start MEDM with this command: ``MEDM -x -macro "P=9id:bss:" apsbss.ui &`` + IOC Management ++++++++++++++ @@ -355,17 +435,6 @@ APS databases into the IOC. See the section titled ":ref:`apsbss_startup`" for the management of the EPICS IOC. -.. _apsbss_epics_gui_screens: - -Displays for MEDM & caQtDM -++++++++++++++++++++++++++ - -Display screen files are provided for viewing some of the EPICS PVs -using either MEDM (``apsbss.adl``) or caQtDM (``apsbss.ui``). - -* MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>` -* caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>` - Downloads +++++++++ diff --git a/docs/source/resources/ui_id_entered.png b/docs/source/resources/ui_id_entered.png index 14769923f71a4e72ca7073eb6f33832e52f2b9b9..c4e5e724b1ab604776cd784db56a223278dcf5f5 100644 GIT binary patch literal 19826 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 Yc~|~!`Tbk-8Uqk`y85}Sb4q9e09O^_)Bpeg literal 18154 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 diff --git a/docs/source/resources/ui_initialized.png b/docs/source/resources/ui_initialized.png index 48aff4fe059b83c151cf550697d97edf819b973c..8bda52dc68cd08023cfb6cd138b758e4956a21e0 100644 GIT binary patch literal 19998 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 literal 17987 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 XKPrsTcn`n)0}&UI7S0sXa`}G%w2fte diff --git a/docs/source/resources/ui_updated.png b/docs/source/resources/ui_updated.png index 58110764442a2afe4a30f9821d5376df7efc392f..8d43f8db3b4d3c2c8012d228ebb8ea34c12dd144 100644 GIT binary patch literal 29821 zcmZ^~1yq+^)IIpnN~s`SQUU_fAV?`v0wOKlAt6dhgP@Xvba!`mN_Ur1ii9*05(3WQ z{r=xtGizq<@>O2=@x;C7p0oEp`vyFJCXI(pj*TD)o~+ChWduP9hyVPqp~82Ti+(>x zkUNO%lSit~iCal7!fJyQttqVAjM{(r{ytNB{8))WcC7gl+l7!tZgf9-%u@Ecteb38 zRwu#OM`DwhEjqdCu|LgGJG-Hy>rT4URqqo{JrsRTUpt@@Kb+;*a)e+w>20~b@pK8woCb*V&fBjSB&_@-& z{PDXZ?sq8l`Q1TIQrwSsD_ynAhb;mRK&?2;Noe4@Vthjf_Wyu48@v zHP#=WnUz)bNl{L2v;1!5VDy<--p0R7y8feeQ_;`8dlWWPw&G*70g20ZzPc{wD1`SN z+#z&c4wK;@^~k@2dC)*xBf(J+79TI9sqNQimz9;JFf}Esps=l#%sX&(u_t%t%^n~~ zK!-g4rbgg-WqT2J?<3{hmaL+pBHi!uA*h7bwVbhw!|$&XQZq3sYuz`gfPbIGa;90i zC?~MDnW40S#E!2Xv!I-9PhW3oHu5@&pZc!BgdCCf9I5@{oPKQw=@Z7aEU=UbN&7j`cO z8P8n_`s?&h*4_ytw^!dyvdJQ!!>1u4WnXqC`p|Lm*WibUW3tdm()qvL8$S8;1(*3+|D;C(#^K=9>o>oPO}>8X zNI;j^4`pUw$(YB3D=6GLzsvkk5!hG5{B*cM7TXAzh3 zzLp>ca+4~|c*PLoN0{XE+hq9a6%1*)cd!Zv!)Q|{@B;rJc}SJGE+|S?K=5As%pAELwuQR6(xagpEV5WPtmw*ZY(_LZ#mBr>+x{5{{j$L$ zA!%{Qs9O7HFnEk*)%^CeeppKN^(Se>nsNo(r#_U3xXS7>PigvgZMH61z3i`Bl(#N2 zY%y+njBp8-+)VA^&>5X0MHr|G@644j3(<93M*n7G?P0aH`oJ8PI4S6I&tf99tim=V zM_(x{#T;Laf4*=aB>WhCn^o-jOIx}|#q9RKH#%lMTP7D+IT%zcuja9eJVi&^3>*#W zun)qkmQj#Ey7mA}>hMtm?7;K32(QW+BphG%L$j8cmb;V*^9`>dv|cO5H6m0*+@T}% z7T&wEI>*u+{jjnMwc6VOPXakP4hOkt!!q}UJrCcTX+Ao^J}ihxG>syBv)ApctgLm* zGx%NLxv+1TdiZvq%1~jP`0c@$URp==g8kTSH>l5Fo_j8jclKqbB;tz^JblV~Nh(f! z>Qb%Fvi;3M%~P^=sU$-icagM^GTd|4n^>YHfOC7qaiOr+ko_YB5 zj=o~}_1;^Rg+*Tcds=}mtTLGl&ku)<$=@N zMb%TYlw%YAQ=1e$T)8<}`J3l5!#e(k^w<}PFcVZIlYE-yvWk)%)#-Z`7t6s0l(#3f zvb36;r*1yW;TIqKf-MsgsO0SJpC&si=-g)K=n@|EWW_efIYMTp@Ow^y()aOxebJpq z(N^c}epE!aY$Wz0IpjBHyH3^=DUep7e3kLDmJ|yL<@bX0LPxHq#HTOiR$LA%HrgDn zUAR;(P!@;FWKK*>RIjqIvn%N8Hd)1w?C@p{sW72@X0|3qfiU<8pOSYQz`FLlo3vOmL=JVtCNcrfNo<$jIv_!=I4ILViq~ni;?ts7MBFs zwC%g`huW%NAoWYNE+}x3;zqn1B*yeL|L((@m*yQ}%NKcO%+1uM2h~w$*=*Yx6 z=yo7f*IO83>=_;|$<0N)AHLnG{$h%Q5}m z|2^J?NG2N_fmfZYeG}P9L`TTUS?SK9nfy==&^5=xFapdwsnS^8M#e6&01g zn;gt z12LBxwqxG*4+@e%IU^dQb#Za|^yyQ4e0=2mGR$XveZ7>lG~w}dXJ=Mw>K!^dpU-*4 z#rW9RPZbrh5I(*t$IU4&y2MH#eD>m}b9yBPJ$RzH4n^QCe2U!otGE z!!tfQ+S}Ks)#7;pA#m+oa4;?wmS1HhFKiv&=Xkidhx_|<=!r>59D0qgxouxw4Hh;w zN-C=Pu`!<^cqcPc)85|R!33E0Mwb%(mW65??c;(s0+thTWGYI0V zk<^p7UWSH*BxY<(d7frsa+09Y7#LVLsI9f1yIsS%8adh7)WXi6 zn;Ga+k6%SbjgO4L4qE^DlgHuL^V@`Qv+!_k)4oWX+3K{|m*d%W4Gn+K&Zr+c1T_sUKC{rjMtGez`Dhfw~=icQIGH)DFa&WNP+qg7M7NuADAtftM`=o-aS-AsB>_Tor8mhf z8^kHG!9VySoq-6O#}hKelTL8SUa=Sj++4 z+1WWaeo8S#6xK&6AZ>?fEG(=zd8v$m;S@2nqT@G|l$0H>jfay(0Dv`n z{S%EB__4C0TV+XfCi}V91S$>yjW{0jdn_z)>9W#N26zZbNu^7p2662KKBEr*-Nlx1 z**1d@4*qFrv=1M;K_RuX<3?c70R7HSP98xV@HS}WrgsICNvI8PZEXb)uqrwJ2y0_j z#K#Z${P{CP6C)#IS}&fUU>yKVIXO8r@A~?)V%-MV6?AlT#riFx79)44sFL8P|3YdC zF7Tul8Hh6>45U8=ZlmcVdDUwm2Yy%F5e( zAAcbxRmn(_-()!QP) zP_F_40-$6&yngK;5+Y^J$HzA+H@m#N4Alkdl>6m*Tx|Ebui>HJ`@61ppC!uhVL@rz z-(QgvlK6coVQ04wIe>wYF>e8_(c|x*-#@!y;dJ@65plLb=|Ziljf=m>HLvFXLmwF~ zP05mv9?`P1f3hUU`7}7n9O`X(JCxWfqwM-wLPUhInVHnJ<<(VG@!8{FxlF7qEOn;+ z(NM=B!yc9gTi(>t(V64Xn2{wd`8?#K)sUx>GbS8Foxr4+{JdD&16D>?mx`V$WqH}8 zFIlwyp$(BbOqa4UIpl{ZRt;_*9yL`}=3&eKz>A=ePT#ums# z+OD8bP(;Kyz%R&a00F%By)Moi4Gn3@V z?aPb5z=1+SLL!Z(i@Oum1e`V}WN56<1S}OzQ$c zV7e#-UPVSkz~7WuSkjJ;jz&gHpJ^pV1YGy`_aP2yY!{0Q3L+vS**G}7&QE?X_eRi& zdCpak^j`~Bm3jGc9ikVo%&4`102SPS7ePUOc8A8q#E=jX0Tg~~ zXJ_Z)A{Z}=f!NsC07rsZIT?+!+@0?`TpzbxY^I{1nB}X$K&}eG+a1bs59H3>yQ|B~ z@1ti6UzE{ta()JGw%w5YZ1j)KF;H9%o!ZZlkq$>2YA0pPiTUF~&U@dX-U0edzf}M) z0`muin>IB+-*ves%zkYs?T>Fl!Yt&m+9)97I3)K61_l_+3bUaA0K$fKbojNtPDV-^ z3p)m~3N0-y2S>@+*w~^;^)JOBySuO!BL^E>Bb5H7q8JDt4-b*wddYr%<|}PXBQ1ohoz~TyhGn)q=KVa7G#3{K62v;MDb7 z5!lN0fv;L_MeAQYFybVY_wrlm#*)9%r6|8++`PI*E6yEGs_)Z;mld2MAACce=@*O~ ze_tT}NIheAB;fCnfXQ83bA$s^1UN=y$K)e6Az?&rE_0u_n7I>$S81&1mwMwc6qFsC zBSr`Qt#$*>?RRe46A)o{y6qtTEp}HRZGjq)+9VmnpPL8?Tv$&+h#OJnFLy3aG8;OXBph!^gTZ@ZsK(P!FPJ(rC zb#{J!S6)^YkY+fnzn#b>pv?@K<=ETERFSqH<=^;(1W?RqZ8ZTZlib&Vf=xh7{O9m6 z)8*wM>srSR4hO6Dwr|OJ)#dkG&Yf=y)U1}9j>BzFPd})sA_mPh0xgZgQLm@AXAL)} z4|;o1TFbv1gy@Tu(>-I?zbwwiL@3FeQi8~?!F=%nZUhbqyLJ)C>}N%7h$tdfb#$O! zSL_Q54WXb&h>6j7{ogP#VOV*U3j7QoA0N~*KmkB$D$E8S-`^B6fh++J+#kasX1|Ji zMd3}%&Hw&4FMywCo9X!FSlE4sHls%&4+Y2O4gq}LpU9ld+@lp za6+j?6w2PxGUl(YQlw`vmAv@<{p5GGx$X8Gi|EU6W9vd=>#bv&1OsDEq}9^mn)qzh zKSWYO0CIy?yPI@3na*r+zIy*W(|6SkM*nsP#7ZdiO zBOh_ee4LoVK;I%FlHBotLaE>4IX^cy_h;1>K=l0`cV;kL$@e%(|Ni35Y?>EMN zwKB0Lcy5y;xBtC%8+@?4xdGfU*x$deu&{o~XiYlw+uhUW8UpE`%DM~?Z`0+TX7#j` zu<&pnRKU29*o}>iuSyBm2xzvCQPTfLBqd!uoQUOpsSMDm%t{aH4t?I|MTaGVU*ii| zReo&)amY=X-|ZC#I*bcHof?5TMoF-B7dc?hvX5p!h(x0A8hCyz(0FG9o7@ zhr06i&6`JlaxwR}U$bHXj0KX5g7U7KzVDBP+snh17)&g*R#Pmjm9G^j21`qwgd`l< zDa#6mv`A|46vSCIYKDu;;>FCpC3xH|AD=Om98~Y{fZq1j*1XLrl-A>DV&P{UH$V57 zaJMimprT1oRL(`ds9E9h5JIjoGru@JtDcwj-%v>R%Vdb3DHnE-NgedeinJy8#BUp& z#E*xdva{X2uP1!jJ+zkAAI&#Ch1&Y@mxXop_wM>45Gh2bGD^NQvCA{f$Q)xCQ&B&2 z`6c^yBG1|~-`esi(P?w>Ge_X0uaD~NlcsMStoaTSXVW~ge?{$^hFUWFUrS)e#!v{; z3)^p914REcKi?31C&7`D)`Q>P@L7qyUG(wAR{Q_j2CZiQDg12oQo+BhsGu{DO;`#_ zQhu_NW{C8lQ?&WVDN0LZ}sk&zQaG$~uUK+Mli z4}eo3soB{d8#B8)I^O>~*^pFJv_0Kl@yOg#colP{jN{Z3RZrZn)vRDM zGbaa49Co5<=DT%gpv~1A$>z;A;Z zQsq&EsX{IXHds!RXOH0Kg3x6-8WH8Y_TSXhp7>PUNDk-VteKq4Q~uZ!WXZULViN zsI8sSsYgd{?d*gqrX+KEHAj_jAj_RyQoHjw2v3c*QY=?S`y3}JrlHIE$s7I6)!;hrAm=qXIR_4ReYYcK##0Oy4?t`C~Fb{Wu17FJf<*=hwjxu9p7P_tog;9S3+ z7I=JkI59r{@JiK*V1kz)ttRQY^}^bk&CZ^bm>3g*iV1^Nqc~H3fkl7c7#)4<)!b{h z{r9@!cNp0Ui)(8S*HyY_X3E0Dhwtm5BffYP(J8{RJ<`_m_4nzFbf0t#(iCT{FH5yp z;C>{Sg3e0bhsJFyzSz*2nHPXN5a5*vXinVAUAur@bM-Tdias^sy1KXkRW>&@{SOZo zX+P2M7gtdDP(2+z{r2`Yfb;1>O*8}u zr7Fe0J3^as$=lO40;Vi1WA*jOouYySm(8h;&Y(Q>X7@|^we*?2hCA{ahAV4pYqJ;W zC6flCAQ6M`0;q4)5CBp6Emv1pi&sGeASA#5G6L9PW5W{g^U{P2W}}hHnQu;dQHdzT z1!$%jVRyf&`-%qt4VUJPjEc(29(w4!r}TR78h!}cw{Lf;sZ$omtWNdTmK%%mjdH3s z$-}gB99nT-zvwZsgB5k3n3@9Orc>{*4lol+$pS2*8_f>P)vcjbCGXfZ%b?U$1CWrj zUip{HFgUMnp&*Y3p)5E(%SG;glS#FVuU4r%&Z)NFIRQSRV_Z@%p zGK+n?0V&qs@PB9l{{A3lzOl30@Kld%UQW?(ay6V6q9r0iMzpFd$HEz5!@%+)rI%Ce zQ)v=Ep-YnZ@Hj2)k(S2WqtgS$p=1xc9ZF>2_z$r)kB&`(_Hot4&o9l(w`Qy_UV@Ac zdg)c?Vmkn@A-B4<^^kqWns)EbZ^fT?k{7zp*T0*7y@HqqR9r?Ku|>mwweaLy{wI5W zwzRZVRpk9q$1d+?5QBDsEnJ3y6oRmT%q$XT>;8WvQs0FZuM`n?pAXON^{uWzE$%z2 zX=vTv&A_6Zo+BJT<#uLPTYj7RN8bC3gM9yV^o9nHXv~d|yB^Ay8pKmiZVhty`YmDA z>VlGuf?P*VoLYYEqx!oo31JO2riZa0$jm>*`ptYpOwO(k9tURxV#C4X8pqd=7o!3J z^Swq40s`1F!FP(3$m1`vS6ol0lSlA)#9jX7XWo$O9_FDx+!oo`N$Bv>^r|=fLEXnK zVdQ^rcX*c)<4}fy`!eVs_gPIE=k9#L(ng}_U+bUxQ-%!0#D}5I$$t1orz9^fF^CDs z_F6u+HNqd!M(Gic8#hOl#}yYWp8Sxd^bMg14dytl|EXCZu)vs|xXsjQkn2L`h=DQM`!%37r#xeNX_2%vf|@TU~#O(*PaM(7FsWOqjp zqxmV)?Tm$;_wh7}x-;i2bKHT0Y9*1xg{LRkT8?p4$XsO=Cr;xOPX3?kBDU4}m!|3* z!=d=rDBkgfkz{`#Z{XPV({!SG_i;DmS1#vQn(QC%q7rHeOpiLwYa}G8pWV6p*X~+F zY@~O*nTyf+v?2{6ZkwQZVuTj(vVuZRk9_Akn}5CXB{M#P+vG!dqYe#8_#8Wz$fy{) zje>Yi${L$S&02B?Dqhp1L!9+>UwSTKzS)o%w2C+K4|U|T?GM`7DJ7YjK=XcsS#d8} z^tql%k_+vg!jOHYROqCJqKZc-u9(*Tr9_Fy@UC8zuxL3Ba<}NMG}lWek9hqUH1A8N z#Y3J;xQ_I1Y>Sy%9ZhZp>kavr#{FDvO9*G6;_Ldg-R$oI*g=#!>SYA<_H2`l$) zKd1>eV<8{id|lTFTt6l2F#fB|%fhnX^w4TP7w80}r$>a8->+U+|F*?Ts~{p-F!_EE z)!Xmyyq=Vq`V^O}b4peHflrWm+duxm^&#d)`zGd{>@ zh5NQjyGuEn^|s6u50TH9W%%U6nJb$lQrwAKr5VdR+rJe0iP$x;gHzZhni2fQGtWI?kd&l=%^eEoz`~vHKP3(ul$7<`w?>mob{-(d4 z>b!G7T4Z$=wIj>u*Nsc_YzRK)RgaeqL?lIo}f3-e?z^QwsAX`WC!( z{MYn9;IEbXj#l07x@tlaCejyFq=Y5?@&`Hpqp-D{&|LSw_cFOYnB_n6^+Q_csWwp% zzNpYY4NWC^yx&OnFE(RNY@CqP3KzxlVSI*G_JTy`k*51C^Gz<#1+WoS;@Qj;kwCi6 zLo?z|sR3bUY{V#0!=tdk@VuTOJC4ATGdVwo?7O{ozTC9P+%A83f(G0fA?3D%lwjR9 zgomWAl%gTZJ~x_kV(C(^uRVy{IPA7$-9owZcfYr5Nz9((3pE&m8eLGl+v6p87Cprh z7VC#T)Ga{L_NWv3$%6cHaem8`^5Cs@e9|3xzr$1Qe6e+xONGNz5g+Q5@RsB7v>(d< zo^(093tsoxSsSNHaD0)7jwC#dvv!}RKWtwhRuLQa;uI0Qp2Zz+^3bI=eS{Qhk$e(}wsGrfP@QymAwXNs};7a4Z|^y)6Zn5Fl95;e+T-}|WUh+TtKD@BYd zx;9yfkf-(64c{l{!s9s{SOGwBFe9pTpDikDo;Mdd-Z*~Z-7%%PN;9}xyYtfT%i;YK z7c*=bP)jXoZ5t{uq$q0$kYy3iCmudu^;(V35+5;;4hmTWJkZwnX!=d`V#l79x9Q)D zqytK^R^IgkH}v_pI#;4g6E4m;`_m2sPBhB%5cL0-RfzMD|2wp*)`pV)&gWlwP7yX} zV*-+QKquX>F-!m>k?SXXyk|_5(;b~Z7~4p3SC4^e&ZObWfv(H(pPD0rfdd_F{Y`JU zT@unF3pe{;uad#n%9kqonw{R(y>uR*PVCZd>Fi;oW~bUQ%vkWUgKLIB&NA-0Rv^WpeRDuVKm&3!a&mIkzJDK4xBvqd;g>+CopnzsK%gLPFegSuIRX_xPM{9~ zLE{<=xoS{9&;0PA^ZOSt!%*vh z9cN~y)?r4dEj}#Oy$T5O0vRs)?j_TA>Z5bPsSYaucj4 zN=k+xahRI&2?z{BZ+%tm3aX?xgs5qh=!rmY9lCldQXrJ3i-UF|AaI=e$ae%(NLw2l zc5M0t$>(tE45{iE#z7@7V@kcyDW1S<^Ue6nW< zU{#<56=L;P%iFrV|7;joXUPxYsbCsF1(wQzRzgrKkGO(Up;#`v5MQ+;`LW%Q+dRkxl7O^8d_u-F*K^qX|i-^Yin6Gj(t=53s!d2P*yy zwY%D+n5b-!4R-zyD|eWFg9pOtAvYUD`v029iI)~b_W4`~|#vk+uBqC}Wn#E}?PR6pgibqx0^4_DBY9iq`RN{-S;!sUZ z@WF%Iw{Jgq@BmzFsm>s>z@Dj$vHUZX)RakMZ!|xR&B-xi#wDXz6;xOkr-;(!AePbW zlaeCF%X=FE83w#e@Mgd)V&C;bt7GZsPvCA3BO9Hz$)TIs-0TURR&XI8cLW4Tkdct* z+qZ7sOf4zlU}Bp76WJfls5|K@-a2SDrlMKKoGZVP*LX&J|9-KE+aG8)HF{pumpFdm zL;=wi$QyE#j4Z$iXffDp#$$vGNqyJdv{kQNyA~M}1MUB@Fu@upb|kj8!9E~>Z+c=& zNnBMxf;s(9@1)*9O7`EE5AAy|%6n zNTu{Aa_~c`Pt1}FJHNKE*@mD6xe>ayVh+DXwzjO@-2Os)LPJ9Xnn9CYT~aza!XhFU zkcFXg!M4aK(VI((oSPd{%J?WJgKcIW_4;*i{6pJ~>LR9K1>T&EyjO2;7*Y*vY*oi5 zdE5?fiP@tJ=aSIo4?-)`0mms-Zy|&Av-{LV_>+++5$>oRPb*5)^1UY)kQMR)L z?yKq23XaAzoeeai?)yRpa*0ij^uHzb>i*nbnTT{JCXlr+_vy-KgzAme9oqja0TWT=(79uxIxrCJ z_uisNM>9D*oPyA}+G={4h&5Taeu4?xI9Enic64L}Y$y#G8OCnh(BB8c6SOa%_(AJ! zuEJaqnna&J-~5J=pPh}FG&VX4?QusZCv^>t>GAOd2#b$+6yRP#MWCVQqi=!%riYzIqB;ktgI-M?r_u`PJMcU#1>|~ z(49w+IUTWwjD{pj{r#5IqCJsOBxv5ydo0qky2r}#?T6%%z$I@5(EJb8-Zr6K8YL4& zh1r4+gmJO^J_V2oxNw`Et7>bvd9HMH^bE&xb$%0C=w4fCa!XlkzL@VbCjBxtwh!(q zFp)KTUI;0mK(CY_;0awAsE}t)3OYKJ2xQjh29UiVrgBuCK=WNzwilude6!DSXPft#SCieE;$?HcqH5UJpOJ))xjoNQ_KIdjLkJ07gY6&JTlg;Mjo9)|zy*6-+^Ccb_6t;*mRsPf>; zd2%-blpJWTL8lb-^?@&6lwgk0pUTPgn(RaR5Br@ScYP(N_B}2u>n3;$P{f}+Sq8Jo zmA8Tx8oFoOvjXbsQ+-ce6FQOBTZ;vFc!RP5n(r-76{A z(G0&h6$iDEJdKqqmf`*G{nCQmr%7V=YqK@cyNkUX`+b4ftZM^F1mC@i(w9CZH)}a8 zM!BIgr7Xdr z3@rvKSmud56=)>M=<6AO@v(aK3K;xPcI}gu6_oE=@q7_69RDUJo^&jVITZ>sGVc8B z{v90+D);Y&g@4UnExgRR8uzb_qkvZdJc~$bdOElDOlh6{nyZ@|D2hsbTeCI00IQ&V z^lDfI$r6l+j$Qx{G<+U3*OnG9Ev@*93j0TozJyRc1WUA@CkUjF6c6_I$samd`)$~~ zu4HIC!~Cgau2`$b*+nN5827w5+V1BtG6yMJ1=NrYZnvYgu=#Aqzjr8mYDZ#^+I zjdEm8O=T`l1&ZwShxmQwZ#IS7FB%7~VL0tv(qPX8ijMpsI+BnPBvZ^@Bp>gm2sz@m zsAJKqk))}7g~K7RjcPGZtcN_JsYKG-v4U?|wI4RI<7TIf_-=-C9+HWh;gq0upWhA6 zE{YfRaMic^f_|u~?e&8h93au0X)#*(6T4S?WF=l_QX z|Nn^T|KqwhPHmJ0-^XScy?!p8*CKIPTN|6YdLfTFzLa0z&pchUvhuM@^kZk@4elAIuCToi3Fb&+MwJDQ5}_ zpHpya_x+Sc+%JyKzb|Ulhkx_mI#g__@_&c+YVVA8z@*w~XYT7CU(f5;0{~Sz#Qr%j zN067lSNpW8|CoeF*j+AA_w@<+M>&2T4#WG&fbN54d*|z& zO;3{`)vxBky^oDFIBlbPgG;!2TkTd8`*H^X0=N+jGvD;y7`s>~uJQ5nmuS1~4e8p7 zdKM1^r%N8Z754B%MJN*s3vC8J%PSl4uh9Bdx@Q!d5UZ0bcC}-z*Vrz!5d6SE%oU}# z=bW}aA3Gtr1-9}EgJaKaFZ@%`>4n&SZ%%U{6IAHgzVdf#)||500b=kbi7+rQ!7E{9yWOVH!q*0;$9um;?Jz5HttDK(3-l4x((6hK2USYP_oAiUl6 zDPluw_2P50fcBMvpc>#Ubg01UxHVVD1FkAMx;E$vj*k-}U|vX1PnVY$x_su?7s&!Q zgIQs}cP~6Law`(Q#`f-Ta>iiyFa~ljKOEI(?qwcFgOi|YX{poqE|NElY)%2*855Ic+3UXdj&$ktGzF=N>EGLI^<$xz)dvTMPSpCI| zp3cq>uChH6dGYbz!0+1L)~46&&H}wGP0iD3eJ}8tI!)%Q0jfh<;befTHthE6*RLa- z#$Ca0es)2>1sxronVESY>0wMh5yKdC!d=|l(8L`aco9+xit&kxV~WP=($eO?3wOiw zzszn`23hPWE2rGVTS$4*!l~U;1`cErAuxpizCuU5yu1)Fg&K}vrn#S-p058b&Xk?p ztOw*n=;z~OT%b3N=H0Rzm2fK*`R+9ErP@zK#!*eA1tEF!z;1c3W-tr6!^&j$6Rsz^ z{dMV#2xtbDH`p5JRKVZ%;XM26>?}P^pWC0U(aA{+gp4c>JSJe))NgdY<=6zw2k+{* z0doNy(V!G^xQ2kG0G~>T7mlQWAA4ffauz!syxV194ddnEfo3-Vi5T%Aq)t+n0Wy32uU0o4& zc6JD7@ZOUICh%!*j(s>0s8T>aC#mUZ{+3cJ(pWqL)!zTvf$<|2|Av4vUJivX7JTV+tQr6O-q4Jd&wRF8sC&&?II)Ir5@CscgyepshfQy+q7sLW^FKy#qKtB|0*%qr`X)DaE$mGlPo4O+X zVtg(3TrCrOuH}9iA64^nB5&NtKhsmme*4RxAlo$SFo0_Ksof>?9kkGRn~t!=*X z^4?vwika?o2yANjhk*#xOvRaT%LYHWzxlx~?smT34Jx6z=sc@|SS~ah^Lc96CAPS? zEBNg1ZVtqGIF*@%ztAW8w0`%hD8TnWd~v{oOG~^UKCj9#X2AunE>OCxej!NcNQ!HA zhqDwjU<_MdI{4g1R2pQdb`xQWv6ae;w^8T2$SfNaX73LjZRpVja3T$$B(t@x?F|G@ z#lVOJ-NwwjwXIezLo^e9ygoj~@%ZBW9J#~6al<1SL?ldd`H@LfZ)h7ss}Y2iW|xDm zFJIot-vz_lKQOa{6Q9-uoKcXiVSqJ?wD;hLFRoY$Yseo@6K-V$G4)C#{Sr5;17!B}&gDOq z4)I(v?usK<$Z%SJ8f}>V<=^$VgP+~Q+Pc2v6!Gi&*Qnr%^){lO6T2z-e_Bd21{&La zNE;R0-liN0I(%Ii%E`;~RkX?7d2DCoWK1uizc#>Qe=JJ6YX6IyzAbWEa*e=3RF`Uh zTf2X8Rst5dd&>A*=^L=vLnwmHP`G- z+Vf4_@(9!$^NDV)pm~pcmbuMibM}5~V5#88@h3BRYYS(RH`{4>aq9C4HJ+kB0B&>+ zNZ;T=xx>>m@~@UK%u^(($3Ll{mA#(bDudJ5=C2UR~iBb zK+K-=`t||VG;?z2pR_Ii*9#z>cWEx-mK|WWS`vL5@d;fNf5)PthIalB1##;>9pi3$ z?;V(L;;?Lt$UGg8su)wteX@Q?XrT0d+%SzZ-iqIEzWVblz0#W>vS@b zgmgj8cRkg`J5&g%JPpU4)})&$(Mh^({rz>XfAi8sg^z)OdcPFs_W=p{6KT=je%8>2 zi1hrsEA}Vc2-^!@1xdB=+V>k9AD1(&Rd|X>Tvj?5JG!$GajlL12=i)${R;f`_I3@5 zbj>*IciNY!sU7ueknb+KHCI(j=ms?GWmm5+oj#Z@l6xR1%~Aa=@1YtIvN|}Oq582$ z-JP-};^6UN<}LE*nCa$wB`Cj zm&|&Tp5BOjjt?&>%4jsn8*TqfhH$n!hi6v4_VC~d%NHqn{Hy_`wU{c0dZE9xa!lV? z#BCz?hqX2R^vpnZUv=(Y)lt2mi;L;DsU%CPh@@$!WrR;m2{VR?i6pk3IwC%?-MzGQ zj_h%_sHJ9Qeb}!@#2+ia{;~AZ4%^6A6RYTLte^FpS?iP>BGRE)XV`z<87&ifN`{OA zZ#S8^xs()*)3u~8w6p?)V#t)Haz3m~?Db!l{16qH{rVjJrFl)~jUCe`I&RJ!KkC}M zP9bbFVBpXup2{0Lw7#Vm`Bf7#m~L_Rd)e8_1JPhM`99!Dk_8Dqfd(Y9w;WLxep zmz(QsY;OCdz5S8eCsuW`O^V1A7+F!EgIFn*(+c^6dyPE=)Y6rgzj`~3+iEhrFKpX=_ zlMV$PPQOM7JN;2oIaDoqzld|a{^&>hupzl=b-1$6mu}mh{vZ_Z%HX{SraIM2WoC?T z#*J{dK7pzBn1(j3TmSF2RA;f>-^`y*U@xz+&t{TnzMDd;6!7 zlBdkwql}$upYR6#+@~_QhU6n;c@*v{F=iIV)e9)7coo$wrBBD)@6b@6x{e*fmyluK zalDar@Fqp~y~y+_}8R$4y$= zvrn>K7>GZ)4BGG23gj-QLsD~v?`J4<6MyON4%m4N`+LItRDeF5xjR6$@1}p~&&B2P z9)mOVr+7SLID&OEi1>30Yc(|w3mJyd^i(T`t6fT~jM6!Nv=~!j>@pJ|9iO_b8=IdK zAExJg{_Ci4z=+`vgD^3I8Y(I57S2T1CwGwVMnfRm=KnTpQBc8kMCVguQNGxTbF&7^%{Br zx;HL*>EM;^t*iCyZrQ_>M~0lSo~U4f`~N%oUYfmJmLLxF-+LaN8@pPQbbtm4Lj5<= zJe4-Q2W8PcI^mS91_T)7+Vy540{4Q7IEId#(KqK~(Q5sF(wCUy1P255C3Nns4%kIR zy%1Kjx>%SuNBIt-n$#JEqMwwby4q7-)oBMNLCkT#(pBN$@5ckXR(!4qcidc_;!I$W zp0u=RUBvgK813p##yzu%%i<_2P7l<{?lIl(W{2-+CB9LWF8a^aI89z=v6`ip1PRo! zk++NHComdu4#;^W&e}ORz_CA!E2dPby9ZDQ1r9bY!f8@ChI^&IgZ6A=Z4F`(ux#j4 zI5;?5p03eE`c_mNJViY%}*8MWqVc)vnoHg)?=Yw_<3y4^V6if%6!EO9fyRPe&X%Gy3Kh$ z129PI328~`?vxOvOS+|`k?y0Ebaz>FNlV|y|2y~2jd|yr zZ-;T5fioP=?mqFW{d`~P#kTSO)snNovCgh{=%^JOYGhzyz@q*@@A*3IFOtPQS7*X^ z{xb}Hy6OFGZBAT*p=>7MDF9KdC9j9a$18-r^U5zMsroz^HAH1F^kKbz`(wsZ71%t` zk<)O#-sHuR$}SO$ZS^-EO!Ch)%}?TLM@9W$W~p$DIj^=9@ z%M#AHE&8cgI4;g#!5gIhfGtEQInbhP%UHQ%)ypELJh z&YH@6=T4DQ0<;-ry}dMOdY#W&)QmVt%BweLNMye9ub3`XskLmqD4DZvbh|h?Oe3e5 zH($FipvWW~$8LULm}u7EeF0|Zq^}|(Pa6*8mBr5LlL+3)s`V_t*a!80c3NUlP7Xbi zmv6R@EdBZgj7A1H&0!vl8=|_Ey|e(5QgAe{&o|TjJw?2Xq6Sfvc4EnKnhzhg2i}I3 ziz`sy_MJr+sUeHRMml^sd3adFO@J1xnsj{nXMgtRs8J?e8mk?xg@GjXj<)y4*c@CQ zM%knH?V`IymDx9_Ht$W#I$r1;*Rns_vzJW$fRnh`5VdAR6a%M z9HBQKh2)Tfg}lbbRZ98i$=U_K_!AQ5)TUVhanWZHp+&(4D%W5k3-@bX=#F#$2ldQo z0cZleQ;5>jr(~QK@hK^@V3x&p_7Tqq-z}*4K9u_UiUwv&%UmJr8(>XaxQw_TOyY0v zT>XJVqBy_qGSX@E{ixizsy+}u*UnyMu$~`LZ!{)BYPd0DZZsDcTb1fAHFg>eM$q`l z`*xFVNlFM_MjqLE{d(QBPp;P2Tbv!#9dF9bi{6gzJWV~Ckk@P0FJ6g-PmsT1M4{K4YuJIS zRp_@tkiar)k3#j#rE`){*4dxOmL)n>FAle|lWtnsO@7Ma!fDg1lk}#%qwhu|;K?E2 zS=ls(?)UQTYon@CD7%Yz2p-|TOEI~ruDmlLf9vwF;&N&dDrb-Sd3KtgB?)k&196Q^ ze0=o~1tEE-+G6s@!op~Ib~N}dbA13=M%1@h6`k|*>otD^*iK3Lp|+;oKXvw**51@y zAJ&zVV?OC_Y%b2GYHj;;j>SAbALP*2bs8=G<91`pnU+1yY*jYDBEv-O#vKG3+Xm|m zH-_*N+xKfZ2oe!PxWoI({ls{ujP3$K?vYLhbn_v%P!}4|J&`Qq)Jr~9W_uO_(0{NP zZ1T`fuUMh;$yzC_1d}dQ8{h&dQd})in3R4MmiHpgctNx)z}pFWoATWw)dZMdpuR7; z_8XvgB9;w%B;iK6Vjo0(tt>CxM-z67aBClroJ$Q`98otlOgctxK3AFMz}jlhR$zbb zJbUAt2-DF zh-!%r6Eky@fz+A_Dx~Jh4g7EPUkqF|nzN3)-JPk`l+Q2FPTP9ouVrWl4=KFRuuy)N z`Sn?>__LSdQ9(h^9umKgA|Z%U5`SgLlK+U{ws~g7Q|*n|gsnrqX0p?B?nQ;+rf*Jb zXC5vt$tSgG_I)Qa%3Fd{Cs8i`mCAxJiYem^VZ3k5x%tpWxSGX`C_B?LYgAAOXy0}v;rCu z;B9{Rb3D-N^%x=H7v+1?iiw4{Z*l+D8rn}0=|E#$ph;}O^^2emizXGuzUoE3B`qsk zu3k7ZqXoUe+xr3lOR%DVhygSd$a<#6dRHb2C2Q%UKQQd0#OKc2y^lB)SfM ztC_HwP3MZ7WBH$z+!Jpk*Bwvy7p;(cidks*B#Y6}ao<*^r9_^7`sPaJ^WsgST1h}J z=g_q&ufp0_Yk?xtcpaS;D%#q;a$xp*7ZgNKPY=d((Dy*Z0=m-k=U~ajP|}zx%~Jp? z9|C|UG`}33^y=zE7{;em=zoU`oriy%Wu~WBR|8**a`+8Y3l`=lyB`SU*c*kRrzw_u zijn$)f;tGgFGhZW4#uC|{^o&YZf4$9kFzfs9v)|C($}f9wD*B!e;b}wU3Ea^6Qv*f z2cOo73+?mk-he6YYocy!3esEk-!<2GN*dTdGjf*yt2Z9!t6xGlv|l@g{n3!~V@Z-!s` z_^xb!-1Melud7Qc(W&)&fKZi<6Zn)|bP>mTA;$fKgruZbuLwpSJ3lx+8>B#M?a+P~ zEbsO7*`xV^-f`~*232`KR^DS*q9J)3tBHpS_R+Bp_;$L z7WbM!6x*cIw4&{X+AaTxlJrsk;k8kQ*sk+&rui8$Y+7xslxcW1nT)1CKC_r# z`<4WvHF8PJKHQKC1D5?Jhx`8c=qSJozI@3Hz_lc|eA;*0--DxwKG{jjs%v{S$3Maf zKFP}AS|(BKxQq%3?C)$bf8WA?e|ri7=q+7c`8751XLg_&12h)_PUe}iGK^vnAteL` zVleJI(MU;kz;xZ(D()l;hUjO{2v5L2$pC!HKtdii?X0Fr+6>6-N50YOf%B~fI1=#s zL2&`U6hx79cok%3{s3(iIM;#YO025lte+hiumOrCCBz?pNX+96R@nZ^Zb_~S6m9cY zui*a1xcN5;iB@dF`d`r>K<6=n$=z$ptN~vX+q7>C+u%Ptpf!l?bjF2|Vea&J@1HyH{N9rdjARw@K^$Z*! zM@L7UU%#@7v4WW(#}BF{~ymrV0DhKhC&8A43G=)aZHT<_@b*PRhgOSf6mJpA=2pl_+Z+ah=>SM zwC>5pBi~?ZhDredxT&craxohk9UVblUN=ZbfD^k;201M7L8%t14|XEhv;y)aSa_R?vd2ve_YPU_<>%R4 zr~M^sE?bq6^|pgC}_EIC|*(}?@{?-l=luGwqZ|Ni>>*AMTKrOvc4iL$eCQ=yZV zjt~8Cz}8jT8TCuXeT}oM_@aFUUt>^NQZuG}>ecGV$7I~^vKh&vlG+z0KAKXRP^Cb# z2o`X#Jix!}bsr%k1KL?{FUNHE8be!0imDwiQU$3b2+*vd^D^@CASd1U9mQ3X=*VA4 zEo_6jrVuz#x5+w1NK7p7ca#?k^!4)t0w3-zZYqM6LXhP=S5@UaQU!YioHezuK!*o2 zyi;aTK0YYOXMsGH?yfFaL3yZfO=%Pr6^*}icD{49T7;n+cp(UELn~D;1T723eMZKa zM)zvi;=o7-kUOZRE~7v-2I9`fOOPZ3(#`4y-e}h(x*GwzJ%rL& z4h0pMdMF<>piQA?Gi4QFJ$Zr`6*br7W<*Z#^!OW^J2?tdR~KQQnaK|h+N2cHQPZfq z&rgXWb3S~Xnb|HPQma;MC@z7fUTV;Bt1Bc;(aTE^DQOP>0e>GOP&pqyV4yyKMiVwN zS^&u=KR-Vx&w!rYJPueED~?b`h975+>V;mvslrmLfe^J+M;Ce z%^Q-ZPswHDVhxQ()4W^`80*6`!@^z|{tAZ;%6Su@IF^YrKhyqNht7d%_ssXB8wMTR zH@pr(_{jpR>iRk*HTB%LB2bcHVPZlyObwj;fV+q^zmRh$i>#?QA*~%0tOM860Tv>H9F)7w9f!jZ1Y)Tud zoX%?Li?v*hmikOgO)ospj&WM&?Ei+f_4F{_x`hY>ky+oEmzP&q*fTo%p68~;Gb<}* z_Lrc{q&G9dma2jF*Mqeo@2?)2Qe3iCB( zpC4N{c5tJ|HyDRz)g6cWVdd7>lu}6CxY2X4-q~oEeVb*=K5+g6MA3lwm~|NL8K8b3 zYY+5QaC;#TvUC}bx~HL_s0iGFB~b13_3fZgLf+P+4XA|saRWQ6S~Ic8M1#?b<-SYw z-rD^xhOE0%@Gx*^mP<~?1y4F00xMK>bear1 zql7L_%b^27yqQMDtEb&(!%iCyJG*3#&YU?6eyS;^eah`%)^eF@!$e)XzKvB>>V3{l!jxek@4CVUyY{TcbV zZi)!CwLK!rl#yA*IX`;yYBED?-E{ZtgpY)M;Y=M%ow0TgBg? zUnb*b5DPF^I;Iq$u1GnY=}Hfg2;8-tG&dd8ovKRLwHVspkEy^s4!Dz5zx-w#8h#zu z|Cs>rc<(BJ&NajHbCaYTorB6=}rC`(fV(f52HBfw;)ApB9 zi%i|*9mAN=DnXVE&RI6R+_&N3SdceS2dO$Fi>trG5GCcaH@bT1Z9^;T&Xat%4+e}7 zLNWiPzHuv7gX(z#0dZ@Rg5=>xr`=@+9VCzr2SHsBrjyh5`JZEOaC@T60HIe*g)(Si zViNbSg~$ZP7|@{`j{n-<*;$2I0;gagiF52_;}svQ!!%i$tIhX|Zx-LF9Q%Az*wvK- zfi;e_f8Ox0IdVIGh5kNk>GlvMB`L0Q6#DNq7CiEXwp0RP2?HdVt`*W*L|+m$$kJI#j{fxQPI5ZkmXIj%)sR$(_3FwZk|Jo#d8r6 zG@s1O+rvXz@^i2X!Z?6Jp+NgxQBJ=Vw$Z`J2y-y|)-cU4|68Rdd}?S1J%0Kx4yfO`}qGft+p0K`@Q@U4f(r~FsM6`U>yDu7N~i;Kd#&3pPZ}(Wv4rZ zuZqr?%=Aq_vf(jTE#0Y~s&1p*VcO{4SE|8K$69+$c-KAsyt{#Q0f`M@6^5GiFTw>R zwV>{O^(o)l;3e%nbF(>}eisN;sbXKQ`K1(Ds97tN))ecxln@`Ux6<9UYXV4QXiO}D zrjfy3FIOW+g(T}Nb80i$vbRIcryD+)IF9i&KjbM6(eJaAP;+Q--v5@DScmQ|K~5ya z#*Y40z~P9v;GiWioF_cE@6P#EVR3(!fs4G>juMKeZbwqwi=JM7+z5541W6-Hh~dqQ>Ex?uiClGVx_nvzwV~tDDR~_ z@;Y_Fl@zUOWR9z9x?e|<6P!*Z!uugNi|OIlX(!!1)ZZetkD_bkTzz1QvnMuSG!S+x zWyD>t#5OtXHNNOnM)jA{jQ{*^_9&%7hcGtn#=!K5Z__ZXzfH6?<|Qm!-Zv#uwynCJ z-hWWAl+o6B*DD4tZP;-gdOVFO$6mQyLuWZUGBVR}w9BMM2jxujUag~AHa3{MaTB%TfH>ouf=eW@Oh++=gnE3eg zZP8MYY$M2xTYsMsFk2cCEo2sguue-CbiTkgC3em#f{0_#XF}#102ar8{y-4qC=1Eg zz0U6L;IOdEx0sk3%~Xal{Ew}S7Qc4A3l6&SZOiPQqFoSYy(AhwG0_BZtyjJ7OmuQ4 z>D~7L9Ko%@tTXxY@vsEcqWm2WpNyf*X+KNCuv{u3HwoLC5!H+LJ^GCTc#8t=SM&yd zJSe}$v76dBQ$ZW$EG&dzg=H}#9g<&Mu?oi)nb?Z+p zlWFXkc=eoIIJ8AS!YZ;03Qjs`yhoZ}ID>H=MkZcf-t}0}>ASePqCsE!8(~|orDp$- zVeQX__CbHgT10(Zc7CF7k>l?G`vjziDOb17ru)@g>&4mDg-*QuOU6VT8pDxNI6$ZpCK#;683=)2_w(pYJcK?E<;d%8_e7)^t_&>pH9Tf6Snb zj5SnNU2TYm;av~8f~t#CT}va$2Er%8#?=H*)tcuN)(qcS*bM?{AobTgeI zE*39%C9JRcfYKafv`=K-@%OVBJ!2v|z<(0CXiS@>G80)fl?h(^`6j1iRzkub1x2pP z&Dk|?+d{OSj}ogFk|X?{%@-LoczB95)Tx3ONBtMb@4XFn8>RAvR5X_(<_pu0G)Qh} zPQKYyW7)k^kMN$PM$=!fCYhRqI5u^)S`!5g_z}!D+KzB-%z=7mYmp2Qr0HaHTIl@H zdw%!v=@+Bv0~JA?M@W2ze)lEJq`a?J_3CxCN#c6Md% zW?yFC@Wa}zt|MuGNby01Q?h?oR({ddP$=m9Xs>d0m5U_U6Ye$^-3F$6ERktyiJnW7 z0CmQ(%q1GLHf=LnY7wk_ulX6{p(!mb4dEW_Di{gJc}z*^8OdpC$3ZGM-}cw$k!$Zs zJQFq1Z*8sMJROW>`@%K9XB?hN9&eQAdcSYhtvC5B@(FVJ+B)eCefQ(_D)&iOfGyk4 z0%d{)dp*%^ZB2H=jZ8`epq%m0%suW{t_t z!ZgCkyLm8ays&E3=b;AooamGsyhGcYtE!3F=gr; z=4x1cR;;2QDsQgiTgi^?D|I3jaB9<+Yur2HNf-zPecC{+?(@Vm_5%QXX zp*Txy9}d$n^z?Zp#oy%&QTBZg_zStaUb$QT0fSw-fZU?Mc+&t_0b31!F2j5#dD9FOa=qQo5 z*A`1$R49l%MYKdCT;1M&rz`?odOtV3KuA`zDh1!+^Jc~8PBRQQ_RFaAi1!>os`gO-c3!kuCzG53b0dZAAYhwxPTL5cyU%dXs+vEyj9dTGPvQr zm!EsMLqD$1EoDCDu$1S=EN3aI=DZ8w5&F$sGowc1u#m!uPsHpSWBxaYW7$Wkw2}{M zTs6W}Bd2K+DumcNyQY+Y9A>?19`Wc#_6NkE_x0*fGJ`Yt{Fk1abN(A9z_l4)2(PVO2i@976a zBO|b{LM;&*&#yjdvvioDDrJiOn4Jr4`XGfLiDRBgqO*4l6m*P}lDJ9myBhR5)f zZdu?=n_E+m6RiAc!Q)3~9!fCuNZX+T!lvPo18O=teGOX8UIIQ4y$yIV>jL8HD)gSZ z*?Ehaef#{(9c+~B?B$TZ5701R>W&tp5Nep3pf^6m;G$F2oC zqU>BV9Qyn&UWGBs@T}u}V&(2+pz-{!$5OTD`-5=v9I}v$;{M#T^E?tw#mPdy{K7|J%2b?r$6#) zvb}PTgmEY(t88(ilc`5GT>hz%kqT0%N;bAdLbg!&p;A>sMMpz&L_gtmLTBu^Qt#BD z*zIn=MN#tB4u%IDE+2kUJIPLsuA9`}#WI+c3DPa9y2|frT%em?nbbYE)pUZ3vMWf? zAd*4@P*d>4O6P3d=Fhim4)Y~0^`@l2Y(LB8mGWuvwo4n9ViMwi;tyc#o^6ivhM#G{ zV(CrD6?j|nJwZgd{3FHKmX4Dp1#-QYKy~cRtIO`wd`f7M5mnFxbZ7QMq<~-CxK?gZ z-IKCVcZjT&K~lA6;@2>{NO8{0(cLqz=y?Qxh}a=VPEU=}((xWFAgKb; z1Z0&NUIssSn)~ZxISQN*^`(l4SvHu}1h4}g)MBcd3W4oZoA&w`EIoPqfY)n91%JrV zV{P+BkjQ2-?oWmcw%7g+7>%BO%Za9<*j(Rz*ML0A68vv~l-n5ByXW)~5zM|_m#T;t z7)9&Q6vCGU z5>sMG`RgyfJE39qXkR;J|D1Vh{(yVDyQf;Ekq0mRQ)Zu&BdgEg!yPMX666`PNx-Ev zq;x{Azen`qOKp!l#evKdyvz8t^9eyc5ygcjRCmS>|2zfIwzF1tdUsdGvdH;WZob7~ za8jPYl*2pf;g@f-z6QilA;sDAczb+qtYh-9DV{#qvYZQ?z_~I}rlBEOShZNR_-9l; zhY8V{>jt@axh{3knZx@T_0hD&p5dy>G;4Dq8lU|jRBd~7eD#lT9b%G2bLPJGf{Ept zX_f&E1)WSd#UIUmMg8JeQuxA>7>77G2NT$b?=IO!?qIKcL;X>HqM1PPg>08pZVC|D zX(3@z?tm6khR2U%+Vnr>=R=YKD<5Cd$K89kZ|j?wVCexuid?!Rc(%~&erRQ3Q3x5H z2n2tBZwUkfDXC2#O&tg@0H1Pm?+uqT8KJkeUYZ;h{o;WPDVLk{43(;{f8dr2B8&3a zmA6JJE3J(T+u~DCPaF10Z!^)2t#V$r2);f+Vs2#lb%yopa5bLK(9pw=85a+Oet$c+ z&xya}6THNJ-B@1l{rF=tljzJ3eE^z-$U7{ODWcoxUDN^yW3A=g)8KxxNOa;zk5p7h z&G+CHlXA`N(>J_{5{xP;5Fs7@}+2ny&VSIl5!k_@g?)Hv@HT> zyl(n=yZ@<{xE+1oBi~qVCA6OxN2Q<1(!(}lnA0Mvz+4uus-ZEdm3%$sy^7K4@c^7? zQBjvwL-!n%!|MBb+YW*8Gl+l$vG4m>)#9qFBgx$P#RuBNTWjm}t}A5qI!%QYI%Q|8 zm8E3mUdfNvq!6JO$5aAQ4HH_;p)_=~x-|DnqDXI?J`RhBjWsS)2fN7mKLUjZmViYy3AZr%$Z?Ddf(BZg(Ri7#(ap3B<8c9yATd3 zI7=K@j3pv)BKJv<7;awbSq@1{Qxmgq{V5e7_cPLfn#)Sc>X1(ZP+C(SlB7=q&*LWB z*O~=Kk~pgtxYyvB7li*)TrqjxXrfk_Je!{6@BcgXtZzLIFa>_kldlZY&sE1pw>a&q z+Wk)Ylcv>0>(CXvqSgQZG#8*Ygu~i;3m#SwpOEmVr3@Nx)g@LNJt1%_`Z_vpAy({iB^=`Y!IV`XLZ=+V4U|?ji3gfBjc5IybWu;nCaxMC2bS2J#!{n%<%J z!5${Ub#_#Y5 zz_(|oD4yBb(2IC|+QUc}vYZKB3%?w^Gym1)aj#zT!5v~_ib>aKs-;=Je)42Gx~zil zziOGhyv{+N0M;mgnIPUpUcMKS9>LcImplT?4iLtXk&)}?;8eDEh9!)|SO*@h0^{kU zrE*9Kh9rP#6?!G>sfnyKZ*7ZDm_Bj+ipUrbi&iz~I_lgWY4C{6%{Iouq%`2Qo-a#G zh__yjO}k(_+S!0hP=P{zW86Q4N@(U)4vr7ZAPo&di1b@UqcrMI|3eG%kJQ8CVazH> zJ?^v`*gl}oQI2oFLf+NE(SX(}u6v)Qz?jrkaF$Uzn(^(4j_(dP{PA&r=2VQj{_Tvg z#LY%a0~q;_dgEXF-ikn}8dlnz95yZGpyta7rT+vshs&i{&yHv|b;{RU8;q=@`ugm#AMJ4<<0`Wzgd{v1Ql7iTXM5V_?mj0LS8o;V&m6igAzd9s+^D5Em0tKp>D};XkoEx8RX^6E8*t zf*c_(^h(J&adXN=OJ3BH(INwn*0=o>uYI+=9qn-h0A zMx+GSO%q`Y%TH41cWNTsEzBjSuqV=3;1Ox+_3&4?&J&fS@C~CVGJ&wXRwybWpbaoXwBg3hr*o@%wRfIV zJEX!R(Vt})&09+2X8ecgX00!iCoGtI^s>~| z){aZF%P8yMw)XPVslNUor}>>N+7_;-T_c)Rp)~EoVM?C%frxa3oF*xWrY4C3>crmO zu-;gb28~OPKrC_T#rVE^QUfMeH?@1ycu!GRp7xn5dItB45w(#3`TGcK6%|bH(Q33R zD?d9K?mk2H$N&yZa*WXj32#VK{wd58^V!tb)^bmvg^V#m1n_g;=;^D|i!dYVyHu+0{~;_h4^6q?jyZqi z5EX4DGsfQI?eF*C#3ib-?>(A2q7>7Lm4MvK zl{>HWWqJs?jJCcqAW~Ho7^WEBZyRIUS|#6nz(vE%X5(jfG4d;NnXd3!k)Fi1vp#$x z_4dzog!k5|I}V{o;+VcYD;9OzrXf+QqGecw^2%Iyhs0T;#K@1H&+6(I?`#U){f0bU z$9Qf{PjE8vc{=ax`zk6@+npc7osG08-b zC+!Wl>1khBWKXjZm)Y2^O02FN<2_hk!jFl@SUgQuOftJKIP&Asio4zGq6{?|mSjW! z^3C!0{!yD5-!n}V+xD0pkHzNR%%< zim8w@Ad?T^B+ET^yN{s$#QvbGba1{r3WN_y-)EPMpE}1wIJK- zEBM%&q33ayDIl)S^PG_g@xXQIz~#X&=iHtrSTn>1CS7*6yu7@s-;FOaq!#DGCG9RI zHR28Z?M4=N}Y_1Fkvf%hm%@oC}^TgP+#2RjnlQ-XsX&BjBR#(o%-H86s=E)RP^1%sJrRycX{8{4|abe z72nO_Kz(4=#TtJ3Vf2$y*R3%Gp_rm7ZmBpLCsDFLuODJ`baY5QJ&c~$mb*Qqpm6O6 zbK!L^y{KaZ6>*Jp(+t)SmWoS-rv|Mn~3(#Gw1fT zQZcj;jM(@*7tZbMJ8wr)8kmESUz=g(RECCP^uB*d80JuXI}}bG zUqQyYQk_$ZzY*gO!-5`>S~x*yP>orKP{|uCyf9y^I9L zl>(!>*P}?y)EVM<_Zu7JG?VESi4?PTJYU$kJ1h=Nj*rcy5&o_b*mg@^+~3^=G#tWI zyMaP(`*#OsPXYf?RV-$QSbn(EVm1GI)Pk^A3^S1)wDD4R+dn(k9%OR!MB{2C-Kxmq z9qKAszWh4>Y%43Tu(r0g*Z400qlV7oM&#=cx;p+v`W{SkcA5;{Q~?jCuonf~Jwmwf zl>$%KcVD)eUKqAj=8;kZO1H&Zu_uI?`8g68%w)`>;nB6bG1Y5h`E{ zzyF!2Q5rg?n@}(k?m9DQ*78gqTbTVZ$&=_b+QR=cOOaw2!f*2(_g;Vb@`WP@O;}!O zP&~}*U0~4v{TB(_X#?h6*}~+pTaMn5Lt5JSBPb$|{Wtx?%@f-4IISY4tHnF$Of9t^ z2c%^i&j$unt7~Zuj0}H+6voewn4j0T)W(1xEiaIyvO4zEVkj$HkJnNUVLhj!YU%Ia z9mtd{(6N2}`eQ>wga6Iy7|v7GY;eca($e}alhnx-oSvSZnMvh$8wm;N^XJb-Mn(l1 z6(PP%hs!M_JPl6HF4i-ZIXQ873~F*}YA-!6ooQ%jDys^08fvX)_(#|S0s>{4Y|6Em{_&@nXR!gwF}*|XSs!i zR`1@ueA|qu)M{*O(YKLI1}zmHKB&=5q@>gJ|AiVY4Y- zo(NY?eiIQ95k0*p-V;^Ue=!X|<>o%~Cs|rrf@tNpnQcB;=_4UF``%JfS^1ofZf&ON zAH1tr{OQed#{3W@Cnpyc7M8s;)fK}8HvrMxpZZ2vNNBp=MNvn`b9=HT#p}ApkvuRU z;Fg?*Mtovo{`c=Hb>*H{=OqOzmstwx>NOT)EQs~>^`}pt`uh5+7HSdjxt2#nJX%S5 z>Uy-!?t1vH^m2FZ7sMJY3ek|motY-)F`jR~fB*KnJ~Qb}T z4cbBql0;->`-XF>?AR6$gfmt*7L@YybkiHmo9rBiHA;aliU;=!;Q@9z(z ztE_}*fN$Z03ra`?sK=$G=<+NzxSd>|E?||S&&|y_pt7K#__CXiu&GhCPSrWhGQQ z@H$VWpw?#YBZN_6BC)#SMOL`kV|OeAB1~e3TDOzJ#Kg(Xu`(vD>Ib;EDkkIxRCIJ) zwu|kntE-!v7Vv_$)1oqHY6STBY(@M+W@hXEu1^FY%0_5EyEfWT;`YU}(=#*g!Q0WR z7Fu+CS7+9>M>M!`rY3b6kC&T1dGbVEoqcdSeC8iA8YaBa$`2nj>*;zf`_%_y6@yu_ zqC!IT?q`ph+rude^7GX~5s~lPLP$bM`JS?umz2N_)|=9Tosg4zz(t4q=u!L&y;~D# z_J^x~-~#^^=^{UP`0&rrkXEfdoeMo9+k66c{L}qB0UiuNun+zXMt2nYz6SDyDJ^YPd%OIcYxccG)FH#arSNlpE8Ds5M1JpF)( zgygD?%xiUH<79Kp?)R66@E%mU+uxt+o~!(g?Nm}xp{AsKXKH%1KHSyZJp0QJE7MFK zE#+os+t}OJd;M#ytgQ6)Me)X=S1I=B*qA5}4ecw?tU5e8%92h{QC6;SIaqFQ7h9fM z>WXP=Ys<^agBNr=-W)r2P4!{K7cy0BKLgU z{gsuJ42_H;BO|A#rZ6xthKGhu&(1a&K3xW{ZEQsTL%V(Zldtcv6_&A)k&BDV@U)!A z`3WGA*S}AnZZk14_0kdfXe}7d7biO=EGkeU5Wlp6&0VCbF`V1kX|kN)C~cjXAo4CMVk+aJp~>QL+Q!Gn zhiHk4i4kx+?rv}Y;*iDfe%j3N?|h>O-l)6l>|g~>{Vwb&qlLbzk`nE+XTTF2*N5&3 z{=2^Fpb@@~4Gs!|@P!;$QC=SVU6BGy%%E6XoN|k2WVLS@PHa?El=J5fFs`K3MKC!1NRn5^``j0^k7j zjvG)Dh$ow9nAu5p`H&B(gr8qWTDrh`rV#-f%*V&~>(?)Hw9@Qs+5s+M?h7P*BBDTl zf296Df7Hw0{h9ma7fO5sS`TmX!TI@lB(=0z@hv!Vu=35-nrJuv7R@wzT=<}2k@7kt z1l81>SNl`HfB!x(Fp#g_jul^7$$@zF>JFMbWJkReC5fNdU z1Of(W*V?~#bc74$Yz%S5d#(l0{@%`RVPV03)WgH0&5r09Zl7=dc*{MaP5ZsQqh>*XxL&q6|QK!O4kr=gwPmi&E2Wr10oy zol?WQex%R$TL9#`dV7sS#BFR|sGqK+c&Q285`nXLv#muGdPYVL7sr+(Y)6*k-Q7WF zI(P5ffr~;xLAkoV*vY4f%~!+X$HKz$bK;u0{IRvUN%E&&5SM`9Y;QqyL>8cjoP~vj zg2HF5o*WK+Y~2HFZ0w+*pzl&~KkEU}U=fgzBt`#cCEhG|csBt7fnJ>As;ZU#F{WqF z);Bg15)v*JJ0iQgyAKZ!697Z%T?VJ7l4{6flai9)q#Ad}7HZeA)P=>y;y--IY|wgt zWFX$2vl;f<&ek^8Gf%tD(c}E^1`QXdTBSDkx2NieIIY5r0C-Cb+5lhtwj!uB+f;OP zlD)kVnuPmK2{AEkwK}g}eP9nREwxop7$`I8V`F0zJGnQpgb8;UvB(JN%E)M`xVRW# z0zyhbTRW*{%(n#M(CuV9Jd#C3SXdYxS$%8M>v9jx?=YB)Twh;*@VoRvy*3_ROcIaN z_}EzZvN8kuLzW0(Bnj74QSDluVA#K|%M=e2NMS8v!XH ziap%jKVy+iXG-F^^RpWVx4;$b9~_`#VVR&CO51LXFalgXYJr`dZt~jM+0oV3-kP7^ zhdlT3<43?lJF5`5_Z#yqfy9pvH~xlht(-tg0$8-@5PKYTBr78*=*>t0Y}LVmoeS-3 zqo_&lZ-s!Tr{^-I5g->NdEjq&(9;Wb8`!In>^u?QusY` z%eX`^m4N?_h1kKjgo6StBi%GO)JE_DFV4?N`CQpJIHK+SY3b<3CMF;yA>(t~uSPR! z0x7gx>Wl(T`tNX17U1ys_;_Ys!m6&tsaasRi+)cT!`S)ylIzT$+9NI>0<_hUNjUNj zI{L)e7^7|@FCAUZ(ubfQzkad0>@PxP`svfB`g-1dAMr*E2u?#uEnvM-QNoR6J-gde zJR{qBwmut>n*}`1g?j7ZEWiO2=q(8S6rrrj2} z(dO38vilGCKsYrTv|%zcGgDDzmY0_w9@+!YjA(ip=>*vpSl6J(4(8mv8v%LU_;2(76C z<)bz;JaV%f?d>VG2T0JY*#lb*tL;`~Abut%CsjOy%UedzTex02ZEJ0HjY9eGs>_On zftEJgW8)V9$)m@Qc^uY-lF`VcwCbIiC!SkO*Aoazl8&%)aEKIqCarn0F!6-OY0(OK z9yVMynKwKp#$TlW6?^(&CxH8_aGoq9ZELH_<+Uf1R(}{!cQP}02ePWFM-U% z!2#T+?jr@J)78;21@$n*W@%|Dd{iK6P+2cR0Bbs#5T!GtQ8F>@%>M9cZEXcAQCO6i zot+KLkfrAUJ|vP_lPBMrals&5`r}8B9JeRWR{v1Q%m2y9$cSOmy?6I6?Ic$7f1yeniN3(I60k` zyW^xfR(g}VdU}*qRcWdSF2mw++r>0L*CfUFz@-B=L+v~m+cbPbgQ3)IYg<`d49k*& zix3II*VEJc;nFGDXmHNhS-p(hlapUDe zYjEBB1raqhwS!v(-hl5ZFW-L@dV@ely{}edOI35#HaK6Je2vj@SO78nyUN!(j-Q8P0si7UA|Ib-AFHydylaxVm<+x+jl`+-j>DGkld zyWGF#=H|l90n|5iZGIjhh@Q{Jd)VaM0a;b=rP#DhzuxSC4 zUgBlf@h=Z4I-xRgL!2LNoOCfYy&wzD$ytN2D=RI9;JqWLrRCle&(2uy5xTKkoGc&U zYM6*GKlHgGwecZDrPI!I60b{X0sU+F0@G<^e+5qAe&!3hfODi>$E|w?2ABPzdh*yu zJ_}SdI}Ck=a-f3|H0m_WZiis*-hbmKA`+jGO@_1cU0P1*_l%d%f?YMVc((AE?3zg1 zR&3}3(#DylYd?E`2SE(e50fOI8<`BTPw5|U&!#MRLr5Nbm~yd0MF*jy^MduwO0T6h z#f=^|N7WaMa=Y9?m^L6SJ`k#egfMQXRDb(Fp2@%IC+T)4aQ5ZCKdI)}dPqgsLK|>y zMM!27&o$)0YI`H$>3F1X4*B!5$^4Jz_Fpxx=xp#z4*O{9w6c|0N$bZe)v6`mep~|K z3LX-g03>`)E3Gc{xs5?I35nA!PFi}-rmw<*xJzZcLeb9S)i4`%iAK>q~&Vj?0&D zC>D9#cV}N2bKla`oG|X&Z65ooaZq^eY(Xe9Ky5&@u0S7&GY;gFEJy1K2cEwC;qa@0*2IGLIClIEh)8d>)ld#+uj=%FG+LqkJw zaBvvPKpL2AZjQgJ2hUxn-vSiJ=%tdXxYwBC`LTk1q~J}#*>HUR!QW7GV-pi|b8|`1 z&{k)f1U85RfujLJf*c43F#kio;}h&7J}7%2qUibfuGWY1ph&RTfwQr+xVU?8P}ZCI zo5O_`p{S_H%31~t335Rz)U>&|5$bgu*3%Y-hHM~g7=dB2 zN;v9UcC^dsZYrFt@NoGgMQI=mPOn2bze(k8$Flv&%`+lebz znq|g4*H;&D(b^u5inP%+s%>T#J8&RLprD8@_wungZBGmgRApv1^~Blz=(sEBD)f6U zjuopf@$7qbbz?oo4zCmU`*{dSn$yO3GM3!4mb3G7mIb9)=3YskL zgL@L<>_^@^f{+%sFdq(H}I$evm7y%T%P(`7=1$}w30DyRMQVZz#{ytjraFSUc1hB20 zU0)K9s7|B%*+55!|7{kIj*bpUIY3zua9yC^zQm?~{yZ59EkjDXUe4h&SsZgf|NYZDh=1IiW>9L)Iqxr&@zwcE*aC=dh%5rbLFb8`@}%?QX&{D^c)-8=KTr6|W`fSlw|A)fF3XuoD$J2F-r$nhKC>}p}U~{&=bW=ti!F913 zclZDOt6S%|1>(EfPDwRn5I9FZK0e}j6QxF78jY&hEyL{?B_u2SrJ!GrTMy$|s`a>$ zhu5n;C^-5u09?$xh>i7T8ECGpt#wpF#mtu+oe-ZYb5(pIU_6zDcc33*w-@~t@osIZ7}A4f%a4w@ zDpjWHI*Mnx1sks+%V^anaJ-U#==CqSNc1__2j*{0IBrXbORr$u%XI0u)_RLZX=cX4khcdf_!phnou1*&)uTZaDix3nP)KvYH zMw?r6dVO72kS7Rlb!%RK-aY^i%nb>dXXPQM`9^;6SLI_GM%OEj^bGs&G*v&K;|C|r zj3~eRilh)hQYb8vu10ayoX*rwJ0hlaNHTklI|dS$q=W=9F)`3N_!n}!=m;R6nx^L8 zm^_N0hOtLmCY1R@LrREe4pYSTfF>+i@(0!VNrRVW4v5zc)Rhi6@w0wr;r;;uqynBG zi7tSm*Z~K@!NGEaP@XX=I{G*_nND?QvPMx|{VZwYO@*wxcHL`PtXHp2!;P69VPk7G zxE|%Jmjwg{Le`n6t6VZlI!)WBj42@Nuk6 z&&apGzp_v=55hYpHdYO|ou6Q|^OnYZh53$7V`7Dum%RSdvirub8CC!8*Vlh4Gsz1G zFg`y+s$O3n(Nr4J|66$Ye6-7Wj zdcIXP1I;8R3JL=g6B=^z-NlZ_m{c~=;W{te3iS>fO#18sTBy#ER++|_)_WBZ=2Ocq zu8Cpc50#ao1N>!feM@_m^!0G^edbaj$&Y8b191c>h*qM|YFj#PcB-O^5XA6(5?A~b z0AiI7AI6(56&{fF)h60saYu^0()9{y`X^?-@$u8`C#Gv_bxx5-hE^-`fw+A4Fs;(p z|HUpYE}~)*LnC8zWu@(SWUS1@X=@w@8(T?7r#JOBB)4;k?5z_w|03N6aT#~ax67L) zEy6{JRf2aC5=Zo2SE$Y1TnlV$OVf>K265>vSEJY@+D99z2A|H4Lqc4spFX`a>$vKJ zf=*-lu9Mv@CMWtL+;0#14^I@Q?QXyrEHU|AQA|4bE6RyCW_Ndo=n(Q3C!(8U6r8SX z=FeHG?3SnMy!!t*XbwXGywV+429-P_e^6q1X$jhOMcQ@IiLXcK`1q0-fUAWj`i*ch zQ>CZPPo=d`8=e*atAIu$@|^}30oDayxf%X^IzA54LBbqfi|Vw0AMzrp%|k~9v$LIP z7%4dP=D$edZD&#nSocmV6ntW$y%Q4B9LgTl2Z9tRLBPe^?fZ{%KpmQ0{N4SlySroZ z;2=}>we^}p$CEenA*4yg#u#N0mR2UY6}-U0MoSC~)ggk##2~>k@$*mr_<__6wPS2t zgJ>vegU7`&q&GDU4d|L~PgI5G_kkeE_~BrAq`DVnw$Ze)=)~pGAAN&}V4C2eKg!6n zwT;HBhE>5$FVwB3k8i!@HN9OXA}?;{&v()7H+1qlCMA??xlq3QsVs1j6_;v&JsFN1q@z{6t~;J^j^N)2#RTE(7OL6mlJ6+L=Our75m2}L!2v%d!BQ*1IF`R* z`GSUt$qR%l>ZReTs=rlLG+h!4t;}5rPdpXvg8-W!l382R@V4!yeof&;6HPp_txa> zURK?{Qp~Ep^H;!p?Eazb*EG);0z+ld*uoA{|Lufx&*xAH%L@dZp89?JPb1vef|o#G z{RMyGAEehrYnq|rbe=in$2ot+M%tj4{mR`C zKc&QAqW0-gAS6@c9Tg92;g{Y6 z!(5E1`VQaFO$DvOE6jz1^CNR6rXqfib3|BpI8rnBKuLZ+!v=Xf8YVFj_}SU9n#nQ77BjRwuSwtd(FuQQY4W@>J{a-U;<`xXHz1~kS%Is|!_-7?ef#hx*1WK1+(IsaY>GQOCGo*41Z}$) z-IG66jv|+K34hqPWm}3k(e8qOqDrV;)m7De6OsXzaz(zbK>%e;C?i@%@eG0}>>r z@r@V!AQLZ6$a;2j1=%xzP@9%E4~b!OvmPQHTCKxeDz=tx9?zAO>H~wt317-}yZu$~ zK>E3dHCUt-;29A{M@_Aybh^HL^wLtB-IRb?lV>o+bAEV4nX2QpHUI9or-r!jz2-xL z^F{LpD+h#kM1<|pD`cmwmwe7&c*~w_$(vD9ct^HbH!&1k9_B!H+gV?lLArx};ASac z`Hro-{~dnh=AnP_W&MEOFB~6PU$4tUah+EYIkM)YOV;3f^s|(P#_>u`K16KTrt5ip z_J$9j`$oxHY;HVTSt{nyD|=dAfqz=kfFu)mX`}=+A~w7zFW7EFgZrAq4BCdc4<3*| zdj>5hXwz968!s;`APWi!E$%I}fjt1W802|q@b>ohf;`{V*N65kKR-Vwrz6OIbA7$j z(NVncd@#@m<0)QO{GNA1QIWN>a#TqPvp19}x8zh+O+>bh`mxf&;reXqT7?o^uqY*_ zu>U0c?LEZB1r?TWgRn9@L>=vwvH5p)gA9Y*im)F?cNrNAIP&ifzqjuf{kqz~wRQW7 zJR7&x^$4nAQ$KV=``Crni|<*ihd%G>F`aeBoT0G<5+TX+un(S$;~3|!G-w;3lC!f%1r@jFn|?NtE$fli>hS6*oJji8R%njtvB8l4XrA>uew`Gv z<5SYl#xak z9E?N%5+JP1vD9O0HDYT?C!OLlT#^+Tx&^qFPbd*oR7>fiipcAJY6IFR!rj9IDI+rzT8`}2iE5vddI|$otNa7*swg*DJ3c{O_#8Ac zcsio4W@&R_8M?!QBHUrBIAWB6NBd0tj707IrkI%c`!nv7c%8oCjb%b<{KoFy3k#dY zAoZDav=2gg=&Dt>-iEAf_4}2r5NPv~W%5X9OG&lsfBvD9f=*GPj3mW8T)>Jh%uY(C z=C@~h^2Tkt#%=`&nU0PQsP3AT7GrfzOdGoal+s|d2&WK5h)GILjE_uxd3&_$xh|;w zUY_}tJLv@#BYU5RB{@Q#`G?0fy_0LQgk+DK{I1#VgR`Np_5EL_zDJITi5+@etz&p2 z4W#j$j3xopY_8owp~@+kuoNpv;3V$pWVrpp?s&9=np-{2cSGya?RXt?cGcejVP;Yi z-{8xzIch}nj5?duy(DKW8xO_k=g-USg!Nd@wCdf8{KVS`swkwpp)A-@8S4{mrppF0 zNq6#C!_;F(*j|$)8Lx&Y2zVWDw;aezbSlV_nw)Lh;gd7EQtmDX=auNszs4XH7txaH zJV>1XP&&WpeCx}7FZbQz!O6IUj;^H;+|RBde~W$--__96#SJjyfesh=Y8Z$$KvExGjgEw%!qbMc_Tf-_^RtH5oWz&25U|y~cWog5p+!>EW<;*?<;` z<*+thQ(H^eT+r{Ae#9QbqY)NUQMP%|@KQ^1Z2HILcvP&18&kw;<=Z*R)J6~c&rF4^ zVIA~`UJ+qjQj&DndbJ8y^7jSFu1TRgm2*&FkTr==QmNOhZ>OiUz(df{&0!?qC1Qls z+nzbKMMXD})RKDqLmpJ4+kx^=P>%ztW3J@7G5s=wa-$<`IuTR7@mCiG6#9!7c4AlN1eyXdXf%HxC zZ%UoR4!HeRo@;Y^o$hjaKiB3t*;d>kA4hPwGMpaa$s%O(OiVGOQ&|7@4=(&Jr`s4p z#h6^&Gv7KqbIE*6ZH5%4y6VO8@(}{q4p40bx5NC!%Fe&5N#Cn1;|ZAwMxsz8@*eTt zPoLKDhjE)_`V?`NtdTDFu94L#sQ=&z_9ai1vgjC^;D}53hk*ic3&YB{xPrB!g0-jP zd1+$hwfKHGeN0+c*NjHbw1R?`C(mJcexi`0%Ad$jQjkZi{rHKk^ObodDL94XZ|}@_ zEevFmnJGx#z3bl+h!^=Dw*$W{F}Po)qxYGsy2jnvo&*8J{V?1fF(yP<)ZS~QuZlsZ zYcx9FeOru6{d@Nmtpa0Y_%^*T1dFDpoX$pdYa6%LBJ&~ljIW`I1sZ2G!PC@V3Tl+k zUEMZZq6|r~ym{jed{rJ_;7=~UI8IoGVdULd8;7Dx` zh3XSClhkbLxl+GYun}UB@!urp#YLeJu<-x>Z;jE?;+tPmRyNV-;TH2;`^NeUtyfO9 z{Fxt7lDID6OI@72Q=A%g1zh(9WpwoGOH?Fmt$e7e^LfKA>Z{Ak^~WOk^0bWoB@dH~ zN>WAp3K14DQYbdZ_J-{}7YJ0bzz7aZP29Y*$D4RNCGkn9Z`+>NwGs!15uJn*HzGCL zj;Jv+KS9t(?|3*1;0TJizUnu1Lhw{3O_pqB{HRVHG$9;%uD5E_68Wm?ZyoALl|Lz8 zZEv#4Vs&@S8}><#%h!bs0Ri_2WeMZr_I`G}Cv!3XwS1PsuiVy>ak}kvo1Eb(P`drm z@CWbjK4GyA>`>C)(KZRDzHibq5bKdetHPU)5#Y%FdXqDg`O?c$8zmsk``r4A<@lYF znKWZiY-U8?tzy`Z3LekNPFqb0R5&ow=B8g9gho61=jNGz`-NJXKZIGkfPM5PP4&zF z>#cRBqIyE-Pry1}`xIWJrzIu~fw0HD1JaAU!T>A6{puX4d2b8(2rhyjn^qo-oqc#kT&W&Lk_W zdDrzh9f3(wKd$>bpDJPbZSndgc%9IjzaNQm6#P@^Gv;)dq099>)oFO$>XNLK&>m@!P1_Ji+J?Ypyk;~pZjyFwcnUD}6X-^u}@wrxZ zuItXiO23Ulz&xtE4;r#}h>Y8PLvWx$2Mqx|zt&JPtl6p=Ph?<1>#M8n9NzQbAt)^? z1HBa-7hn$*ldy4g-2Rh}^ZL=ms!vzUd3P+kil}M<`M=DA&a0K@mNkx4%fCxB5uY(t zwHmCRM>4OD7G%kmI$kgkakkF=94@$Zi?2vG*x&1GiHkXjW%Mh;(yBLP_q<3M+Z=jQ z@lcqMR#$VGbhz11TsoeiH9^>t@rg}Toy^0zbg9X-GB-zM;P$XG80+fPUADpq9>sVY zJoxIW#HZs9mxT-|R-jj`<7NiY4aF37o3V?jM8v*uTqH>>3tj*xhdI zaR*zOsqv`{mVXr!nDBmm#waT;CTAK$mAg$&L&FK(n2Zcc#A3n7Pmc3%OHw}QE8FX< zxsdhQJw79OE7JVb;#4z#*IB~<$*XxsL2oXPudxdO_we{Kj;GS`yNQgb)oA^<-hx=E z;UwN4T)V4;Eb$8(RW5w4_9!p3?EcJWDjJ~%^yt1cJyS@jpM1np+CrG+Q%IDLX4+1A z+~hd=ZyW>=kBcL5*&bB|f(Ljpr4FJ~=?lp`k2~~a0lr9iH@`+|m+PNRpP|$6dqnWP z)>QWBZ|)AlL%00S>l)^GrIBBsma!nwRX$WtG9hz@#*O>}pXaw>?)H8r|6VA|XAD)t ziIXcgreRXnh%^oPx^0Suxw*5`J&J!P32BJvZBa7%*tr~1;k+x{nRb98o0$0E3>8R=_-*Xeppf>y3Ia%Uv7ONL8TwP#A zMeQKLjSTjRwR-9KQou`=lr%ZN`)9Mac~|tNxJ368^<{P0mkunOtreqtkJsM?K3rjP z%g*k&bX?{3yK*Q#nc8W})vS7s05<_x&Hz`vwg-+@u)MA!ljBp8QLDvmw6g(n8v2QF$^#%V9dBuy1Nr~X|E{A0YFjwB{=x2}MX%_V@N%s;zM z7G3viPg!zvzWO^JUj|?u===np^tM9OITxH-S26Bx%bHA1RU+nvajdQl@u)0b44F8Z9rI)6CFt_P2tkv&vg zzO4 zxOjMIZ^4fbUP3)0!SIeFkaxjC9=;^fTWDxCAD^83k2lM?e$Qs$ml22MA=5#nh$!Dg zy+i8xw+|JfWQqKPZ-b5pX3-PcyN)()1qSLOs~MXAjPDlcSJ!ol`}{qzJScnEeSs@F z;dt|k#YDTi;rDs&kY7_)#@F_BWwbbUCyErq?|8&~YZ_tyY&)J70daSAWpvbj+@GUJ zgFwfX9w5oYd|O{K64|DnzxQ-?wIH*rd#uw*+j{HWj@#NfhdlEWG&fy6J$JJj@ z?)ZdE-{W>QUoZdTaXRLPZ_-1sQo7%KxoU|hcTmD(tWp*8xBos`?oDSG#u1t zKWply-C|hHyQzyXPJ1$ER>%jQ!6Attpf%Q0XC;y}bpF|pZQ`J#`7QhSn=djwZb8%= z9_#QcG0g2c_bam?uW#rZoq(E}PV|zNPRMF=hf9CPNFcxJ z;D*+I#T{tQe@lZ@Z^glzc^vxTzgaU+%VhytA4^8Y#v?U$XtiOYH&vzBDV#eNXn}fr zdvoM}N=ll7rX^vdEts5=lMCD*(;-%HF3|7k+$wf@LB=P7>o}#(9<_MNyreY1UifT6eUNk65btc;DONuhy?LPfDpcU#tz!E)E|;Lh;Enh=*pIUL4UJsRTZwo zR7rLZH5lk=e$?*ox4eToroHrJyDXX&gMr~FOGYCyJ|ZjaSwq`KcZ<1ZNBYjEvqR?X zgk{cago09nIa4StbgDs%Ff%jjjb&Z{SAwCTke=Qg_->F#D=mo-uYSaj{D6rO@H^2l zFx+p3HWPF(n?E3f=?DQ%a&kt-2EAXX5v9z?^a1{NnY%nL_K?5S9vIFWTIP*uVq<5D zl`UnZI3p>kG)5B8iYVN>b7;tT^O1je0OMSQbONXA3D#@JwQ&ML`9J14NV*tz!cQ6e z?q?v8A1-?=bWli0K3mT>Nea0=-Q7oDQ9P7l%#7M>?Po2OqW1tV;q|n~A$gCUFBM|y zj}PgjtcS7+6(hy^Vl|fcRH@y zN;7kFJ0~XG!1fEKPP_N-O&7iY-ul>piWUBMumo+kZsuWgo~rffnHy@7W7UOsr{o9=v=q)df|B2L;N{%WmIxRZ(e7F^ zL(|e!p`KU^QYgxG|5v+`0Q@pd{}>0iQKR0*#PD$1KT=dCk@rz^(MWQZ?5Cb&lc87x zF?q{8?fdz(FtS(YWQ~As9_%xCJYYO(rpC?$lr_-TV7>_apa@`^5slz3)vU7muSFLD z##B}o7Eq^eAtS@@!JrxRR=^1j&XkL$f9$-S!lzrOP@UB=duH`olKZ?w?kC7ZeS3Px zh~;Hyw_r&gXVpJPi-KPYGv}RSO(&1Mxl|nfEk)ucdJD>0k_>-cjL-W;^YL#R-~P;+ zOKsXEqxxfa7WY;Lf1{$Wlb!8m$QgOxd$0Q;I@{*g`Pc3aL6XU z)Prf+`FR$bS-~6W7}}qR05gm(aCf5#f^%G0_{QotHs%5*GcZ}kgY63xY6P#_aXvH7 z>Qo&Mf|)rrH}?c_h=FQignO&qL+59_lw7ux2UQ}E9SzsoSudY1Mx_JgE;Xy%Iv4~a zd^3KpARDzHU0&>XA#UGxV_sur)qy%Ta&~Uc#aRHO`ToHXXblf-7p_65g7?(Vqw)TU zXOtiLWDot2(x3x(F(%+-Cm~AR+2)^7m+3abv*P&nTQ*N;EsN4G*@9Y9`_2n<>22@A zJPe=GDUvCifBfHGfWEf7aTXqy*uz#8fr^qaAY^U515IbJ^qp)?sEUfVfk{pQro+>2 zd4K+VGthN)brp9A)>U2bKMJr^z4%s8kOa=)^OmnsQS9vO32en_bEkTL3orR6Y#)0E zYkHy-|8!{$Fmm(aaYt@$Wklbeb}2O%eIZ_Nck-z8El`IfZG6XzKajRT$>3r*BKV$Z zcq8p7ns6R&j$t&Pu{yh#AfKYA2g(~9ZBjv;ZQMoM>8#g*^o5j{j9X2EiWc9oO?^Y;o^i}lUs z-$WlBC39EM!*CibM0rI;!o!wEutnEK#EmuP9F_kOl1HR;l1GyrY!FzgMpA>*0+#iL zh8*^=j*U@Z{?cl50QM1uDV{1Lc47BMpBDH|OY z!7dD`<|e(<2m~Xr%S`so-)ykBvDa}JH-S+#DoS~9;|*8>G5+x1`~`>q&c-${ec1e& znP;hn1!}eNLQi`)A*6I|uX#9)7FsI{e{z}(j-%`| z%jYYDDQ0zTt>yP`L~(gJ8z*NdOp4k-8iEN>UELXILIUENn{UA62x7F>f!-TNOu}y5 z;4nmZOU`6IHZ~SASA_H2PhW8GKBuMK-P?N-{0c^m(9rx;Ch^wMpg2jcEWCA8Q0{qi zQT4#!)nz0k#zCj{hR)O9sVNT_Lj(wymX&pKaDaY2*i2Ev;^S`&zR(c8jfMuc5E#5o zLaa+|E-h7nnH+#0(H*zR)N~1LmRoPZ5Cn5X(4c#F#Fy3>ib2Eyv%CNpe+LI6^Y@O9 z;FZx{zzivv2@xQX!(i|&a4-vLT!Hx+o`{gs(&ByF>7t=l z6P1i$MTLnLIF3h_RuDW^R#wm@11|;|*v3*46TvqEixcLV;YL*+-i(e`+oYD2m4S~8 z99T&Ai8z8XU_$TY_?Wum`tm@Qi>n5vitz~vYpbgn)QY{odhJZ}A;2{QO^%FfueXC+dX!@ zw+g=bqWj=JcXvHwSt+n1X{Uuh zfkcfIL?$rZ(Q&sKyq-|J$AyInJ__4u206p?WLp~n*4?%7iVg69i0eFj_z;m1JX2w= ziU6NH6Fq(20sS6~ia`>)v5A8TfNJ6y1B1Pj6U|+-6$LptXo;hBL_|lUeuH5Z(BUY0 z=oRjG11k(AWWVPP=K_X4#wcJ63;MO-iZe1XA>(uXO2^8>qY3uYM&t_LDW|Z@mG+MVWW$7w4rL;4}UC$eG`Tq@d|>1vrwuOu$BY_ zbXHnUAQHJ9%phnNO@g5xds!_kDrzUJ`h3_K48q{p>h6MJHb4X@-p~aBx6Du9DU`Uo zI~-;QL5gb)A_!(OgdrO6#DMJ+K`oQ`AJyn)V&Tz{I{iM1RggRf$HVDqzS}57QgoDz zNjAp}6P^F7uCI=YGJLoFfFT4WC8Pxe5kv(9B?Oh0ZV5p^Lb^d(1VuqwKvI!zhVJeV zknTp2lJ5KPJ9pi?&RXYui$5I3Va@l>`#!Osz4vSiyJg}Nm>e7x=5qFShW8Cw8DW<_ z>%LOpB`!HFt8nfVtE8wnv%Jg}bPnJX989?wZojHs5O#v#8-c;DFV2-eFzbvDxX^wA zXfE)KmEhaAZ*U5yttiv2Z@~n%tcxuWy( zHh|TNjY84V(ZxvYzPm&QClz*WT2j)?+?=xB9E=EnT;~WSG6L!bsSar5fjZ8?$tkO- zXag7vC}yT@aMP`&<>a6@&&$k|5f#-3$~6LPS{I1XAgByOL3z$r{cpi8#HdrAaRkn@ z1;nDRVQYv|EK;`m(9o@}as8KD66Kq?0D(jvC#WD~0HbiNfMu&886w`}v^Ew%#J3)Mt! z@qw=F(7`2qA;4H;<0Eez@7?wm*X~ycyg3*0^>TLK$HoJT$qEoQfULNv82`u3b#bc? zmb=GoTzR^JR?Qz15^2zD80Eo%+gA#L0{FJ+rLN{gV%51pC{qdzoix4qEuA4ECe5Vs zqrvnOxjYkV=l#TYe&Z##u8PmnOaM7_^GbY;zw7nF88_>x!NFZO>#*-*9JsoZpdN?u zK~1>2`%hAlKP!}1n^Uy*dn@OY=0pEyJC6UgFrr)cA0fQ2B!@0uSJl0`o8y=*dcoAZ zctj_oxzOiN=IA0}IQo9s>iRmN5rq6Z?CiESHf=z%w6W2bnB=*8S3lO}t(s)CRo0D_ zr=8vZdG*nq8_u}g>JTj?Kht6;Dk&A1^wMi+X!KwWD~XiADTML>=<%nu)%L;fUz^V# zBs+vW%WV&FyF?xkHqn{${z9TZD7VEusH(4+82XP)O7?M;{G6cuu+qe4ue&xrHq4&@QJR1sh6sAv9)IM-TEG#U5 zPc-t;Vxr6zr0!>DXTT%@Jr1aKpj!peE93$e(hM}(9$GqhEmS*@YvzD^Ss1j_*!`<& zZcc`ekK&Sa{dYEU9UY4nuh)NVPh0gv3;k11rY4mF=c(d#)a~pFU#{`=Z-Yex_je*Y z8O;`+oYU2370IW^@=i{Umd-^kAu4v)xlR+wdJb|;ZVb9P(FwW^b6SsN(b+&bdDfvhyWYdi2*46txG6#wWji zTFnwO-D$8cW1?XxH|bHdnhN+3U80YnRvi}_s-_yz@%8?otE=XlT-Tu?qBl2$#RBo5 zO$QkhgxhBB-Ms*~X+_1Kut){n5AB!!PS@5#ynz((%v4oRFL+H^O>J>`+1bucSydH+ zv`(Q>JLphA$&A#2{B(5mTIT`<9UX`|g`+vt>4a~%;i6Mph98bLH#Csxsol>nDA3H- zM3CLxoc_Y^um9M=GcLz}v(q$r#8b?WpIc|lHJ_v`a1c+=^}pFn@CRPb<}JKPc_pT* zYNEgB@1LNWaHBbLIM*1XPAKv>i!uSS`A@d4vMIh?O;^opzh~Zz`#bV;bhLQgy3vfA z35)Qf@u4u*b2m4^P^rHnUSg|R8Xt{~srmR|ajvHQ_>h$zNXP#?kZJ9oje0 z*l%Cm)^?DW@q0KBvrj}oKmgdmb`B1K!NIuRQxA*46#+~{;A%7C{xb7iR`xRD;^LBz zDJXahG5GkQ7(_X!T54;(V1+=$iFnJhg7^w9gBadpA6ZshdZAL=f&Sjf#Hg5bm32~* z_+MLRhF0=h3|@ED)rpxK^PHT$aJa8o8=IT-j}BfzD0@wW+SoA=jK@o1EI8c}xahE42V-c8mP}gJ9;!OF6hHe3DfG~1$orvWTV;ir8m6T#hXl-6-M%1FwtFNC7z5lH6n%a(YKBmp6u;b)f z7pIp+kuE;sf^K@3z1*7#I2{(!*g{U+;_UvZ7$~GVIXa^A?^257xIb5~{H1wY=&y{NTypAAHqEeo0)?7X=SQS&binLZ zCeN??3rwGzcVVnxvmSgEhxNKtE}0Y;0&a4}?t+5_a%GTNo4+v^sP}rpW_jGvKoUwHRo? zKyU*P0{jEYAxCFtTm)7Xnivpq>FMb~+kuf|S@9Z-r#jJ>P9~MV%#P~aMqqI-$@|0MJwjfzmO8s0y_B_XJ z$AY)-hxIgP$AWyZK7Zi5$HRk#;B;J*t~)-wBagfP=}`K2i6;+EcqTy~+lal<%GRf| zdiq+bO%#ohz=N=H-UHq9VlhUEki@OHtKYpO3>lv)sNa?pKf^izYMDKJ+QXGQ2Kqo<^2?{XV4UQ_ya6Y;%Gr75(@4a6R#5fJIAFoih^ z!@}nL2plAb$S;0Dt;cm66mJqgi7p;-uh3t^^ht7ivmROvou1L@!7G* zRKARk5poTTxt3gW_Q1kEwE>lh6Bu|}jdS9=AmVbm9mq^vgr)n)a~9M#pvdX!l9e4H z%i_FqM*;fk5=-rKk6^mvrmoAu&mPDpo-8c&oS=0huUggGd?O$*;vsLra`3G7notlv z3Vc5Q316b~>Mr`R3ghkV8zF!o6eK$F`bp~Dc?Mw@iYCXp8ag@zQE47Cr+H8_$a*|A z;Ng~BVApg{()LnAL2|(L$CRuD3MlWor%wBYA8+y(l-j#BHr;JPUtbcNhNz1XzM1D{i1wwU|e0tz2j%#`P59spz1DYZ$JYpVsK5)r$|gs|T3VT!8Qn@=^;-Vk485V|?}m^J)(c=kIeg5OOH zbAzNn7mP7tW8K;$FIMoWcGMTBrC>e@vTSGq;8Xxp5U$QIx3~B9a4zKLAf^!igyYGq z0or-+@&REnj5@&JXnOIfvcXTi$(Ayzs1mbI z?re)<A-?r##$BpOouDt_J>{~J0}M?i-uU=sw8 z99!EhXv#J>ZD8IE!8OyBWaAhth#(Rm08QZ_kc@VIvwg)hBOb#yioVJO&_+pnclYGj z7(N2w08!M_6KP9-HwrS<6}@w`!Usn=3k$s_R&4CU502dkvD1oyK?4w_BIc&u?5VDk z#A4$K9e{02=jxse63u$s{XTsH#WLZ>tSLTJT?imd^>2s44D7D%LqS17*_byl4S_NF z1-$X<<;yJ?1q0?}&}lF0!1?guHJ-??U#W?S5d>T@08oZ*7#xsBK4$M%eb|+Qo0OFW z5{?XDPxki5FP8hRK6tx>D*Q~uV7g8J+{9!VhVPe94RE_47Q~HMJh8=l>)upxr`ADN zm+Wy!l-TP6w4VIKP-RO(W=s@4X@U5Yf1Toxh&a@3oOH=)ZDvhqBPjIY2Jzw8i!s%o zv#}s6fBrP!!$+=Z$I1ErcahnJ8x-OVLynlkPLHhrdWYWDQu@l>pIr1{mjl)Zg&~kr ze2$imypz~%P`@u3flpF&$(<|T)6*ei9WHgpg82@7(i?xqm)FA1o>z&9Yj;~szG)0N z(GgeZFgK@OG?8=N*=(*n7q}U#WJ&1X)011zt8M(9#FoW#z3MmAn!>JgEl}3L@N{ya z2i&Yif6rN3T1z)A_2ibu$6NRca!z(-!M42^Gu(ao#MQMNxWzzJs4)j)C4irxumwIm z7zaVV1#An>Gu)GESU#0z0t@JzWb+e!e0)Mf3o0vV5D=e3&IeZ_SXcAcuhRvfO#xJ( zV`Rhy?~Dv}D5F=^z`wmsy~k&%h6HB&qUvyV9ACmNwgH1=Aaz zug9Q7D%v&)XDSVO*R~h?NfUP@@O9#-Yx#Y2#SrhrM21%|7u>@D`-e-fQUHQ<3V??6 z)1bcc>NHrjw1&3PWofv4rfF?!d$2dr>=FIuD0pCsFMWjV4qHkpU#G+Gb=!HW!^85) zy<9Wv)s>@Pr+UY&dnKP8SHEa#zd74qYGlm8$iF=Cj&EjZ33!$+kSf8ZEu-ki`Ps2G z_}W1y*bdlGXjV!}b8Bl_PEL1UUqLB57Z;bVt}gg}L3U+jO$DtkICTN^c44{&E-GNe zP+VN>(envFK0qKm#%M#DpQ&OJC5*qWjeCtx`J^1LSOBdZ~)pdoqZZ9;hPW%}o%_YM_m4~dR!NUm*V z6onaPY6`wk(H@=38thG^@tRW!ucZNxMc`FI>p)lNm&l7fq+YZ)TF9@e0&!{WhyeYQ z2RjVcIw+w~abT$SORYDks+5%l-k-$vIM2@SVjwy?Z`NOiUlY65rdB&s=72xgkST zYa1T!8wuV}ND?4NWastfP)(Tn7JU2W`NQLY5ZXeJ3d3Q-U}RxIAmzabEVaw8_px^a+ z%C)s8NS(D;_Vn3E;P@d_<=Pf+`<3Yt(V2Stk@tne^6xhZY=2YE^%D@(bUY$oV0a%D zPKw5zM*>C+Dz7IksbQ&Ae==+e{mD_j|B0QWdO~5x`u2kv_lcN|&eW-qpQ%D_Sjb(SUp}x2ZP&jxjmsdx7V-;7qov>y|%Fpd_Zd#BV}=>P9rmqMQ<7+ z5*|Fv&XA|#R8ntgiDmv&=?s7Lxp+>FZ&>)zfT?qc*92qj>kivpo_E&qx>$wQ);oR7 zmlWuu@=7+lIz37{f3NtJ?L6w0*7EML+JnUAb#f4@Os}q@5)Ocu^4J653lB83EbK$% zIV#P_y`GnQ-P)S+ZG6%k>AQ%9I4qwM)>LX;-AP1Id;BVGg=0<%ky`6ljVs}PhZu|k ze+1Og`m=w(w=S|NO#B>-9e`?PwnO(Rb0qYbu&zj5zt;J5WiZB!?c?W~@35xKCy$!( zKYBbEoj5UW=T>We*zf2l*Vh_`Lb={n=d!SLqak+3ttW>pNe+` z6QuJT(|s!YNdAj=p|Oz**ea+0GL*x&aP0ntNeApBGcVLhjJq9!BcqN3gpY?RD(Un} zJ?}l@!oUpn%Q)sHbA0$Ppv=DX@ZbTYQtmVHq3->9UPX!d#-cqj=ugv zS((mxX~>RX4f+!qnM~`{rS{&hq_i&dNS6j@NO)A!dr_uhqiGIqUS(HxJ%u&nHqY-=Pyjyo0yo zB{29<9osCc>tt1wo=)ToQK?fdjMv%ACgQa4<30+;`0*IaL`T0yyx;xMciL0QrWav5 z-Z)$Rh9)1?PtVB_c7kMP*g7`uRy#hzpQezinZ4p#KhpJ9Yt*A}&QU0v4c;2V-4Zmbnyz6*8{L4Pzi6BB`Rmuj6UU4OL^kFN2mNMMXb;{LxOmNzce`H^}lswN%7d*R2ifdMA6Ip?b2yS{GKVT%EGk<{FQ4RFRhRjbwL9velxTWq(N zYGyM3vQ69KW}_ZR3_4%Cy1KpCk(Ta}lV0y@S5}It>Hqia%Ss{Tec14nJ!!R^St*x4 zF*6@<~Q6&61tNb{qYb285SaJ>Y_?}^ z;pIi*p2SLBxWj$<>ygVZ-*esPEmC^u#49v=i2mbxYak`{5?0+#%$#2}MJ?4ycv*qo zEHo`d3_=RBWs=t>)8pz%$S6k&?EUr{MkDAcC?agPZ4mDyVusKWS~^F%M8o`hb5E0m z6gFRo+B$ih{A%Y9B@;=peRGaa{q>(8_A8S2^NB)St<-pOOIY5?lBD_eg}q9_jF)$J zS=^tVB@U-W9*65UUzN9-syZAvJ>mlv1Ois!D{E_X)YMwfp95Evm60(YX2xJI1QTMw z!N3VocH15%1wdl24r=TD*0buD^jPL2M{mp2&c8#q&2~wDzxS@a8`Ez1h|^wb?Z2|* zaBE>nzGzd!7-rQ2ezu>6j5HLAj87R&-@UwKR6Cn;13G$3_e(~(>-5aaIGB*`=>*(P z49cj!3|B%;5ai8uo8Zw3<=)kPoa3{Fe(1sN=S8}ol6CC3yFByu?o&~>*}ooBUZ+Vs0sMsW z(RD|@62u?$aO9?$(WRv#;6sdnLF-HQBrvWfM8Fw!Z*LEnTYY_f2qPz~x)l|Pv9YIPgV|NMy~6t{dA{@u5uDtr7~ZbiX&d8Y zLBk@qGe=6<1h~gy!o$0}MuPf-`YG?{3I-S2=VR6d4CEp-g^I zbwzw3Y4=j4wsVP1j~3nFQuV~FE9tH`%Wx&a|E|dS^d|u?U0b@#zl8v3;4;jD88ITg znGo3+Zh)BQfW3%#uTgUu^ps(Unm?=To<3_g!!V{8i{dhZyRtnrShBOXL>|}+Se8~6 zpJcN~tk!SlbKJil^!U0HqF-ZB-(0p6RPt=TXUb_`XZV=d((=5Z@ZNdt1oNTY#`O2` zQhJF*r&lHn)8j!AH06J^(0~BU;p$rDHa9lf9uF~3WPiY`tz53yUbbRuIyVR3{Yrw! zM1+{SW>SCl^X$3op!L8A;uEx4~ir_770oz-U*LJxMCV1bW?SkcWfyH6r@r#Z(AU&~F_qQG+KG z2yMW`?2M7m#$cb?&fbR@ZZ*>r7h}-SlLsBNZat~eun`TO5fD5+;8~s=*P3q8tNApD z_i3|oO~ANJ44XeJM#<*LNGABPa0*nyNJzMDb}~P1)ND}-Nbii!#gfs+Tz*1!jJ2DmUp<>f!e#Eb$_p|_Wa zQnASmtb@TpPX877WMBk?@#pdVtUVDob+xlX`CffOr}v#BVje+@XO%I23CF{|5#b+% zBP*PpufED#q!^>V72B&j_TF;cF{oij_k7A!Z1k^bMEIA?c-TA!bC~MpSJY$H2=GtC z&|IrMhE*2TL;8&sSR>tKUIscf?w8DW2(~-t?o7uED)@HD3Y;-wiMun8FeL zw5KlOu5amO!q3!rBjT*m6^VNL?Hq2W>IdWywBREsIif z`29q}W<|ZKWyD5fVJn{OFr#>uvJGn$3!Kkr*$T8F=kL@_GwGX%}4YkA+TT6{~bBWhv`dN=fk8vqKr5wY;F|h zERS1Rs-eaD#i<=H_O7 z(L}L%RNls25iuFoaA$#c8XCh`AdU0NG;hQ5Ui0^^s4zx8)GldjjF))^F_3LDAT!I> z%*-4^{U^rFcEM4vimywS+CrkLLdB^(Rn%MtEmI^-txswPEO@2H7F(=(l5O0^{-nLDT{r+vf;KT z_ScGj>XL1gu!oSsoCG1tij<74sH6xMC8Jl=>E>ot@;1KX71!9^vr_9vI!VcRNYq?) zK^+Q#W}%%Q3#l{g-7+N6;J48Fv*yZOdy^W8Am`J}3xyxQ%qn6ivVZZ z1IASUUYnZ}5E8-{3%uJP0Z&*1jc8#t`!fmz@1X$^5)_nIUN)i-KZ)MT^952p6j16B zWTn|%cB?hoe_@Vjnq_2tE5t@J}FlAp!_E;A+s~Yo>r=j5YfvC#Qvp2`-lH@ls=^jIx9}Rp^L? zan8>;-m4BhYRw1vyawjFI8hAS=kQ96Xvxy7NizF z$Jmkbt)AY|ZM%Dapkc`2A37BgeYQ;l`%Nw=&ypWS8)Rqj@+Bs6XP@kR$sfx*)CZkf z(r~cVWs^$2Xc?pz7Ls0+ z2LKA*^AiU&tT$1_D=L2uzSQHc?Hf!V%7N&ie)XFkN>~W_qFBclp(?VPR|`*c-t>k4 z^L<23S#E zE*JNJWL#BAiT(EN=Jihi*>!!rzgqc@9IBwysWx4+5+X#X>I^_f`K9r-C&B08D1`pS zDtG@j7GYc?H3J}kWesdUY* zAKS1)PZV7b2@k)5d=^^=kKf6{W-eMXO^g}+q}#}WOM<)F;LMsWDRTL8TT{h@ufIw&iO6t84S&cKQLhYw>W(jW?}yi|MLHrcVjP!8fxL6 zc+;gYnz;c_&Qaby@ZR>|;07EY7;YQ;xDf3cz>k# ttdsT|$rju(qkoqMegPijhu{;UkAAnWFwz^O!`I6p(&F-Bxli=p{vY0R(7XTu From ffe7c676ed8d8592dca576b124e9a393b538c1b6 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Sat, 11 Jul 2020 15:12:12 -0500 Subject: [PATCH 44/47] ENH #333 add version option --- apstools/beamtime/apsbss.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py index 037ccd6af..385853242 100755 --- a/apstools/beamtime/apsbss.py +++ b/apstools/beamtime/apsbss.py @@ -50,7 +50,6 @@ ~main """ -import argparse import datetime import dm # APS data management library import os @@ -471,11 +470,21 @@ def trim(text, length=40): def get_options(): """Handle command line arguments.""" + import argparse + from apstools._version import get_versions + version = get_versions()['version'] + parser = argparse.ArgumentParser( prog=os.path.split(sys.argv[0])[-1], description=__doc__.strip().splitlines()[0], ) + parser.add_argument('-v', + '--version', + action='version', + help='print version number and exit', + version=version) + subcommand = parser.add_subparsers(dest='subcommand', title='subcommand') subcommand.add_parser('beamlines', help="print list of beamlines") From fb96acad33577f020a610dc6833da3fa19826b03 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Sat, 11 Jul 2020 15:24:20 -0500 Subject: [PATCH 45/47] MNT #333 --- apstools/beamtime/apsbss.py | 6 +++- docs/source/applications/apsbss.rst | 45 +++++++++++++++-------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py index 385853242..0cf66cab1 100755 --- a/apstools/beamtime/apsbss.py +++ b/apstools/beamtime/apsbss.py @@ -68,6 +68,8 @@ api_bss = dm.BssApsDbApi(DM_APS_DB_WEB_SERVICE_URL) api_esaf = dm.EsafApsDbApi(DM_APS_DB_WEB_SERVICE_URL) +parser = None + _cache_ = {} @@ -470,8 +472,10 @@ def trim(text, length=40): def get_options(): """Handle command line arguments.""" + global parser import argparse from apstools._version import get_versions + version = get_versions()['version'] parser = argparse.ArgumentParser( @@ -699,7 +703,7 @@ def main(): cmd_report(args) else: - print("subcommand not recognized. Use -h for usage information.") + parser.print_usage() if __name__ == "__main__": diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index dc00d1475..005100169 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -381,27 +381,28 @@ Example - ``apsbss`` command line Before using the command-line interface, find out what the *apsbss* application expects:: - $ apsbss -h - usage: apsbss.py [-h] - {beamlines,current,cycles,esaf,proposal,clear,setup,update} - ... - - Retrieve specific records from the APS Proposal and ESAF databases - - optional arguments: - -h, --help show this help message and exit - - subcommand: - {beamlines,current,cycles,esaf,proposal,clear,setup,update} - beamlines print list of beamlines - current print current ESAF(s) and proposal(s) - cycles print APS cycle names - esaf print specific ESAF - proposal print specific proposal - clear EPICS PVs: clear - setup EPICS PVs: setup - update EPICS PVs: update from BSS - report EPICS PVs: report what is in the PVs + $ apsbss -h + usage: apsbss [-h] [-v] + {beamlines,current,cycles,esaf,proposal,clear,setup,update,report} + ... + + Retrieve specific records from the APS Proposal and ESAF databases. + + optional arguments: + -h, --help show this help message and exit + -v, --version print version number and exit + + subcommand: + {beamlines,current,cycles,esaf,proposal,clear,setup,update,report} + beamlines print list of beamlines + current print current ESAF(s) and proposal(s) + cycles print APS run cycle names + esaf print specific ESAF + proposal print specific proposal + clear EPICS PVs: clear + setup EPICS PVs: setup + update EPICS PVs: update from BSS + report EPICS PVs: report what is in the PVs See :ref:`beamtime_source_docs` for the source code documentation of each of these subcommands. @@ -419,7 +420,7 @@ using either MEDM (``apsbss.adl``) or caQtDM (``apsbss.ui``). Start caQtDM with this command: ``caQtDM -macro "P=9id:bss:" apsbss.ui &`` -Start MEDM with this command: ``MEDM -x -macro "P=9id:bss:" apsbss.ui &`` +Start MEDM with this command: ``medm -x -macro "P=9id:bss:" apsbss.ui &`` IOC Management ++++++++++++++ From c739f8377fb663accf7f7d76885f1a1d6998a56b Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Sat, 11 Jul 2020 15:26:38 -0500 Subject: [PATCH 46/47] TST #333 Codacy --- apstools/beamtime/apsbss.py | 2 +- docs/source/applications/apsbss.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py index 0cf66cab1..a263eebf9 100755 --- a/apstools/beamtime/apsbss.py +++ b/apstools/beamtime/apsbss.py @@ -484,7 +484,7 @@ def get_options(): ) parser.add_argument('-v', - '--version', + '--version', action='version', help='print version number and exit', version=version) diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst index 005100169..a1e55ed13 100644 --- a/docs/source/applications/apsbss.rst +++ b/docs/source/applications/apsbss.rst @@ -171,7 +171,7 @@ To print the full report (including start and end of each cycle):: $ apsbss cycles --full ====== =================== =================== - cycle start end + cycle start end ====== =================== =================== 2020-2 2020-06-09 07:00:00 2020-10-01 07:00:00 2020-1 2020-01-28 08:00:00 2020-06-09 07:00:00 From e3e32055f205b66c97b8e8d665cee2633a504e60 Mon Sep 17 00:00:00 2001 From: Pete Jemian Date: Sat, 11 Jul 2020 15:59:32 -0500 Subject: [PATCH 47/47] MNT #343 --- .travis.yml | 2 +- conda-recipe/meta.yaml | 3 ++- environment.yml | 40 ++++++++++++++++++++++++---------------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index d862e038c..4c289ada5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ before_script: - export PATH=$HOME/miniconda/bin:$PATH - export PY_ENV=travis-$TRAVIS_PYTHON_VERSION - export MY_CONDA_CHANNELS="-c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag" - - export MY_CONDA_PACKAGES="aps-dm-api bluesky>=1.6.2 coverage databroker docopt epics-base numpy ophyd>=1.5.1 pyepics>=3.4.2 pandas requests xlrd" + - export MY_CONDA_PACKAGES="aps-dm-api bluesky>=1.6.2 coverage databroker>=1.0.6 docopt epics-base numpy ophyd>=1.5.1 pyepics>=3.4.2 pandas requests xlrd" - conda create -y -n $PY_ENV $MY_CONDA_CHANNELS python=$TRAVIS_PYTHON_VERSION $MY_CONDA_PACKAGES - source activate $PY_ENV - which pip diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index c5e87440a..d4e579c52 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -26,6 +26,7 @@ build: script: python -m pip install --no-deps --ignore-installed . noarch: python entry_points: + - apsbss = apstools.beamtime.apsbss:main - apstools_plan_catalog = apstools.examples:main - bluesky_snapshot = apstools.snapshot:snapshot_cli - bluesky_snapshot_viewer = apstools.snapshot:snapshot_gui @@ -35,7 +36,7 @@ requirements: - python - pip run: - - python + - python>=3.6.0 - aps-dm-api - bluesky>=1.6.2 - databroker>=1.0.6 diff --git a/environment.yml b/environment.yml index df599f443..9ea47f937 100644 --- a/environment.yml +++ b/environment.yml @@ -1,19 +1,27 @@ name: apstoolsdoc channels: -- nsls2forge -- defaults + - defaults + - conda-forge + - aps-anl-tag + - nsls2forge dependencies: -- python=3.6 -- qt=5 -- pyqt=5 -- sphinx -- sphinx_rtd_theme -- bluesky -- ophyd -- prettytable -- pip: - - ipython-genutils==0.2.0 - - prompt-toolkit==1.0.15 - - sphinx-rtd-theme - - super-state-machine - - versioneer + - python>=3.6.0 + - aps-dm-api + - bluesky>=1.6.2 + - databroker>=1.0.6 + - h5py + - ophyd>=1.5.1 + - pip + - pyEpics>=3.4.2 + - pyqt=5 + - pyRestTable + - qt=5 + - spec2nexus>=2021.1.7 + - sphinx + - xlrd + - pip: + - ipython-genutils==0.2.0 + - prompt-toolkit==1.0.15 + - sphinx-rtd-theme + - super-state-machine + - versioneer