Skip to content

Commit bc2244b

Browse files
authored
Study associations (#3487)
* TST: study associations endpoint test * MAINT: be defensive on artifact prep assocation expectations * API: add /api/v1/study/<id>/associations to retrieve comprehensive id, path, processing information for a study * DOC: note schema, add some additiona doc strings and comments * LINT: pass w/ ruff check * LINT: now with flake8 * Rollback: allow an artifact to be unlinked * DOC: note internal rest endpoint * LINT: flake8 * Remove extraneous sort * An incomplete prep will not have an artifact
1 parent c9e45cb commit bc2244b

File tree

5 files changed

+432
-2
lines changed

5 files changed

+432
-2
lines changed

qiita_db/artifact.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,8 +1463,17 @@ def prep_templates(self):
14631463
FROM qiita.preparation_artifact
14641464
WHERE artifact_id = %s"""
14651465
qdb.sql_connection.TRN.add(sql, [self.id])
1466-
return [qdb.metadata_template.prep_template.PrepTemplate(pt_id)
1467-
for pt_id in qdb.sql_connection.TRN.execute_fetchflatten()]
1466+
templates = [qdb.metadata_template.prep_template.PrepTemplate(pt_id) # noqa
1467+
for pt_id in qdb.sql_connection.TRN.execute_fetchflatten()] # noqa
1468+
1469+
if len(templates) > 1:
1470+
# We never expect an artifact to be associated with multiple
1471+
# preparations
1472+
ids = [p.id for p in templates]
1473+
msg = f"Artifact({self.id}) associated with preps: {sorted(ids)}"
1474+
raise ValueError(msg)
1475+
1476+
return templates
14681477

14691478
@property
14701479
def study(self):

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: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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+
9+
10+
from qiita_db.handlers.oauth2 import authenticate_oauth
11+
from .rest_handler import RESTHandler
12+
13+
14+
# terms used more than once
15+
_STUDY = 'study'
16+
_PREP = 'prep'
17+
_FILEPATH = 'filepath'
18+
_STATUS = 'status'
19+
_ARTIFACT = 'artifact'
20+
_SAMPLE = 'sample'
21+
_METADATA = 'metadata'
22+
_TEMPLATE = 'template'
23+
_ID = 'id'
24+
_PROCESSING = 'processing'
25+
_TYPE = 'type'
26+
27+
# payload keys
28+
STUDY_ID = f'{_STUDY}_{_ID}'
29+
STUDY_SAMPLE_METADATA_FILEPATH = f'{_STUDY}_{_SAMPLE}_{_METADATA}_{_FILEPATH}'
30+
PREP_TEMPLATES = f'{_PREP}_{_TEMPLATE}s'
31+
PREP_ID = f'{_PREP}_{_ID}'
32+
PREP_STATUS = f'{_PREP}_{_STATUS}'
33+
PREP_SAMPLE_METADATA_FILEPATH = f'{_PREP}_{_SAMPLE}_{_METADATA}_{_FILEPATH}'
34+
PREP_DATA_TYPE = f'{_PREP}_data_{_TYPE}'
35+
PREP_HUMAN_FILTERING = f'{_PREP}_human_filtering'
36+
PREP_ARTIFACTS = f'{_PREP}_{_ARTIFACT}s'
37+
ARTIFACT_ID = f'{_ARTIFACT}_{_ID}'
38+
ARTIFACT_STATUS = f'{_ARTIFACT}_{_STATUS}'
39+
ARTIFACT_PARENT_IDS = f'{_ARTIFACT}_parent_{_ID}s'
40+
ARTIFACT_BASAL_ID = f'{_ARTIFACT}_basal_{_ID}'
41+
ARTIFACT_PROCESSING_ID = f'{_ARTIFACT}_{_PROCESSING}_{_ID}'
42+
ARTIFACT_PROCESSING_NAME = f'{_ARTIFACT}_{_PROCESSING}_name'
43+
ARTIFACT_PROCESSING_ARGUMENTS = f'{_ARTIFACT}_{_PROCESSING}_arguments'
44+
ARTIFACT_FILEPATHS = f'{_ARTIFACT}_{_FILEPATH}s'
45+
ARTIFACT_FILEPATH = f'{_ARTIFACT}_{_FILEPATH}'
46+
ARTIFACT_FILEPATH_TYPE = f'{_ARTIFACT}_{_FILEPATH}_{_TYPE}'
47+
ARTIFACT_FILEPATH_ID = f'{_ARTIFACT}_{_FILEPATH}_{_ID}'
48+
49+
50+
def _most_recent_template_path(template):
51+
"""Obtain the most recent available template filepath"""
52+
filepaths = template.get_filepaths()
53+
54+
# the test dataset shows that a prep can exist without a prep template
55+
if len(filepaths) == 0:
56+
return None
57+
58+
# [0] -> the highest file by ID
59+
# [1] -> the filepath
60+
return filepaths[0][1]
61+
62+
63+
def _set_study(payload, study):
64+
"""Set study level information"""
65+
filepath = _most_recent_template_path(study.sample_template)
66+
67+
payload[STUDY_ID] = study.id
68+
payload[STUDY_SAMPLE_METADATA_FILEPATH] = filepath
69+
70+
71+
def _set_prep_templates(payload, study):
72+
"""Set prep template level information"""
73+
template_data = []
74+
for pt in study.prep_templates():
75+
_set_prep_template(template_data, pt)
76+
payload[PREP_TEMPLATES] = template_data
77+
78+
79+
def _get_human_filtering(prep_template):
80+
"""Obtain the human filtering if applied"""
81+
# .current_human_filtering does not describe what the human filter is
82+
# so we will examine the first artifact off the prep
83+
if prep_template.artifact is not None:
84+
return prep_template.artifact.human_reads_filter_method
85+
86+
87+
def _set_prep_template(template_payload, prep_template):
88+
"""Set an individual prep template information"""
89+
filepath = _most_recent_template_path(prep_template)
90+
91+
current_template = {}
92+
current_template[PREP_ID] = prep_template.id
93+
current_template[PREP_STATUS] = prep_template.status
94+
current_template[PREP_SAMPLE_METADATA_FILEPATH] = filepath
95+
current_template[PREP_DATA_TYPE] = prep_template.data_type()
96+
current_template[PREP_HUMAN_FILTERING] = _get_human_filtering(prep_template) # noqa
97+
98+
_set_artifacts(current_template, prep_template)
99+
100+
template_payload.append(current_template)
101+
102+
103+
def _get_artifacts(prep_template):
104+
"""Get artifact information associated with a prep"""
105+
if prep_template.artifact is None:
106+
return []
107+
108+
pending_artifact_objects = [prep_template.artifact, ]
109+
all_artifact_objects = set(pending_artifact_objects[:])
110+
111+
while pending_artifact_objects:
112+
artifact = pending_artifact_objects.pop()
113+
pending_artifact_objects.extend(artifact.children)
114+
all_artifact_objects.update(set(artifact.children))
115+
116+
return sorted(all_artifact_objects, key=lambda artifact: artifact.id)
117+
118+
119+
def _set_artifacts(template_payload, prep_template):
120+
"""Set artifact information specific to a prep"""
121+
prep_artifacts = []
122+
123+
if prep_template.artifact is None:
124+
basal_id = None
125+
else:
126+
basal_id = prep_template.artifact.id
127+
128+
for artifact in _get_artifacts(prep_template):
129+
_set_artifact(prep_artifacts, artifact, basal_id)
130+
template_payload[PREP_ARTIFACTS] = prep_artifacts
131+
132+
133+
def _set_artifact(prep_artifacts, artifact, basal_id):
134+
"""Set artifact specific information"""
135+
artifact_payload = {}
136+
artifact_payload[ARTIFACT_ID] = artifact.id
137+
138+
# Prep uses .status, artifact uses .visibility
139+
# favoring .status as visibility implies a UI
140+
artifact_payload[ARTIFACT_STATUS] = artifact.visibility
141+
142+
parents = [parent.id for parent in artifact.parents]
143+
artifact_payload[ARTIFACT_PARENT_IDS] = parents if parents else None
144+
artifact_payload[ARTIFACT_BASAL_ID] = basal_id
145+
146+
_set_artifact_processing(artifact_payload, artifact)
147+
_set_artifact_filepaths(artifact_payload, artifact)
148+
149+
prep_artifacts.append(artifact_payload)
150+
151+
152+
def _set_artifact_processing(artifact_payload, artifact):
153+
"""Set processing parameter information associated with an artifact"""
154+
processing_parameters = artifact.processing_parameters
155+
if processing_parameters is None:
156+
artifact_processing_id = None
157+
artifact_processing_name = None
158+
artifact_processing_arguments = None
159+
else:
160+
command = processing_parameters.command
161+
artifact_processing_id = command.id
162+
artifact_processing_name = command.name
163+
artifact_processing_arguments = processing_parameters.values
164+
165+
artifact_payload[ARTIFACT_PROCESSING_ID] = artifact_processing_id
166+
artifact_payload[ARTIFACT_PROCESSING_NAME] = artifact_processing_name
167+
artifact_payload[ARTIFACT_PROCESSING_ARGUMENTS] = artifact_processing_arguments # noqa
168+
169+
170+
def _set_artifact_filepaths(artifact_payload, artifact):
171+
"""Set filepath information associated with an artifact"""
172+
artifact_filepaths = []
173+
for filepath_data in artifact.filepaths:
174+
local_payload = {}
175+
local_payload[ARTIFACT_FILEPATH] = filepath_data['fp']
176+
local_payload[ARTIFACT_FILEPATH_ID] = filepath_data['fp_id']
177+
local_payload[ARTIFACT_FILEPATH_TYPE] = filepath_data['fp_type']
178+
artifact_filepaths.append(local_payload)
179+
180+
# the test study includes an artifact which does not have filepaths
181+
if len(artifact_filepaths) == 0:
182+
artifact_filepaths = None
183+
184+
artifact_payload[ARTIFACT_FILEPATHS] = artifact_filepaths
185+
186+
187+
class StudyAssociationHandler(RESTHandler):
188+
@authenticate_oauth
189+
def get(self, study_id):
190+
study = self.safe_get_study(study_id)
191+
if study is None:
192+
return
193+
194+
# schema:
195+
# STUDY_ID: <int>,
196+
# STUDY_SAMPLE_METADATA_FILEPATH: <path>,
197+
# PREP_TEMPLATES: None | list[dict]
198+
# PREP_ID: <int>,
199+
# PREP_STATUS: <str>,
200+
# PREP_SAMPLE_METADATA_FILEPATH: <path>,
201+
# PREP_DATA_TYPE: <str>,
202+
# PREP_HUMAN_FILTERING: None | <str>,
203+
# PREP_ARTIFACTS: None | list[dict]
204+
# ARTIFACT_ID: <int>,
205+
# ARTIFACT_STATUS: <str>,
206+
# ARTIFACT_PARENT_IDS: None | list[int],
207+
# ARTIFACT_BASAL_ID: None | <int>,
208+
# ARTIFACT_PROCESSING_ID: None | <int>,
209+
# ARTIFACT_PROCESSING_NAME: None | <str,
210+
# ARTIFACT_PROCESSING_ARGUMENTS: None | dict[noschema]
211+
# ARTIFACT_FILEPATHS: None | list[dict]
212+
# ARTIFACT_FILEPATH_ID: <int>,
213+
# ARTIFACT_FILEPATH: <path>,
214+
# ARTIFACT_FILEPATH_TYPE': <str>
215+
#
216+
payload = {}
217+
_set_study(payload, study)
218+
_set_prep_templates(payload, study)
219+
self.write(payload)
220+
self.finish()

qiita_pet/support_files/doc/source/dev/rest.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ This is the currently internal but planned to be external (general users) API.
106106
+--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+
107107
|GET | ``/api/v1/study/<int>/status`` | The status of a study (whether or not the study: is public, has sample information, sample information has warnings and a list of existing preparations. |
108108
+--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+
109+
|GET | ``/api/v1/study/<int>/associations`` | Comprehensive information about a study, associated prep and artifact information, and file locations |
110+
+--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+
109111
|GET | ``/api/v1/person`` | Get list of persons. |
110112
+--------+-----------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+
111113
|GET | ``/api/v1/person?name=foo&affiliation=bar`` | See if a person exists. |

0 commit comments

Comments
 (0)