basepair 1.5__0-py3.8.egg

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. EGG-INFO/PKG-INFO +27 -0
  2. EGG-INFO/SOURCES.txt +38 -0
  3. EGG-INFO/dependency_links.txt +1 -0
  4. EGG-INFO/requires.txt +5 -0
  5. EGG-INFO/scripts/basepair +637 -0
  6. EGG-INFO/top_level.txt +1 -0
  7. EGG-INFO/zip-safe +1 -0
  8. basepair/__init__.py +32 -0
  9. basepair/__pycache__/__init__.cpython-38.pyc +0 -0
  10. basepair/__pycache__/api.cpython-38.pyc +0 -0
  11. basepair/api.py +1353 -0
  12. basepair/helpers/__init__.py +3 -0
  13. basepair/helpers/__pycache__/__init__.cpython-38.pyc +0 -0
  14. basepair/helpers/__pycache__/eprint.cpython-38.pyc +0 -0
  15. basepair/helpers/__pycache__/nice_print.cpython-38.pyc +0 -0
  16. basepair/helpers/__pycache__/set_filter.cpython-38.pyc +0 -0
  17. basepair/helpers/eprint.py +8 -0
  18. basepair/helpers/nice_print.py +170 -0
  19. basepair/helpers/set_filter.py +18 -0
  20. basepair/infra/__init__.py +0 -0
  21. basepair/infra/__pycache__/__init__.cpython-38.pyc +0 -0
  22. basepair/infra/configuration/__init__.py +1 -0
  23. basepair/infra/configuration/__pycache__/__init__.cpython-38.pyc +0 -0
  24. basepair/infra/configuration/__pycache__/parser.cpython-38.pyc +0 -0
  25. basepair/infra/configuration/parser.py +67 -0
  26. basepair/infra/webapp/__init__.py +12 -0
  27. basepair/infra/webapp/__pycache__/__init__.cpython-38.pyc +0 -0
  28. basepair/infra/webapp/__pycache__/abstract.cpython-38.pyc +0 -0
  29. basepair/infra/webapp/__pycache__/analysis.cpython-38.pyc +0 -0
  30. basepair/infra/webapp/__pycache__/file.cpython-38.pyc +0 -0
  31. basepair/infra/webapp/__pycache__/gene.cpython-38.pyc +0 -0
  32. basepair/infra/webapp/__pycache__/genome.cpython-38.pyc +0 -0
  33. basepair/infra/webapp/__pycache__/genome_file.cpython-38.pyc +0 -0
  34. basepair/infra/webapp/__pycache__/host.cpython-38.pyc +0 -0
  35. basepair/infra/webapp/__pycache__/module.cpython-38.pyc +0 -0
  36. basepair/infra/webapp/__pycache__/pipeline.cpython-38.pyc +0 -0
  37. basepair/infra/webapp/__pycache__/project.cpython-38.pyc +0 -0
  38. basepair/infra/webapp/__pycache__/sample.cpython-38.pyc +0 -0
  39. basepair/infra/webapp/__pycache__/upload.cpython-38.pyc +0 -0
  40. basepair/infra/webapp/__pycache__/user.cpython-38.pyc +0 -0
  41. basepair/infra/webapp/abstract.py +128 -0
  42. basepair/infra/webapp/analysis.py +10 -0
  43. basepair/infra/webapp/file.py +10 -0
  44. basepair/infra/webapp/gene.py +10 -0
  45. basepair/infra/webapp/genome.py +10 -0
  46. basepair/infra/webapp/genome_file.py +10 -0
  47. basepair/infra/webapp/host.py +10 -0
  48. basepair/infra/webapp/module.py +10 -0
  49. basepair/infra/webapp/pipeline.py +10 -0
  50. basepair/infra/webapp/project.py +10 -0
  51. basepair/infra/webapp/sample.py +10 -0
  52. basepair/infra/webapp/upload.py +10 -0
  53. basepair/infra/webapp/user.py +48 -0
  54. basepair/utils/__init__.py +0 -0
  55. basepair/utils/__pycache__/__init__.cpython-38.pyc +0 -0
  56. basepair/utils/__pycache__/colors.cpython-38.pyc +0 -0
  57. basepair/utils/__pycache__/monitor.cpython-38.pyc +0 -0
  58. basepair/utils/__pycache__/s3.cpython-38.pyc +0 -0
  59. basepair/utils/__pycache__/sqs.cpython-38.pyc +0 -0
  60. basepair/utils/__pycache__/swf.cpython-38.pyc +0 -0
  61. basepair/utils/colors.py +43 -0
  62. basepair/utils/monitor.py +38 -0
  63. basepair/utils/s3.py +77 -0
  64. basepair/utils/sqs.py +147 -0
  65. basepair/utils/swf.py +129 -0
basepair/api.py ADDED
@@ -0,0 +1,1353 @@
1
+ '''
2
+ Python interface to Basepair's REST API
3
+
4
+ Copyright [2015] - [2018] Basepair INC
5
+ All Rights Reserved.
6
+
7
+ NOTICE: All information contained herein is, and remains
8
+ the property of Basepair INC and its suppliers, if any.
9
+ The intellectual and technical concepts contained
10
+ herein are proprietary to Basepair INC
11
+ and its suppliers and may be covered by U.S. and Foreign Patents,
12
+ patents in process, and are protected by trade secret or copyright law.
13
+ Dissemination of this information or reproduction of this material
14
+ is strictly forbidden unless prior written permission is obtained
15
+ from Basepair INC.
16
+ '''
17
+
18
+ # pylint: disable=too-many-lines
19
+
20
+ from __future__ import division
21
+ from __future__ import print_function
22
+
23
+ import os
24
+ import sys
25
+ import json
26
+ import subprocess
27
+ from subprocess import CalledProcessError
28
+ import time
29
+ import datetime
30
+
31
+ # App imports
32
+ from .helpers import eprint, NicePrint, SetFilter
33
+ from .infra.configuration import Parser
34
+ from .infra.webapp import Analysis, File, Gene, Genome, GenomeFile, Host, Module, Pipeline, Project, Sample, Upload, User
35
+
36
+ class BpApi(): # pylint: disable=too-many-instance-attributes,too-many-public-methods
37
+ ''' A wrapper over the REST API for accessing the Basepair system
38
+
39
+ Use it thus:
40
+
41
+ > import basepair
42
+ > bp = basepair.connect()
43
+
44
+ conf is a JSON object with at least these keys:
45
+ {
46
+ "api": {
47
+ "host": "app.basepairtech.com",
48
+ "prefix": "/api/v2/",
49
+ "username": "utk",
50
+ "key": "xxx"
51
+ },
52
+ }
53
+
54
+ Some of the common methods are:
55
+
56
+ # create a sample
57
+ sample_id = bp.create_sample(info_dict)
58
+
59
+ # create an analysis
60
+ analysis_id = bp.create_analysis(workflow_id, sample_id)
61
+
62
+ # download results - bam, dedup file
63
+ filename = bp.get_file_by_tags(sample_id, ['bam', 'dedup'])
64
+
65
+ # delete sample
66
+ bp.delete_sample(sample_id)
67
+ '''
68
+
69
+ def __init__(self, conf=None, scratch='.', use_cache=False, user_cache_for_host_conf=False, verbose=None): # pylint: disable=too-many-arguments
70
+ self.verbose = verbose
71
+
72
+ if conf:
73
+ self.conf = conf
74
+ elif 'BP_CONFIG_FILE' in os.environ:
75
+ self.conf = json.load(open(os.environ['BP_CONFIG_FILE']))
76
+ else:
77
+ if 'BP_USERNAME' not in os.environ:
78
+ eprint('ERROR: BP_USERNAME not set in env')
79
+ sys.exit(1)
80
+ if 'BP_API_KEY' not in os.environ:
81
+ eprint('ERROR: BP_API_KEY not set in env')
82
+ sys.exit(1)
83
+ username = os.environ['BP_USERNAME']
84
+ api_key = os.environ['BP_API_KEY']
85
+ self.conf = {
86
+ 'api': {
87
+ 'key': api_key,
88
+ 'host': 'app.basepairtech.com',
89
+ 'prefix': '/api/v2/',
90
+ 'ssl': True,
91
+ 'username': username,
92
+ }
93
+ }
94
+
95
+ if self.conf.get('api', {}).get('ssl') is None:
96
+ eprint('ERROR: The config file need to be updated. Please visit:')
97
+ eprint('https://test.basepairtech.com/api/v2/users/api_key')
98
+ eprint('To get your new config file.')
99
+ sys.exit(1)
100
+
101
+ self.scratch = self.conf.get('scratch', scratch).rstrip('/')
102
+ self.use_cache = use_cache
103
+
104
+ if use_cache and self.verbose == 1:
105
+ eprint('Warning: caching data.')
106
+
107
+ self._get_user_id()
108
+
109
+ self.genomes = self.get_genomes()
110
+
111
+ # Get configuration from host only if it is not set in the incoming config
112
+ cache = False
113
+ configuration = self.conf
114
+ if user_cache_for_host_conf:
115
+ eprint('INFO: Use cached host cloud service configuration.')
116
+ cache = '{}/json/config.json'.format(self.scratch)
117
+
118
+ if self.conf.get('api', {}).get('cli'):
119
+ configuration = (User(self.conf.get('api'))).get_configuration(cache=cache)
120
+ self.configuration = Parser(configuration)
121
+
122
+ self.DATATYPES = ['dna-seq', 'chip-seq', 'rna-seq', 'atac-seq', 'other'] # pylint: disable=invalid-name
123
+ self.LIST_TYPES = ['samples', 'analyses', 'genomes', 'workflows', 'analysis'] # pylint: disable=invalid-name
124
+
125
+ ################################################################################################
126
+ ### ANALYSIS ###################################################################################
127
+ ################################################################################################
128
+ def create_analysis(
129
+ self,
130
+ workflow_id,
131
+ control_id=None,
132
+ control_ids=[],
133
+ ignore_validation_warnings=False,
134
+ params=None,
135
+ project_id=None,
136
+ sample_id=None,
137
+ sample_ids=[],
138
+ ): # pylint: disable=dangerous-default-value,too-many-arguments
139
+ '''Create analysis
140
+ Parameters
141
+ ----------
142
+ workflow_id: {str} Workflow id. Run bp.get_workflows() to see what is available. [Required]
143
+
144
+ control_id: {str} Control sample id.
145
+ control_ids: {list} Control sample id.
146
+ ignore_validation_warnings: {bool} Ignore validation warnings
147
+ params: {dict} Dictionary of parameter values.
148
+ project_id: {str} Project id.
149
+ sample_id: {str} Sample id.
150
+ sample_ids: {list} List of sample id.
151
+ '''
152
+
153
+ # get api version
154
+ prefix = self.conf.get('api', {}).get('prefix', '/api/v2/')
155
+ if control_id:
156
+ control_ids.append(control_id)
157
+ if sample_id:
158
+ sample_ids.append(sample_id)
159
+
160
+ # check if valid workflow id
161
+ if not self._check_workflow(workflow_id):
162
+ return None
163
+
164
+ # check if all sample ids are valid
165
+ if not all([self._check_sample(item_id) for item_id in sample_ids]):
166
+ return None
167
+
168
+ data = {
169
+ 'controls': self._parsed_sample_list(control_ids, prefix),
170
+ 'samples': self._parsed_sample_list(sample_ids, prefix),
171
+ 'ignore_validation_warning': ignore_validation_warnings,
172
+ 'workflow': '{}pipelines/{}'.format(prefix, workflow_id)
173
+ }
174
+
175
+ if project_id:
176
+ data['projects'] = ['{}projects/{}'.format(prefix, project_id)]
177
+ eprint(json.dumps(data, indent=2))
178
+
179
+ if params:
180
+ data['params'] = params
181
+
182
+ analysis_api = Analysis(self.conf.get('api'))
183
+ info = analysis_api.save(payload=data)
184
+ analysis_id = info.get('id')
185
+ if self.verbose and analysis_id:
186
+ eprint('created: analysis {} with sample id(s) {}'.format(
187
+ analysis_id,
188
+ ','.join(sample_ids),
189
+ ))
190
+ return analysis_id
191
+
192
+ def delete_analysis(self, uid):
193
+ '''Delete method'''
194
+ return (Analysis(self.conf.get('api'))).delete(uid)
195
+
196
+ def download_analysis(self, uid, outdir='.', tagkind=None, tags=None):
197
+ '''
198
+ Download files from one or more analysis.
199
+ Parameters
200
+ ----------
201
+ uid: {int} One or more uids identifying the analysesself [Required]
202
+ outdir: {str} Output directory to download results to
203
+ tagkind: {str} Type of tag filtering to do. Options: exact, diff, subset
204
+ tags: {list} List of list of tags to filter files by
205
+ '''
206
+ if tagkind not in ['exact', 'diff', 'subset']:
207
+ eprint('Invalid tagkind, choose one of: exact, diff, subset')
208
+ return None
209
+ if tags is not None:
210
+ is_not_valid = not (isinstance(tags, list) and isinstance(tags[0], list))
211
+ if is_not_valid:
212
+ eprint('Invald tags argument. Provide a list of list of tags.')
213
+ return None
214
+ if not isinstance(uid, list):
215
+ uid = [uid]
216
+ for item_id in uid:
217
+ analysis = self.get_analysis(item_id)
218
+ self.get_analysis_files(
219
+ analysis=analysis,
220
+ dirname=outdir,
221
+ kind=tagkind,
222
+ tags=tags,
223
+ )
224
+ return None
225
+
226
+ def fusionsalysis(
227
+ self,
228
+ workflow_id,
229
+ control_id=None,
230
+ control_ids=[],
231
+ params=None,
232
+ sample_id=None,
233
+ sample_ids=[]
234
+ ): # pylint: disable=dangerous-default-value,too-many-arguments
235
+ '''Create analysis
236
+ Parameters
237
+ ----------
238
+ Parameters
239
+ ----------
240
+ workflow_id: {str} Workflow id. Run bp.get_workflows() to see what is available. [Required]
241
+
242
+ control_id: {str} Control sample id.
243
+ control_ids: {list} Control sample id.
244
+ params: {dict} Dictionary of parameter values.
245
+ sample_id: {str} Sample id.
246
+ sample_ids: {list} List of sample id.
247
+ '''
248
+
249
+ prefix = self.conf.get('api', {}).get('prefix', '/api/v2/')
250
+ if control_id:
251
+ control_ids.append(control_id)
252
+ if sample_id:
253
+ sample_ids.append(sample_id)
254
+
255
+ data = {
256
+ 'controls': self._parsed_sample_list(control_ids, prefix),
257
+ 'samples': self._parsed_sample_list(sample_ids, prefix),
258
+ 'workflow': '{}pipelines/{}'.format(prefix, workflow_id),
259
+ }
260
+
261
+ if params:
262
+ data['params'] = params
263
+
264
+ info = (Analysis(self.conf.get('api'))).save(payload=data)
265
+ if info.get('error'):
266
+ eprint('failed:', ','.join(sample_ids), info.get('msg'))
267
+ return None
268
+
269
+ analysis_id = info.get('id')
270
+ if self.verbose and analysis_id:
271
+ eprint('created: analysis {} w/ sample {}'.format(analysis_id, ','.join(sample_ids)))
272
+ return analysis_id
273
+
274
+ def get_analysis(self, uid):
275
+ '''Get resource'''
276
+ return (Analysis(self.conf.get('api'))).get(
277
+ uid,
278
+ cache='{}/json/analysis.{}.json'.format(self.scratch, uid) if self.use_cache else False,
279
+ )
280
+
281
+ def get_analysis_owner(self, analysis_id):
282
+ '''get owner user for analysis'''
283
+ user_id = self._get_analysis_owner_id(analysis_id)
284
+ return self.get_user(user_id) if user_id else None
285
+
286
+ def get_analyses(self):
287
+ '''Get resource list'''
288
+ return (Analysis(self.conf.get('api'))).list_all()
289
+
290
+ def update_analysis(self, uid, data):
291
+ '''Update resource'''
292
+ info = (Analysis(self.conf.get('api'))).save(
293
+ obj_id=uid,
294
+ payload=data,
295
+ )
296
+ if info.get('error'):
297
+ eprint('cudnt update analysis {}, msg {}'.format(uid, info.get('msg')))
298
+ if self.verbose:
299
+ eprint('analysis', uid, 'updated')
300
+ return info
301
+
302
+ ################################################################################################
303
+ ### FILE #######################################################################################
304
+ ################################################################################################
305
+ def create_file(self, uid, data):
306
+ '''Create resource'''
307
+ return (File(self.conf.get('api'))).save(obj_id=uid, payload=data)
308
+
309
+ def get_file(self, uid):
310
+ '''Get resource'''
311
+ return (File(self.conf.get('api'))).get(
312
+ uid,
313
+ cache='{}/json/file.{}.json'.format(self.scratch, uid) if self.use_cache else False,
314
+ )
315
+
316
+ def update_file(self, uid, data):
317
+ '''Update resource'''
318
+ return (File(self.conf.get('api'))).save(obj_id=uid, payload=data)
319
+
320
+ ################################################################################################
321
+ ### GENE #######################################################################################
322
+ ################################################################################################
323
+ def create_gene(self, data):
324
+ '''Create resource'''
325
+ return (Gene(self.conf.get('api'))).save(payload=data)
326
+
327
+ def delete_gene(self, uid):
328
+ '''Delete method'''
329
+ return (Gene(self.conf.get('api'))).delete(uid)
330
+
331
+ def get_gene(self, uid):
332
+ '''Get resource'''
333
+ return (Gene(self.conf.get('api'))).get(
334
+ uid,
335
+ cache='{}/json/gene.{}.json'.format(self.scratch, uid) if self.use_cache else False,
336
+ )
337
+
338
+ def get_genes(self):
339
+ '''Get resource list'''
340
+ info = (Gene(self.conf.get('api'))).list(
341
+ params={'limit': 0}
342
+ )
343
+ return info.get('objects', [])
344
+
345
+ def get_genes_by_info(self, genome=None, symbol=None, tx_id=None):
346
+ '''Get genes by info'''
347
+ params = {'limit': 0}
348
+ if genome:
349
+ params['genome__name'] = genome
350
+ if symbol:
351
+ params['symbol__iexact'] = symbol
352
+ if tx_id:
353
+ params['tx_id'] = tx_id
354
+ info = (Gene(self.conf.get('api'))).list(params)
355
+ return info.get('objects', [])
356
+
357
+ def update_gene(self, uid, data):
358
+ '''Update resource'''
359
+ info = (Gene(self.conf.get('api'))).save(obj_id=uid, payload=data)
360
+ if self.verbose and not info.get('error'):
361
+ eprint('gene', uid, 'updated')
362
+ return info
363
+
364
+ ################################################################################################
365
+ ### GENOME #####################################################################################
366
+ ################################################################################################
367
+ def create_genome(self, data):
368
+ '''Create resource'''
369
+ return (Genome(self.conf.get('api'))).save(payload=data)
370
+
371
+ def delete_genome(self, uid):
372
+ '''Delete method'''
373
+ return (Genome(self.conf.get('api'))).delete(uid)
374
+
375
+ def get_genome(self, uid):
376
+ '''Get resource'''
377
+ return (Genome(self.conf.get('api'))).get(
378
+ uid,
379
+ cache='{}/json/genome.{}.json'.format(self.scratch, uid) if self.use_cache else False,
380
+ )
381
+
382
+ def get_genomes(self):
383
+ '''Get resource list'''
384
+ return (Genome(self.conf.get('api'))).list_all()
385
+
386
+ def update_genome(self, uid, data):
387
+ '''Update resource'''
388
+ info = (Genome(self.conf.get('api'))).save(obj_id=uid, payload=data)
389
+ if self.verbose and not info.get('error'):
390
+ eprint('genome', uid, 'updated')
391
+ return info
392
+
393
+ ################################################################################################
394
+ ### GENOME FILE ################################################################################
395
+ ################################################################################################
396
+ def get_genomefile_by_filters(self, filters=None):
397
+ '''Get genome files by filters'''
398
+ if not filters:
399
+ eprint('Filters required.')
400
+ return None
401
+ filters['limit'] = 0
402
+ info = (GenomeFile(self.conf.get('api'))).list(params=filters)
403
+ return info.get('objects', [])
404
+
405
+ ################################################################################################
406
+ ### HOST #######################################################################################
407
+ ################################################################################################
408
+ def get_host_by_domain(self, domain):
409
+ '''Get host by domain'''
410
+ info = (Host(self.conf.get('api'))).list({
411
+ 'domain__exact': domain,
412
+ 'limit': 1,
413
+ })
414
+ return info.get('objects')[0] if info.get('objects') else None
415
+
416
+ ################################################################################################
417
+ ### MODULE #####################################################################################
418
+ ################################################################################################
419
+ def get_module(self, uid):
420
+ '''Get resource'''
421
+ return (Module(self.conf.get('api'))).get(
422
+ uid,
423
+ cache='{}/json/module.{}.json'.format(self.scratch, uid) if self.use_cache else False,
424
+ )
425
+
426
+ ################################################################################################
427
+ ### PIPELINE / WORKFLOW ########################################################################
428
+ ################################################################################################
429
+ def get_workflow(self, uid):
430
+ '''Get resource'''
431
+ return (Pipeline(self.conf.get('api'))).get(
432
+ uid,
433
+ cache='{}/json/workflow.{}.json'.format(self.scratch, uid) if self.use_cache else False,
434
+ )
435
+
436
+ def get_workflows(self):
437
+ '''Get resource list'''
438
+ return (Pipeline(self.conf.get('api'))).list_all()
439
+
440
+ ################################################################################################
441
+ ### PROJECT ####################################################################################
442
+ ################################################################################################
443
+ def create_project(self, data):
444
+ '''Create resource'''
445
+ info = (Project(self.conf.get('api'))).save(payload=data)
446
+ if self.verbose == 2:
447
+ eprint('Creating project:')
448
+ eprint('Data:', data)
449
+
450
+ project_id = info.get('id', None)
451
+ if project_id: # success
452
+ if self.verbose:
453
+ eprint('created: project with id', project_id)
454
+ else: # failure
455
+ eprint('failed project creation:', data['name'], info.get('msg'))
456
+ return project_id
457
+
458
+ def update_project(self, uid, data, params=None):
459
+ '''Update resource'''
460
+ info = (Project(self.conf.get('api'))).save(
461
+ obj_id=uid,
462
+ payload=data,
463
+ params=params
464
+ )
465
+ if self.verbose and not info.get('error'):
466
+ eprint('project', uid, 'updated')
467
+ return info
468
+
469
+ ################################################################################################
470
+ ### SAMPLE #####################################################################################
471
+ ################################################################################################
472
+ def create_sample(self, data, source='api', upload=True): # pylint: disable=too-many-branches,too-many-locals,too-many-statements
473
+ '''Create sample with the provided info
474
+
475
+ Parameters
476
+ ----------
477
+ data : dict
478
+ Dictionary of sample information.
479
+
480
+ source : str
481
+ TODO
482
+
483
+ upload : bool
484
+ Whether to upload the sample to the server or not.
485
+
486
+ '''
487
+
488
+ # get api version
489
+ prefix = self.conf.get('api', {}).get('prefix', '/api/v2/')
490
+
491
+ # some input validation
492
+ data['genome'] = self._get_genome_by_name(data.get('genome'))
493
+
494
+ if data.get('default_workflow'):
495
+ if not self._check_workflow(data['default_workflow']):
496
+ del data['default_workflow']
497
+ eprint('Provided workflow is not valid.')
498
+
499
+ if 'filepaths1' in data and data['filepaths1'] is None:
500
+ if self.verbose:
501
+ eprint('filepaths1 is None.')
502
+ del data['filepaths1']
503
+
504
+ if 'filepaths2' in data and data['filepaths2'] is None:
505
+ if self.verbose:
506
+ eprint('filepaths2 is None.')
507
+ del data['filepaths2']
508
+
509
+ # if only one sample as str, but into list
510
+ if data.get('filepaths1') and isinstance(data['filepaths1'], str):
511
+ data['filepaths1'] = [data['filepaths1']]
512
+
513
+ if data.get('filepaths2') and isinstance(data['filepaths2'], str):
514
+ data['filepaths2'] = [data['filepaths2']]
515
+
516
+ if data.get('filepaths2') and not data.get('filepaths1'):
517
+ eprint('ERROR: Parameter filepaths1 cannot be empty if filepaths2 is specified.')
518
+ return None
519
+
520
+ # validate unique file between filepaths1 and filepaths2
521
+ does_repeast = data.get('filepaths2') \
522
+ and any(path in data.get('filepaths2') for path in data.get('filepaths1'))
523
+ if does_repeast:
524
+ eprint('ERROR: Same file cannot be use in filepaths1 and filepaths2.')
525
+ return None
526
+
527
+ if data.get('default_workflow'):
528
+ data['default_workflow'] = '{}pipelines/{}'.format(prefix, data['default_workflow'])
529
+
530
+ if data.get('projects'):
531
+ project = (Project(self.conf.get('api'))).get(data.get('projects'))
532
+ if project.get('error'):
533
+ eprint('ERROR: Looking for project. {}'.format(project.get('msg')))
534
+ return None
535
+ data['projects'] = [project.get('resource_uri')]
536
+
537
+ info = (Sample(self.conf.get('api'))).save(payload=data)
538
+ sample_id = info.get('id')
539
+
540
+ if self.verbose == 2:
541
+ eprint('Creating sample:')
542
+ eprint('Data:', data)
543
+
544
+ if sample_id: # success
545
+ if self.verbose:
546
+ eprint('created: sample with id', sample_id)
547
+ else: # failure
548
+ eprint('failed sample creation:', data['name'], info.get('msg'))
549
+ return None
550
+
551
+ # do the actual upload, update filepath
552
+ files_to_upload = []
553
+ for path in data.get('filepaths1', []):
554
+ path = os.path.abspath(os.path.expanduser(
555
+ os.path.expandvars(path)
556
+ ))
557
+ for path in data.get('filepaths2', []):
558
+ path = os.path.abspath(os.path.expanduser(
559
+ os.path.expandvars(path)
560
+ ))
561
+
562
+ # if a sample id exists, then upload the files
563
+ if self.verbose:
564
+ eprint('Sample id: {}'.format(sample_id))
565
+
566
+ order = 0
567
+ all_files = data.get('filepaths1', []) + data.get('filepaths2', [])
568
+ for filepath in all_files:
569
+ if self.verbose:
570
+ eprint('Creating upload {}'.format(filepath))
571
+
572
+ upload_id, filepath, key = self.create_upload(
573
+ sample_id,
574
+ filepath,
575
+ order,
576
+ is_paired_end=filepath in data.get('filepaths2', []),
577
+ source=source
578
+ )
579
+ files_to_upload.append([upload_id, filepath, key])
580
+ order += 1
581
+
582
+ if upload:
583
+ for upload_id, filepath, key in files_to_upload:
584
+ if self.verbose:
585
+ eprint('Uploading. upload_id: {}, filepath: {}, key: {}'.format(
586
+ upload_id,
587
+ filepath,
588
+ key
589
+ ))
590
+ self.upload_file(upload_id, filepath, key)
591
+ return sample_id
592
+
593
+ def delete_sample(self, uid):
594
+ '''Delete method'''
595
+ info = (Sample(self.conf.get('api'))).delete(uid)
596
+ if info.get('error'):
597
+ eprint('error: deleting {}, msg {}'.format(uid, info.get('msg')))
598
+ if self.verbose:
599
+ eprint('deleted sample', uid)
600
+ return info
601
+
602
+ def get_sample(self, uid, add_analysis=True):
603
+ '''Get resource'''
604
+ info = (Sample(self.conf.get('api'))).get(
605
+ uid,
606
+ cache='{}/json/sample.{}.json'.format(self.scratch, uid) if self.use_cache else False,
607
+ )
608
+ if info and add_analysis:
609
+ info = self._add_full_analysis(info)
610
+ return info
611
+
612
+ def get_sample_owner(self, sample_id):
613
+ '''Get sample owner'''
614
+ user_id = self._get_sample_owner_id(sample_id)
615
+ return self.get_user(user_id) if user_id else None
616
+
617
+ def get_samples(self):
618
+ '''Get resource list'''
619
+ return (Sample(self.conf.get('api'))).list_all()
620
+
621
+ def sample_name_to_id(self, name):
622
+ '''Get sample id from name'''
623
+ response = (Sample(self.conf.get('api'))).list({'limit': 2, 'name': name})
624
+ items = response.get('objects')
625
+ if not items:
626
+ eprint('WARNING: no sample by name', name)
627
+ return None
628
+ if len(items) > 1:
629
+ eprint('WARNING: multiple sample by name', name)
630
+ return items[0].get('id')
631
+
632
+ def update_sample(self, uid, data):
633
+ '''Update resource'''
634
+ info = (Sample(self.conf.get('api'))).save(obj_id=uid, payload=data)
635
+ if self.verbose and not info.get('error'):
636
+ eprint('sample', uid, 'updated')
637
+ return info
638
+
639
+ ################################################################################################
640
+ ### UPLOAD #####################################################################################
641
+ ################################################################################################
642
+ def create_upload(self, sample_id, filepath, order, is_paired_end, source='api', uri=None): # pylint: disable=too-many-arguments
643
+ '''Create a upload object and actually upload the file'''
644
+ prefix = self.conf.get('api', {}).get('prefix', '/api/v2/')
645
+ filesize = None
646
+ key = filepath
647
+ status = 'in_progress'
648
+ if filepath.startswith('ftp://') or uri:
649
+ status = 'completed'
650
+ else:
651
+ key = 'uploads/{}/{}/{}'.format(
652
+ self.conf['user']['id'],
653
+ sample_id,
654
+ os.path.basename(filepath)
655
+ )
656
+ filesize = os.stat(filepath).st_size if os.path.exists(filepath) else 0
657
+
658
+ # update record with location of uploaded file
659
+ info = (Upload(self.conf.get('api'))).save(payload={
660
+ 'filesize': filesize,
661
+ 'is_paired_end': is_paired_end,
662
+ 'key': key,
663
+ 'order': order,
664
+ 'sample': '{}samples/{}'.format(prefix, sample_id),
665
+ 'source': source,
666
+ 'status': status,
667
+ 'uri': uri,
668
+ # 'timetaken': seconds_to_upload,
669
+ })
670
+ upload_id = info.get('id')
671
+ if upload_id:
672
+ if self.verbose:
673
+ eprint('\tcreated: upload with id', upload_id)
674
+ return upload_id, filepath, key
675
+ return None, None, None
676
+
677
+ def delete_upload(self, uid):
678
+ '''Delete method'''
679
+ return (Upload(self.conf.get('api'))).delete(uid)
680
+
681
+ def get_upload(self, uid):
682
+ '''Get resource'''
683
+ return (Upload(self.conf.get('api'))).get(
684
+ uid,
685
+ cache='{}/json/upload.{}.json'.format(self.scratch, uid) if self.use_cache else False,
686
+ )
687
+
688
+ def get_uploads(self):
689
+ '''Get resource list'''
690
+ info = (Upload(self.conf.get('api'))).list(
691
+ params={'limit': 0}
692
+ )
693
+ return info.get('objects', [])
694
+
695
+ def update_upload(self, uid, data):
696
+ '''Update resource'''
697
+ return (Upload(self.conf.get('api'))).save(obj_id=uid, payload=data)
698
+
699
+ def upload_file(self, upload_id, filepath, key):
700
+ '''Upload file to S3 and update info'''
701
+ starttime = time.time()
702
+ self.copy_file(filepath, key, action='to')
703
+ seconds_to_upload = int(time.time() - starttime)
704
+ (Upload(self.conf.get('api'))).save(obj_id=upload_id, payload={
705
+ 'filesize': os.stat(filepath).st_size,
706
+ 'seq_length': 0,
707
+ 'status': 'completed',
708
+ 'timetaken': seconds_to_upload,
709
+ })
710
+
711
+ def upload_uri_to_id(self, uri):
712
+ '''Get upload from uri and return upload id'''
713
+ response = (Upload(self.conf.get('api'))).list({'limit': 2, 'uri': uri})
714
+ items = response.get('objects')
715
+ if not items:
716
+ eprint('WARNING: no upload by uri', uri)
717
+ return None
718
+ if len(items) > 1:
719
+ eprint('WARNING: multiple upload by uri', uri)
720
+ return items[0].get('id')
721
+
722
+ ################################################################################################
723
+ ### USER #######################################################################################
724
+ ################################################################################################
725
+ def get_user(self, uid):
726
+ '''Get resource'''
727
+ return (User(self.conf.get('api'))).get(
728
+ uid,
729
+ cache='{}/json/user.{}.json'.format(self.scratch, uid) if self.use_cache else False,
730
+ )
731
+
732
+ ################################################################################################
733
+ ### GENERAL HELPERS AND METHODS ################################################################
734
+ ################################################################################################
735
+ def copy_file(self, src, dest, action='to'):
736
+ '''Copy file'''
737
+ return self.copy_file_to_s3(src, dest) if action == 'to' \
738
+ else self.copy_file_from_s3(src, dest)
739
+
740
+ def copy_file_from_s3(self, src, dest):
741
+ '''
742
+ Low level function to copy a file from cloud to disk
743
+ Parameters
744
+ ----------
745
+ src: {str} File path on AWS S3, not including the bucket [Required]
746
+ dest: {str} Directory or file path where file will be downloaded to [Required]
747
+ '''
748
+ storage_cfg = self.configuration.get_user_storage()
749
+ if not src.startswith('s3://'):
750
+ src = 's3://{}/{}'.format(storage_cfg.get('bucket'), src)
751
+ cmd = self.get_copy_cmd(src, dest)
752
+ try:
753
+ return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
754
+ except subprocess.CalledProcessError as stderr:
755
+ eprint("Error downloading {}.".format(src))
756
+ eprint("Return code: {}".format(stderr.returncode))
757
+ eprint("Ouput: {}".format(stderr.output))
758
+ return False
759
+
760
+ def copy_file_to_s3(self, src, dest, params=None):
761
+ '''Low level function to copy a file to cloud from disk'''
762
+ storage_cfg = self.configuration.get_user_storage()
763
+ dest = 's3://{}/{}'.format(storage_cfg.get('bucket'), dest)
764
+ cmd = self.get_copy_cmd(src, dest, sse=True, params=params)
765
+ if self.verbose:
766
+ eprint('copying', src, 'to', dest)
767
+ try:
768
+ return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
769
+ except CalledProcessError as error:
770
+ eprint('error in copy file to s3')
771
+ eprint(cmd)
772
+ eprint(error.output)
773
+ return None
774
+
775
+ def download_file(self, filekey, dirname=None, filename=None, is_json=False, load=False): # pylint: disable=too-many-arguments
776
+ '''
777
+ High level function, downloads to scratch dir and opens and
778
+ parses files to JSON if asked. Uses low level copy_file()
779
+ Parameters
780
+ ----------
781
+ filekey: {str} The full file path on AWS S3 [Required]
782
+ dirname: {str} Directory to download the file to
783
+ filename: {str} Give the download file a new name
784
+ isjson: {bool} Whether the downloaded file is JSON format
785
+ load: {bool} Load the file after downloading
786
+ Returns
787
+ -------
788
+ Absolute filepath to downloaded file
789
+ '''
790
+
791
+ if not filename:
792
+ # don't renaming the file
793
+ # get the download directory
794
+ prefix = self.scratch
795
+ if dirname:
796
+ prefix = dirname if dirname.startswith('/') else os.path.join(self.scratch, dirname)
797
+ filepath = os.path.join(prefix, os.path.basename(filekey))
798
+ else:
799
+ # rename the download file
800
+ filepath = self.get_filepath(filename, dirname=dirname)
801
+ filepath = os.path.expanduser(filepath)
802
+
803
+ # if file not already there, download it
804
+ if not os.path.exists(filepath) or os.path.getsize(filepath) == 0:
805
+ if self.verbose:
806
+ eprint('downloading', filepath)
807
+ if not os.path.exists(os.path.dirname(filepath)):
808
+ os.makedirs(os.path.dirname(filepath))
809
+ self.copy_file(filekey, filepath, action='from')
810
+ elif self.verbose:
811
+ eprint('exists', filepath)
812
+
813
+ if load:
814
+ data = open(filepath, 'r').read().strip()
815
+ return json.loads(data) if is_json else data
816
+ return filepath
817
+
818
+ def download_raw_files(self, sample, outdir=None):
819
+ '''
820
+ Download raw data associated with a sample
821
+ Parameters
822
+ ----------
823
+ sample: {dict} From calling bp.get_sample() [Required]
824
+ outdir: {str} Output directory to save files to
825
+ '''
826
+ uploads = sample['uploads']
827
+ files = [(upload.get('uri') or upload.get('key')) for upload in uploads]
828
+ files_downloaded = []
829
+ for file_i in files:
830
+ outdir = outdir if outdir else self.scratch
831
+ fileout = os.path.join(outdir, os.path.split(file_i)[1])
832
+ if os.path.exists(fileout):
833
+ eprint('Not downloading. File exists: {}'.format(fileout))
834
+ output = True
835
+ else:
836
+ output = self.copy_file_from_s3(file_i, outdir)
837
+
838
+ if output:
839
+ eprint('Downloaded {} to {}.'.format(os.path.split(file_i)[1], outdir))
840
+ files_downloaded.append(fileout)
841
+ return files_downloaded
842
+
843
+ @classmethod
844
+ def filter_files_by_tags(cls, files, tags, exclude=None, kind='exact', multiple=False): # pylint: disable=too-many-arguments
845
+ '''
846
+ Filter files that have all the tags. If exclude tags provided,
847
+ only return the files that don't have the exclude tags.append
848
+ In some cases, multiple files are expected, set it to true.
849
+ Parameters
850
+ ---------
851
+ files: {dict} List dictionaries containing file information [Required]
852
+ tags: {list} List of tags [Required]
853
+ kind: {str} Type of tag filtering. Options: exact, subset
854
+ exclude: {list} List of tags to exclude
855
+ multiple: {bool} Whether to return all matching files or only one
856
+ Returns
857
+ -------
858
+ A list of files
859
+ '''
860
+
861
+ if tags is None:
862
+ return files
863
+
864
+ if not isinstance(tags, list):
865
+ eprint('Invalid tags argument. Provide a list of tags.')
866
+ return None
867
+
868
+ # filter for matches
869
+ files = [
870
+ file for file in files if file['tags'] and getattr(SetFilter, kind)(tags, file['tags'])
871
+ ]
872
+
873
+ # exclude files by tag
874
+ if exclude:
875
+ files = [
876
+ file for file in files if file['tags'] and SetFilter.diff(file['tags'], exclude)
877
+ ]
878
+
879
+ # check if no files left after filtering
880
+ if len(files) == 0:
881
+ eprint('No matching files after filtering by tags:', tags)
882
+ return None
883
+
884
+ # return files
885
+ if multiple:
886
+ return files
887
+ if len(files) > 1:
888
+ eprint('multiple matches for tags:', tags)
889
+ return files[0:1]
890
+ return files
891
+
892
+ def get_analysis_files(self, analysis, dirname=None, kind='exact', tags=None): # pylint: disable=too-many-arguments
893
+ '''
894
+ For a analysis, go through files and get that match the tags
895
+ Parameters
896
+ ----------
897
+ analysis: {dict} The dictionary returned by bp.get_analysis() [Required]
898
+ dirname: {str} Directory to download files to
899
+ kind: {str} Type of tag filtering. Options: exact, subset
900
+ tags: {list} List of lists of tags to filter by
901
+ '''
902
+ # some input checking
903
+ if tags:
904
+ is_not_valid = not (isinstance(tags, list) and all([isinstance(item, list) for item in tags]))
905
+ if is_not_valid:
906
+ eprint('Invald tags argument. Provide a list of list of tags.')
907
+ return None
908
+ else:
909
+ tags = [None]
910
+
911
+ # filter the files
912
+ matching_files = []
913
+ for tags_sub in tags:
914
+ matching_files += self.filter_files_by_tags(
915
+ analysis['files'],
916
+ tags_sub,
917
+ kind=kind,
918
+ multiple=True,
919
+ )
920
+
921
+ return [self.download_file(
922
+ matching_file['path'],
923
+ dirname=dirname,
924
+ ) for matching_file in matching_files] if matching_files else None
925
+
926
+ def get_bam_file(self, sample, tags=None, multiple=False):
927
+ '''Get bam file. If you want deduped bam file, call with tags = ['bam', 'dedup']'''
928
+ return self.get_file_by_tags(
929
+ sample,
930
+ analysis_tags=['alignment'],
931
+ multiple=multiple,
932
+ tags=tags if tags else ['bam'],
933
+ )
934
+
935
+ def get_bigwig_file(self, sample, multiple=False):
936
+ '''Get bigwig file - for ChIP-Seq'''
937
+ return self.get_file_by_tags(
938
+ sample,
939
+ analysis_tags=['alignment'],
940
+ kind='exact',
941
+ multiple=multiple,
942
+ tags=['bigwig'],
943
+ )
944
+
945
+ def get_copy_cmd(self, src, dest, sse=False, params=None):
946
+ '''
947
+ Create copy cmd based on AWS cli utility
948
+ Parameters
949
+ ----------
950
+ src: {str} Path to file on AWS S3 [Required]
951
+ dest: {str} Destination directory or path where file will be downloaded to [Required]
952
+ sse: {bool} Whether to use sever side encryption
953
+ params: {dict} Other download paramters
954
+ '''
955
+ _params = ''
956
+ if sse:
957
+ _params = '--sse'
958
+ if params:
959
+ for arg, val in params.items():
960
+ _params += ' {} "{}"'.format(arg, val)
961
+
962
+ storage_cfg = self.configuration.get_user_storage()
963
+ credential = self.configuration.get_cli_credentials_from(storage_cfg)
964
+ return '{}aws s3 cp "{}" "{}" {}'.format(credential, src, dest, _params)
965
+
966
+ def get_expression_count_file(self, sample, features='transcripts', multiple=False):
967
+ '''Get expression count text file - for RNA-Seq'''
968
+ tags = ['expression_count', 'by_transcript', 'text']
969
+ if features == 'genes':
970
+ tags = ['expression_count', 'by_gene', 'text']
971
+ if self.verbose:
972
+ eprint('getting file w tags', tags)
973
+ return self.get_file_by_tags(
974
+ sample,
975
+ analysis_tags=['alignment'],
976
+ kind='exact',
977
+ multiple=multiple,
978
+ tags=tags,
979
+ )
980
+
981
+ def get_file_by_node(
982
+ self,
983
+ sample,
984
+ analysis_tags=None,
985
+ dest=None,
986
+ dirname=None,
987
+ multiple=False,
988
+ node=None,
989
+ ): # pylint: disable=too-many-arguments,too-many-branches,too-many-locals
990
+ '''For a sample, go through analysis and get files that match the tags'''
991
+ matches = []
992
+ for analysis in sample['analyses_full']:
993
+ should_continue = analysis['status'] == 'error' or (analysis_tags and not (
994
+ analysis['tags'] and (set(analysis_tags) <= set(analysis['tags']))
995
+ ))
996
+ if should_continue:
997
+ continue
998
+
999
+ matching_file = self._filter_files_by_node(
1000
+ analysis['files'], node, multiple=multiple)
1001
+
1002
+ if matching_file:
1003
+ if multiple:
1004
+ for file in matching_file:
1005
+ matches.append([analysis['id'], file])
1006
+ else:
1007
+ matches.append([analysis['id'], matching_file])
1008
+
1009
+ if len(matches) == 0:
1010
+ eprint('WARNING: no matching file for', node)
1011
+ return None
1012
+
1013
+ if len(matches) > 1:
1014
+ matches.sort(
1015
+ key=lambda item: datetime.datetime.strptime(item[1]['last_updated'], '%Y-%m-%dT%H:%M:%S.%f'),
1016
+ reverse=True,
1017
+ )
1018
+ if not multiple:
1019
+ eprint('WARNING: multiple matching file for', node)
1020
+ for file in matches:
1021
+ eprint('\t', file[1]['last_updated'], file[1]['path'])
1022
+
1023
+ if multiple:
1024
+ filepath = []
1025
+ for match in matches:
1026
+ file1 = match[1]
1027
+ path = self.download_file(file1['path'], dirname=dirname)
1028
+ filepath.append(path)
1029
+ else:
1030
+ file1 = matches[0][1]
1031
+ if dest:
1032
+ dirname = dest
1033
+ filepath = self.download_file(file1['path'], filename=dest, dirname=dirname)
1034
+ return filepath
1035
+
1036
+ def get_file_by_tags(
1037
+ self,
1038
+ sample,
1039
+ analysis_tags=None,
1040
+ dest=None,
1041
+ dirname=None,
1042
+ download=True,
1043
+ exclude=None,
1044
+ kind='exact',
1045
+ multiple=False,
1046
+ tags=None,
1047
+ #workflow_id=None,
1048
+ ): # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements
1049
+ '''
1050
+ For a sample, go through analysis and get files that match the tags.
1051
+ Parameters
1052
+ ----------
1053
+ sample: {dict} Sample information [Required]
1054
+ analysis_tags: {list} Analysis tags to filter analyses when looking for files
1055
+ dest: {str} File name to save the file to
1056
+ dirname: {str} Ouput directory to save the file to
1057
+ download: {bool} Whether to download the file(s)
1058
+ exclude: {list} List of tags to exclude
1059
+ kind: {str} Type of tag filtering to do. Options: exact, diff, or subset
1060
+ multiple: {bool} Whether to return multiple files or just the first one
1061
+ tags: {list} List of list of tags for file filtering. If just list of tags, will convert to list of lists.
1062
+ workflow_id: {int} Workflow id to look for files in. NOT IMPLEMENTED YE
1063
+ '''
1064
+ # some error checking
1065
+ if tags is None:
1066
+ eprint("Tags is None")
1067
+ return None
1068
+
1069
+ # make sure tags is a list of lists
1070
+ if isinstance(tags, list):
1071
+ if not isinstance(tags[0], list):
1072
+ tags = [tags]
1073
+
1074
+ matches = []
1075
+ matching_file = []
1076
+ for analysis in sample['analyses_full']:
1077
+ if analysis['status'] == 'error':
1078
+ eprint('analysis ended in error, skipping.')
1079
+ continue
1080
+
1081
+ # dont even look if not the right type of analysis
1082
+ should_continue = analysis_tags and not(
1083
+ analysis['tags'] and set(analysis_tags) <= set(analysis['tags'])
1084
+ )
1085
+ if should_continue:
1086
+ continue
1087
+
1088
+ if analysis['params'] and 'info' in analysis['params']:
1089
+ if not analysis['params']['info'].get('genome_id'):
1090
+ eprint('Could not find genome for analysis {}'.format(analysis['id']))
1091
+ continue
1092
+ sample_genome_id = self.get_id_from_url(sample.get('genome', ''))
1093
+ if not sample_genome_id:
1094
+ eprint('Could not find genome for analysis {}'.format(analysis['id']))
1095
+ continue
1096
+ if int(analysis['params']['info']['genome_id']) != int(sample_genome_id):
1097
+ eprint(
1098
+ 'analysis genome {}'.format(analysis['params']['info']['genome_id']),
1099
+ 'different from sample genome {}.'.format(sample_genome_id),
1100
+ )
1101
+ continue
1102
+
1103
+ if self.verbose:
1104
+ eprint('looking at', analysis['id'], 'status', analysis['status'])
1105
+ for tags_sub in tags:
1106
+ filtered_files = self.filter_files_by_tags(
1107
+ analysis['files'],
1108
+ tags_sub,
1109
+ exclude=exclude,
1110
+ kind=kind,
1111
+ multiple=multiple,
1112
+ )
1113
+
1114
+ if filtered_files:
1115
+ matching_file += filtered_files
1116
+
1117
+ if matching_file:
1118
+ if multiple:
1119
+ for file in matching_file:
1120
+ matches.append([analysis['id'], file])
1121
+ else:
1122
+ matches.append([analysis['id'], matching_file[0]])
1123
+
1124
+ if not matches:
1125
+ eprint('WARNING: no matching file for', tags)
1126
+ eprint('in analyses with ids', [analysis['id'] for analysis in sample['analyses_full']])
1127
+ eprint('for sample', sample['id'])
1128
+ return None
1129
+
1130
+ if len(matches) > 1:
1131
+ matches.sort(
1132
+ key=lambda item: datetime.datetime.strptime(item[1]['last_updated'], '%Y-%m-%dT%H:%M:%S.%f'),
1133
+ reverse=True,
1134
+ )
1135
+ if not multiple:
1136
+ eprint('WARNING: multiple matching file for', tags)
1137
+ for file in matches:
1138
+ eprint('\t', file[1]['last_updated'], file[1]['path'])
1139
+
1140
+ if multiple:
1141
+ filepath = []
1142
+ for match in matches:
1143
+ file1 = match[1]
1144
+
1145
+ if download:
1146
+ path = self.download_file(file1['path'], dirname=dirname)
1147
+ filepath.append(path)
1148
+ else:
1149
+ filepath.append(file1['path'])
1150
+ else:
1151
+ file1 = matches[0][1]
1152
+ if download:
1153
+ filepath = self.download_file(file1['path'], filename=dest, dirname=dirname)
1154
+ else:
1155
+ filepath = file1['path']
1156
+ eprint('else')
1157
+ return filepath
1158
+
1159
+ def get_filepath(self, filename, dirname=None):
1160
+ '''Use scratch and [dirname] to construct a full filepath'''
1161
+ # move along, nothin to do
1162
+ if filename.startswith('/'):
1163
+ filepath = filename
1164
+ else:
1165
+ if dirname:
1166
+ filename = '{}/{}'.format(dirname.rstrip('/'), filename)
1167
+ # adding dirname made it full
1168
+ filepath = filename if filename.startswith('/') else self.scratch + '/' + filename
1169
+ return filepath
1170
+
1171
+ def get_history(self, analysis_id, outdir=None):
1172
+ '''
1173
+ Get SWF history for the analysis
1174
+ Parameters
1175
+ ----------
1176
+ analysis_id: {int} Analysis ID [Required]
1177
+ outdir: {str} Directory to save data. Use instead of scratch.
1178
+ '''
1179
+ user_id = self._get_analysis_owner_id(analysis_id)
1180
+ filekey = 'log/analyses/{}/{}/history.json'.format(user_id, analysis_id)
1181
+ filename = '{}/history/history.{}.json'.format(
1182
+ outdir if outdir else self.scratch,
1183
+ analysis_id
1184
+ )
1185
+ return self.download_file(filekey, filename, load=True, is_json=True)
1186
+
1187
+ def get_id_from_url(self, url):
1188
+ '''Parse URL to get the id'''
1189
+ return self.parse_url(url)['id']
1190
+
1191
+ def get_log(self, analysis_id, outdir=None):
1192
+ '''
1193
+ Get log file for the analysis
1194
+ Parameters
1195
+ ----------
1196
+ analysis_id: {int} Analysis ID [Required]
1197
+ outdir: {str} Directory to save data. Use instead of scratch.
1198
+ '''
1199
+ user_id = self._get_analysis_owner_id(analysis_id)
1200
+ filekey = 'log/analyses/{}/{}/worker.log'.format(user_id, analysis_id)
1201
+ filename = '{}/worker.{}.log'.format(outdir if outdir else self.scratch, analysis_id)
1202
+ return self.download_file(filekey, filename, load=False, is_json=False)
1203
+
1204
+ def get_window_score_filename(self, sample, kind=None, flanking=None):
1205
+ '''Get window score filename'''
1206
+ suffix = 'score-{}-{}Kb'.format(kind, flanking / 1e3)
1207
+ suffix = suffix.replace('.0Kb', 'Kb')
1208
+ return '{}/score/{}.{}.dedup.{}.txt'.format(
1209
+ self.scratch,
1210
+ sample['slug'],
1211
+ sample['genome'],
1212
+ suffix,
1213
+ )
1214
+
1215
+ @classmethod
1216
+ def parse_url(cls, url):
1217
+ '''Parse URL to get the id, and other stuff'''
1218
+ parts = url.rsplit('/', 1)
1219
+ return {'id': parts[1]}
1220
+
1221
+ def print_data(self, data_type='', is_json=False, uid=None):
1222
+ '''
1223
+ Print data associated with genomes, samples, etc..
1224
+ Parameters
1225
+ ----------
1226
+ data_type: {str} Type of data to print (e.g. workflows)
1227
+ is_json: {bool} By default, data is printed in a human-readable format
1228
+ uid: {list} One or more ids of the objects you want
1229
+ '''
1230
+
1231
+ if not isinstance(uid, list):
1232
+ uid = [uid]
1233
+
1234
+ detail_methods = {
1235
+ 'analysis': 'get_analysis',
1236
+ 'genome': 'get_genome',
1237
+ 'sample': 'get_sample',
1238
+ }
1239
+
1240
+ list_methods = {
1241
+ 'analyses': 'get_analyses',
1242
+ 'genomes': 'get_genomes',
1243
+ 'samples': 'get_samples',
1244
+ 'workflows': 'get_workflows',
1245
+ }
1246
+
1247
+ # get the appropriate data
1248
+ data = []
1249
+
1250
+ # if it is a detail
1251
+ if data_type in detail_methods:
1252
+ if uid is None:
1253
+ eprint('Your uid is invalid: {}'.format(uid))
1254
+ return
1255
+
1256
+ for item_id in uid:
1257
+ data_tmp = getattr(self, list_methods.get(data_type))(item_id)
1258
+ if data_tmp.get('id'):
1259
+ data.append(data_tmp)
1260
+ else:
1261
+ eprint('{} with uid {} invalid.'.format(data_type.capitalize(), item_id))
1262
+
1263
+ # if it is a list
1264
+ if data_type in list_methods:
1265
+ data = getattr(self, list_methods.get(data_type))()
1266
+
1267
+
1268
+ if not data:
1269
+ eprint('Nothing found for the parameters you gave.')
1270
+ return
1271
+
1272
+ # print the data as json
1273
+ if is_json:
1274
+ for item in data:
1275
+ eprint(item)
1276
+ eprint()
1277
+ return
1278
+
1279
+ # print the data human readable
1280
+ getattr(NicePrint, data_type)(data)
1281
+
1282
+ ### Private methods ###
1283
+ def _add_full_analysis(self, sample):
1284
+ '''Add full analysis info to the sample'''
1285
+ analysis_ids = [self.parse_url(uri)['id'] for uri in sample['analyses']]
1286
+ analyses = [self.get_analysis(uid) for uid in analysis_ids]
1287
+ # remove null analyses, probably deleted or no ownership
1288
+ analyses = [analysis for analysis in analyses if not analysis.get('error')]
1289
+ sample['analyses_full'] = analyses
1290
+ return sample
1291
+
1292
+ def _check_sample(self, uid):
1293
+ '''Check if the sample is in the Basepair database'''
1294
+ info = self.get_sample(uid, add_analysis=False)
1295
+ if info.get('id'):
1296
+ return True
1297
+ eprint('The provided sample id: {id}, does not exist in Basepair.'.format(id=uid))
1298
+ return False
1299
+
1300
+ def _check_workflow(self, uid):
1301
+ '''Check if the workflow is in the Basepair database'''
1302
+ info = self.get_workflow(uid)
1303
+ if info.get('id'):
1304
+ return True
1305
+ eprint('The provided workflow id: {id}, does not exist in Basepair.'.format(id=uid))
1306
+ return False
1307
+
1308
+ @classmethod
1309
+ def _filter_files_by_node(cls, files, node, multiple=False):
1310
+ '''Filter files that are from the node.'''
1311
+ # In some cases, multiple files are expected, set it to true.
1312
+ files = [file for file in files if file['node'] == node]
1313
+ if multiple:
1314
+ return files
1315
+ if not files:
1316
+ return None
1317
+ if len(files) > 1:
1318
+ eprint('multiple matches for node:', node)
1319
+ return files[0]
1320
+
1321
+ def _get_analysis_owner_id(self, analysis_id):
1322
+ '''Get analysis owner id'''
1323
+ info = self.get_analysis(analysis_id)
1324
+ return self.parse_url(info['owner'])['id'] if info else None
1325
+
1326
+ def _get_genome_by_name(self, genome_name):
1327
+ '''Check if the genome is in the Basepair database'''
1328
+ if genome_name:
1329
+ available_genomes = [item for item in self.genomes if item.get('name') == genome_name]
1330
+ if not available_genomes:
1331
+ eprint(
1332
+ 'The provided genome, {}, does not exist in Basepair. Proceeding anyway...'.format(genome_name)
1333
+ )
1334
+ return None
1335
+ return available_genomes[0].get('resource_uri')
1336
+ return None
1337
+
1338
+ def _get_sample_owner_id(self, sample_id):
1339
+ '''Get sample owner id'''
1340
+ info = self.get_sample(sample_id)
1341
+ return self.parse_url(info['owner'])['id'] if info else None
1342
+
1343
+ def _get_user_id(self):
1344
+ '''Get userid, username provided in conf'''
1345
+ user = (User(self.conf.get('api'))).list(
1346
+ params={'limit': 1, 'username': self.conf['api']['username']}
1347
+ )
1348
+ self.conf['user'] = user.get('objects', [None])[0]
1349
+
1350
+ @classmethod
1351
+ def _parsed_sample_list(cls, items, prefix):
1352
+ '''Parse sample id list into sample resurce uri list'''
1353
+ return ['{}samples/{}'.format(prefix, item_id) for item_id in items]