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