Skip to content

Commit 8f6d193

Browse files
ElDeveloperantgonza
authored andcommitted
A minimal REST API for Qiita (#2094)
* TST: Add initial test cases for study handler * ENH: Add initial study rest api * API: test if a study exists * ENH: oauth2 forced * Get back basic study deets * TST: test for samples collection * API: rest get sample IDs from a study * ENH: samples/info handler * broken routes * API: request sample metadata * ENH/API: Add methods to check for a study person * ENH/API: Add POST methods for study person * TST: Add tests for from_name_and_affiliation * TST: study creation * BUG: Add headers to tests * ENH: create study * Adjust GET on study description * API: Add endpoints for preparation creation * TST: 200 :D * TST: Correctly verify study instantiation * TST: prep artifact creation * ENH/API: associate artifacts with a preparation * TST: test study statys * ENH: study status * Removed trailing whitespace * STY: PEP8 * MAINT: refactor, centralize setup boilerplate * REFACTOR: Remove repeated code * DOC: Remove unnecessary comments * REFACTOR: Missing removal of pattern * STY: Fix PEP8 errors * BUG: Incorrectly changed error code * BUG/TST: Fix typo in tests * Addressing an @antgonza comment * Another @antgonza comment * RVW: Address review comments * ENH: Cleanup webserver and name-spaces * ENH: Improve error messages * ENH: Add more descriptive error message * TST: Exercise different argument types * DOC: Add documentation for REST API * ENH: Remove extra comma
1 parent d803f42 commit 8f6d193

18 files changed

+1201
-2
lines changed

qiita_db/study.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,33 @@ def exists(cls, name, affiliation):
12671267
qdb.sql_connection.TRN.add(sql, [name, affiliation])
12681268
return qdb.sql_connection.TRN.execute_fetchlast()
12691269

1270+
@classmethod
1271+
def from_name_and_affiliation(cls, name, affiliation):
1272+
"""Gets a StudyPerson object based on the name and affiliation
1273+
1274+
Parameters
1275+
----------
1276+
name: str
1277+
Name of the person
1278+
affiliation : str
1279+
institution with which the person is affiliated
1280+
1281+
Returns
1282+
-------
1283+
StudyPerson
1284+
The StudyPerson for the name and affiliation
1285+
"""
1286+
with qdb.sql_connection.TRN:
1287+
if not cls.exists(name, affiliation):
1288+
raise qdb.exceptions.QiitaDBLookupError(
1289+
'Study person does not exist')
1290+
1291+
sql = """SELECT study_person_id FROM qiita.{0}
1292+
WHERE name = %s
1293+
AND affiliation = %s""".format(cls._table)
1294+
qdb.sql_connection.TRN.add(sql, [name, affiliation])
1295+
return cls(qdb.sql_connection.TRN.execute_fetchlast())
1296+
12701297
@classmethod
12711298
def create(cls, name, email, affiliation, address=None, phone=None):
12721299
"""Create a StudyPerson object, checking if person already exists.

qiita_db/test/test_study.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ def test_delete(self):
4646
self.assertFalse(
4747
qdb.study.StudyPerson.exists('SomeDude', 'affil'))
4848

49+
def test_retrieve_non_existant_people(self):
50+
with self.assertRaises(qdb.exceptions.QiitaDBLookupError):
51+
qdb.study.StudyPerson.from_name_and_affiliation('Boaty McBoatFace',
52+
'UCSD')
53+
54+
p = qdb.study.StudyPerson.from_name_and_affiliation('LabDude',
55+
'knight lab')
56+
self.assertEqual(p.name, 'LabDude')
57+
self.assertEqual(p.affiliation, 'knight lab')
58+
self.assertEqual(p.address, '123 lab street')
59+
self.assertEqual(p.phone, '121-222-3333')
60+
self.assertEqual(p.email, 'lab_dude@foo.bar')
61+
4962
def test_iter(self):
5063
"""Make sure that each and every StudyPerson is retrieved"""
5164
expected = [

qiita_pet/handlers/rest/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
from .study import StudyHandler, StudyCreatorHandler, StudyStatusHandler
10+
from .study_samples import (StudySamplesHandler, StudySamplesInfoHandler,
11+
StudySamplesCategoriesHandler)
12+
from .study_person import StudyPersonHandler
13+
from .study_preparation import (StudyPrepCreatorHandler,
14+
StudyPrepArtifactCreatorHandler)
15+
16+
17+
__all__ = ['StudyHandler', 'StudySamplesHandler', 'StudySamplesInfoHandler',
18+
'StudySamplesCategoriesHandler', 'StudyPersonHandler',
19+
'StudyCreatorHandler', 'StudyPrepCreatorHandler',
20+
'StudyPrepArtifactCreatorHandler', 'StudyStatusHandler']
21+
22+
23+
ENDPOINTS = (
24+
(r"/api/v1/study$", StudyCreatorHandler),
25+
(r"/api/v1/study/([0-9]+)$", StudyHandler),
26+
(r"/api/v1/study/([0-9]+)/samples/categories=([a-zA-Z\-0-9\.:,_]*)",
27+
StudySamplesCategoriesHandler),
28+
(r"/api/v1/study/([0-9]+)/samples", StudySamplesHandler),
29+
(r"/api/v1/study/([0-9]+)/samples/info", StudySamplesInfoHandler),
30+
(r"/api/v1/person(.*)", StudyPersonHandler),
31+
(r"/api/v1/study/([0-9]+)/preparation/([0-9]+)/artifact",
32+
StudyPrepArtifactCreatorHandler),
33+
(r"/api/v1/study/([0-9]+)/preparation(.*)", StudyPrepCreatorHandler),
34+
(r"/api/v1/study/([0-9]+)/status$", StudyStatusHandler)
35+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
from qiita_db.study import Study
9+
from qiita_db.exceptions import QiitaDBUnknownIDError
10+
from qiita_pet.handlers.util import to_int
11+
from qiita_pet.handlers.base_handlers import BaseHandler
12+
13+
14+
class RESTHandler(BaseHandler):
15+
def fail(self, msg, status, **kwargs):
16+
out = {'message': msg}
17+
out.update(kwargs)
18+
19+
self.write(out)
20+
self.set_status(status)
21+
self.finish()
22+
23+
def safe_get_study(self, study_id):
24+
study_id = to_int(study_id)
25+
s = None
26+
try:
27+
s = Study(study_id)
28+
except QiitaDBUnknownIDError:
29+
self.fail('Study not found', 404)
30+
finally:
31+
return s

qiita_pet/handlers/rest/study.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
class StudyHandler(RESTHandler):
20+
21+
@authenticate_oauth
22+
def get(self, study_id):
23+
study = self.safe_get_study(study_id)
24+
if study is None:
25+
return
26+
27+
info = study.info
28+
pi = info['principal_investigator']
29+
lp = info['lab_person']
30+
self.write({'title': study.title,
31+
'contacts': {'principal_investigator': [
32+
pi.name,
33+
pi.affiliation,
34+
pi.email],
35+
'lab_person': [
36+
lp.name,
37+
lp.affiliation,
38+
lp.email]},
39+
'study_abstract': info['study_abstract'],
40+
'study_description': info['study_description'],
41+
'study_alias': info['study_alias']})
42+
self.finish()
43+
44+
45+
class StudyCreatorHandler(RESTHandler):
46+
47+
@authenticate_oauth
48+
def post(self):
49+
try:
50+
payload = json_decode(self.request.body)
51+
except ValueError:
52+
self.fail('Could not parse body', 400)
53+
return
54+
55+
required = {'title', 'study_abstract', 'study_description',
56+
'study_alias', 'owner', 'contacts'}
57+
58+
if not required.issubset(payload):
59+
self.fail('Not all required arguments provided', 400)
60+
return
61+
62+
title = payload['title']
63+
study_abstract = payload['study_abstract']
64+
study_desc = payload['study_description']
65+
study_alias = payload['study_alias']
66+
67+
owner = payload['owner']
68+
if not User.exists(owner):
69+
self.fail('Unknown user', 403)
70+
return
71+
else:
72+
owner = User(owner)
73+
74+
contacts = payload['contacts']
75+
76+
if Study.exists(title):
77+
self.fail('Study title already exists', 409)
78+
return
79+
80+
pi_name = contacts['principal_investigator'][0]
81+
pi_aff = contacts['principal_investigator'][1]
82+
if not StudyPerson.exists(pi_name, pi_aff):
83+
self.fail('Unknown principal investigator', 403)
84+
return
85+
else:
86+
pi = StudyPerson.from_name_and_affiliation(pi_name, pi_aff)
87+
88+
lp_name = contacts['lab_person'][0]
89+
lp_aff = contacts['lab_person'][1]
90+
if not StudyPerson.exists(lp_name, lp_aff):
91+
self.fail('Unknown lab person', 403)
92+
return
93+
else:
94+
lp = StudyPerson.from_name_and_affiliation(lp_name, lp_aff)
95+
96+
info = {'lab_person_id': lp,
97+
'principal_investigator_id': pi,
98+
'study_abstract': study_abstract,
99+
'study_description': study_desc,
100+
'study_alias': study_alias,
101+
102+
# TODO: we believe it is accurate that mixs is false and
103+
# metadata completion is false as these cannot be known
104+
# at study creation here no matter what.
105+
# we do not know what should be done with the timeseries.
106+
'mixs_compliant': False,
107+
'metadata_complete': False,
108+
'timeseries_type_id': 1}
109+
study = Study.create(owner, title, [1], info)
110+
111+
self.set_status(201)
112+
self.write({'id': study.id})
113+
self.finish()
114+
115+
116+
class StudyStatusHandler(RESTHandler):
117+
@authenticate_oauth
118+
def get(self, study_id):
119+
study = self.safe_get_study(study_id)
120+
if study is None:
121+
return
122+
123+
public = study.status == 'public'
124+
st = study.sample_template
125+
sample_information = st is not None
126+
if sample_information:
127+
with warnings.catch_warnings():
128+
try:
129+
st.validate(SAMPLE_TEMPLATE_COLUMNS)
130+
except Warning:
131+
sample_information_warnings = True
132+
else:
133+
sample_information_warnings = False
134+
else:
135+
sample_information_warnings = False
136+
137+
preparations = []
138+
for prep in study.prep_templates():
139+
pid = prep.id
140+
art = prep.artifact is not None
141+
# TODO: unclear how to test for warnings on the preparations as
142+
# it requires knowledge of the preparation type. It is possible
143+
# to tease this out, but it replicates code present in
144+
# PrepTemplate.create, see:
145+
# https://github.com/biocore/qiita/issues/2096
146+
preparations.append({'id': pid, 'has_artifact': art})
147+
148+
self.write({'is_public': public,
149+
'has_sample_information': sample_information,
150+
'sample_information_has_warnings':
151+
sample_information_warnings,
152+
'preparations': preparations})
153+
self.set_status(200)
154+
self.finish()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
from qiita_db.handlers.oauth2 import authenticate_oauth
10+
from qiita_db.study import StudyPerson
11+
from qiita_db.exceptions import QiitaDBLookupError
12+
from .rest_handler import RESTHandler
13+
14+
15+
class StudyPersonHandler(RESTHandler):
16+
@authenticate_oauth
17+
def get(self, *args, **kwargs):
18+
name = self.get_argument('name')
19+
affiliation = self.get_argument('affiliation')
20+
21+
try:
22+
p = StudyPerson.from_name_and_affiliation(name, affiliation)
23+
except QiitaDBLookupError:
24+
self.fail('Person not found', 404)
25+
return
26+
27+
self.write({'address': p.address, 'phone': p.phone, 'email': p.email,
28+
'id': p.id})
29+
self.finish()
30+
31+
@authenticate_oauth
32+
def post(self, *args, **kwargs):
33+
name = self.get_argument('name')
34+
affiliation = self.get_argument('affiliation')
35+
email = self.get_argument('email')
36+
37+
phone = self.get_argument('phone', None)
38+
address = self.get_argument('address', None)
39+
40+
if StudyPerson.exists(name, affiliation):
41+
self.fail('Person already exists', 409)
42+
return
43+
44+
p = StudyPerson.create(name=name, affiliation=affiliation, email=email,
45+
phone=phone, address=address)
46+
47+
self.set_status(201)
48+
self.write({'id': p.id})
49+
self.finish()

0 commit comments

Comments
 (0)