diff --git a/doc/RELEASE_NOTES.txt b/doc/RELEASE_NOTES.txt index 175db19f5ad3c9d63ea8abecc3b1cc3eb66dd3e4..c48e1db6479028998f64b83b8626de08b15ce9d8 100644 --- a/doc/RELEASE_NOTES.txt +++ b/doc/RELEASE_NOTES.txt @@ -1,3 +1,17 @@ +Release 1.1 () +============================= + +- Added the following functionality for managing DAQs: + - Max. run time specification causes DAQ to be stopped automatically + - Target directory specification causes files to be uploaded into + a specific directory relative to experiment root path + - Upload data/target directory on exit specify upload of the given + directory automatically after DAQ completes +- Added the following functionality for managing uploads: + - Target directory specification causes files to be uploaded into + a specific directory relative to experiment root path +- Introduced sphinx as python API documentation framework + Release 1.0 (01/31/2017) ============================= diff --git a/doc/sphinx/.svnignore b/doc/sphinx/.svnignore new file mode 100644 index 0000000000000000000000000000000000000000..9ef96044faba86acfbf59bd0deb4545625df837f --- /dev/null +++ b/doc/sphinx/.svnignore @@ -0,0 +1,2 @@ +build + diff --git a/doc/sphinx/source/dm.aps_bss.api.rst b/doc/sphinx/source/dm.aps_bss.api.rst new file mode 100644 index 0000000000000000000000000000000000000000..00946b37a6dbeaf1c387c384bfb6c322e01a6941 --- /dev/null +++ b/doc/sphinx/source/dm.aps_bss.api.rst @@ -0,0 +1,12 @@ +.. automodule:: dm.aps_bss.api + +.. currentmodule:: dm.aps_bss.api + +ApsBssApi +--------- + +.. autoclass:: dm.aps_bss.api.apsBssApi.ApsBssApi() + :show-inheritance: + :members: __init__, listRuns, getCurrentRun, listBeamlineProposals, getBeamlineProposal + + diff --git a/doc/sphinx/source/index.rst b/doc/sphinx/source/index.rst index d9d39b903dc1dcbabd6c744a2e340a89a62b73ab..d7220f53355315c8812f0e9bc38bea52a622de8b 100644 --- a/doc/sphinx/source/index.rst +++ b/doc/sphinx/source/index.rst @@ -13,6 +13,7 @@ The `dm` package contains python APIs for accessing Data Management services. :caption: Contents: dm.daq_web_service.api + dm.aps_bss.api Indices and tables ================== diff --git a/src/python/dm/aps_bss/__init__.py b/src/python/dm/aps_bss/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/python/dm/aps_bss/api/__init__.py b/src/python/dm/aps_bss/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/python/dm/aps_bss/api/apsBssApi.py b/src/python/dm/aps_bss/api/apsBssApi.py new file mode 100755 index 0000000000000000000000000000000000000000..7a9f8e63ca84c287975ba9cdc26e622bf489490c --- /dev/null +++ b/src/python/dm/aps_bss/api/apsBssApi.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +import os +from dm.common.exceptions.configurationError import ConfigurationError +from dm.common.api.dmApi import DmApi +from dm.aps_bss.impl.bssClient import BssClient + +class ApsBssApi(DmApi): + ''' Data Management API for APS Beamline Scheduling System. ''' + def __init__(self, beamlineName=None, username=None, password=None): + ''' + Constructor. + + :param beamlineName: beamline name (if not provided, environment variable DM_BEAMLINE_NAME must be set) + :type beamlineName: str + + :param username: BSS username (if not provided, environment variable DM_BSS_LOGIN_FILE must be set) + :type username: str + + :param password: BSS password (if not provided, environment variable DM_BSS_LOGIN_FILE must be set) + :type password: str + + :raises ConfigurationError: in case beamline name or username/password are not provided, and corresponding environment variables are not set + + Note that environment variable DM_BSS_LOGIN_FILE should contain + path to a file containing BSS username and password as a single line in + the '<username>|<password>' form. + + >>> api = ApsBssApi(beamlineName='1-ID-B,C,E', username='dm', password='XYZ') + ''' + DmApi.__init__(self) + if not username or not password: + loginFile = os.environ.get('DM_BSS_LOGIN_FILE') + if loginFile: + try: + # Assume form <username>|<password> + tokenList = open(loginFile).readline().split('|') + if len(tokenList) == 2: + username = tokenList[0].strip() + password = tokenList[1].strip() + except Exception, ex: + raise ConfigurationError('Could not determine username/password from BSS login file: %s' % str(ex)) + if not username or not password: + raise ConfigurationError('Username and/or password were not provided, and DM_BSS_LOGIN_FILE is not set.') + + if not beamlineName: + beamlineName = os.environ.get('DM_BEAMLINE_NAME') + if not beamlineName: + raise ConfigurationError('Beamline name was not provided, and DM_BEAMLINE_NAME is not set.') + + self.beamlineName = beamlineName + self.bssClient = BssClient(username, password) + + @DmApi.execute2 + def listRuns(self): + ''' + List all available runs. + + :returns: list of RunInfo objects + + :raises DmException: for any error + + >> runs = api.listRuns() + >> for run in runs: + >> print run['name'] + ''' + return self.bssClient.listRuns() + + @DmApi.execute2 + def getCurrentRun(self): + ''' + Find current run. + + :returns: RunInfo object + + :raises DmException: for any error + + >> run = api.getCurrentRun() + ''' + return self.bssClient.getCurrentRun() + + @DmApi.execute2 + def listBeamlineProposals(self, runName=None): + ''' + List beamline proposals for a given run. + + :param runName: run name (if not provided, current run name will be used) + :type runName: str + + :returns: list of ProposalInfo objects + + :raises DmException: for any error + + >> proposals = api.listBeamlineProposals() + >> for proposal in proposals: + >> print proposal['title'] + ''' + if not runName: + runName = self.getCurrentRun()['name'] + return self.bssClient.listBeamlineProposals(self.beamlineName, runName) + + @DmApi.execute2 + def getBeamlineProposal(self, proposalId, runName=None): + ''' + Get beamline proposal with a given id. + + :param porposalId: proposal id + :type runName: str + + :param runName: run name (if not provided, current run name will be used) + :type runName: str + + :returns: ProposalInfo object + + :raises ObjectNotFound: if proposal with the given id does not exist + + :raises DmException: for any other error + + >> proposal = api.getBeamlineProposal(42096) + >> for experimenter in proposal['experimenters']: + >> print experimenter['badge'], experimenter['lastName'] + ''' + if not runName: + runName = self.getCurrentRun()['name'] + return self.bssClient.getBeamlineProposal(proposalId, self.beamlineName, runName) + +####################################################################### +# Testing. +if __name__ == '__main__': + api = ApsBssApi('1-ID-B,C,E') + print 'RUNS', api.listRuns() + print 'CURRRENT RUN: ', api.getCurrentRun() + print 'PROPOSALS: ', api.listBeamlineProposals() + print + print 'FIND PROPOSAL: ', api.getBeamlineProposal(42096) + print 'FIND PROPOSAL: ', api.getBeamlineProposal(52096) + + diff --git a/src/python/dm/aps_bss/impl/__init__.py b/src/python/dm/aps_bss/impl/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/python/dm/aps_bss/impl/bssClient.py b/src/python/dm/aps_bss/impl/bssClient.py new file mode 100644 index 0000000000000000000000000000000000000000..a5224e4c377b5ac3888f1581acace377206474bd --- /dev/null +++ b/src/python/dm/aps_bss/impl/bssClient.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python + +import datetime + +from suds.wsse import Security +from suds.wsse import UsernameToken +from suds.client import Client + +from dm.common.exceptions.objectNotFound import ObjectNotFound +from dm.common.objects.runInfo import RunInfo +from dm.common.objects.beamlineInfo import BeamlineInfo +from dm.common.objects.proposalInfo import ProposalInfo +from dm.common.objects.apsUserInfo import ApsUserInfo +from dm.common.exceptions.dmException import DmException +from dm.common.utility.loggingManager import LoggingManager + +class BssClient: + + WSDL_URL = 'https://schedule.aps.anl.gov/beamschedds/springws' + CACHE_DURATION_IN_SECONDS = 10 + + def __init__(self, username, password): + self.logger = LoggingManager.getInstance().getLogger(self.__class__.__name__) + self.__configure(username, password) + + def __configure(self, username, password): + beamlineScheduleServiceUrl = self.WSDL_URL + '/beamlineScheduleService/beamlineScheduleWebService.wsdl' + runScheduleServiceUrl = self.WSDL_URL + '/runScheduleService/runScheduleWebService.wsdl' + + try: + self.runScheduleServiceClient = Client(runScheduleServiceUrl) + self.runScheduleServiceClient.options.cache.setduration(seconds=self.CACHE_DURATION_IN_SECONDS) + self.setSoapHeader(self.runScheduleServiceClient, username, password) + + self.beamlineScheduleServiceClient = Client(beamlineScheduleServiceUrl) + self.beamlineScheduleServiceClient.options.cache.setduration(seconds=self.CACHE_DURATION_IN_SECONDS) + self.setSoapHeader(self.beamlineScheduleServiceClient, username, password) + except Exception, ex: + self.logger.error('Cannot open BSS connection: %s' % str(ex)) + raise DmException(exception=ex) + + @classmethod + def setSoapHeader(cls, client, username, password): + security = Security() + token = UsernameToken(username, password) + token.setcreated() + security.tokens.append(token) + client.set_options(wsse=security) + + def listRuns(self): + ''' Return list of all runs. ''' + result = self.runScheduleServiceClient.service.findAllRuns() + runArray = result.run + runs = [] + for run in runArray: + runs.append(RunInfo({'name' : run.runName, 'startTime' : run.startTime, 'endTime' : run.endTime})) + return runs + + def listRunsBetweenDates(self, startDate, endDate): + ''' Find list of runs between given startDate and endDate. ''' + result = self.runScheduleServiceClient.service.findAllRuns() + runArray = result.run + runs = [] + for run in runArray: + if run.startTime >= startDate and run.endTime <= endDate: + runs.append(RunInfo({'name' : run.runName, 'startTime' : run.startTime, 'endTime' : run.endTime})) + return runs + + def getRunForDates(self, startDate, endDate): + ''' Find run that spans given startDate and endDate. ''' + result = self.runScheduleServiceClient.service.findAllRuns() + runArray = result.run + for run in runArray: + if startDate >= run.startTime and endDate <= run.endTime: + return RunInfo({'name' : run.runName, 'startTime' : run.startTime, 'endTime' : run.endTime}) + raise ObjectNotFound('No run found.') + + def getCurrentRun(self): + ''' Find current run. ''' + now = datetime.datetime.now() + return self.getRunForDates(now, now) + + def listBeamlines(self): + ''' Find list of all beamlines. ''' + result = self.beamlineScheduleServiceClient.service.findAllBeamlines() + beamlineArray = result.beamline + beamlines = [] + for beamline in beamlineArray: + beamlines.append(BeamlineInfo({'name' : beamline.beamlineName, 'id' : beamline.id})) + return beamlines + + def createProposalInfo(self, proposal): + experimenterArray = proposal.experimenters.experimenter + experimenters = [] + for experimenter in experimenterArray: + user = ApsUserInfo({ + 'id' : experimenter.id, + 'badge' : experimenter.badge, + 'email' : experimenter.email, + 'firstName' : experimenter.firstName, + 'instId' : experimenter.instId, + 'institution' : experimenter.institution, + 'lastName' : experimenter.lastName + }) + if hasattr(experimenter, 'piFlag'): + user['piFlag'] = experimenter.piFlag + + experimenters.append(user) + proposalInfo = ProposalInfo({ + 'title' : proposal.proposalTitle, + 'id' : proposal.id, + 'experimenters' : experimenters + }) + return proposalInfo + + def listBeamlineProposals(self, beamlineName, runName): + ''' Find beamline schedule for a given beamlineName and runName. ''' + schedule = self.beamlineScheduleServiceClient.service.findBeamlineSchedule(beamlineName, runName) + activitiesArray = schedule.activities.activity + proposals = [] + for activity in activitiesArray: + if hasattr(activity, 'beamtimeRequest'): + proposal = activity.beamtimeRequest.proposal + proposals.append(self.createProposalInfo(proposal)) + return proposals + + def getBeamlineProposal(self, proposalId, beamlineName, runName): + ''' Find proposal with a given id, beamlineName and runName. ''' + schedule = self.beamlineScheduleServiceClient.service.findBeamlineSchedule(beamlineName, runName) + activitiesArray = schedule.activities.activity + proposals = [] + for activity in activitiesArray: + if hasattr(activity, 'beamtimeRequest'): + proposal = activity.beamtimeRequest.proposal + if proposal.id == proposalId: + return self.createProposalInfo(proposal) + raise ObjectNotFound('Proposal with id %s does not exist (beamline: %s; run: %s).' % (proposalId, beamlineName, runName)) + +if __name__ == '__main__': + bss = BssClient('DMADMIN', 'A2Fxew@11:76am') + now = datetime.datetime.now() + days = datetime.timedelta(days=180) + before = now - days + print before, now + runNames = bss.getRunsBetweenDates(before, now) + print 'RUNS: ', runNames + print 'CURRENT RUN: ', bss.getCurrentRun() + print bss.listBeamlines() + print bss.listBeamlineProposals('1-ID-B,C,E', '2017-1') + print + print bss.getBeamlineProposal(51190, '1-ID-B,C,E', '2017-1') + print + print bss.getBeamlineProposal(48258, '1-ID-B,C,E', '2017-1') + + diff --git a/src/python/dm/daq_web_service/api/experimentDaqApi.py b/src/python/dm/daq_web_service/api/experimentDaqApi.py index f0e0b00d36ce3d295031fecde34bb1064c3e200e..db32234b4cf0a32ceb1e7846c05c67231ed975ae 100755 --- a/src/python/dm/daq_web_service/api/experimentDaqApi.py +++ b/src/python/dm/daq_web_service/api/experimentDaqApi.py @@ -15,7 +15,7 @@ from daqRestApi import DaqRestApi class ExperimentDaqApi(DaqRestApi): ''' - This class is used to access experiment interface provided by the + Data Management API for accessing experiment interface provided by the DAQ service on a DM experiment station. '''