-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
d803f42
commit 8f6d193
Showing
18 changed files
with
1,201 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# ----------------------------------------------------------------------------- | ||
# 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 .study import StudyHandler, StudyCreatorHandler, StudyStatusHandler | ||
from .study_samples import (StudySamplesHandler, StudySamplesInfoHandler, | ||
StudySamplesCategoriesHandler) | ||
from .study_person import StudyPersonHandler | ||
from .study_preparation import (StudyPrepCreatorHandler, | ||
StudyPrepArtifactCreatorHandler) | ||
|
||
|
||
__all__ = ['StudyHandler', 'StudySamplesHandler', 'StudySamplesInfoHandler', | ||
'StudySamplesCategoriesHandler', 'StudyPersonHandler', | ||
'StudyCreatorHandler', 'StudyPrepCreatorHandler', | ||
'StudyPrepArtifactCreatorHandler', 'StudyStatusHandler'] | ||
|
||
|
||
ENDPOINTS = ( | ||
(r"/api/v1/study$", StudyCreatorHandler), | ||
(r"/api/v1/study/([0-9]+)$", StudyHandler), | ||
(r"/api/v1/study/([0-9]+)/samples/categories=([a-zA-Z\-0-9\.:,_]*)", | ||
StudySamplesCategoriesHandler), | ||
(r"/api/v1/study/([0-9]+)/samples", StudySamplesHandler), | ||
(r"/api/v1/study/([0-9]+)/samples/info", StudySamplesInfoHandler), | ||
(r"/api/v1/person(.*)", StudyPersonHandler), | ||
(r"/api/v1/study/([0-9]+)/preparation/([0-9]+)/artifact", | ||
StudyPrepArtifactCreatorHandler), | ||
(r"/api/v1/study/([0-9]+)/preparation(.*)", StudyPrepCreatorHandler), | ||
(r"/api/v1/study/([0-9]+)/status$", StudyStatusHandler) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# ----------------------------------------------------------------------------- | ||
# 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 qiita_db.study import Study | ||
from qiita_db.exceptions import QiitaDBUnknownIDError | ||
from qiita_pet.handlers.util import to_int | ||
from qiita_pet.handlers.base_handlers import BaseHandler | ||
|
||
|
||
class RESTHandler(BaseHandler): | ||
def fail(self, msg, status, **kwargs): | ||
out = {'message': msg} | ||
out.update(kwargs) | ||
|
||
self.write(out) | ||
self.set_status(status) | ||
self.finish() | ||
|
||
def safe_get_study(self, study_id): | ||
study_id = to_int(study_id) | ||
s = None | ||
try: | ||
s = Study(study_id) | ||
except QiitaDBUnknownIDError: | ||
self.fail('Study not found', 404) | ||
finally: | ||
return s |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# ----------------------------------------------------------------------------- | ||
# 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. | ||
# ----------------------------------------------------------------------------- | ||
import warnings | ||
|
||
from tornado.escape import json_decode | ||
|
||
from qiita_db.handlers.oauth2 import authenticate_oauth | ||
from qiita_db.study import StudyPerson, Study | ||
from qiita_db.user import User | ||
from .rest_handler import RESTHandler | ||
from qiita_db.metadata_template.constants import SAMPLE_TEMPLATE_COLUMNS | ||
|
||
|
||
class StudyHandler(RESTHandler): | ||
|
||
@authenticate_oauth | ||
def get(self, study_id): | ||
study = self.safe_get_study(study_id) | ||
if study is None: | ||
return | ||
|
||
info = study.info | ||
pi = info['principal_investigator'] | ||
lp = info['lab_person'] | ||
self.write({'title': study.title, | ||
'contacts': {'principal_investigator': [ | ||
pi.name, | ||
pi.affiliation, | ||
pi.email], | ||
'lab_person': [ | ||
lp.name, | ||
lp.affiliation, | ||
lp.email]}, | ||
'study_abstract': info['study_abstract'], | ||
'study_description': info['study_description'], | ||
'study_alias': info['study_alias']}) | ||
self.finish() | ||
|
||
|
||
class StudyCreatorHandler(RESTHandler): | ||
|
||
@authenticate_oauth | ||
def post(self): | ||
try: | ||
payload = json_decode(self.request.body) | ||
except ValueError: | ||
self.fail('Could not parse body', 400) | ||
return | ||
|
||
required = {'title', 'study_abstract', 'study_description', | ||
'study_alias', 'owner', 'contacts'} | ||
|
||
if not required.issubset(payload): | ||
self.fail('Not all required arguments provided', 400) | ||
return | ||
|
||
title = payload['title'] | ||
study_abstract = payload['study_abstract'] | ||
study_desc = payload['study_description'] | ||
study_alias = payload['study_alias'] | ||
|
||
owner = payload['owner'] | ||
if not User.exists(owner): | ||
self.fail('Unknown user', 403) | ||
return | ||
else: | ||
owner = User(owner) | ||
|
||
contacts = payload['contacts'] | ||
|
||
if Study.exists(title): | ||
self.fail('Study title already exists', 409) | ||
return | ||
|
||
pi_name = contacts['principal_investigator'][0] | ||
pi_aff = contacts['principal_investigator'][1] | ||
if not StudyPerson.exists(pi_name, pi_aff): | ||
self.fail('Unknown principal investigator', 403) | ||
return | ||
else: | ||
pi = StudyPerson.from_name_and_affiliation(pi_name, pi_aff) | ||
|
||
lp_name = contacts['lab_person'][0] | ||
lp_aff = contacts['lab_person'][1] | ||
if not StudyPerson.exists(lp_name, lp_aff): | ||
self.fail('Unknown lab person', 403) | ||
return | ||
else: | ||
lp = StudyPerson.from_name_and_affiliation(lp_name, lp_aff) | ||
|
||
info = {'lab_person_id': lp, | ||
'principal_investigator_id': pi, | ||
'study_abstract': study_abstract, | ||
'study_description': study_desc, | ||
'study_alias': study_alias, | ||
|
||
# TODO: we believe it is accurate that mixs is false and | ||
# metadata completion is false as these cannot be known | ||
# at study creation here no matter what. | ||
# we do not know what should be done with the timeseries. | ||
'mixs_compliant': False, | ||
'metadata_complete': False, | ||
'timeseries_type_id': 1} | ||
study = Study.create(owner, title, [1], info) | ||
|
||
self.set_status(201) | ||
self.write({'id': study.id}) | ||
self.finish() | ||
|
||
|
||
class StudyStatusHandler(RESTHandler): | ||
@authenticate_oauth | ||
def get(self, study_id): | ||
study = self.safe_get_study(study_id) | ||
if study is None: | ||
return | ||
|
||
public = study.status == 'public' | ||
st = study.sample_template | ||
sample_information = st is not None | ||
if sample_information: | ||
with warnings.catch_warnings(): | ||
try: | ||
st.validate(SAMPLE_TEMPLATE_COLUMNS) | ||
except Warning: | ||
sample_information_warnings = True | ||
else: | ||
sample_information_warnings = False | ||
else: | ||
sample_information_warnings = False | ||
|
||
preparations = [] | ||
for prep in study.prep_templates(): | ||
pid = prep.id | ||
art = prep.artifact is not None | ||
# TODO: unclear how to test for warnings on the preparations as | ||
# it requires knowledge of the preparation type. It is possible | ||
# to tease this out, but it replicates code present in | ||
# PrepTemplate.create, see: | ||
# https://github.com/biocore/qiita/issues/2096 | ||
preparations.append({'id': pid, 'has_artifact': art}) | ||
|
||
self.write({'is_public': public, | ||
'has_sample_information': sample_information, | ||
'sample_information_has_warnings': | ||
sample_information_warnings, | ||
'preparations': preparations}) | ||
self.set_status(200) | ||
self.finish() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# ----------------------------------------------------------------------------- | ||
# 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 qiita_db.handlers.oauth2 import authenticate_oauth | ||
from qiita_db.study import StudyPerson | ||
from qiita_db.exceptions import QiitaDBLookupError | ||
from .rest_handler import RESTHandler | ||
|
||
|
||
class StudyPersonHandler(RESTHandler): | ||
@authenticate_oauth | ||
def get(self, *args, **kwargs): | ||
name = self.get_argument('name') | ||
affiliation = self.get_argument('affiliation') | ||
|
||
try: | ||
p = StudyPerson.from_name_and_affiliation(name, affiliation) | ||
except QiitaDBLookupError: | ||
self.fail('Person not found', 404) | ||
return | ||
|
||
self.write({'address': p.address, 'phone': p.phone, 'email': p.email, | ||
'id': p.id}) | ||
self.finish() | ||
|
||
@authenticate_oauth | ||
def post(self, *args, **kwargs): | ||
name = self.get_argument('name') | ||
affiliation = self.get_argument('affiliation') | ||
email = self.get_argument('email') | ||
|
||
phone = self.get_argument('phone', None) | ||
address = self.get_argument('address', None) | ||
|
||
if StudyPerson.exists(name, affiliation): | ||
self.fail('Person already exists', 409) | ||
return | ||
|
||
p = StudyPerson.create(name=name, affiliation=affiliation, email=email, | ||
phone=phone, address=address) | ||
|
||
self.set_status(201) | ||
self.write({'id': p.id}) | ||
self.finish() |
Oops, something went wrong.