Skip to content

Commit 6bb8729

Browse files
committed
API: add /api/v1/study/<id>/associations to retrieve comprehensive id, path, processing information for a study
1 parent 4c1e91f commit 6bb8729

File tree

3 files changed

+243
-15
lines changed

3 files changed

+243
-15
lines changed

qiita_pet/handlers/rest/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# -----------------------------------------------------------------------------
88

99
from .study import StudyHandler, StudyCreatorHandler, StudyStatusHandler
10+
from .study_association import StudyAssociationHandler
1011
from .study_samples import (StudySamplesHandler, StudySamplesInfoHandler,
1112
StudySamplesCategoriesHandler,
1213
StudySamplesDetailHandler,
@@ -25,6 +26,7 @@
2526
ENDPOINTS = (
2627
(r"/api/v1/study$", StudyCreatorHandler),
2728
(r"/api/v1/study/([0-9]+)$", StudyHandler),
29+
(r"/api/v1/study/([0-9]+)/associations$", StudyAssociationHandler),
2830
(r"/api/v1/study/([0-9]+)/samples/categories=([a-zA-Z\-0-9\.:,_]*)",
2931
StudySamplesCategoriesHandler),
3032
(r"/api/v1/study/([0-9]+)/samples", StudySamplesHandler),
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# -----------------------------------------------------------------------------
2+
# Copyright (c) 2014--, The Qiita Development Team.
3+
#
4+
# Distributed under the terms of the BSD 3-clause License.
5+
#
6+
# The full license is in the file LICENSE, distributed with this software.
7+
# -----------------------------------------------------------------------------
8+
import warnings
9+
10+
from tornado.escape import json_decode
11+
12+
from qiita_db.handlers.oauth2 import authenticate_oauth
13+
from qiita_db.study import StudyPerson, Study
14+
from qiita_db.user import User
15+
from .rest_handler import RESTHandler
16+
from qiita_db.metadata_template.constants import SAMPLE_TEMPLATE_COLUMNS
17+
18+
19+
# terms used more than once
20+
_STUDY = 'study'
21+
_PREP = 'prep'
22+
_FILEPATH = 'filepath'
23+
_STATUS = 'status'
24+
_ARTIFACT = 'artifact'
25+
_SAMPLE = 'sample'
26+
_METADATA = 'metadata'
27+
_TEMPLATE = 'template'
28+
_ID = 'id'
29+
_PROCESSING = 'processing'
30+
_TYPE = 'type'
31+
32+
# payload keys
33+
STUDY_ID = f'{_STUDY}_{_ID}'
34+
STUDY_SAMPLE_METADATA_FILEPATH = f'{_STUDY}_{_SAMPLE}_{_METADATA}_{_FILEPATH}'
35+
PREP_TEMPLATES = f'{_PREP}_{_TEMPLATE}s'
36+
PREP_ID = f'{_PREP}_{_ID}'
37+
PREP_STATUS = f'{_PREP}_{_STATUS}'
38+
PREP_SAMPLE_METADATA_FILEPATH = f'{_PREP}_{_SAMPLE}_{_METADATA}_{_FILEPATH}'
39+
PREP_DATA_TYPE = f'{_PREP}_data_{_TYPE}'
40+
PREP_HUMAN_FILTERING = f'{_PREP}_human_filtering'
41+
PREP_ARTIFACTS = f'{_PREP}_{_ARTIFACT}s'
42+
ARTIFACT_ID = f'{_ARTIFACT}_{_ID}'
43+
ARTIFACT_STATUS = f'{_ARTIFACT}_{_STATUS}'
44+
ARTIFACT_PARENT_IDS = f'{_ARTIFACT}_parent_{_ID}s'
45+
ARTIFACT_BASAL_ID = f'{_ARTIFACT}_basal_{_ID}'
46+
ARTIFACT_PROCESSING_ID = f'{_ARTIFACT}_{_PROCESSING}_{_ID}'
47+
ARTIFACT_PROCESSING_NAME = f'{_ARTIFACT}_{_PROCESSING}_name'
48+
ARTIFACT_PROCESSING_ARGUMENTS = f'{_ARTIFACT}_{_PROCESSING}_arguments'
49+
ARTIFACT_FILEPATHS = f'{_ARTIFACT}_{_FILEPATH}s'
50+
ARTIFACT_FILEPATH = f'{_ARTIFACT}_{_FILEPATH}'
51+
ARTIFACT_FILEPATH_TYPE = f'{_ARTIFACT}_{_FILEPATH}_{_TYPE}'
52+
ARTIFACT_FILEPATH_ID = f'{_ARTIFACT}_{_FILEPATH}_{_ID}'
53+
54+
55+
def _most_recent_template_path(template):
56+
filepaths = template.get_filepaths()
57+
58+
# the test dataset shows that a prep can exist without a prep template
59+
if len(filepaths) == 0:
60+
return None
61+
62+
metadata_paths = sorted(filepaths, reverse=True)
63+
64+
# [0] -> the highest file by ID
65+
# [1] -> the filepath
66+
return metadata_paths[0][1]
67+
68+
69+
def _set_study(payload, study):
70+
filepath = _most_recent_template_path(study.sample_template)
71+
72+
payload[STUDY_ID] = study.id
73+
payload[STUDY_SAMPLE_METADATA_FILEPATH] = filepath
74+
75+
76+
def _set_prep_templates(payload, study):
77+
template_data = []
78+
for pt in study.prep_templates():
79+
_set_prep_template(template_data, pt)
80+
payload[PREP_TEMPLATES] = template_data
81+
82+
83+
def _get_human_filtering(prep_template):
84+
# .current_human_filtering does not describe what the human filter is
85+
if prep_template.artifact is not None:
86+
return prep_template.artifact.human_reads_filter_method
87+
88+
89+
def _set_prep_template(template_payload, prep_template):
90+
filepath = _most_recent_template_path(prep_template)
91+
92+
current_template = {}
93+
current_template[PREP_ID] = prep_template.id
94+
current_template[PREP_STATUS] = prep_template.status
95+
current_template[PREP_SAMPLE_METADATA_FILEPATH] = filepath
96+
current_template[PREP_DATA_TYPE] = prep_template.data_type()
97+
current_template[PREP_HUMAN_FILTERING] = _get_human_filtering(prep_template)
98+
99+
_set_artifacts(current_template, prep_template)
100+
101+
template_payload.append(current_template)
102+
103+
104+
def _get_artifacts(prep_template):
105+
pending_artifact_objects = [prep_template.artifact, ]
106+
all_artifact_objects = set(pending_artifact_objects[:])
107+
108+
while pending_artifact_objects:
109+
artifact = pending_artifact_objects.pop()
110+
pending_artifact_objects.extend(artifact.children)
111+
all_artifact_objects.update(set(artifact.children))
112+
113+
return sorted(all_artifact_objects, key=lambda artifact: artifact.id)
114+
115+
116+
def _set_artifacts(template_payload, prep_template):
117+
prep_artifacts = []
118+
119+
if prep_template.artifact is None:
120+
basal_id = None
121+
else:
122+
basal_id = prep_template.artifact.id
123+
124+
for artifact in _get_artifacts(prep_template):
125+
_set_artifact(prep_artifacts, artifact, basal_id)
126+
template_payload[PREP_ARTIFACTS] = prep_artifacts
127+
128+
129+
def _set_artifact(prep_artifacts, artifact, basal_id):
130+
artifact_payload = {}
131+
artifact_payload[ARTIFACT_ID] = artifact.id
132+
133+
# Prep uses .status, artifact uses .visibility
134+
# favoring .status as visibility implies a UI
135+
artifact_payload[ARTIFACT_STATUS] = artifact.visibility
136+
137+
parents = [parent.id for parent in artifact.parents]
138+
artifact_payload[ARTIFACT_PARENT_IDS] = parents if parents else None
139+
artifact_payload[ARTIFACT_BASAL_ID] = basal_id
140+
141+
_set_artifact_processing(artifact_payload, artifact)
142+
_set_artifact_filepaths(artifact_payload, artifact)
143+
144+
prep_artifacts.append(artifact_payload)
145+
146+
147+
def _set_artifact_processing(artifact_payload, artifact):
148+
processing_parameters = artifact.processing_parameters
149+
if processing_parameters is None:
150+
artifact_processing_id = None
151+
artifact_processing_name = None
152+
artifact_processing_arguments = None
153+
else:
154+
command = processing_parameters.command
155+
artifact_processing_id = command.id
156+
artifact_processing_name = command.name
157+
artifact_processing_arguments = processing_parameters.values
158+
159+
artifact_payload[ARTIFACT_PROCESSING_ID] = artifact_processing_id
160+
artifact_payload[ARTIFACT_PROCESSING_NAME] = artifact_processing_name
161+
artifact_payload[ARTIFACT_PROCESSING_ARGUMENTS] = artifact_processing_arguments
162+
163+
164+
def _set_artifact_filepaths(artifact_payload, artifact):
165+
artifact_filepaths = []
166+
for filepath_data in artifact.filepaths:
167+
local_payload = {}
168+
local_payload[ARTIFACT_FILEPATH] = filepath_data['fp']
169+
local_payload[ARTIFACT_FILEPATH_ID] = filepath_data['fp_id']
170+
local_payload[ARTIFACT_FILEPATH_TYPE] = filepath_data['fp_type']
171+
artifact_filepaths.append(local_payload)
172+
173+
# the test study includes an artifact which does not have filepaths
174+
if len(artifact_filepaths) == 0:
175+
artifact_filepaths = None
176+
177+
artifact_payload[ARTIFACT_FILEPATHS] = artifact_filepaths
178+
179+
180+
class StudyAssociationHandler(RESTHandler):
181+
@authenticate_oauth
182+
def get(self, study_id):
183+
study = self.safe_get_study(study_id)
184+
if study is None:
185+
return
186+
187+
payload = {}
188+
_set_study(payload, study)
189+
_set_prep_templates(payload, study)
190+
self.write(payload)
191+
self.finish()
192+
193+
194+
# get all the things
195+

qiita_pet/test/rest/test_study_associations.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616

1717
class StudyAssociationTests(RESTHandlerTestCase):
1818
def test_get_valid(self):
19-
IGNORE = IGNORE
20-
exp = {'study': 1,
19+
IGNORE = 'IGNORE'
20+
exp = {'study_id': 1,
21+
'study_sample_metadata_filepath': IGNORE,
2122
'prep_templates': [{'prep_id': 1,
22-
'prep_filepath': IGNORE,
23-
'prep_datatype': '18S',
23+
'prep_status': 'private',
24+
'prep_sample_metadata_filepath': IGNORE,
25+
'prep_data_type': '18S',
2426
'prep_human_filtering': 'The greatest human filtering method',
2527
'prep_artifacts': [{'artifact_id': 1,
26-
'artifact_parent_ids': [1],
28+
'artifact_status': 'private',
29+
'artifact_parent_ids': None,
2730
'artifact_basal_id': 1,
2831
'artifact_processing_id': None,
2932
'artifact_processing_name': None,
@@ -35,7 +38,8 @@ def test_get_valid(self):
3538
'artifact_filepath': IGNORE,
3639
'artifact_filepath_type': 'raw_barcodes'}]},
3740
{'artifact_id': 2,
38-
'artifact_parent_ids': None,
41+
'artifact_status': 'private',
42+
'artifact_parent_ids': [1],
3943
'artifact_basal_id': 1,
4044
'artifact_processing_id': 1,
4145
'artifact_processing_name': 'Split libraries FASTQ',
@@ -60,6 +64,7 @@ def test_get_valid(self):
6064
'artifact_filepath_id': 5,
6165
'artifact_filepath_type': 'preprocessed_demux'}]},
6266
{'artifact_id': 3,
67+
'artifact_status': 'private',
6368
'artifact_parent_ids': [1],
6469
'artifact_basal_id': 1,
6570
'artifact_processing_id': 1,
@@ -69,14 +74,15 @@ def test_get_valid(self):
6974
'min_per_read_length_fraction': '0.75',
7075
'sequence_max_n': '0',
7176
'rev_comp_barcode': 'False',
72-
'rev_comp_mapping_barcodes': 'False',
77+
'rev_comp_mapping_barcodes': 'True',
7378
'rev_comp': 'False',
7479
'phred_quality_threshold': '3',
7580
'barcode_type': 'golay_12',
7681
'max_barcode_errors': '1.5',
7782
'phred_offset': 'auto'},
78-
'artifact_filepaths': []},
83+
'artifact_filepaths': None},
7984
{'artifact_id': 4,
85+
'artifact_status': 'private',
8086
'artifact_parent_ids': [2],
8187
'artifact_basal_id': 1,
8288
'artifact_processing_id': 3,
@@ -92,6 +98,7 @@ def test_get_valid(self):
9298
'artifact_filepath': IGNORE,
9399
'artifact_filepath_type': 'biom'}]},
94100
{'artifact_id': 5,
101+
'artifact_status': 'private',
95102
'artifact_parent_ids': [2],
96103
'artifact_basal_id': 1,
97104
'artifact_processing_id': 3,
@@ -107,6 +114,7 @@ def test_get_valid(self):
107114
'artifact_filepath': IGNORE,
108115
'artifact_filepath_type': 'biom'}]},
109116
{'artifact_id': 6,
117+
'artifact_status': 'private',
110118
'artifact_parent_ids': [2],
111119
'artifact_basal_id': 1,
112120
'artifact_processing_id': 3,
@@ -122,29 +130,52 @@ def test_get_valid(self):
122130
'artifact_filepath': IGNORE,
123131
'artifact_filepath_type': 'biom'}]}]},
124132
{'prep_id': 2,
125-
'prep_filepath': IGNORE,
126-
'prep_datatype': '18S',
133+
'prep_status': 'private',
134+
'prep_sample_metadata_filepath': IGNORE,
135+
'prep_data_type': '18S',
127136
'prep_human_filtering': None,
128137
'prep_artifacts': [{'artifact_id': 7,
129-
'artifact_parent_ids': [],
138+
'artifact_parent_ids': None,
130139
'artifact_basal_id': 7,
140+
'artifact_status': 'private',
131141
'artifact_processing_id': None,
132142
'artifact_processing_name': None,
133143
'artifact_processing_arguments': None,
134144
'artifact_filepaths': [{'artifact_filepath_id': 22,
135145
'artifact_filepath': IGNORE,
136146
'artifact_filepath_type': 'biom'}]}]}]}
137147

138-
response = self.get('/api/v1/study-association/1', headers=self.headers)
148+
response = self.get('/api/v1/study/1/associations', headers=self.headers)
139149
self.assertEqual(response.code, 200)
140150
obs = json_decode(response.body)
151+
152+
def _process_dict(d):
153+
return [(d, k) for k in d]
154+
155+
def _process_list(l):
156+
if l is None:
157+
return []
158+
159+
return [dk for d in l
160+
for dk in _process_dict(d)]
161+
162+
stack = _process_dict(obs)
163+
while stack:
164+
(d, k) = stack.pop()
165+
if k.endswith('filepath'):
166+
d[k] = IGNORE
167+
elif k.endswith('filepaths'):
168+
stack.extend(_process_list(d[k]))
169+
elif k.endswith('templates'):
170+
stack.extend(_process_list(d[k]))
171+
elif k.endswith('artifacts'):
172+
stack.extend(_process_list(d[k]))
173+
141174
self.assertEqual(obs, exp)
142175

143176
def test_get_invalid(self):
144-
response = self.get('/api/v1/study-association/0', headers=self.headers)
177+
response = self.get('/api/v1/study/0/associations', headers=self.headers)
145178
self.assertEqual(response.code, 404)
146-
self.assertEqual(json_decode(response.body),
147-
{'message': 'Study not found'})
148179

149180

150181
if __name__ == '__main__':

0 commit comments

Comments
 (0)