#!/usr/bin/env python # xia2ccp4i.py # Copyright (C) (2013) STFC Rutherford Appleton Laboratory, UK. # # Author: David Waterman. # # This code is distributed under the BSD license, a copy of which is # included in the root directory of this package. # """ xia2ccp4i.py: A demonstration for a user friendly way to link data processing with downstream analysis. This script finds data processed by xia2, creates a standalone CCP4 project and starts ccp4i with it. """ import sys import os import shutil import subprocess import re import datetime import string import getopt __author__ = "David Waterman" __credits__ = "Andrey Lebedev" __email__ = "david.waterman@stfc.ac.uk" class ProjectMaker(object): """A function object to make a new CCP4 project directory""" _database_template = string.Template( '''#CCP4I VERSION CCP4Interface 2.2.0 #CCP4I SCRIPT DEF CCP4_Project_Database #CCP4I $TIME #CCP4I USER $USER #CCP4I PROJECT $PROJ_NAME $PROJ_DIR NJOBS 0 ''') _directories_template = string.Template( '''#CCP4I VERSION CCP4Interface 2.2.0 #CCP4I SCRIPT DEF directories #CCP4I $TIME #CCP4I USER $USER DEF_DIR_ALIAS,0 _text "" DEF_DIR_ALIAS,1 _text TEMPORARY DEF_DIR_PATH,0 _dir "" DEF_DIR_PATH,1 _dir /tmp/$USER LOG_DIRECTORY _log_directory PROJECT N_DEF_DIRS _positiveint1 1 N_PROJECTS _positiveint1 1 PROJECT_ALIAS,0 _text "" PROJECT_ALIAS,1 _text $PROJ_NAME PROJECT_DATABASE _dir "" PROJECT_DB,0 _dir "" PROJECT_DB,1 _dir $PROJ_DIR/CCP4_DATABASE PROJECT_MENU _menu "" PROJECT_PATH,1 _dir $PROJ_DIR PROJECT_PATH,0 _dir "" ''') _status_template = string.Template( '''#CCP4I VERSION CCP4Interface 2.2.0 #CCP4I SCRIPT DEF status #CCP4I $TIME #CCP4I USER $USER CURRENT_MODULE _text automation CURRENT_PROJECT _default_project $PROJ_NAME MG_CURRENT_PROJECT _default_project $PROJ_NAME ''') _reason_for_failure = None def __call__(self, project_name, directory="."): """Test whether making a project in the specified directory is possible, and if so make it and return True. If not possible, return False and cache the reason for failure""" proj_dir = os.path.realpath(directory) db_dir = os.path.join(proj_dir, "CCP4_DATABASE") proj_name = project_name.strip().replace(" ", "_") # Does CCP4_PROJECT already exist here? if os.path.exists(db_dir): self._reason_for_failure = ("ERROR: Directory '%s' already " "contains CCP4_DATABASE") % directory return False # Is the project_name valid? if not re.match("[_\-a-zA-Z0-9]+$", proj_name): self._reason_for_failure = "ERROR: " + proj_name + \ " is not a valid CCP4 Project name" return False # Go ahead and make CCP4_DATABASE if we have write access try: os.mkdir(db_dir) except OSError, err: self._reason_for_failure = ("ERROR: Problem encountered while " "creating CCP4_DATABASE in " + proj_dir) return False # Create strings for .def files mapping = {'TIME':datetime.datetime.now().strftime("DATE %d %b %Y %T"), 'USER':os.environ["USER"], 'PROJ_NAME':proj_name, 'PROJ_DIR':proj_dir} database_txt = self._database_template.substitute(mapping) directories_txt = self._directories_template.substitute(mapping) status_txt = self._status_template.substitute(mapping) # Now create files in the project directory try: database = open(os.path.join(db_dir, "database.def"), 'w') except OSError as err: self._reason_for_failure = ("ERROR: Problem encountered while " "writing database.def") return False database.write(database_txt) database.close() try: directories = open(os.path.join(db_dir, "directories.def"), 'w') except OSError as err: self._reason_for_failure = ("ERROR: Problem encountered while " "writing directories.def") return False directories.write(directories_txt) directories.close() try: status = open(os.path.join(db_dir, "status.def"), 'w') except OSError as err: self._reason_for_failure = ("ERROR: Problem encountered while " "writing status.def") return False status.write(status_txt) status.close() # All jobs done, return True return True def reason_for_failure(self): return self._reason_for_failure class Xia2Info(object): """Check a xia2 processing directory exists and contains what is needed. Provide access to those things.""" def __init__(self, xia2dir): self._problems = [] self._proj_name = None self._data_files = [] # check the processing directory exists if not os.path.exists(xia2dir): self._problems.append("Directory %s is not " "accessible" % xia2dir) return # check we can read the xia2-summary.dat file try: f = open(os.path.join(xia2dir, "xia2-summary.dat")) summary = f.readlines() f.close() except IOError as err: self._problems.append("Unable to read xia2-summary.dat " "inside %s" % xia2dir) return # attempt to extract the project name for line in summary: if line.startswith("Project:"): self._proj_name = (line.partition("Project:")[2]).strip() if not self._proj_name: self._problems.append("Unable to find a project name in " "xia2-summary.dat") else: # check the project name is sensible if self._proj_name.lower() in ["automatic", "default"]: self._problems.append("The project name '%s' read from " "xia2-summary.dat is probably not " "desirable" % self._proj_name) self._proj_name = None # make a list of the data files datadir = os.path.join(xia2dir, "DataFiles") if not os.path.exists(datadir): self._problems.append("Directory %s is not accessible" % datadir) return self._data_files = [os.path.join(datadir, e) \ for e in os.listdir(datadir)] return def list_problems(self): return self._problems def get_project_name(self): return self._proj_name def get_data_files(self): if len(self._data_files) == 0: return None else: return self._data_files def usage(myname="xia2ccp4i.py"): """return a usage string""" msg = """%s [options] Options: -h, --help show this help message and exit -p FOO, --project=FOO override the project name taken from xia2, and call the CCP4 project FOO -x BAR, --xia2dir=BAR specify the location of the top-level xia2 processing directory (omission implies BAR=".") Extract project name and data from a xia2 processing run, create a standalone ccp4i project and start ccp4i with that project. Please note that this standalone ccp4i project will not be in the global projects database of ccp4i. However it may be added later using the 'Directories&ProjectDir' button in ccp4i. The list of jobs performed in the standalone project will remain intact after the import. """ return msg % myname if __name__ == '__main__': # Use getopt rather than argparse for python < 2.7 compatibility try: opts, args = getopt.getopt(sys.argv[1:], "hp:x:", ["help", "project=", "xia2dir="]) except getopt.GetoptError as err: # Exit with a useful message: print str(err) sys.exit(usage()) proj_name = None xia2dir = "." for o, a in opts: if o in ("-h", "--help"): print usage() sys.exit() elif o in ("-p", "--project"): proj_name = a elif o in ("-x", "--xia2dir"): xia2dir = os.path.realpath(a) # Check the xia2 processing output process_info = Xia2Info(xia2dir) # If proj_name has not been specified, attempt to get it from the xia2 info if proj_name is None: proj_name = process_info.get_project_name() if proj_name is None: msg = "\n".join(process_info.list_problems()) msg += ("\nNothing has been done. To force a project name, re-run " "using the --project=FOO option") sys.exit(msg) # Copy the data into a new CCP4 Project directory proj_dir = os.path.join(xia2dir, "CCP4Project") try: os.mkdir(proj_dir) except OSError as err: msg = "Unable to create a CCP4Project directory. Does it already exist?" sys.exit(msg) try: for f in process_info.get_data_files(): shutil.copy2(f, proj_dir) except IOError as err: msg = "Unable to copy data files to " + proj_dir sys.exit(msg) # Make the project, or exit with a failure message project_maker = ProjectMaker() if not project_maker(proj_name, directory=proj_dir): sys.exit(project_maker.reason_for_failure()) # Now start ccp4i and exit with its call value db_dir = os.path.join(proj_dir, "CCP4_DATABASE") try: val = subprocess.call("ccp4i", cwd=db_dir) sys.exit(val) except OSError as err: print ("It seems that ccp4i is not currently available. To start ccp4i " "with the isolated project %s, please set up the CCP4 " "environment and type:\n\n" "cd %s\n" "ccp4i") % (proj_name, db_dir)