-
Couldn't load subscription status.
- Fork 79
A minimal REST API for Qiita #2094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 35 commits
b806436
dabad2f
a9f4eeb
5e73446
ab8a067
64862f2
748a49f
346fd2f
dc0f7c6
553f816
6df3420
9981b06
99a8eb8
3888571
a77736c
69c63c3
6b2e3f6
cee5dce
e19e6fa
6ad2ef2
a0f7463
9218611
d3fb608
cfe09d6
9c3d42a
ca34017
fd54712
43e133e
a18b5d8
b32d906
13eb32c
0872833
09f06bd
d4e010c
53b3803
1a3808c
23c9993
e460390
e76e82a
3f58dcd
f243a1d
595d9a1
e337632
9f528ea
e970392
133b9f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # ----------------------------------------------------------------------------- | ||
| # 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'] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # ----------------------------------------------------------------------------- | ||
| # 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): | ||
|
||
| self.write({'message': msg}) | ||
| self.set_status(status) | ||
| self.finish() | ||
|
|
||
| def study_boilerplate(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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| # ----------------------------------------------------------------------------- | ||
| # 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.study_boilerplate(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'], | ||
| 'efo': study.efo}) | ||
|
||
| self.finish() | ||
|
|
||
|
|
||
| class StudyCreatorHandler(RESTHandler): | ||
|
|
||
| @authenticate_oauth | ||
| def post(self, *args, **kwargs): | ||
|
||
| try: | ||
| payload = json_decode(self.request.body) | ||
| except ValueError: | ||
| self.fail('Could not parse body', 400) | ||
| return | ||
|
|
||
| required = {'title', 'study_abstract', 'study_description', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will not require There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, I've just removed this. |
||
| 'study_alias', 'owner', 'efo', '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) | ||
|
|
||
| efo = payload['efo'] | ||
| contacts = payload['contacts'] | ||
|
|
||
| if Study.exists(title): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should all the errors/warnings be passed at once? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only means we could find to determine if a study already existed. Not sure what else can be passed back. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking in getting the warnings like this, unknown principal investigator, lab person etc all at once vs. each test by itself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would complicate the logic quite a bit as some checks can't be done if others fail There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with @antgonza - it would be better to check the 3 things (study, PI and lab person) and return all errors at once - otherwise it becomes an iterative process to find all the errors. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The awkward thing about this is that we would have to mix the error codes, and this is also the way that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok - sounds good - I don't think is that critical here as it is during metadata addition. |
||
| 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, efo, info) | ||
|
|
||
| self.set_status(201) | ||
| self.write({'id': study.id}) | ||
| self.finish() | ||
|
|
||
|
|
||
| class StudyStatusHandler(RESTHandler): | ||
| @authenticate_oauth | ||
| def get(self, study_id): | ||
| study = self.study_boilerplate(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(): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think these warnings are stored in redis at the template generation time if it is done through the GUI. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome, thanks for creating that issue, I just added a link to it. |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what this refers to? data_type, information_type, other? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The call to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it! Depending on the data_type is how the validation happen. One simply option is to return all the validations avaialble? Not pretty but this will allow us to test and figure out a better solution. Other option will be to force the user to pass that, I guess a similar issue than artifact_type/id. BTW I think this is important cause the first use case will be either target gene or shotgun sequencing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'd need to hardcode the variable names of all the prep template constants which can be passed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be englobed here: #2096 |
||
| # to tease this out, but it replicates code present in | ||
| # PrepTemplate.create | ||
| 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() | ||
| 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() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # ----------------------------------------------------------------------------- | ||
| # 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 os | ||
|
|
||
| import pandas as pd | ||
| from tornado.escape import json_decode | ||
|
|
||
| from qiita_db.util import get_mountpoint | ||
| from qiita_db.artifact import Artifact | ||
| from qiita_pet.handlers.util import to_int | ||
| from qiita_db.exceptions import QiitaDBUnknownIDError, QiitaError | ||
| from qiita_db.metadata_template.prep_template import PrepTemplate | ||
| from qiita_db.handlers.oauth2 import authenticate_oauth | ||
| from .rest_handler import RESTHandler | ||
|
|
||
|
|
||
| class StudyPrepCreatorHandler(RESTHandler): | ||
| # TODO: do something smart about warnings, perhaps this should go in its | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can refer to #2096 but I'm thinking if we can just return them in the json response? {'id': p.id, 'warnings': warnings}There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, something like that would work! I've linked to #2096 just in case. |
||
| # own endpoint i.e. /api/v1/study/<int>/preparation/validate | ||
|
|
||
| @authenticate_oauth | ||
| def post(self, study_id, *args, **kwargs): | ||
| data_type = self.get_argument('data_type') | ||
| investigation_type = self.get_argument('investigation_type', None) | ||
|
|
||
| study_id = self.study_boilerplate(study_id) | ||
| if study_id is None: | ||
| return | ||
|
|
||
| data = pd.DataFrame.from_dict(json_decode(self.request.body), | ||
| orient='index') | ||
|
|
||
| try: | ||
| p = PrepTemplate.create(data, study_id, data_type, | ||
| investigation_type) | ||
| except QiitaError as e: | ||
| self.fail(e.message, 406) | ||
| return | ||
|
|
||
| self.write({'id': p.id}) | ||
| self.set_status(201) | ||
| self.finish() | ||
|
|
||
|
|
||
| class StudyPrepArtifactCreatorHandler(RESTHandler): | ||
|
|
||
| @authenticate_oauth | ||
| def post(self, study_id, prep_id): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the study id really required? you can get it from the prep. Or is it like this just becomes it makes more sense for how the rest endpoint is constructed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It makes more sense for how the rest endpoint is constructed, we too noticed this, but didn't quite know what to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah - I think it is more important to have a consistent endpoint :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🍻 |
||
| study = self.study_boilerplate(study_id) | ||
| if study is None: | ||
| return | ||
|
|
||
| prep_id = to_int(prep_id) | ||
| try: | ||
| p = PrepTemplate(prep_id) | ||
| except QiitaDBUnknownIDError: | ||
| self.fail('Preparation not found', 404) | ||
| return | ||
|
|
||
| if p.study_id != study.id: | ||
| self.fail('Preparation ID not associated with the study', 409) | ||
| return | ||
|
|
||
| artifact_deets = json_decode(self.request.body) | ||
| _, upload = get_mountpoint('uploads')[0] | ||
| base = os.path.join(upload, study_id) | ||
| filepaths = [(os.path.join(base, fp), fp_type) | ||
| for fp, fp_type in artifact_deets['filepaths']] | ||
|
|
||
| try: | ||
| art = Artifact.create(filepaths, | ||
| artifact_deets['artifact_type'], | ||
| artifact_deets['artifact_name'], | ||
| p) | ||
| except QiitaError as e: | ||
| self.fail(e.message, 406) | ||
| return | ||
|
|
||
| self.write({'id': art.id}) | ||
| self.set_status(201) | ||
| self.finish() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the specific cast to int needed? The object returned should be an int if the column study_person_id is a bigserial.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point, not sure where this came from, fixed!