forked from qiita-spots/qiita
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsoftware.py
2044 lines (1778 loc) · 74.4 KB
/
software.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -----------------------------------------------------------------------------
# Copyright (c) 2014--, The Qiita Development Team.
#
# Distributed under the terms of the BSD 3-clause License.
#
# The full license is in the file LICENSE, distributed with this software.
# -----------------------------------------------------------------------------
from json import dumps, loads
from copy import deepcopy
import inspect
import warnings
import networkx as nx
from qiita_core.qiita_settings import qiita_config
import qiita_db as qdb
from configparser import ConfigParser
class Command(qdb.base.QiitaObject):
r"""An executable command available in the system
Attributes
----------
active
post_processing_cmd
analysis_only
default_parameter_sets
description
merging_scheme
name
naming_order
optional_parameters
outputs
parameters
required_parameters
software
description
cli
parameters_table
Methods
-------
_check_id
activate
Class Methods
-------------
create
exists
get_commands_by_input_type
get_html_generator
get_validator
See Also
--------
qiita_db.software.Software
"""
_table = "software_command"
@classmethod
def get_commands_by_input_type(cls, artifact_types, active_only=True,
exclude_analysis=True, prep_type=None):
"""Returns the commands that can process the given artifact types
Parameters
----------
artifact_type : list of str
The artifact types
active_only : bool, optional
If True, return only active commands, otherwise return all commands
Default: True
exclude_analysis : bool, optional
If True, return commands that are not part of the analysis pipeline
Returns
-------
generator of qiita_db.software.Command
The commands that can process the given artifact tyoes
"""
with qdb.sql_connection.TRN:
sql = """SELECT DISTINCT command_id
FROM qiita.command_parameter
JOIN qiita.parameter_artifact_type
USING (command_parameter_id)
JOIN qiita.artifact_type USING (artifact_type_id)
JOIN qiita.software_command USING (command_id)
WHERE artifact_type IN %s"""
if active_only:
sql += " AND active = True"
if exclude_analysis:
sql += " AND is_analysis = False"
qdb.sql_connection.TRN.add(sql, [tuple(artifact_types)])
cids = set(qdb.sql_connection.TRN.execute_fetchflatten())
if prep_type is not None:
dws = [w for w in qdb.software.DefaultWorkflow.iter()
if prep_type in w.data_type]
if dws:
cmds = {n.default_parameter.command.id
for w in dws for n in w.graph.nodes}
cids = cmds & cids
return [cls(cid) for cid in cids]
@classmethod
def get_html_generator(cls, artifact_type):
"""Returns the command that generete the HTML for the given artifact
Parameters
----------
artifact_type : str
The artifact type to search the HTML generator for
Returns
-------
qiita_db.software.Command
The newly created command
Raises
------
qdb.exceptions.QiitaDBError when the generete the HTML command can't
be found
"""
with qdb.sql_connection.TRN:
sql = """SELECT command_id
FROM qiita.software_command
JOIN qiita.software_artifact_type USING (software_id)
JOIN qiita.artifact_type USING (artifact_type_id)
WHERE artifact_type = %s
AND name = 'Generate HTML summary'
AND active = true"""
qdb.sql_connection.TRN.add(sql, [artifact_type])
try:
res = qdb.sql_connection.TRN.execute_fetchlast()
except IndexError:
raise qdb.exceptions.QiitaDBError(
"There is no command to generate the HTML summary for "
"artifact type '%s'" % artifact_type)
return cls(res)
@classmethod
def get_validator(cls, artifact_type):
"""Returns the command that validates the given artifact
Parameters
----------
artifact_type : str
The artifact type to search the Validate for
Returns
-------
qiita_db.software.Command
The newly created command
Raises
------
qdb.exceptions.QiitaDBError when the Validate command can't be found
"""
with qdb.sql_connection.TRN:
sql = """SELECT command_id
FROM qiita.software_command
JOIN qiita.software_artifact_type USING (software_id)
JOIN qiita.artifact_type USING (artifact_type_id)
WHERE artifact_type = %s
AND name = 'Validate'
AND active = true"""
qdb.sql_connection.TRN.add(sql, [artifact_type])
try:
res = qdb.sql_connection.TRN.execute_fetchlast()
except IndexError:
raise qdb.exceptions.QiitaDBError(
"There is no command to generate the Validate for "
"artifact type '%s'" % artifact_type)
return cls(res)
def _check_id(self, id_):
"""Check that the provided ID actually exists in the database
Parameters
----------
id_ : int
The ID to test
Notes
-----
This function overwrites the base function, as the sql layout doesn't
follow the same conventions done in the other classes.
"""
with qdb.sql_connection.TRN:
sql = """SELECT EXISTS(
SELECT *
FROM qiita.software_command
WHERE command_id = %s)"""
qdb.sql_connection.TRN.add(sql, [id_])
return qdb.sql_connection.TRN.execute_fetchlast()
@classmethod
def exists(cls, software, name):
"""Checks if the command already exists in the system
Parameters
----------
qiita_db.software.Software
The software to which this command belongs to.
name : str
The name of the command
Returns
-------
bool
Whether the command exists in the system or not
"""
with qdb.sql_connection.TRN:
sql = """SELECT EXISTS(SELECT *
FROM qiita.software_command
WHERE software_id = %s
AND name = %s)"""
qdb.sql_connection.TRN.add(sql, [software.id, name])
return qdb.sql_connection.TRN.execute_fetchlast()
@classmethod
def create(cls, software, name, description, parameters, outputs=None,
analysis_only=False):
r"""Creates a new command in the system
The supported types for the parameters are:
- string: the parameter is a free text input
- integer: the parameter is an integer
- float: the parameter is a float
- artifact: the parameter is an artifact instance, the artifact id
will be stored
- reference: the parameter is a reference instance, the reference
id will be stored
- choice: the format of this should be `choice:<json-dump-of-list>`
in which json-dump-of-list is the JSON dump of a list containing
the acceptable values
Parameters
----------
software : qiita_db.software.Software
The software to which this command belongs to.
name : str
The name of the command
description : str
The description of the command
parameters : dict
The description of the parameters that this command received. The
format is: {parameter_name: (parameter_type, default, name_order,
check_biom_merge, qiita_optional_parameter (optional))},
where parameter_name, parameter_type and default are strings,
name_order is an optional integer value and check_biom_merge is
an optional boolean value. name_order is used to specify the order
of the parameter when automatically naming the artifacts.
check_biom_merge is used when merging artifacts in the analysis
pipeline. qiita_optional_parameter is an optional bool to "force"
the parameter to be optional
outputs : dict, optional
The description of the outputs that this command generated. The
format is either {output_name: artifact_type} or
{output_name: (artifact_type, check_biom_merge)}
analysis_only : bool, optional
If true, then the command will only be available on the analysis
pipeline. Default: False.
Returns
-------
qiita_db.software.Command
The newly created command
Raises
------
QiitaDBError
- If parameters is empty
- If the parameters dictionary is malformed
- If one of the parameter types is not supported
- If the default value of a choice parameter is not listed in
the available choices
QiitaDBDuplicateError
- If the command already exists
Notes
-----
If the default value for a parameter is NULL, then the parameter will
be required. On the other hand, if it is provided, the parameter will
be optional and the default value will be used when the user doesn't
overwrite it.
"""
# Perform some sanity checks in the parameters dictionary
if not parameters:
raise qdb.exceptions.QiitaDBError(
"Error creating command %s. At least one parameter should "
"be provided." % name)
sql_param_values = []
sql_artifact_params = []
for pname, vals in parameters.items():
qiita_optional_parameter = False
if 'qiita_optional_parameter' in vals:
qiita_optional_parameter = True
vals.remove('qiita_optional_parameter')
lenvals = len(vals)
if lenvals == 2:
ptype, dflt = vals
name_order = None
check_biom_merge = False
elif lenvals == 4:
ptype, dflt, name_order, check_biom_merge = vals
else:
raise qdb.exceptions.QiitaDBError(
"Malformed parameters dictionary, the format should be "
"either {param_name: [parameter_type, default]} or "
"{parameter_name: (parameter_type, default, name_order, "
"check_biom_merge)}. Found: %s for parameter name %s"
% (vals, pname))
# Check that the type is one of the supported types
supported_types = ['string', 'integer', 'float', 'reference',
'boolean', 'prep_template', 'analysis']
if ptype not in supported_types and not ptype.startswith(
('choice', 'mchoice', 'artifact')):
supported_types.extend(['choice', 'mchoice', 'artifact'])
raise qdb.exceptions.QiitaDBError(
"Unsupported parameters type '%s' for parameter %s. "
"Supported types are: %s"
% (ptype, pname, ', '.join(supported_types)))
if ptype.startswith(('choice', 'mchoice')) and dflt is not None:
choices = set(loads(ptype.split(':')[1]))
dflt_val = dflt
if ptype.startswith('choice'):
# In the choice case, the dflt value is a single string,
# create a list with it the string on it to use the
# issuperset call below
dflt_val = [dflt_val]
else:
# jsonize the list to store it in the DB
dflt = dumps(dflt)
if not choices.issuperset(dflt_val):
raise qdb.exceptions.QiitaDBError(
"The default value '%s' for the parameter %s is not "
"listed in the available choices: %s"
% (dflt, pname, ', '.join(choices)))
if ptype.startswith('artifact'):
atypes = loads(ptype.split(':')[1])
sql_artifact_params.append(
[pname, 'artifact', atypes])
else:
# a parameter will be required (not optional) if
# qiita_optional_parameter is false and there is the default
# value (dflt) is None
required = not qiita_optional_parameter and dflt is None
sql_param_values.append([pname, ptype, required, dflt,
name_order, check_biom_merge])
with qdb.sql_connection.TRN:
if cls.exists(software, name):
raise qdb.exceptions.QiitaDBDuplicateError(
"command", "software: %d, name: %s"
% (software.id, name))
# Add the command to the DB
sql = """INSERT INTO qiita.software_command
(name, software_id, description, is_analysis)
VALUES (%s, %s, %s, %s)
RETURNING command_id"""
sql_params = [name, software.id, description, analysis_only]
qdb.sql_connection.TRN.add(sql, sql_params)
c_id = qdb.sql_connection.TRN.execute_fetchlast()
# Add the parameters to the DB
sql = """INSERT INTO qiita.command_parameter
(command_id, parameter_name, parameter_type,
required, default_value, name_order, check_biom_merge)
VALUES (%s, %s, %s, %s, %s, %s, %s)
RETURNING command_parameter_id"""
sql_params = [
[c_id, pname, p_type, reqd, default, no, chm]
for pname, p_type, reqd, default, no, chm in sql_param_values]
qdb.sql_connection.TRN.add(sql, sql_params, many=True)
qdb.sql_connection.TRN.execute()
# Add the artifact parameters
sql_type = """INSERT INTO qiita.parameter_artifact_type
(command_parameter_id, artifact_type_id)
VALUES (%s, %s)"""
supported_types = []
for pname, p_type, atypes in sql_artifact_params:
sql_params = [c_id, pname, p_type, True, None, None, False]
qdb.sql_connection.TRN.add(sql, sql_params)
pid = qdb.sql_connection.TRN.execute_fetchlast()
sql_params = [
[pid, qdb.util.convert_to_id(at, 'artifact_type')]
for at in atypes]
qdb.sql_connection.TRN.add(sql_type, sql_params, many=True)
supported_types.extend([atid for _, atid in sql_params])
# If the software type is 'artifact definition', there are a couple
# of extra steps
if software.type == 'artifact definition':
# If supported types is not empty, link the software with these
# types
if supported_types:
sql = """INSERT INTO qiita.software_artifact_type
(software_id, artifact_type_id)
VALUES (%s, %s)"""
sql_params = [[software.id, atid]
for atid in supported_types]
qdb.sql_connection.TRN.add(sql, sql_params, many=True)
# If this is the validate command, we need to add the
# provenance and name parameters. These are used internally,
# that's why we are adding them here
if name == 'Validate':
sql = """INSERT INTO qiita.command_parameter
(command_id, parameter_name, parameter_type,
required, default_value)
VALUES (%s, 'name', 'string', 'False',
'dflt_name'),
(%s, 'provenance', 'string', 'False', NULL)
"""
qdb.sql_connection.TRN.add(sql, [c_id, c_id])
# Add the outputs to the command
if outputs:
sql_args = []
for pname, at in outputs.items():
if isinstance(at, tuple):
sql_args.append(
[pname, c_id,
qdb.util.convert_to_id(at[0], 'artifact_type'),
at[1]])
else:
try:
at_id = qdb.util.convert_to_id(at, 'artifact_type')
except qdb.exceptions.QiitaDBLookupError:
msg = (f'Error creating {software.name}, {name}, '
f'{description} - Unknown artifact_type: '
f'{at}')
raise ValueError(msg)
sql_args.append([pname, c_id, at_id, False])
sql = """INSERT INTO qiita.command_output
(name, command_id, artifact_type_id,
check_biom_merge)
VALUES (%s, %s, %s, %s)"""
qdb.sql_connection.TRN.add(sql, sql_args, many=True)
qdb.sql_connection.TRN.execute()
return cls(c_id)
@property
def software(self):
"""The software to which this command belongs to
Returns
-------
qiita_db.software.Software
the software to which this command belongs to
"""
with qdb.sql_connection.TRN:
sql = """SELECT software_id
FROM qiita.software_command
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
return Software(qdb.sql_connection.TRN.execute_fetchlast())
@property
def name(self):
"""The name of the command
Returns
-------
str
The name of the command
"""
with qdb.sql_connection.TRN:
sql = """SELECT name
FROM qiita.software_command
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
return qdb.sql_connection.TRN.execute_fetchlast()
@property
def post_processing_cmd(self):
"""Additional processing commands required for merging
Returns
-------
str
Returns the additional processing command for merging
"""
with qdb.sql_connection.TRN:
sql = """SELECT post_processing_cmd
FROM qiita.software_command
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
cmd = qdb.sql_connection.TRN.execute_fetchlast()
if cmd:
# assume correctly formatted json data
# load data into dictionary; don't return JSON
return loads(qdb.sql_connection.TRN.execute_fetchlast())
return None
@property
def description(self):
"""The description of the command
Returns
-------
str
The description of the command
"""
with qdb.sql_connection.TRN:
sql = """SELECT description
FROM qiita.software_command
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
return qdb.sql_connection.TRN.execute_fetchlast()
@property
def parameters(self):
"""Returns the parameters that the command accepts
Returns
-------
dict
Dictionary of {parameter_name: [ptype, dflt]}
"""
with qdb.sql_connection.TRN:
sql = """SELECT parameter_name, parameter_type, default_value
FROM qiita.command_parameter
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
res = qdb.sql_connection.TRN.execute_fetchindex()
return {pname: [ptype, dflt] for pname, ptype, dflt in res}
@property
def required_parameters(self):
"""Returns the required parameters that the command accepts
Returns
-------
dict
Dictionary of {parameter_name: ptype}
"""
with qdb.sql_connection.TRN:
sql = """SELECT command_parameter_id, parameter_name,
parameter_type, array_agg(
artifact_type ORDER BY artifact_type) AS
artifact_type
FROM qiita.command_parameter
LEFT JOIN qiita.parameter_artifact_type
USING (command_parameter_id)
LEFT JOIN qiita.artifact_type USING (artifact_type_id)
WHERE command_id = %s AND required = True
GROUP BY command_parameter_id"""
qdb.sql_connection.TRN.add(sql, [self.id])
res = qdb.sql_connection.TRN.execute_fetchindex()
return {pname: (ptype, atype) for _, pname, ptype, atype in res}
@property
def optional_parameters(self):
"""Returns the optional parameters that the command accepts
Returns
-------
dict
Dictionary of {parameter_name: [ptype, default]}
"""
with qdb.sql_connection.TRN:
sql = """SELECT parameter_name, parameter_type, default_value
FROM qiita.command_parameter
WHERE command_id = %s AND required = false"""
qdb.sql_connection.TRN.add(sql, [self.id])
res = qdb.sql_connection.TRN.execute_fetchindex()
# Define a function to load the json storing the default parameters
# if ptype is multiple choice. When I added it to the for loop as
# a one liner if, made the code a bit hard to read
def dflt_fmt(dflt, ptype):
if ptype.startswith('mchoice'):
return loads(dflt)
return dflt
return {pname: [ptype, dflt_fmt(dflt, ptype)]
for pname, ptype, dflt in res}
@property
def default_parameter_sets(self):
"""Returns the list of default parameter sets
Returns
-------
generator
generator of qiita_db.software.DefaultParameters
"""
with qdb.sql_connection.TRN:
sql = """SELECT default_parameter_set_id
FROM qiita.default_parameter_set
WHERE command_id = %s
ORDER BY default_parameter_set_id"""
qdb.sql_connection.TRN.add(sql, [self.id])
res = qdb.sql_connection.TRN.execute_fetchflatten()
for pid in res:
yield DefaultParameters(pid)
@property
def outputs(self):
"""Returns the list of output artifact types
Returns
-------
list of str
The output artifact types
"""
with qdb.sql_connection.TRN:
sql = """SELECT name, artifact_type
FROM qiita.command_output
JOIN qiita.artifact_type USING (artifact_type_id)
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
return qdb.sql_connection.TRN.execute_fetchindex()
@property
def active(self):
"""Returns if the command is active or not
Returns
-------
bool
Whether the command is active or not
Notes
-----
This method differentiates between commands based on analysis_only or
the software type. The commands that are not for analysis (processing)
and are from an artifact definition software will return as active
if they have the same name than a command that is active; this helps
for situations where the processing plugins are updated but some
commands didn't change its version.
"""
with qdb.sql_connection.TRN:
cmd_type = self.software.type
if self.analysis_only or cmd_type == 'artifact definition':
sql = """SELECT active
FROM qiita.software_command
WHERE command_id = %s"""
else:
sql = """SELECT EXISTS (
SELECT active FROM qiita.software_command
WHERE name IN (
SELECT name FROM qiita.software_command
WHERE command_id = %s) AND active = true)"""
qdb.sql_connection.TRN.add(sql, [self.id])
return qdb.sql_connection.TRN.execute_fetchlast()
def activate(self):
"""Activates the command"""
sql = """UPDATE qiita.software_command
SET active = %s
WHERE command_id = %s"""
qdb.sql_connection.perform_as_transaction(sql, [True, self.id])
@property
def analysis_only(self):
"""Returns if the command is an analysis-only command
Returns
-------
bool
Whether the command is analysis only or not
"""
with qdb.sql_connection.TRN:
sql = """SELECT is_analysis
FROM qiita.software_command
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
return qdb.sql_connection.TRN.execute_fetchlast()
@property
def naming_order(self):
"""The ordered list of parameters to use to name the output artifacts
Returns
-------
list of str
"""
with qdb.sql_connection.TRN:
sql = """SELECT parameter_name
FROM qiita.command_parameter
WHERE command_id = %s AND name_order IS NOT NULL
ORDER BY name_order"""
qdb.sql_connection.TRN.add(sql, [self.id])
return qdb.sql_connection.TRN.execute_fetchflatten()
@property
def merging_scheme(self):
"""The values to check when merging the output result
Returns
-------
dict of {'parameters': [list of str],
'outputs': [list of str]
'ignore_parent_command': bool}
"""
with qdb.sql_connection.TRN:
sql = """SELECT parameter_name
FROM qiita.command_parameter
WHERE command_id = %s AND check_biom_merge = TRUE
ORDER BY parameter_name"""
qdb.sql_connection.TRN.add(sql, [self.id])
params = qdb.sql_connection.TRN.execute_fetchflatten()
sql = """SELECT name
FROM qiita.command_output
WHERE command_id = %s AND check_biom_merge = TRUE
ORDER BY name"""
qdb.sql_connection.TRN.add(sql, [self.id])
outputs = qdb.sql_connection.TRN.execute_fetchflatten()
sql = """SELECT ignore_parent_command
FROM qiita.software_command
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
ipc = qdb.sql_connection.TRN.execute_fetchlast()
return {'parameters': params,
'outputs': outputs,
'ignore_parent_command': ipc}
@property
def resource_allocation(self):
"""The resource allocation defined in the database for this command
Returns
-------
str
"""
with qdb.sql_connection.TRN:
sql = """SELECT allocation FROM
qiita.processing_job_resource_allocation
WHERE name = %s and
job_type = 'RESOURCE_PARAMS_COMMAND'"""
qdb.sql_connection.TRN.add(sql, [self.name])
result = qdb.sql_connection.TRN.execute_fetchflatten()
# if no matches for both type and name were found, query the
# 'default' value for the type
if not result:
sql = """SELECT allocation FROM
qiita.processing_job_resource_allocation WHERE
name = %s and job_type = 'RESOURCE_PARAMS_COMMAND'"""
qdb.sql_connection.TRN.add(sql, ['default'])
result = qdb.sql_connection.TRN.execute_fetchflatten()
if not result:
raise ValueError("Could not match '%s' to a resource "
"allocation!" % self.name)
return result[0]
@property
def processing_jobs(self):
"""All the processing_jobs that used this command
Returns
-------
list of qiita_db.processing_job.ProcessingJob
List of jobs that used this command.
"""
with qdb.sql_connection.TRN:
sql = """SELECT processing_job_id FROM
qiita.processing_job
WHERE command_id = %s"""
qdb.sql_connection.TRN.add(sql, [self.id])
jids = qdb.sql_connection.TRN.execute_fetchflatten()
return [qdb.processing_job.ProcessingJob(j) for j in jids]
class Software(qdb.base.QiitaObject):
r"""A software package available in the system
Attributes
----------
name
version
description
commands
publications
environment_name
start_script
Methods
-------
add_publications
create
See Also
--------
qiita_db.software.Command
"""
_table = "software"
@classmethod
def iter(cls, active=True):
"""Iterates over all active software
Parameters
----------
active : bool, optional
If True will only return active software
Returns
-------
list of qiita_db.software.Software
The software objects
"""
sql = """SELECT software_id
FROM qiita.software {0}
ORDER BY software_id""".format(
'WHERE active = True' if active else '')
with qdb.sql_connection.TRN:
qdb.sql_connection.TRN.add(sql)
for s_id in qdb.sql_connection.TRN.execute_fetchflatten():
yield cls(s_id)
@classmethod
def deactivate_all(cls):
"""Deactivates all the plugins in the system"""
with qdb.sql_connection.TRN:
sql = "UPDATE qiita.software SET active = False"
qdb.sql_connection.TRN.add(sql)
sql = "UPDATE qiita.software_command SET active = False"
qdb.sql_connection.TRN.add(sql)
qdb.sql_connection.TRN.execute()
@classmethod
def from_file(cls, fp, update=False):
"""Installs/updates a plugin from a plugin configuration file
Parameters
----------
fp : str
Path to the plugin configuration file
update : bool, optional
If true, update the values in the database with the current values
in the config file. Otherwise, use stored values and warn if config
file contents and database contents do not match
Returns
-------
qiita_db.software.Software
The software object for the contents of `fp`
Raises
------
qiita_db.exceptions.QiitaDBOperationNotPermittedError
If the plugin type in the DB and in the config file doesn't match
If the (client_id, client_secret) pair in the DB and in the config
file doesn't match
"""
config = ConfigParser()
with open(fp, newline=None) as conf_file:
config.read_file(conf_file)
name = config.get('main', 'NAME')
version = config.get('main', 'VERSION')
description = config.get('main', 'DESCRIPTION')
env_script = config.get('main', 'ENVIRONMENT_SCRIPT')
start_script = config.get('main', 'START_SCRIPT')
software_type = config.get('main', 'PLUGIN_TYPE')
publications = config.get('main', 'PUBLICATIONS')
publications = loads(publications) if publications else []
client_id = config.get('oauth2', 'CLIENT_ID')
client_secret = config.get('oauth2', 'CLIENT_SECRET')
if cls.exists(name, version):
# This plugin already exists, check that all the values are the
# same and return the existing plugin
with qdb.sql_connection.TRN:
sql = """SELECT software_id
FROM qiita.software
WHERE name = %s AND version = %s"""
qdb.sql_connection.TRN.add(sql, [name, version])
instance = cls(qdb.sql_connection.TRN.execute_fetchlast())
warning_values = []
sql_update = """UPDATE qiita.software
SET {0} = %s
WHERE software_id = %s"""
values = [description, env_script, start_script]
attrs = ['description', 'environment_script', 'start_script']
for value, attr in zip(values, attrs):
if value != instance.__getattribute__(attr):
if update:
qdb.sql_connection.TRN.add(
sql_update.format(attr), [value, instance.id])
else:
warning_values.append(attr)
# Having a different plugin type should be an error,
# independently if the user is trying to update plugins or not
if software_type != instance.type:
raise qdb.exceptions.QiitaDBOperationNotPermittedError(
'The plugin type of the plugin "%s" version %s does '
'not match the one in the system' % (name, version))
if publications != instance.publications:
if update:
instance.add_publications(publications)
else:
warning_values.append('publications')
if (client_id != instance.client_id or
client_secret != instance.client_secret):
if update:
sql = """INSERT INTO qiita.oauth_identifiers
(client_id, client_secret)
SELECT %s, %s
WHERE NOT EXISTS(SELECT *
FROM qiita.oauth_identifiers
WHERE client_id = %s
AND client_secret = %s)"""
qdb.sql_connection.TRN.add(
sql, [client_id, client_secret,
client_id, client_secret])
sql = """UPDATE qiita.oauth_software
SET client_id = %s
WHERE software_id = %s"""
qdb.sql_connection.TRN.add(
sql, [client_id, instance.id])
else:
raise qdb.exceptions.QiitaDBOperationNotPermittedError(
'The (client_id, client_secret) pair of the '
'plugin "%s" version "%s" does not match the one '
'in the system' % (name, version))
if warning_values:
warnings.warn(
'Plugin "%s" version "%s" config file does not match '
'with stored information. Check the config file or '
'run "qiita plugin update" to update the plugin '
'information. Offending values: %s'
% (name, version, ", ".join(sorted(warning_values))),
qdb.exceptions.QiitaDBWarning)
qdb.sql_connection.TRN.execute()
else:
# This is a new plugin, create it
instance = cls.create(
name, version, description, env_script, start_script,
software_type, publications=publications, client_id=client_id,
client_secret=client_secret)
return instance
@classmethod
def exists(cls, name, version):
"""Returns whether the plugin (name, version) already exists
Parameters
----------
name : str
The name of the plugin
version : str
The version of the plugin
"""
with qdb.sql_connection.TRN:
sql = """SELECT EXISTS(
SELECT * FROM qiita.software
WHERE name = %s AND version = %s)"""
qdb.sql_connection.TRN.add(sql, [name, version])
return qdb.sql_connection.TRN.execute_fetchlast()
@classmethod
def create(cls, name, version, description, environment_script,
start_script, software_type, publications=None,
client_id=None, client_secret=None):
r"""Creates a new software in the system
Parameters
----------
name : str
The name of the software
version : str
The version of the software
description : str
The description of the software
environment_script : str
The script used to start the environment in which the plugin runs