Implement a client side config file generator.
[htsworkflow.git] / htsworkflow / pipelines / retrieve_config.py
1 #!/usr/bin/env python
2
3 from ConfigParser import RawConfigParser
4 import logging
5 from optparse import OptionParser, IndentedHelpFormatter
6 import os
7 import sys
8 import urllib
9 import urllib2
10
11 try:
12     import json
13 except ImportError, e:
14     import simplejson as json
15
16 from htsworkflow.frontend.auth import apidata
17 from htsworkflow.util.url import normalize_url
18 from htsworkflow.pipelines.genome_mapper import getAvailableGenomes
19 from htsworkflow.pipelines.genome_mapper import constructMapperDict
20
21 __docformat__ = "restructredtext en"
22
23 CONFIG_SYSTEM = '/etc/htsworkflow.ini'
24 CONFIG_USER = os.path.expanduser('~/.htsworkflow.ini')
25 GERALD_CONFIG_SECTION = 'gerald_config'
26
27 #Disable or enable commandline arg parsing; disabled by default.
28 DISABLE_CMDLINE = True
29
30 LANE_LIST = ['1','2','3','4','5','6','7','8']
31
32 class FlowCellNotFound(Exception): pass
33 class WebError404(Exception): pass
34
35 def retrieve_flowcell_info(base_host_url, flowcell):
36     """
37     Return a dictionary describing a 
38     """
39     url = base_host_url + '/experiments/config/%s/json' % (flowcell)
40   
41     try:
42         apipayload = urllib.urlencode(apidata)
43         web = urllib2.urlopen(url, apipayload)
44     except urllib2.URLError, e:
45         errmsg = 'URLError: %d %s' % (e.code, e.msg)
46         logging.error(errmsg)
47         logging.error('opened %s' % (url,))
48         raise IOError(errmsg)
49     
50     contents = web.read()
51     headers = web.info()
52     
53     if web.getcode() == 403:
54         msg = "403 - Forbbidden, probably need api key"
55         raise FlowCellNotFound(msg)
56     
57     if web.getcode() == 404:
58         msg = "404 - Not Found: Flowcell (%s); base_host_url (%s);\n full url(%s)\n " \
59               "Did you get right port #?" % (flowcell, base_host_url, url)
60         raise FlowCellNotFound(msg)
61   
62     if len(contents) == 0:
63         msg = "No information for flowcell (%s) returned; full url(%s)" % (flowcell, url)
64         raise FlowCellNotFound(msg)
65
66     data = json.loads(contents)
67     return data
68
69 def is_sequencing(lane_info):
70     """
71     Determine if we are just sequencing and not doing any follow-up analysis
72     """
73     if lane_info['experiment_type'] in ('De Novo','Whole Genome'):
74         return True
75     else:
76         return False
77     
78 def group_lane_parameters(flowcell_info):
79     """
80     goup lanes that can share GERALD configuration blocks.
81
82     (The same species, read length, and eland vs sequencing)
83     """
84     lane_groups = {}
85     for lane_number, lane_info in flowcell_info['lane_set'].items():
86         index = (lane_info['read_length'],
87                  lane_info['library_species'],
88                  is_sequencing(lane_info))
89         lane_groups.setdefault(index, []).append(lane_number)
90     return lane_groups
91
92 def format_gerald_header(flowcell_info):
93     """
94     Generate comment describing the contents of the flowcell
95     """
96     # I'm using '\n# ' to join the lines together, that doesn't include the
97     # first element so i needed to put the # in manually
98     config = ['# FLOWCELL: %s' % (flowcell_info['flowcell_id'])]
99     config += ['']
100     config += ['CONTROL-LANE: %s' % (flowcell_info['control_lane'],)]
101     config += ['']
102     config += ['Flowcell Notes:']
103     config.extend(flowcell_info['notes'].split('\r\n'))
104     config += ['']
105     for lane_number in LANE_LIST:
106         lane_info = flowcell_info['lane_set'][lane_number]
107         config += ['Lane%s: %s | %s' % (lane_number, lane_info['library_id'],
108                                         lane_info['library_name'])]
109     config += ['']
110     return "\n# ".join(config)
111
112 def format_gerald_config(options, flowcell_info, genome_map):
113     """
114     Generate a GERALD config file
115     """
116     # so we can add nothing or _pair if we're a paired end run
117     run_type_suffix = { False: "", True: "_pair" }
118
119     # it's convienent to have helpful information describing the flowcell
120     # in the config file... things like which lane is which library.
121     config = [format_gerald_header(flowcell_info)]
122
123     analysis_suffix = run_type_suffix[flowcell_info['paired_end']]
124     lane_groups = group_lane_parameters(flowcell_info)
125     for lane_index, lane_numbers in lane_groups.items():
126         # lane_index is return value of group_lane_parameters
127         read_length, species, is_sequencing = lane_index
128         lane_numbers.sort()
129         lane_prefix = u"".join(lane_numbers)
130         
131         if not is_sequencing:
132             config += ['%s:ANALYSIS eland%s' % (lane_prefix, analysis_suffix)]
133         else:
134             config += ['%s:ANALYSIS sequence%s' % (lane_prefix, analysis_suffix)]
135         #config += ['%s:READ_LENGTH %s' % ( lane_prefix, read_length ) ]
136         config += ['%s:USE_BASES Y%s' % ( lane_prefix, read_length ) ]
137         species_path = genome_map.get(species, "Unknown")
138         config += ['%s:ELAND_GENOME %s' % (lane_prefix, species_path) ]
139
140     # add in option for running script after 
141     if options.post_run is not None:
142         post_run = options.post_run  % {'runfolder': options.runfolder}
143         config += ['POST_RUN_COMMAND %s' % (post_run,) ]
144         
145     config += [''] # force trailing newline
146     
147     return "\n".join(config)
148               
149 class DummyOptions:
150   """
151   Used when command line parsing is disabled; default
152   """
153   def __init__(self):
154     self.url = None
155     self.output_filepath = None
156     self.flowcell = None
157     self.genome_dir = None
158
159 class PreformattedDescriptionFormatter(IndentedHelpFormatter):
160   
161   #def format_description(self, description):
162   #  
163   #  if description:
164   #      return description + "\n"
165   #  else:
166   #     return ""
167       
168   def format_epilog(self, epilog):
169     """
170     It was removing my preformated epilog, so this should override
171     that behavior! Muhahaha!
172     """
173     if epilog:
174         return "\n" + epilog + "\n"
175     else:
176         return ""
177
178
179 def constructOptionParser():
180     """
181     returns a pre-setup optparser
182     """
183     parser = OptionParser(formatter=PreformattedDescriptionFormatter())
184
185     parser.set_description('Retrieves eland config file from hts_frontend web frontend.')
186   
187     parser.epilog = """
188 Config File:
189   * %s (System wide)
190   * %s (User specific; overrides system)
191   * command line overrides all config file options
192   
193   Example Config File:
194   
195     [%s]
196     config_host=http://somewhere.domain:port
197     genome_dir=/path to search for genomes
198     
199 """ % (CONFIG_SYSTEM, CONFIG_USER, GERALD_CONFIG_SECTION)
200   
201     #Special formatter for allowing preformatted description.
202     ##parser.format_epilog(PreformattedDescriptionFormatter())
203
204     parser.add_option("-u", "--url",
205                       action="store", type="string", dest="url")
206   
207     parser.add_option("-o", "--output-file",
208                       action="store", type="string", dest="output_filepath",
209                       help="config file destination. If runfolder is specified defaults "
210                            "to <runfolder>/config-auto.txt" )
211   
212     parser.add_option("-f", "--flowcell",
213                       action="store", type="string", dest="flowcell")
214
215     parser.add_option("-g", "--genome_dir",
216                       action="store", type="string", dest="genome_dir")
217
218     parser.add_option("-r", "--runfolder",
219                       action="store", type="string",
220                       help="specify runfolder for post_run command ")
221     
222     return parser
223     
224 def constructConfigParser():
225     """
226     returns a pre-setup config parser
227     """
228     parser = RawConfigParser()
229     parser.read([CONFIG_SYSTEM, CONFIG_USER])
230     if not parser.has_section(GERALD_CONFIG_SECTION):
231         parser.add_section(GERALD_CONFIG_SECTION)
232   
233     return parser
234
235
236 def getCombinedOptions(argv=None):
237     """
238     Returns optparse options after it has be updated with ConfigParser
239     config files and merged with parsed commandline options.
240
241     expects command line arguments to be passed in
242     """
243     cl_parser = constructOptionParser()
244     conf_parser = constructConfigParser()
245
246     if argv is None:
247         options = DummyOptions()
248     else:
249         options, args = cl_parser.parse_args(argv)
250         
251     if options.url is None:
252         if conf_parser.has_option(GERALD_CONFIG_SECTION, 'config_host'):
253             options.url = conf_parser.get(GERALD_CONFIG_SECTION, 'config_host')
254       
255     options.url = normalize_url(options.url)
256   
257     if options.genome_dir is None:
258         if conf_parser.has_option(GERALD_CONFIG_SECTION, 'genome_dir'):
259             options.genome_dir = conf_parser.get(GERALD_CONFIG_SECTION, 'genome_dir')
260
261     if conf_parser.has_option(GERALD_CONFIG_SECTION, 'post_run'):
262         options.post_run = conf_parser.get(GERALD_CONFIG_SECTION, 'post_run')
263     else:
264         options.post_run = None
265
266     if options.output_filepath is None:
267         if options.runfolder is not None:
268             options.output_filepath = os.path.join(options.runfolder, 'config-auto.txt')
269             
270     logging.info('USING OPTIONS:')
271     logging.info(u'     URL: %s' % (options.url,))
272     logging.info(u'     OUT: %s' % (options.output_filepath,))
273     logging.info(u'      FC: %s' % (options.flowcell,))
274     #logging.info(': %s' % (options.genome_dir,))
275     logging.info(u'post_run: %s' % ( unicode(options.post_run),))
276     
277     return options
278
279
280 def saveConfigFile(options):
281   """
282   retrieves the flowcell eland config file, give the base_host_url
283   (i.e. http://sub.domain.edu:port)
284   """
285   flowcell_info = retrieve_flowcell_info(options.url, options.flowcell)
286
287   available_genomes = getAvailableGenomes(options.genome_dir)
288   genome_map = constructMapperDict(available_genomes)
289
290   config = format_gerald_config(options, flowcell_info, genome_map)
291
292   if options.output_filepath is not None:
293       outstream = open(options.output_filepath, 'w')
294       logging.info('Writing config file to %s' % (options.output_filepath,))
295   else:
296       outstream = sys.stdout
297       
298   outstream.write(config)
299   
300
301
302