diff --git a/api/__init__.py b/api/__init__.py index ea91f1d7..84660184 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -88,6 +88,7 @@ def create_app(): annotations_lookup_db.init_app(bar_app) eplant2_db.init_app(bar_app) eplant_poplar_db.init_app(bar_app) + eplant_rice_db.init_app(bar_app) eplant_tomato_db.init_app(bar_app) poplar_nssnp_db.init_app(bar_app) tomato_nssnp_db.init_app(bar_app) @@ -147,6 +148,7 @@ def create_app(): annotations_lookup_db = SQLAlchemy(metadata=MetaData()) eplant2_db = SQLAlchemy(metadata=MetaData()) eplant_poplar_db = SQLAlchemy(metadata=MetaData()) +eplant_rice_db = SQLAlchemy(metadata=MetaData()) eplant_tomato_db = SQLAlchemy(metadata=MetaData()) poplar_nssnp_db = SQLAlchemy(metadata=MetaData()) tomato_nssnp_db = SQLAlchemy(metadata=MetaData()) diff --git a/api/models/eplant_rice.py b/api/models/eplant_rice.py new file mode 100644 index 00000000..6bfe42b6 --- /dev/null +++ b/api/models/eplant_rice.py @@ -0,0 +1,9 @@ +from api import eplant_rice_db as db + + +class GeneAnnotation(db.Model): + __bind_key__ = "eplant_rice" + __tablename__ = "gene_annotation" + + gene = db.Column(db.String(20), nullable=False, primary_key=True) + annotation = db.Column(db.String(64000), nullable=False, primary_key=False) diff --git a/api/models/summarization.py b/api/models/summarization.py index 3ee76d95..18c58f62 100644 --- a/api/models/summarization.py +++ b/api/models/summarization.py @@ -4,9 +4,11 @@ class SummarizationGeneExpression(db.Model): __bind_key__ = "summarization" __tablename__ = "summarization" - Gene = db.Column(db.String(24), primary_key=True) - Value = db.Column(db.Integer, primary_key=True) - Sample = db.Column(db.String(32), primary_key=True) + proj_id = db.Column(db.String(5), nullable=False) + sample_id = db.Column(db.Integer, nullable=False, server_default=db.FetchedValue()) + data_probeset_id = db.Column(db.String(24), primary_key=True) + data_signal = db.Column(db.Integer, primary_key=True) + data_bot_id = db.Column(db.String(32), primary_key=True) class Requests(db.Model): diff --git a/api/resources/api_manager.py b/api/resources/api_manager.py index 0a7327ca..627be14b 100644 --- a/api/resources/api_manager.py +++ b/api/resources/api_manager.py @@ -1,260 +1,196 @@ from api import summarization_db as db from api.models.summarization import Users, Requests from api.utils.bar_utils import BARUtils +from api.utils.api_manager_utils import ApiManagerUtils from flask import request from flask_restx import Namespace, Resource from datetime import datetime from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.types import String, Float -import os +from sqlalchemy.orm import object_mapper import uuid -import requests import pandas -from cryptography.fernet import Fernet -from smtplib import SMTP_SSL -from ssl import create_default_context -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText CAPTCHA_KEY_FILE = "/home/bpereira/data/bar.summarization/key" - api_manager = Namespace("API Manager", description="API Manager", path="/api_manager") -class ApiManagerUtils: - @staticmethod - def check_admin_pass(password): - # Replace below with key from script in /home/bpereira/dev/pw-key - key = os.environ.get("ADMIN_ENCRYPT_KEY") - cipher_suite = Fernet(key) - with open(os.environ.get("ADMIN_PASSWORD_FILE"), "rb") as f: - for line in f: - encrypted_key = line - uncipher_text = cipher_suite.decrypt(encrypted_key) - plain_text_encryptedpassword = bytes(uncipher_text).decode("utf-8") - if password == plain_text_encryptedpassword: - return True - else: - return False - - @staticmethod - def validate_captcha(value): - """Validates a reCaptcha value using our secret token""" - if os.environ.get("BAR"): - with open(CAPTCHA_KEY_FILE, "rb") as f: - for line in f: - key = line - if key: - ret = requests.post( - "https://www.google.com/recaptcha/api/siteverify", - data={"secret": key, "response": value}, - ) - return ret.json()["success"] - else: - return False - else: - return True - - @staticmethod - def send_email_notification(): - if os.environ.get("BAR"): - with open(os.environ.get("ADMIN_EMAIL"), "r") as f: - for line in f: - recipient = line - port = 465 - key = os.environ.get("EMAIL_PASS_KEY") - cipher_suite = Fernet(key) - with open(os.environ.get("EMAIL_PASS_FILE"), "rb") as f: - for line in f: - encrypted_key = line - uncipher_text = cipher_suite.decrypt(encrypted_key) - password = bytes(uncipher_text).decode("utf-8") - context = create_default_context() - smtp_server = "smtp.gmail.com" - sender_email = "bar.summarization@gmail.com" - subject = "[Bio-Analytic Resource] New API key request" - text = """\ - There is a new API key request. - You can approve or reject it at http://bar.utoronto.ca/~bpereira/webservices/bar-request-manager/build/index.html - """ - m_text = MIMEText(text, _subtype="plain", _charset="UTF-8") - msg = MIMEMultipart() - msg["From"] = sender_email - msg["To"] = recipient - msg["Subject"] = subject - msg.attach(m_text) - with SMTP_SSL(smtp_server, port, context=context) as server: - server.login("bar.summarization@gmail.com", password) - server.sendmail(sender_email, recipient, msg.as_string()) - - @api_manager.route("/validate_admin_password", methods=["POST"], doc=False) class ApiManagerValidate(Resource): def post(self): """Verify admin password""" - if request.method == "POST": - response_json = request.get_json() - password = response_json["password"] - if ApiManagerUtils.check_admin_pass(password): - return BARUtils.success_exit(True) - else: - return BARUtils.success_exit(False) + response_json = request.get_json() + password = response_json["password"] + if ApiManagerUtils.check_admin_pass(password): + return BARUtils.success_exit(True) + else: + return BARUtils.success_exit(False) @api_manager.route("/validate_api_key", methods=["POST"], doc=False) class ApiManagerValidateKey(Resource): def post(self): """Verify if an API key provided by the user exists in the database""" - if request.method == "POST": - tbl = Users() - json = request.get_json() - key = json["key"] - try: - row = tbl.query.filter_by(api_key=key).first() - except SQLAlchemyError: - return BARUtils.error_exit("Internal server error"), 500 - - if row is None: - return BARUtils.error_exit("API key not found"), 404 + tbl = Users() + json = request.get_json() + key = json["key"] + try: + row = tbl.query.filter_by(api_key=key).first() + except SQLAlchemyError: + return BARUtils.error_exit("Internal server error"), 500 + + if row is None: + return BARUtils.error_exit("API key not found"), 404 + else: + if row.uses_left > 0: + return BARUtils.success_exit(True) else: - if row.uses_left > 0: - return BARUtils.success_exit(True) - else: - return BARUtils.error_exit("API key expired"), 401 + return BARUtils.error_exit("API key expired"), 401 @api_manager.route("/request", methods=["POST"], doc=False) class ApiManagerRequest(Resource): def post(self): - if request.method == "POST": - captchaVal = request.headers.get("captchaVal") - if ApiManagerUtils.validate_captcha(captchaVal): - response_json = request.get_json() - df = pandas.DataFrame.from_records([response_json]) - con = db.get_engine(bind="summarization") - try: - reqs = Requests() - users = Users() - row_req = reqs.query.filter_by(email=df.email[0]).first() - row_users = users.query.filter_by(email=df.email[0]).first() - - if row_req is None and row_users is None: - df.to_sql("requests", con, if_exists="append", index=False) - ApiManagerUtils.send_email_notification() - return BARUtils.success_exit("Data added") - else: - return BARUtils.error_exit("E-mail already in use"), 409 - except SQLAlchemyError: - return BARUtils.error_exit("Internal server error"), 500 - else: - return BARUtils.error_exit("Failed Captcha verification") + captchaVal = request.headers.get("captchaVal") + if ApiManagerUtils.validate_captcha(captchaVal): + response_json = request.get_json() + df = pandas.DataFrame.from_records([response_json]) + con = db.get_engine(bind="summarization") + try: + reqs = Requests() + users = Users() + row_req = reqs.query.filter_by(email=df.email[0]).first() + row_users = users.query.filter_by(email=df.email[0]).first() + + if row_req is None and row_users is None: + df.to_sql("requests", con, if_exists="append", index=False) + ApiManagerUtils.send_email_notification() + return BARUtils.success_exit("Data added") + else: + return BARUtils.error_exit("E-mail already in use"), 409 + except SQLAlchemyError: + return BARUtils.error_exit("Internal server error"), 500 + else: + return BARUtils.error_exit("Failed Captcha verification") @api_manager.route("/get_pending_requests", methods=["POST"], doc=False) class ApiManagerGetPending(Resource): def post(self): """Returns list of pending requests from the database""" - if request.method == "POST": - response_json = request.get_json() - password = response_json["password"] - if ApiManagerUtils.check_admin_pass(password): - table = Requests() - values = [] - try: - rows = table.query.filter_by().all() - except SQLAlchemyError: - return BARUtils.error_exit("Internal server error"), 500 - [ - values.append( - { - "first_name": row.first_name, - "last_name": row.last_name, - "email": row.email, - "notes": row.notes, - } - ) - for row in rows - ] - return BARUtils.success_exit(values) - else: - return BARUtils.error_exit("Forbidden"), 403 + response_json = request.get_json() + password = response_json["password"] + if ApiManagerUtils.check_admin_pass(password): + table = Requests() + values = [] + try: + rows = table.query.filter_by().all() + except SQLAlchemyError: + return BARUtils.error_exit("Internal server error"), 500 + [ + values.append( + { + "first_name": row.first_name, + "last_name": row.last_name, + "email": row.email, + "notes": row.notes, + } + ) + for row in rows + ] + return BARUtils.success_exit(values) + else: + return BARUtils.error_exit("Forbidden"), 403 @api_manager.route("/reject_request", methods=["POST"], doc=False) class ApiManagerRejectRequest(Resource): def post(self): """Delete a request from the database""" - if request.method == "POST": + response_json = request.get_json() + password = response_json["password"] + if ApiManagerUtils.check_admin_pass(password): response_json = request.get_json() - password = response_json["password"] - if ApiManagerUtils.check_admin_pass(password): - response_json = request.get_json() - table = Requests() - try: - el = table.query.filter_by(email=response_json["email"]).one() - except SQLAlchemyError: - return BARUtils.error_exit("Internal server error"), 500 - db.session.delete(el) - db.session.commit() - # table.query.filter_by(email=response_json['email']).delete() - return BARUtils.success_exit(True) - else: - return BARUtils.error_exit("Forbidden"), 403 + table = Requests() + try: + el = table.query.filter_by(email=response_json["email"]).one() + except SQLAlchemyError: + return BARUtils.error_exit("Internal server error"), 500 + db.session.delete(el) + db.session.commit() + # table.query.filter_by(email=response_json['email']).delete() + return BARUtils.success_exit(True) + else: + return BARUtils.error_exit("Forbidden"), 403 @api_manager.route("/approve_request", methods=["POST"], doc=False) class ApiManagerApproveRequest(Resource): def post(self): """Approve a request from the database and add it to the Users table""" - if request.method == "POST": - response_json = request.get_json() - email = response_json["email"] - password = response_json["password"] - if ApiManagerUtils.check_admin_pass(password): - table = Requests() - values = [] - try: - rows = table.query.filter_by(email=email).all() - except SQLAlchemyError: - return BARUtils.error_exit("Internal server error"), 500 - key = uuid.uuid4().hex - [ - values.append( - { - "first_name": row.first_name, - "last_name": row.last_name, - "email": row.email, - "date_added": datetime.now(), - "status": "user", - "api_key": key, - "uses_left": 100, - } + response_json = request.get_json() + email = response_json["email"] + password = response_json["password"] + if ApiManagerUtils.check_admin_pass(password): + table = Requests() + values = [] + try: + rows = table.query.filter_by(email=email).all() + except SQLAlchemyError: + return BARUtils.error_exit("Internal server error"), 500 + key = uuid.uuid4().hex + [ + values.append( + { + "first_name": row.first_name, + "last_name": row.last_name, + "email": row.email, + "date_added": datetime.now(), + "status": "user", + "api_key": key, + "uses_left": 100, + } + ) + for row in rows + ] + df = pandas.DataFrame.from_records([values[0]]) + con = db.get_engine(bind="summarization") + try: + df.to_sql("users", con, if_exists="append", index=False) + + class UserTable(db.Model): + __bind_key__ = "summarization" + __tablename__ = key + __table_args__ = ( + db.Index( + "data_probeset_id", + "data_probeset_id", + "data_bot_id", + "data_signal", + ), + ) + + proj_id = db.Column(db.String(5), nullable=False) + sample_id = db.Column( + db.Integer, nullable=False, server_default=db.FetchedValue() ) - for row in rows - ] - df = pandas.DataFrame.from_records([values[0]]) - values_df = pandas.DataFrame(columns=["Gene", "Sample", "Value"]) - con = db.get_engine(bind="summarization") - try: - df.to_sql("users", con, if_exists="append", index=False) - values_df.to_sql( - key, - con, - index_label="index", - dtype={ - values_df.index.name: String(42), - "Gene": String(32), - "Sample": String(32), - "Value": Float, - }, - if_exists="append", - index=True, + data_probeset_id = db.Column( + db.String(24), nullable=False, primary_key=True ) - el = table.query.filter_by(email=email).one() - db.session.delete(el) - db.session.commit() - except SQLAlchemyError: - return BARUtils.error_exit("Internal server error"), 500 - return BARUtils.success_exit(key) - else: - return BARUtils.error_exit("Forbidden"), 403 + data_signal = db.Column( + db.Float, server_default=db.FetchedValue(), primary_key=True + ) + data_bot_id = db.Column( + db.String(32), nullable=False, primary_key=True + ) + + UserTable.__table__.create( + db.session().get_bind(object_mapper(UserTable())), checkfirst=True + ) + el = table.query.filter_by(email=email).one() + db.session.delete(el) + db.session.commit() + except SQLAlchemyError: + return BARUtils.error_exit("Internal server error"), 500 + return BARUtils.success_exit(key) + else: + return BARUtils.error_exit("Forbidden"), 403 diff --git a/api/resources/efp_image.py b/api/resources/efp_image.py index 288f6657..2c02f4b8 100644 --- a/api/resources/efp_image.py +++ b/api/resources/efp_image.py @@ -93,7 +93,7 @@ def get(self, efp="", view="", mode="", gene_1="", gene_2=""): + gene_1 + "&secondaryGene=" + gene_2 - + "&grey_low=None&grey_stddev=None" + + "&grey_low=None&grey_stddev=None&navbar=0" ) efp_html = requests.get(efp_url) diff --git a/api/resources/gene_annotation.py b/api/resources/gene_annotation.py index 7351f355..df27a912 100644 --- a/api/resources/gene_annotation.py +++ b/api/resources/gene_annotation.py @@ -1,10 +1,13 @@ -from flask_restx import Namespace, Resource +from flask_restx import Namespace, Resource, fields +from flask import request from markupsafe import escape from sqlalchemy.exc import OperationalError -from api.models.eplant_poplar import GeneAnnotation as eplant_poplar_annotation -from api.models.eplant_tomato import GeneAnnotation as eplant_tomato_annotation +from api.models.eplant_rice import GeneAnnotation as EplantRiceAnnotation +from api.models.eplant_poplar import GeneAnnotation as EplantPoplarAnnotation +from api.models.eplant_tomato import GeneAnnotation as EplantTomatoAnnotation from api.models.eplant2 import AgiAnnotation, TAIR10, GeneRIFs from api.utils.bar_utils import BARUtils +from marshmallow import Schema, ValidationError, fields as marshmallow_fields gene_annotation = Namespace( @@ -20,8 +23,9 @@ def get(self, query=""): Endpoint returns gene locus for given gene keywords """ annotation_db_list = { - "tomato": eplant_tomato_annotation, - "poplar": eplant_poplar_annotation, + "tomato": EplantTomatoAnnotation, + "poplar": EplantPoplarAnnotation, + "rice": EplantRiceAnnotation, "arabidopsis": [AgiAnnotation, TAIR10, GeneRIFs], } @@ -29,62 +33,73 @@ def get(self, query=""): res = [] for species, db in annotation_db_list.items(): - try: - if species == "arabidopsis": - + if species == "arabidopsis": + try: agi_info = AgiAnnotation.query.filter( AgiAnnotation.annotation.op("regexp")(query) ).all() + tair10_curator_info = TAIR10.query.filter( TAIR10.Curator_summary.op("regexp")(query) ).all() + tair10_computational_info = TAIR10.query.filter( TAIR10.Computational_description.op("regexp")(query) ).all() + RIFs_info = GeneRIFs.query.filter( GeneRIFs.RIF.op("regexp")(query) ).all() - res += [ - { - "gene": i.agi, - "species": species, - "gene_annotation": i.annotation, - } - for i in agi_info - ] - res += [ - { - "gene": i.Model_name, - "species": species, - "gene_annotation": i.Curator_summary, - } - for i in tair10_curator_info - ] - res += [ - { - "gene": i.Model_name, - "species": species, - "gene_annotation": i.Computational_description, - } - for i in tair10_computational_info - ] - res += [ - {"gene": i.gene, "species": species, "gene_annotation": i.RIF} - for i in RIFs_info - ] - else: + except OperationalError: + return BARUtils.error_exit("An internal error has occurred"), 500 + + res += [ + { + "gene": i.agi, + "species": species, + "gene_annotation": i.annotation, + } + for i in agi_info + ] + + res += [ + { + "gene": i.Model_name, + "species": species, + "gene_annotation": i.Curator_summary, + } + for i in tair10_curator_info + ] + + res += [ + { + "gene": i.Model_name, + "species": species, + "gene_annotation": i.Computational_description, + } + for i in tair10_computational_info + ] + + res += [ + {"gene": i.gene, "species": species, "gene_annotation": i.RIF} + for i in RIFs_info + ] + + else: + try: rows = db.query.filter(db.annotation.op("regexp")(query)).all() - res += [ - { - "gene": i.gene, - "species": species, - "gene_annotation": i.annotation, - } - for i in rows - ] - except OperationalError: - return BARUtils.error_exit("An internal error has occurred"), 500 + except OperationalError: + return BARUtils.error_exit("An internal error has occurred"), 500 + + res += [ + { + "gene": i.gene, + "species": species, + "gene_annotation": i.annotation, + } + for i in rows + ] if len(res) == 0: return ( @@ -94,3 +109,73 @@ def get(self, query=""): else: # return first 10 matches return {"status": "success", "query": query, "result": res[:10]} + + +anntn_post_ex = gene_annotation.model( + "AntnRiceGenes", + { + "species": fields.String(required=True, example="rice"), + "genes": fields.List( + required=True, + example=["LOC_Os01g01010", "LOC_Os01g01050"], + cls_or_instance=fields.String, + ), + }, +) + + +class GeneIntrnsSchema(Schema): + species = marshmallow_fields.String(required=True) + genes = marshmallow_fields.List(cls_or_instance=marshmallow_fields.String) + + +@gene_annotation.route("/") +class GeneAnnotationPost(Resource): + @gene_annotation.expect(anntn_post_ex) + def post(self, query=""): + """ + Returns gene annotation(s) give a set of gene(s) for a species + Supported species: 'rice' + """ + + json_data = request.get_json() + + try: + json_data = GeneIntrnsSchema().load(json_data) + except ValidationError as err: + return BARUtils.error_exit(err.messages), 400 + + species = json_data["species"].lower() + genes = json_data["genes"] + + if species == "rice": + for gene in genes: + if not BARUtils.is_rice_gene_valid(gene): + return BARUtils.error_exit("Invalid gene id"), 400 + + try: + rows = EplantRiceAnnotation.query.filter( + EplantRiceAnnotation.gene.in_(genes) + ).all() + if len(rows) == 0: + return ( + BARUtils.error_exit( + "No data for the given species/genes" + ), + 400, + ) + else: + print(rows) + res = [ + { + "gene": i.gene, + "annotation": i.annotation, + } + for i in rows + ] + return BARUtils.success_exit(res) + except OperationalError: + return BARUtils.error_exit("An internal error has occurred."), 500 + + else: + return BARUtils.error_exit("Invalid species"), 400 diff --git a/api/resources/summarization_gene_expression.py b/api/resources/summarization_gene_expression.py index 3f29ac8c..b2dc7286 100644 --- a/api/resources/summarization_gene_expression.py +++ b/api/resources/summarization_gene_expression.py @@ -2,7 +2,6 @@ import json as jsonlib import tempfile import os -import re import pandas from api import summarization_db as db from api import limiter @@ -14,6 +13,9 @@ from sqlalchemy.inspection import inspect from scour.scour import scourString from cryptography.fernet import Fernet +from api.utils.summarization_gene_expression_utils import ( + SummarizationGeneExpressionUtils, +) DATA_FOLDER = "/home/bpereira/data/summarization-data" @@ -33,73 +35,6 @@ ) -class SummarizationGeneExpressionUtils: - @staticmethod - def get_table_object(table_name): - metadata = db.MetaData() - table_object = db.Table( - table_name, - metadata, - autoload=True, - autoload_with=db.get_engine(bind="summarization"), - ) - return table_object - - @staticmethod - def is_valid(string): - """Checks if a given string only contains alphanumeric characters - :param string: The string to be checked - """ - if re.search(r"([^_0-9A-Za-z])+", string): - return False - else: - return True - - @staticmethod - def validate_api_key(key): - """Checks if a given API key is in the Users database - :param key: The API key to be checked - """ - tbl = SummarizationGeneExpressionUtils.get_table_object("users") - con = db.get_engine(bind="summarization") - try: - row = con.execute( - db.select([tbl.c.uses_left]).where(tbl.c.api_key == key) - ).first() - except SQLAlchemyError as e: - error = str(e.__dict__["orig"]) - return error - if row is None: - return False - else: - if row.uses_left > 0: - return True - else: - return False - - @staticmethod - def decrement_uses(key): - """Subtracts 1 from the uses_left column of the user whose key matches the given string - :param key: The user's API key - """ - if SummarizationGeneExpressionUtils.validate_api_key(key): - tbl = SummarizationGeneExpressionUtils.get_table_object("users") - con = db.get_engine(bind="summarization") - try: - con.execute( - db.update(tbl) - .where(tbl.c.api_key == key) - .values(uses_left=(tbl.c.uses_left - 1)) - ) - db.session.commit() - except SQLAlchemyError as e: - error = str(e.__dict__["orig"]) - return error - return True - else: - return False - - @summarization_gene_expression.route("/summarize", methods=["POST"]) class SummarizationGeneExpressionSummarize(Resource): decorators = [ @@ -108,72 +43,71 @@ class SummarizationGeneExpressionSummarize(Resource): def post(self): """Takes a Google Drive folder ID (containing BAM files) and submits them to the Cromwell server for summarization""" - if request.method == "POST": - json = request.get_json() - key = request.headers.get("X-Api-Key") - species = json["species"] - email = json["email"] - aliases = json["aliases"] - csvEmail = json["csvEmail"] - if json["overwrite"] is True: - overwrite = "append" - else: - overwrite = "replace" - gtf = GTF_DICT[species] - if SummarizationGeneExpressionUtils.decrement_uses(key): - inputs = { - "geneSummarization.summarizeGenesScript": "./summarize_genes.R", - "geneSummarization.downloadFilesScript": "./downloadDriveFiles.py", - "geneSummarization.chrsScript": "./chrs.py", - "geneSummarization.folderId": json["folderId"], - "geneSummarization.credentials": "./data/credentials.json", - "geneSummarization.token": "./data/token.pickle", - "geneSummarization.species": species, - "geneSummarization.gtf": gtf, - "geneSummarization.aliases": str(aliases), - "geneSummarization.id": key, - "geneSummarization.pairedEndScript": "./paired.sh", - "geneSummarization.insertDataScript": "./insertData.py", - "geneSummarization.barEmailScript": "./bar_email.py", - "geneSummarization.email": email, - "geneSummarization.csvEmail": csvEmail, - "geneSummarization.overwrite": overwrite, - } - # Send request to Cromwell - path = os.path.join(SUMMARIZATION_FILES_PATH, "rpkm.wdl") - file = tempfile.TemporaryFile(mode="w+") - file.write(jsonlib.dumps(inputs)) - file.seek(0) - files = { - "workflowSource": ("rpkm.wdl", open(path, "rb")), - "workflowInputs": ("rpkm_inputs.json", file.read()), - } - id_and_status = requests.post( - CROMWELL_URL + "/api/workflows/v1", files=files - ) - id_and_status = id_and_status.json() - file.close() - gkey = os.environ.get("DRIVE_LIST_KEY") - cipher_suite = Fernet(gkey) - with open(os.environ.get("DRIVE_LIST_FILE"), "rb") as f: - for line in f: - encrypted_key = line - uncipher_text = cipher_suite.decrypt(encrypted_key) - plain_text_gkey = bytes(uncipher_text).decode("utf-8") - r = requests.get( - "https://www.googleapis.com/drive/v3/files?corpora=user&includeItemsFromAllDrives=true&q=%27" - + json["folderId"] - + "%27%20in%20parents&supportsAllDrives=true&key=" - + plain_text_gkey - ) - # Return ID for future accessing - if r.status_code == 200: - fs = [x["name"] for x in r.json()["files"] if ".bam" in x["name"]] - else: - fs = r.status_code - return BARUtils.success_exit((id_and_status["id"], fs)), 200 + json = request.get_json() + key = request.headers.get("X-Api-Key") + species = json["species"] + email = json["email"] + aliases = json["aliases"] + csv_email = json["csvEmail"] + if json["overwrite"] is True: + overwrite = "append" + else: + overwrite = "replace" + gtf = GTF_DICT[species] + if SummarizationGeneExpressionUtils.decrement_uses(key): + inputs = { + "geneSummarization.summarizeGenesScript": "./summarize_genes.R", + "geneSummarization.downloadFilesScript": "./downloadDriveFiles.py", + "geneSummarization.chrsScript": "./chrs.py", + "geneSummarization.folderId": json["folderId"], + "geneSummarization.credentials": "./data/credentials.json", + "geneSummarization.token": "./data/token.pickle", + "geneSummarization.species": species, + "geneSummarization.gtf": gtf, + "geneSummarization.aliases": str(aliases), + "geneSummarization.id": key, + "geneSummarization.pairedEndScript": "./paired.sh", + "geneSummarization.insertDataScript": "./insertData.py", + "geneSummarization.barEmailScript": "./bar_email.py", + "geneSummarization.email": email, + "geneSummarization.csv_email": csv_email, + "geneSummarization.overwrite": overwrite, + } + # Send request to Cromwell + path = os.path.join(SUMMARIZATION_FILES_PATH, "rpkm.wdl") + file = tempfile.TemporaryFile(mode="w+") + file.write(jsonlib.dumps(inputs)) + file.seek(0) + files = { + "workflowSource": ("rpkm.wdl", open(path, "rb")), + "workflowInputs": ("rpkm_inputs.json", file.read()), + } + id_and_status = requests.post( + CROMWELL_URL + "/api/workflows/v1", files=files + ) + id_and_status = id_and_status.json() + file.close() + gkey = os.environ.get("DRIVE_LIST_KEY") + cipher_suite = Fernet(gkey) + with open(os.environ.get("DRIVE_LIST_FILE"), "rb") as f: + for line in f: + encrypted_key = line + uncipher_text = cipher_suite.decrypt(encrypted_key) + plain_text_gkey = bytes(uncipher_text).decode("utf-8") + r = requests.get( + "https://www.googleapis.com/drive/v3/files?corpora=user&includeItemsFromAllDrives=true&q=%27" + + json["folderId"] + + "%27%20in%20parents&supportsAllDrives=true&key=" + + plain_text_gkey + ) + # Return ID for future accessing + if r.status_code == 200: + fs = [x["name"] for x in r.json()["files"] if ".bam" in x["name"]] else: - return BARUtils.error_exit("Invalid API key") + fs = r.status_code + return BARUtils.success_exit((id_and_status["id"], fs)), 200 + else: + return BARUtils.error_exit("Invalid API key") @summarization_gene_expression.route( @@ -183,40 +117,41 @@ class SummarizationGeneExpressionProgress(Resource): @summarization_gene_expression.param("job_id", _in="path", default="") def get(self, job_id): """Get progress of a job given its ID""" - if request.method == "GET": - progress = requests.get( - CROMWELL_URL + "/api/workflows/v1/" + job_id + "/status" - ) - if progress.status_code == 200: - return BARUtils.success_exit(progress.status), 200 - else: - return BARUtils.error_exit(progress.status_code) + progress = requests.get( + CROMWELL_URL + "/api/workflows/v1/" + job_id + "/status" + ) + if progress.status_code == 200: + return BARUtils.success_exit(progress.status), 200 + else: + return BARUtils.error_exit(progress.status_code) @summarization_gene_expression.route("/user", methods=["GET"], doc=False) class SummarizationGeneExpressionUser(Resource): def get(self): """Get a user's details from the server""" - if request.method == "GET": - key = request.headers.get("X-Api-Key") - tbl = SummarizationGeneExpressionUtils.get_table_object("users") - con = db.get_engine(bind="summarization") - values = [] - try: - rows = con.execute(db.select("*").where(tbl.c.api_key == key)) - except SQLAlchemyError: - return BARUtils.error_exit("Internal server error"), 500 - [ - values.append( - [ - row.first_name, - row.last_name, - row.email, - ] - ) - for row in rows - ] - return BARUtils.success_exit(values) + key = request.headers.get("X-Api-Key") + tbl = SummarizationGeneExpressionUtils.get_table_object("users") + con = db.get_engine(bind="summarization") + values = [] + + validated_key = SummarizationGeneExpressionUtils.validated_api_key(key) + + try: + rows = con.execute(db.select("*").where(tbl.c.api_key == validated_key)) + except SQLAlchemyError: + return BARUtils.error_exit("Internal server error"), 500 + [ + values.append( + [ + row.first_name, + row.last_name, + row.email, + ] + ) + for row in rows + ] + return BARUtils.success_exit(values) @summarization_gene_expression.route("/tsv_upload", methods=["POST"], doc=False) @@ -225,54 +160,51 @@ class SummarizationGeneExpressionTsvUpload(Resource): def post(self): """Takes a TSV file from Kallisto converts to RPKM""" - if request.method == "POST": - if "file" not in request.files: - return BARUtils.error_exit("No file attached"), 400 - file = request.files["file"] - if file: - filename = secure_filename(file.filename) - key = request.headers.get("X-Api-Key") - overwrite = request.form.get("overwrite") - email = request.form.get("email") - if overwrite == "true": - overwrite = "replace" - else: - overwrite = "append" - # Create folder for user data if it doesn't exist - dirName = os.path.join("/DATA/users/www-data/", secure_filename(key)) - if not os.path.exists(dirName): - os.makedirs(dirName) - file.save(os.path.join(dirName, secure_filename(filename))) - if SummarizationGeneExpressionUtils.decrement_uses(key): - inputs = ( - """ - { - "tsvUpload.insertDataScript": "./insertData.py", - "tsvUpload.conversionScript": "./kallistoToRpkm.R", - "tsvUpload.id": """ - + key - + """, - "tsvUpload.tsv": """ - + os.path.join(dirName, secure_filename(filename)) - + """, - "tsvUpload.overwrite": """ - + overwrite - + """, - "tsvUpload.email": """ - + email - + """ - } - """ - ) - path = os.path.join(SUMMARIZATION_FILES_PATH, "tsvUpload.wdl") - files = { - "workflowSource": ("tsvUpload.wdl", open(path, "rb")), - "workflowInputs": ("rpkm_inputs.json", inputs), + key = request.headers.get("X-Api-Key") + overwrite = request.form.get("overwrite") + email = request.form.get("email") + if overwrite == "true": + overwrite = "replace" + else: + overwrite = "append" + # Create folder for user data if it doesn't exist + files = request.files.items() + for i in files: + filename = secure_filename(i[0]) + dir_name = os.path.join("/DATA/users/www-data/", secure_filename(key)) + if not os.path.exists(dir_name): + os.makedirs(dir_name) + i[1].save(os.path.join(dir_name, filename)) + if SummarizationGeneExpressionUtils.decrement_uses(key): + inputs = ( + """ + { + "tsvUpload.insertDataScript": "./insertData.py", + "tsvUpload.conversionScript": "./kallistoToRpkm.R", + "tsvUpload.id": """ + + key + + """, + "tsvUpload.tsv": """ + + str(os.listdir(dir_name)) + + """, + "tsvUpload.overwrite": """ + + overwrite + + """, + "tsvUpload.email": """ + + email + + """ } - requests.post(CROMWELL_URL + "/api/workflows/v1", files=files) - return BARUtils.success_exit(key) - else: - return BARUtils.error_exit("Invalid API key") + """ + ) + path = os.path.join(SUMMARIZATION_FILES_PATH, "tsvUpload.wdl") + files = { + "workflowSource": ("tsvUpload.wdl", open(path, "rb")), + "workflowInputs": ("rpkm_inputs.json", inputs), + } + requests.post(CROMWELL_URL + "/api/workflows/v1", files=files) + return BARUtils.success_exit(key) + else: + return BARUtils.error_exit("Invalid API key") @summarization_gene_expression.route("/csv_upload", methods=["POST"], doc=False) @@ -281,52 +213,51 @@ class SummarizationGeneExpressionCsvUpload(Resource): def post(self): """Takes a CSV file containing expression data and inserts the data into the database""" - if request.method == "POST": - if "file" not in request.files: - return BARUtils.error_exit("No file attached"), 400 - file = request.files["file"] - if file: - filename = secure_filename(file.filename) - key = request.headers.get("X-Api-Key") - overwrite = request.form.get("overwrite") - email = request.form.get("email") - if overwrite == "true": - overwrite = "replace" - else: - overwrite = "append" - dirName = os.path.join("/DATA/users/www-data/", secure_filename(key)) - if not os.path.exists(dirName): - os.makedirs(dirName) - file.save(os.path.join(dirName, secure_filename(filename))) - if SummarizationGeneExpressionUtils.decrement_uses(key): - inputs = ( + if "file" not in request.files: + return BARUtils.error_exit("No file attached"), 400 + file = request.files["file"] + if file: + filename = secure_filename(file.filename) + key = request.headers.get("X-Api-Key") + overwrite = request.form.get("overwrite") + email = request.form.get("email") + if overwrite == "true": + overwrite = "replace" + else: + overwrite = "append" + dir_name = os.path.join("/DATA/users/www-data/", secure_filename(key)) + if not os.path.exists(dir_name): + os.makedirs(dir_name) + file.save(os.path.join(dir_name, secure_filename(filename))) + if SummarizationGeneExpressionUtils.decrement_uses(key): + inputs = ( + """ + { + "csvUpload.insertDataScript": "./insertData.py", + "csvUpload.id": """ + + key + + """, + "csvUpload.csv": """ + + os.path.join(dir_name, filename) + + """, + "csvUpload.overwrite": """ + + overwrite + + """, + "csvUpload.email": """ + + email + + """ + } """ - { - "csvUpload.insertDataScript": "./insertData.py", - "csvUpload.id": """ - + key - + """, - "csvUpload.csv": """ - + os.path.join(dirName, filename) - + """, - "csvUpload.overwrite": """ - + overwrite - + """, - "csvUpload.email": """ - + email - + """ - } - """ - ) - path = os.path.join(SUMMARIZATION_FILES_PATH, "csvUpload.wdl") - files = { - "workflowSource": ("csvUpload.wdl", open(path, "rb")), - "workflowInputs": ("rpkm_inputs.json", inputs), - } - requests.post(CROMWELL_URL + "/api/workflows/v1", files=files) - return BARUtils.success_exit(key) - else: - return BARUtils.error_exit("Invalid API key") + ) + path = os.path.join(SUMMARIZATION_FILES_PATH, "csvUpload.wdl") + files = { + "workflowSource": ("csvUpload.wdl", open(path, "rb")), + "workflowInputs": ("rpkm_inputs.json", inputs), + } + requests.post(CROMWELL_URL + "/api/workflows/v1", files=files) + return BARUtils.success_exit(key) + else: + return BARUtils.error_exit("Invalid API key") @summarization_gene_expression.route("/insert", methods=["POST"], doc=False) @@ -335,20 +266,24 @@ def post(self): """This function adds a CSV's data to the database. This is only called by the Cromwell server after receiving the user's file.""" if request.remote_addr != "127.0.0.1": return BARUtils.error_exit("Forbidden"), 403 - if request.method == "POST": - key = request.headers.get("X-Api-Key") - if SummarizationGeneExpressionUtils.decrement_uses(key): - csv = request.get_json()["csv"] - db_id = request.get_json()["uid"] - df = pandas.read_csv(csv) - db_id = db_id.split(".")[0] - df = df.melt(id_vars=["Gene"], var_name="Sample", value_name="Value") - db_id = db_id.split("/")[len(db_id.split("/")) - 1] - con = db.get_engine(bind="summarization") - df.to_sql(db_id, con, if_exists="append", index=True) - return BARUtils.success_exit("Success") - else: - return BARUtils.error_exit("Invalid API key") + + key = request.headers.get("X-Api-Key") + if SummarizationGeneExpressionUtils.decrement_uses(key): + csv = request.get_json()["csv"] + db_id = request.get_json()["uid"] + df = pandas.read_csv(csv) + db_id = db_id.split(".")[0] + df = df.melt( + id_vars=["data_probeset_id"], + var_name="data_bot_id", + value_name="data_signal", + ) + db_id = db_id.split("/")[len(db_id.split("/")) - 1] + con = db.get_engine(bind="summarization") + df.to_sql(db_id, con, if_exists="append", index=True) + return BARUtils.success_exit("Success") + else: + return BARUtils.error_exit("Invalid API key") @summarization_gene_expression.route( @@ -374,19 +309,21 @@ def get(self, table_id, sample, gene): values = {} try: rows = con.execute( - tbl.select(tbl.c.Value).where(tbl.c.Gene == gene) + tbl.select(tbl.c.data_signal).where( + tbl.c.data_probeset_id == gene + ) ) except SQLAlchemyError: return BARUtils.error_exit("Internal server error"), 500 for row in rows: - values.update({str(row.Sample): float(row.Value)}) + values.update({str(row.data_bot_id): float(row.data_signal)}) else: values = [] try: rows = con.execute( - tbl.select(tbl.c.Value) - .where(tbl.c.Sample == sample) - .where(tbl.c.Gene == gene) + tbl.select(tbl.c.data_signal) + .where(tbl.c.data_bot_id == sample) + .where(tbl.c.data_probeset_id == gene) ) except SQLAlchemyError: return BARUtils.error_exit("Internal server error"), 500 @@ -405,10 +342,10 @@ def get(self, table_id=""): tbl = SummarizationGeneExpressionUtils.get_table_object(table_id) values = [] try: - rows = con.execute(db.select([tbl.c.Sample]).distinct()) + rows = con.execute(db.select([tbl.c.data_bot_id]).distinct()) except SQLAlchemyError: return BARUtils.error_exit("Internal server error"), 500 - [values.append(row.Sample) for row in rows] + [values.append(row.data_bot_id) for row in rows] return BARUtils.success_exit(values) @@ -423,10 +360,10 @@ def get(self, table_id=""): tbl = SummarizationGeneExpressionUtils.get_table_object(table_id) values = [] try: - rows = con.execute(db.select([tbl.c.Gene]).distinct()) + rows = con.execute(db.select([tbl.c.data_probeset_id]).distinct()) except SQLAlchemyError: return BARUtils.error_exit("Internal server error"), 500 - [values.append(row.Gene) for row in rows] + [values.append(row.data_probeset_id) for row in rows] return BARUtils.success_exit(values) else: return BARUtils.error_exit("Invalid API key") @@ -445,13 +382,13 @@ def get(self, table_id="", user_string=""): values = [] try: rows = con.execute( - db.select([tbl.c.Gene]) - .where(tbl.c.Gene.contains(user_string)) + db.select([tbl.c.data_probeset_id]) + .where(tbl.c.data_probeset_id.contains(user_string)) .distinct() ) except SQLAlchemyError: return BARUtils.error_exit("Internal server error"), 500 - [values.append(row.Gene) for row in rows] + [values.append(row.data_probeset_id) for row in rows] return BARUtils.success_exit(values) @@ -482,43 +419,57 @@ def get(self, table_id=""): class SummarizationGeneExpressionSave(Resource): def post(self): """Saves the given file if the user has a valid API key""" - if request.method == "POST": - api_key = request.headers.get("x-api-key") - if api_key is None: - return BARUtils.error_exit("Invalid API key"), 403 - elif SummarizationGeneExpressionUtils.decrement_uses(api_key): - if "file" in request.files: - file = request.files["file"] - if file.content_type == "text/json": - extension = ".json" - elif file.content_type == "image/svg+xml": - extension = ".svg" - else: - return BARUtils.error_exit("Invalid file type"), 400 - filename = os.path.join( - DATA_FOLDER, api_key, file.filename + extension - ) - file.save(filename) - return BARUtils.success_exit(True) + api_key = request.headers.get("x-api-key") + + validated_key = SummarizationGeneExpressionUtils.validated_api_key(api_key) + + if validated_key is None: + return BARUtils.error_exit("Invalid API key"), 403 + elif SummarizationGeneExpressionUtils.decrement_uses(validated_key): + if "file" in request.files: + file = request.files["file"] + if file.content_type == "text/json": + extension = ".json" + elif file.content_type == "image/svg+xml": + extension = ".svg" else: - return BARUtils.error_exit("No file attached"), 400 + return BARUtils.error_exit("Invalid file type"), 400 + + dir_name = secure_filename(os.path.join(DATA_FOLDER, validated_key)) + filename = secure_filename( + os.path.join(dir_name, file.filename + extension) + ) + + if not os.path.exists(dir_name): + os.makedirs(dir_name) + file.save(filename) + return BARUtils.success_exit(True) else: - return BARUtils.error_exit("Invalid API key") + return BARUtils.error_exit("No file attached"), 400 + else: + return BARUtils.error_exit("Invalid API key"), 403 @summarization_gene_expression.route("/get_file_list", methods=["POST"]) class SummarizationGeneExpressionGetFileList(Resource): def post(self): """Returns a list of files stored in the user's folder""" - if request.method == "POST": - api_key = request.headers.get("x-api-key") - files = [] - if os.path.exists(os.path.join(DATA_FOLDER, api_key)): - for file in os.walk(os.path.join(DATA_FOLDER, api_key)): - files.append(file[2]) - return BARUtils.success_exit(files) - else: - return BARUtils.error_exit("No folder found") + api_key = request.headers.get("x-api-key") + + # Check Key + validated_key = SummarizationGeneExpressionUtils.validated_api_key(api_key) + if validated_key is None: + return BARUtils.error_exit("Invalid API key"), 403 + + files = [] + sec_file = secure_filename(os.path.join(DATA_FOLDER, validated_key)) + + if os.path.exists(sec_file): + for file in os.walk(sec_file): + files.append(file[2]) + return BARUtils.success_exit(files) + else: + return BARUtils.error_exit("No folder found") @summarization_gene_expression.route("/get_file/") @@ -526,25 +477,32 @@ class SummarizationGeneExpressionGetFile(Resource): @summarization_gene_expression.param("file_id", _in="path", default="test") def get(self, file_id): """Returns a specific file stored in the user's folder""" - if request.method == "GET": - api_key = request.headers.get("x-api-key") - filename = os.path.join(DATA_FOLDER, api_key, file_id) - if os.path.isfile(filename): - return send_file(filename) - else: - return BARUtils.error_exit("File not found"), 404 + api_key = request.headers.get("x-api-key") + + # Check key + validated_key = SummarizationGeneExpressionUtils.validated_api_key(api_key) + if validated_key is None: + return BARUtils.error_exit("Invalid API key"), 403 + + filename = secure_filename(os.path.join(DATA_FOLDER, validated_key, file_id)) + if os.path.isfile(filename): + return send_file(filename) + else: + return BARUtils.error_exit("File not found"), 404 @summarization_gene_expression.route("/clean_svg") class SummarizationGeneExpressionCleanSvg(Resource): def post(self): - if request.method == "POST": - api_key = request.headers.get("x-api-key") - if api_key is None: - return BARUtils.error_exit("Invalid API key"), 403 - elif SummarizationGeneExpressionUtils.decrement_uses(api_key): - in_string = request.get_json()["svg"] - out_string = scourString(in_string, options={"remove_metadata": True}) - return BARUtils.success_exit(out_string) - else: - return BARUtils.error_exit("Invalid API key") + api_key = request.headers.get("x-api-key") + + validated_key = SummarizationGeneExpressionUtils.validated_api_key(api_key) + + if validated_key is None: + return BARUtils.error_exit("Invalid API key"), 403 + elif SummarizationGeneExpressionUtils.decrement_uses(validated_key): + in_string = request.get_json()["svg"] + out_string = scourString(in_string, options={"remove_metadata": True}) + return BARUtils.success_exit(out_string) + else: + return BARUtils.error_exit("Invalid API key") diff --git a/api/resources/thalemine.py b/api/resources/thalemine.py index 751f42a7..d25b195c 100644 --- a/api/resources/thalemine.py +++ b/api/resources/thalemine.py @@ -75,3 +75,34 @@ def get(self, gene_id=""): ) return resp.json() + + +@thalemine.route("/gene_information/") +class ThaleMineGeneInformation(Resource): + @thalemine.param("gene_id", _in="path", default="At1g01010") + @cache.cached() + def get(self, gene_id=""): + """This end point retrieves gene infromation from ThaleMine given an AGI ID""" + gene_id = escape(gene_id) + + # Is data valid + if not BARUtils.is_arabidopsis_gene_valid(gene_id): + return BARUtils.error_exit("Invalid gene id"), 400 + + query = ( + ' ' + ) + query = query.format(gene_id) + + # Now query the web service + payload = {"format": "json", "query": query} + resp = requests.post( + "https://bar.utoronto.ca/thalemine/service/query/results", + data=payload, + headers=request_headers, + ) + + return resp.json() diff --git a/api/utils/api_manager_utils.py b/api/utils/api_manager_utils.py new file mode 100644 index 00000000..2caa5577 --- /dev/null +++ b/api/utils/api_manager_utils.py @@ -0,0 +1,86 @@ +import os +import requests +from cryptography.fernet import Fernet +from smtplib import SMTP_SSL +from ssl import create_default_context +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +CAPTCHA_KEY_FILE = "/home/bpereira/data/bar.summarization/key" + + +class ApiManagerUtils: + @staticmethod + def check_admin_pass(password): + # Replace below with key from script in /home/bpereira/dev/pw-key + key = os.environ.get("ADMIN_ENCRYPT_KEY") + cipher_suite = Fernet(key) + + with open(os.environ.get("ADMIN_PASSWORD_FILE"), "rb") as f: + for line in f: + encrypted_key = line + + decipher_text = cipher_suite.decrypt(encrypted_key) + plain_text_encrypted_password = bytes(decipher_text).decode("utf-8") + + if password == plain_text_encrypted_password: + return True + else: + return False + + @staticmethod + def validate_captcha(value): + """Validates a reCaptcha value using our secret token""" + if os.environ.get("BAR"): + with open(CAPTCHA_KEY_FILE, "rb") as f: + for line in f: + key = line + + if key: + ret = requests.post( + "https://www.google.com/recaptcha/api/siteverify", + data={"secret": key, "response": value}, + ) + return ret.json()["success"] + else: + return False + else: + return True + + @staticmethod + def send_email_notification(): + if os.environ.get("BAR"): + with open(os.environ.get("ADMIN_EMAIL"), "r") as f: + for line in f: + recipient = line + + port = 465 + key = os.environ.get("EMAIL_PASS_KEY") + cipher_suite = Fernet(key) + + with open(os.environ.get("EMAIL_PASS_FILE"), "rb") as f: + for line in f: + encrypted_key = line + + decipher_text = cipher_suite.decrypt(encrypted_key) + password = bytes(decipher_text).decode("utf-8") + context = create_default_context() + smtp_server = "smtp.gmail.com" + + sender_email = "bar.summarization@gmail.com" + subject = "[Bio-Analytic Resource] New API key request" + text = """\ + There is a new API key request. + You can approve or reject it at http://bar.utoronto.ca/~bpereira/webservices/bar-request-manager/build/index.html + """ + + m_text = MIMEText(text, _subtype="plain", _charset="UTF-8") + msg = MIMEMultipart() + msg["From"] = sender_email + msg["To"] = recipient + msg["Subject"] = subject + msg.attach(m_text) + + with SMTP_SSL(smtp_server, port, context=context) as server: + server.login("bar.summarization@gmail.com", password) + server.sendmail(sender_email, recipient, msg.as_string()) diff --git a/api/utils/summarization_gene_expression_utils.py b/api/utils/summarization_gene_expression_utils.py new file mode 100644 index 00000000..06744222 --- /dev/null +++ b/api/utils/summarization_gene_expression_utils.py @@ -0,0 +1,63 @@ +from api import summarization_db as db +from sqlalchemy.exc import SQLAlchemyError + + +class SummarizationGeneExpressionUtils: + @staticmethod + def get_table_object(table_name): + metadata = db.MetaData() + table_object = db.Table( + table_name, + metadata, + autoload=True, + autoload_with=db.get_engine(bind="summarization"), + ) + return table_object + + @staticmethod + def validated_api_key(key): + """Checks if a given API key is in the Users database + :param key: The API key to be checked + """ + tbl = SummarizationGeneExpressionUtils.get_table_object("users") + + # The key is an alphanumeric string + key = str(key) + if not key.isalnum(): + return None + + con = db.get_engine(bind="summarization") + try: + row = con.execute( + db.select([tbl.c.uses_left]).where(tbl.c.api_key == key) + ).first() + except SQLAlchemyError: + return None + if row is None: + return None + else: + if row.uses_left > 0: + return key + else: + return None + + @staticmethod + def decrement_uses(key): + """Subtracts 1 from the uses_left column of the user whose key matches the given string + :param key: The user's API key + """ + + # The key will be valid here. + tbl = SummarizationGeneExpressionUtils.get_table_object("users") + con = db.get_engine(bind="summarization") + try: + con.execute( + db.update(tbl) + .where(tbl.c.api_key == key) + .values(uses_left=(tbl.c.uses_left - 1)) + ) + db.session.commit() + except SQLAlchemyError as e: + error = str(e.__dict__["orig"]) + return error + return True diff --git a/config/BAR_API.cfg b/config/BAR_API.cfg index 140ec1ac..5c452903 100644 --- a/config/BAR_API.cfg +++ b/config/BAR_API.cfg @@ -17,6 +17,7 @@ SQLALCHEMY_BINDS = { 'poplar_nssnp' : 'mysql://root:root@localhost/poplar_nssnp', 'tomato_nssnp' : 'mysql://root:root@localhost/tomato_nssnp', 'eplant_poplar' : 'mysql://root:root@localhost/eplant_poplar', + 'eplant_rice' : 'mysql://root:root@localhost/eplant_rice', 'eplant_tomato' : 'mysql://root:root@localhost/eplant_tomato', 'tomato_sequence' : 'mysql://root:root@localhost/tomato_sequence', 'rice_interactions': 'mysql://root:root@localhost/rice_interactions' diff --git a/config/databases/eplant_rice.sql b/config/databases/eplant_rice.sql new file mode 100644 index 00000000..959c988c --- /dev/null +++ b/config/databases/eplant_rice.sql @@ -0,0 +1,60 @@ +-- MySQL dump 10.13 Distrib 8.0.23, for Linux (x86_64) +-- +-- Host: localhost Database: eplant_rice +-- ------------------------------------------------------ +-- Server version 8.0.23-3 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Current Database: `eplant_rice` +-- + +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `eplant_rice` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; + +USE `eplant_rice`; + +-- +-- Table structure for table `gene_annotation` +-- + +DROP TABLE IF EXISTS `gene_annotation`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `gene_annotation` ( + `gene` varchar(20) NOT NULL, + `annotation` mediumtext NOT NULL, + PRIMARY KEY (`gene`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `gene_annotation` +-- +-- WHERE: 1 limit 5 + +LOCK TABLES `gene_annotation` WRITE; +/*!40000 ALTER TABLE `gene_annotation` DISABLE KEYS */; +INSERT INTO `gene_annotation` VALUES ('LOC_Os01g01010','protein TBC domain containing protein, expressed'),('LOC_Os01g01030','protein monocopper oxidase, putative, expressed'),('LOC_Os01g01050', 'protein R3H domain containing protein, expressed'); +/*!40000 ALTER TABLE `gene_annotation` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2021-06-26 19:08:20 diff --git a/config/databases/summarization.sql b/config/databases/summarization.sql index 833ae6c3..53161344 100644 --- a/config/databases/summarization.sql +++ b/config/databases/summarization.sql @@ -32,11 +32,12 @@ DROP TABLE IF EXISTS `bb5a52387069485486b2f4861c2826dd`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `bb5a52387069485486b2f4861c2826dd` ( - `index` bigint(20) DEFAULT NULL, - `Gene` text DEFAULT NULL, - `Sample` text DEFAULT NULL, - `Value` float DEFAULT NULL, - KEY `ix_bb5a52387069485486b2f4861c2826dd_index` (`index`) + `proj_id` varchar(5) NOT NULL, + `sample_id` int(10) unsigned NOT NULL DEFAULT 0, + `data_probeset_id` varchar(24) NOT NULL, + `data_signal` float DEFAULT 0, + `data_bot_id` varchar(32) NOT NULL, + KEY `data_probeset_id` (`data_probeset_id`,`data_bot_id`,`data_signal`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -46,7 +47,7 @@ CREATE TABLE `bb5a52387069485486b2f4861c2826dd` ( LOCK TABLES `bb5a52387069485486b2f4861c2826dd` WRITE; /*!40000 ALTER TABLE `bb5a52387069485486b2f4861c2826dd` DISABLE KEYS */; -INSERT INTO `bb5a52387069485486b2f4861c2826dd` VALUES (0,'AT1G01010','sample1',32),(1,'AT1G01020','sample1',546),(2,'AT1G01030','sample1',43),(3,'AT1G01010','sample2',54),(4,'AT1G01020','sample2',65),(5,'AT1G01030','sample2',123); +INSERT INTO `bb5a52387069485486b2f4861c2826dd` VALUES (1, 1, 'AT1G01010',32,'sample1'),(1, 2,'AT1G01020',546,'sample1'),(1, 3, 'AT1G01030',43,'sample1'),(1, 4, 'AT1G01010',54,'sample2'),(1, 5, 'AT1G01020',65,'sample2'),(1, 6, 'AT1G01030',123,'sample2'); /*!40000 ALTER TABLE `bb5a52387069485486b2f4861c2826dd` ENABLE KEYS */; UNLOCK TABLES; diff --git a/config/init.sh b/config/init.sh index 82cfd24b..a8f1de8e 100755 --- a/config/init.sh +++ b/config/init.sh @@ -17,6 +17,7 @@ mysql -u $DB_USER -p$DB_PASS < ./config/databases/poplar_nssnp.sql mysql -u $DB_USER -p$DB_PASS < ./config/databases/tomato_nssnp.sql mysql -u $DB_USER -p$DB_PASS < ./config/databases/eplant_poplar.sql mysql -u $DB_USER -p$DB_PASS < ./config/databases/eplant_tomato.sql +mysql -u $DB_USER -p$DB_PASS < ./config/databases/eplant_rice.sql mysql -u $DB_USER -p$DB_PASS < ./config/databases/tomato_sequence.sql mysql -u $DB_USER -p$DB_PASS < ./config/databases/rice_interactions.sql diff --git a/requirements.txt b/requirements.txt index fc7e5df0..57b3b7d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,34 @@ aniso8601==9.0.1 appdirs==1.4.4 +async-timeout==4.0.2 attrs==21.4.0 -black==20.8b1 +black==22.3.0 certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 -charset-normalizer==2.0.10 -click==8.0.3 -coverage==6.2 -cryptography==36.0.1 +charset-normalizer==2.0.12 +click==8.0.4 +coverage==6.3.2 +cryptography==36.0.2 Deprecated==1.2.13 docopt==0.6.2 flake8==4.0.1 -Flask==2.0.2 +Flask==2.1.0 Flask-Caching==1.10.1 Flask-Cors==3.0.10 -Flask-Limiter==2.1 +Flask-Limiter==2.2.0 flask-marshmallow==0.14.0 flask-restx==0.5.1 Flask-SQLAlchemy==2.5.1 greenlet==1.1.2 idna==3.3 iniconfig==1.1.1 -itsdangerous==2.0.1 -Jinja2==3.0.3 +itsdangerous==2.1.2 +Jinja2==3.1.1 jsonschema==4.4.0 -limits==2.3.1 -MarkupSafe==2.0.1 -marshmallow==3.14.1 +limits==2.4.0 +MarkupSafe==2.1.1 +marshmallow==3.15.0 mccabe==0.6.1 more-itertools==8.12.0 mypy-extensions==0.4.3 @@ -36,6 +37,7 @@ numpy==1.21.5 packaging==21.3 pandas==1.3.5 pathspec==0.9.0 +platformdirs==2.5.1 pluggy==1.0.0 py==1.11.0 pycodestyle==2.8.0 @@ -43,19 +45,21 @@ pycparser==2.21 pyflakes==2.4.0 pyparsing==3.0.7 pyrsistent==0.18.1 -pytest==6.2.5 +pytest==7.1.1 python-dateutil==2.8.2 -pytz==2021.3 -redis==4.1.1 -regex==2022.1.18 +pytz==2022.1 +redis==4.2.0 +regex==2022.3.15 requests==2.27.1 scour==0.38.2 six==1.16.0 -SQLAlchemy==1.4.31 +SQLAlchemy==1.4.32 toml==0.10.2 +tomli==2.0.1 typed-ast==1.5.2 -typing_extensions==4.0.1 -urllib3==1.26.8 +typing_extensions==4.1.1 +urllib3==1.26.9 wcwidth==0.2.5 -Werkzeug==2.0.2 -wrapt==1.13.3 +Werkzeug==2.1.0 +wrapt==1.14.0 +zipp==3.7.0 diff --git a/tests/data/get_thalemine_gene_information_1.json b/tests/data/get_thalemine_gene_information_1.json new file mode 100644 index 00000000..d6950784 --- /dev/null +++ b/tests/data/get_thalemine_gene_information_1.json @@ -0,0 +1,43 @@ +{ + "modelName": "genomic", + "columnHeaders": [ + "Gene > DB identifier", + "Gene > Name", + "Gene > Secondary Identifier", + "Gene > Brief Description", + "Gene > Symbol", + "Gene > TAIR Aliases", + "Gene > TAIR Computational Description", + "Gene > TAIR Curator Summary", + "Gene > TAIR Short Description" + ], + "rootClass": "Gene", + "start": 0, + "views": [ + "Gene.primaryIdentifier", + "Gene.name", + "Gene.secondaryIdentifier", + "Gene.briefDescription", + "Gene.symbol", + "Gene.tairAliases", + "Gene.tairComputationalDescription", + "Gene.tairCuratorSummary", + "Gene.tairShortDescription" + ], + "results": [ + [ + "AT1G01010", + "NAC domain containing protein 1", + "locus:2200935", + "NAC domain containing protein 1", + "NAC001", + "ANAC001, NAC001, NTL10", + "NAC domain containing protein 1;(source:Araport11)", + null, + "NAC domain containing protein 1" + ] + ], + "wasSuccessful": true, + "error": null, + "statusCode": 200 +} diff --git a/tests/data/get_thalemine_gene_information_2.json b/tests/data/get_thalemine_gene_information_2.json new file mode 100644 index 00000000..ae043283 --- /dev/null +++ b/tests/data/get_thalemine_gene_information_2.json @@ -0,0 +1,31 @@ +{ + "modelName": "genomic", + "columnHeaders": [ + "Gene > DB identifier", + "Gene > Name", + "Gene > Secondary Identifier", + "Gene > Brief Description", + "Gene > Symbol", + "Gene > TAIR Aliases", + "Gene > TAIR Computational Description", + "Gene > TAIR Curator Summary", + "Gene > TAIR Short Description" + ], + "rootClass": "Gene", + "start": 0, + "views": [ + "Gene.primaryIdentifier", + "Gene.name", + "Gene.secondaryIdentifier", + "Gene.briefDescription", + "Gene.symbol", + "Gene.tairAliases", + "Gene.tairComputationalDescription", + "Gene.tairCuratorSummary", + "Gene.tairShortDescription" + ], + "results": [], + "wasSuccessful": true, + "error": null, + "statusCode": 200 +} diff --git a/tests/resources/test_api_manager.py b/tests/resources/test_api_manager.py index efe7d7ab..e5ade5b6 100644 --- a/tests/resources/test_api_manager.py +++ b/tests/resources/test_api_manager.py @@ -4,10 +4,21 @@ class TestIntegrations(TestCase): + # Currently, the test needs to run in order below def setUp(self): self.app_client = app.test_client() - def test_0_validate_api_key(self): + def test_0_validate_admin_password(self): + # Invalid password + response = self.app_client.post( + "/api_manager/validate_admin_password", json={"password": "abc"} + ) + data = json.loads(response.get_data(as_text=True)) + expected = {"wasSuccessful": True, "data": False} + self.assertEqual(data, expected) + + def test_1_validate_api_key(self): + # Valid API key response = self.app_client.post( "/api_manager/validate_api_key", json={"key": "bb5a52387069485486b2f4861c2826dd"}, @@ -17,7 +28,17 @@ def test_0_validate_api_key(self): self.assertEqual(response.status_code, 200) self.assertEqual(data, expected) - def test_1_request(self): + # Invalid API key + response = self.app_client.post( + "/api_manager/validate_api_key", + json={"key": "abc"}, + ) + data = json.loads(response.get_data(as_text=True)) + expected = {"wasSuccessful": False, "error": "API key not found"} + self.assertEqual(response.status_code, 404) + self.assertEqual(data, expected) + + def test_2_request(self): # testPassword set in the test config response = self.app_client.post( "/api_manager/request", @@ -30,10 +51,11 @@ def test_1_request(self): ) data = json.loads(response.get_data(as_text=True)) expected = {"wasSuccessful": True, "data": "Data added"} - self.assertEqual(response.status_code, 200) self.assertEqual(data, expected) + self.assertEqual(response.status_code, 200) - def test_2_get_pending_requests(self): + def test_3_get_pending_requests(self): + # Password is correct response = self.app_client.post( "/api_manager/get_pending_requests", json={"password": "testPassword"} ) @@ -58,7 +80,17 @@ def test_2_get_pending_requests(self): self.assertEqual(response.status_code, 200) self.assertEqual(data, expected) - def test_3_reject_request(self): + # Password is not correct + response = self.app_client.post( + "/api_manager/get_pending_requests", json={"password": "abc"} + ) + data = json.loads(response.get_data(as_text=True)) + expected = {"wasSuccessful": False, "error": "Forbidden"} + self.assertEqual(response.status_code, 403) + self.assertEqual(data, expected) + + def test_4_reject_request(self): + # Correct password response = self.app_client.post( "/api_manager/reject_request", json={"password": "testPassword", "email": "unittest@gmail.com"}, @@ -67,3 +99,13 @@ def test_3_reject_request(self): expected = {"wasSuccessful": True, "data": True} self.assertEqual(response.status_code, 200) self.assertEqual(data, expected) + + # Incorrect password + response = self.app_client.post( + "/api_manager/reject_request", + json={"password": "abc", "email": "unittest@gmail.com"}, + ) + data = json.loads(response.get_data(as_text=True)) + expected = {"wasSuccessful": False, "error": "Forbidden"} + self.assertEqual(response.status_code, 403) + self.assertEqual(data, expected) diff --git a/tests/resources/test_gene_annotation.py b/tests/resources/test_gene_annotation.py index 407add8d..569421da 100644 --- a/tests/resources/test_gene_annotation.py +++ b/tests/resources/test_gene_annotation.py @@ -1,5 +1,6 @@ from api import app from unittest import TestCase +import json class TestIntegrations(TestCase): @@ -7,8 +8,87 @@ def setUp(self): self.app_client = app.test_client() def test_keyword_search(self): + # A working version test_query = "alpha-1 protein" - response = self.app_client.get(f"/gene_annotation/{test_query}").json - self.assertEqual(response["status"], "success") - annotation = response["result"][0]["gene_annotation"] - self.assertIn(test_query, annotation) + response = self.app_client.get(f"/gene_annotation/{test_query}") + expected = { + "status": "success", + "query": "alpha-1 protein", + "result": [ + { + "gene": "Potri.001G000700.1", + "species": "poplar", + "gene_annotation": "similar to PUR alpha-1 protein; similar to PUR alpha-1 GI:5081612 from (Arabidopsis thaliana); similar to PUR alpha-1 protein; similar to PUR alpha-1 GI:5081612 from (Arabidopsis thaliana); [ co-ortholog (1of2) of At2g32080, ]", + }, + { + "gene": "Potri.001G000700.2", + "species": "poplar", + "gene_annotation": "similar to PUR alpha-1 protein; similar to PUR alpha-1 GI:5081612 from (Arabidopsis thaliana); similar to PUR alpha-1 protein; similar to PUR alpha-1 GI:5081612 from (Arabidopsis thaliana); [ co-ortholog (1of2) of At2g32080, ]", + }, + ], + } + self.assertEqual(response.json, expected) + + # Not working version + test_query = "alpha-1 protein" + response = self.app_client.get("/gene_annotation/abc") + expected = { + "wasSuccessful": False, + "error": "There are no data found for the given query", + } + self.assertEqual(response.json, expected) + + def test_post_anntns(self): + """ + This function test retrieving gene annotations for various species' genes via POST. + """ + + # Valid request + response = self.app_client.post( + "/gene_annotation/", + json={"species": "rice", "genes": ["LOC_Os01g01010", "LOC_Os01g01050"]}, + ) + data = json.loads(response.get_data(as_text=True)) + expected = { + "wasSuccessful": True, + "data": [ + { + "gene": "LOC_Os01g01010", + "annotation": "protein TBC domain containing protein, expressed" + }, + { + "gene": "LOC_Os01g01050", + "annotation": "protein R3H domain containing protein, expressed" + } + ] + } + self.assertEqual(data, expected) + + # Invalid species + response = self.app_client.post( + "/interactions/", + json={"species": "poplar", "genes": ["LOC_Os01g01080", "LOC_Os01g73310"]}, + ) + data = json.loads(response.get_data(as_text=True)) + expected = {"wasSuccessful": False, "error": "Invalid species"} + self.assertEqual(data, expected) + + # Invalid gene ID + response = self.app_client.post( + "/interactions/", json={"species": "rice", "genes": ["abc", "xyz"]} + ) + data = json.loads(response.get_data(as_text=True)) + expected = {"wasSuccessful": False, "error": "Invalid gene id"} + self.assertEqual(data, expected) + + # No data for valid gene IDs + response = self.app_client.post( + "/interactions/", + json={"species": "rice", "genes": ["LOC_Os01g01085", "LOC_Os01g52565"]}, + ) + data = json.loads(response.get_data(as_text=True)) + expected = { + "wasSuccessful": False, + "error": "No data for the given species/genes", + } + self.assertEqual(data, expected) diff --git a/tests/resources/test_gene_information.py b/tests/resources/test_gene_information.py index bf945a3d..21824139 100644 --- a/tests/resources/test_gene_information.py +++ b/tests/resources/test_gene_information.py @@ -64,6 +64,12 @@ def test_get_arabidopsis_gene_isoform(self): expected = {"wasSuccessful": True, "data": ["Potri.001G000300.1"]} self.assertEqual(response.json, expected) + response = self.app_client.get( + "/gene_information/gene_isoforms/tomato/Solyc00g005000" + ) + expected = {"wasSuccessful": True, "data": ["Solyc00g005000.3.1"]} + self.assertEqual(response.json, expected) + # Data not found, but gene is valid response = self.app_client.get( "/gene_information/gene_isoforms/arabidopsis/At3g24651" @@ -84,6 +90,15 @@ def test_get_arabidopsis_gene_isoform(self): } self.assertEqual(response.json, expected) + response = self.app_client.get( + "/gene_information/gene_isoforms/tomato/Solyc00g005001" + ) + expected = { + "wasSuccessful": False, + "error": "There are no data found for the given gene", + } + self.assertEqual(response.json, expected) + # Invalid Gene response = self.app_client.get( "/gene_information/gene_isoforms/arabidopsis/At3g2465x" @@ -97,6 +112,12 @@ def test_get_arabidopsis_gene_isoform(self): expected = {"wasSuccessful": False, "error": "Invalid gene id"} self.assertEqual(response.json, expected) + response = self.app_client.get( + "/gene_information/gene_isoforms/tomato/Solyc00g00500x" + ) + expected = {"wasSuccessful": False, "error": "Invalid gene id"} + self.assertEqual(response.json, expected) + # Invalid Species response = self.app_client.get("/gene_information/gene_isoforms/x/At3g24650") expected = {"wasSuccessful": False, "error": "No data for the given species"} @@ -134,6 +155,14 @@ def test_post_arabidopsis_gene_isoform(self): } self.assertEqual(response.json, expected) + data = {"species": "tomato", "genes": ["Solyc00g005000"]} + response = self.app_client.post("/gene_information/gene_isoforms/", json=data) + expected = { + "wasSuccessful": True, + "data": {"Solyc00g005000": ["Solyc00g005000.3.1"]}, + } + self.assertEqual(response.json, expected) + # Invalid data in JSON data = { "species": "arabidopsis", @@ -190,6 +219,11 @@ def test_post_arabidopsis_gene_isoform(self): expected = {"wasSuccessful": False, "error": "Invalid gene id"} self.assertEqual(response.json, expected) + data = {"species": "tomato", "genes": ["abc"]} + response = self.app_client.post("/gene_information/gene_isoforms/", json=data) + expected = {"wasSuccessful": False, "error": "Invalid gene id"} + self.assertEqual(response.json, expected) + # Check if there is data for the given gene data = {"species": "arabidopsis", "genes": ["AT1G01011"]} response = self.app_client.post("/gene_information/gene_isoforms/", json=data) @@ -221,3 +255,11 @@ def test_post_arabidopsis_gene_isoform(self): "error": "No data for the given species/genes", } self.assertEqual(response.json, expected) + + data = {"species": "tomato", "genes": ["Solyc00g005001"]} + response = self.app_client.post("/gene_information/gene_isoforms/", json=data) + expected = { + "wasSuccessful": False, + "error": "No data for the given species/genes", + } + self.assertEqual(response.json, expected) diff --git a/tests/resources/test_sequence.py b/tests/resources/test_sequence.py index e00e756e..aacd0b26 100644 --- a/tests/resources/test_sequence.py +++ b/tests/resources/test_sequence.py @@ -10,6 +10,7 @@ def test_get_gene_alias_list(self): """This function tests the gene sequence endpoint :return: """ + # Valid request response = self.app_client.get("/sequence/tomato/Solyc00g005445.1.1") expected = { "status": "success", @@ -22,3 +23,13 @@ def test_get_gene_alias_list(self): ], } self.assertEqual(response.json, expected) + + # Invalid species + response = self.app_client.get("/sequence/abc/Solyc00g005445.1.1") + expected = {"wasSuccessful": False, "error": "Invalid species"} + self.assertEqual(response.json, expected) + + # Invalid gene id + response = self.app_client.get("/sequence/Tomato/abc") + expected = {"wasSuccessful": False, "error": "Invalid gene id"} + self.assertEqual(response.json, expected) diff --git a/tests/resources/test_snps.py b/tests/resources/test_snps.py index e18bec95..593fdbf2 100644 --- a/tests/resources/test_snps.py +++ b/tests/resources/test_snps.py @@ -28,6 +28,14 @@ def test_get_phenix(self): } self.assertEqual(response.json, expected) + # Valid request + response = self.app_client.get("/snps/phenix/SOLYC01G097110.2.1/AT5G01040.1") + expected = { + "wasSuccessful": True, + "data": "//bar.utoronto.ca/phenix-pdbs/SOLYC01G097110.2.1-AT5G01040.1-phenix.pdb", + } + self.assertEqual(response.json, expected) + # Invalid fixed gene response = self.app_client.get("/snps/phenix/abc/AT5G01040.1") expected = {"wasSuccessful": False, "error": "Invalid fixed pdb gene id"} @@ -105,3 +113,18 @@ def test_get_gene_alias(self): "error": "There are no data found for the given gene", } self.assertEqual(response.json, expected) + + def test_get_samples(self): + """This functions test sample. Maybe this is place holder code?""" + # Valid data + response = self.app_client.get("/snps/tomato/samples") + expected = { + "wasSuccessful": True, + "data": {"001": {"alias": "Moneymaker", "species": "Solanum lycopersicum"}}, + } + self.assertEqual(response.json, expected) + + # Invalid data + response = self.app_client.get("/snps/abc/samples") + expected = {"wasSuccessful": False, "error": "Invalid gene id"} + self.assertEqual(response.json, expected) diff --git a/tests/resources/test_thalemine.py b/tests/resources/test_thalemine.py index d70c4ce5..a040b312 100644 --- a/tests/resources/test_thalemine.py +++ b/tests/resources/test_thalemine.py @@ -85,3 +85,36 @@ def test_get_thalemine_publications(self): response = self.app_client.get("/thalemine/publications/At1g0101x") expected = {"wasSuccessful": False, "error": "Invalid gene id"} self.assertEqual(response.json, expected) + + def test_get_thalemine_gene_information(self): + """This tests the data returned by the ThaleMine gene information endpoint + :return: + """ + # Valid data + response = self.app_client.get("/thalemine/gene_information/At1g01010") + + # Delete query time data. It will always be different + response = response.json + response.pop("executionTime") + + # Note: pytest is running from project root. So path is relative to project root + # Also, delete execution time from output before saving. + with open("tests/data/get_thalemine_gene_information_1.json") as json_file: + expected = load(json_file) + self.assertEqual(response, expected) + + # Valid but does not exists + response = self.app_client.get("/thalemine/gene_information/At1g01011") + + # Again, delete time data. + response = response.json + response.pop("executionTime") + + with open("tests/data/get_thalemine_gene_information_2.json") as json_file: + expected = load(json_file) + self.assertEqual(response, expected) + + # Invalid gene id + response = self.app_client.get("/thalemine/gene_information/At1g0101x") + expected = {"wasSuccessful": False, "error": "Invalid gene id"} + self.assertEqual(response.json, expected)