diff --git a/api/resources/api_manager.py b/api/resources/api_manager.py index 1d99c8ac..0a7327ca 100644 --- a/api/resources/api_manager.py +++ b/api/resources/api_manager.py @@ -40,14 +40,14 @@ def check_admin_pass(password): @staticmethod def validate_captcha(value): """Validates a reCaptcha value using our secret token""" - if(os.environ.get("BAR")): + 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} + data={"secret": key, "response": value}, ) return ret.json()["success"] else: @@ -128,7 +128,7 @@ class ApiManagerRequest(Resource): def post(self): if request.method == "POST": captchaVal = request.headers.get("captchaVal") - if(ApiManagerUtils.validate_captcha(captchaVal)): + if ApiManagerUtils.validate_captcha(captchaVal): response_json = request.get_json() df = pandas.DataFrame.from_records([response_json]) con = db.get_engine(bind="summarization") diff --git a/api/resources/efp_image.py b/api/resources/efp_image.py index 66b66abf..f86e3286 100644 --- a/api/resources/efp_image.py +++ b/api/resources/efp_image.py @@ -2,11 +2,14 @@ import re import requests import random -import redis.connection +import os +import time +import redis.exceptions from flask_restx import Namespace, Resource from markupsafe import escape from flask import send_from_directory from api.utils.bar_utils import BARUtils +from api.utils.efp_utils import eFPUtils efp_image = Namespace( "eFP Image", description="eFP Image generation service", path="/efp_image" @@ -18,8 +21,14 @@ class eFPImageList(Resource): def get(self): """This end point returns the list of species available""" # This are the only species available so far - # If this is updated, update get request and test as well - species = ["efp_arabidopsis"] + # If this is updated, update efp_utils.py and unit tests as well + species = [ + "efp_arabidopsis", + "efp_cannabis", + "efp_arachis", + "efp_soybean", + "efp_maize", + ] return BARUtils.success_exit(species) @@ -29,19 +38,6 @@ def get(self): doc=False, ) class eFPImage(Resource): - @staticmethod - def is_efp_mode(efp_mode): - """This function checks if the eFP mode is valid - :param efp_mode: string eFP Mode - :return: True or False - """ - # This are case sensitive - valid_modes = ["Absolute", "Relative", "Compare"] - if efp_mode in valid_modes: - return True - else: - return False - @efp_image.param("efp", _in="path", default="efp_arabidopsis") @efp_image.param("view", _in="path", default="Developmental_Map") @efp_image.param("mode", _in="path", default="Absolute") @@ -49,10 +45,6 @@ def is_efp_mode(efp_mode): @efp_image.param("gene_2", _in="path", default="At3g27340") def get(self, efp="", view="", mode="", gene_1="", gene_2=""): """This end point returns eFP images.""" - # list of allowed eFPs - # See endpoint able - species = ["efp_arabidopsis"] - # Escape input data efp = escape(efp) view = escape(view) @@ -60,32 +52,29 @@ def get(self, efp="", view="", mode="", gene_1="", gene_2=""): gene_1 = escape(gene_1) gene_2 = escape(gene_2) - # Validate values - if efp not in species: - return BARUtils.error_exit("Invalid eFP."), 400 - - # Validate view - if not BARUtils.is_efp_view_name(view): - return BARUtils.error_exit("Invalid eFP View name."), 400 - - # Validate mode - if not self.is_efp_mode(mode): - return BARUtils.error_exit("Invalid eFP mode."), 400 + validation = eFPUtils.is_efp_input_valid(efp, view, mode, gene_1, gene_2) + if validation[0] is False: + return BARUtils.error_exit(validation[1]), 400 - # Validate gene ids - if not BARUtils.is_arabidopsis_gene_valid(gene_1): - return BARUtils.error_exit("Gene 1 is invalid."), 400 + # Data are valid. Clear directory before running + for file in os.listdir("output"): + # Full file name is required at this point + file = os.path.join("output", file) - if mode == "Compare": - if not BARUtils.is_arabidopsis_gene_valid(gene_2): - return BARUtils.error_exit("Gene 2 is invalid."), 400 + # Check if it is a png file and is greater than 5 minutes old + if ( + os.path.isfile(file) + and (os.path.splitext(file)[1] == ".png") + and (os.stat(file).st_mtime < (time.time() - 5 * 60)) + ): + os.remove(file) # Check if request is cached try: r = BARUtils.connect_redis() key = "BAR_API_efp_image_" + "_".join([efp, view, mode, gene_1, gene_2]) efp_image_base64 = r.get(key) - except redis.connection.ConnectionError: + except redis.exceptions.ConnectionError: # Failed redis connection r = None key = None diff --git a/api/utils/bar_utils.py b/api/utils/bar_utils.py index c1fe521b..80d02d2d 100644 --- a/api/utils/bar_utils.py +++ b/api/utils/bar_utils.py @@ -72,6 +72,58 @@ def is_tomato_gene_valid(gene, isoform_id=False): else: return False + @staticmethod + def is_cannabis_gene_valid(gene): + """This function verifies if cannabis gene is valid: AGQN03000001 + :param gene: + :return: True if valid + """ + if gene and re.search(r"^AGQN\d{0,10}$", gene, re.I): + return True + else: + return False + + @staticmethod + def is_arachis_gene_valid(gene): + """This function verifies if arachis gene is valid: Adur10000_comp0_c0_seq1 + :param gene: + :return: True if valid + """ + if gene and re.search( + r"Adur\d{1,10}_comp\d{1,3}_\D{1,3}\d{1,3}_seq\d{1,5}", gene, re.I + ): + return True + else: + return False + + @staticmethod + def is_soybean_gene_valid(gene): + """This function verifies if soybean gene is valid: Glyma06g47400 + :param gene: + :return: True if valid + """ + if gene and re.search( + r"^((Glyma\d{1,3}g\d{1,6}\.?\d?)|(Glyma\.\d{1,3}g\d{1,8}))$", gene, re.I + ): + return True + else: + return False + + @staticmethod + def is_maize_gene_valid(gene): + """This function verifies if maize gene is valid: Zm00001d046170 + :param gene: + :return: True if valid + """ + if gene and re.search( + r"^(AC[0-9]{6}\.[0-9]{1}_FG[0-9]{3})|(AC[0-9]{6}\.[0-9]{1}_FGT[0-9]{3})|(GRMZM(2|5)G[0-9]{6})|(GRMZM(2|5)G[0-9]{6}_T[0-9]{2})|(Zm\d+d\d+)$", + gene, + re.I, + ): + return True + else: + return False + @staticmethod def is_integer(data): """Check if the input is at max ten figure number. @@ -91,17 +143,6 @@ def format_poplar(poplar_gene): """ return poplar_gene.translate(str.maketrans("pOTRIg", "PotriG")) - @staticmethod - def is_efp_view_name(efp_view): - """This function is used for validated eFP View names for eFP service - :param efp_view: string view name - :return: True if valid - """ - if efp_view and re.search(r"^[a-z1-9_]{1,20}$", efp_view, re.I): - return True - else: - return False - @staticmethod def connect_redis(): """This function connects to redis diff --git a/api/utils/efp_utils.py b/api/utils/efp_utils.py new file mode 100644 index 00000000..b78d1522 --- /dev/null +++ b/api/utils/efp_utils.py @@ -0,0 +1,106 @@ +import re +from api.utils.bar_utils import BARUtils + + +class eFPUtils: + @staticmethod + def is_efp_view_name(efp_view): + """This function is used for validated eFP View names for eFP service + :param efp_view: string view name + :return: True if valid + """ + if efp_view and re.search(r"^[a-z1-9_]{1,50}$", efp_view, re.I): + return True + else: + return False + + @staticmethod + def is_efp_mode(efp_mode): + """This function checks if the eFP mode is valid + :param efp_mode: string eFP Mode + :return: True or False + """ + # These are case-sensitive + valid_modes = ["Absolute", "Relative", "Compare"] + if efp_mode in valid_modes: + return True + else: + return False + + @staticmethod + def is_efp_input_valid(efp, view, mode, gene_1, gene_2=None): + """Test if eFP input is valid + :return: List with boolean and string + """ + species = [ + "efp_arabidopsis", + "efp_cannabis", + "efp_arachis", + "efp_soybean", + "efp_maize", + ] + + # Validate values + if efp not in species: + return False, "Invalid eFP." + + # Validate view + if not eFPUtils.is_efp_view_name(view): + return False, "Invalid eFP View name." + + # Validate mode + if not eFPUtils.is_efp_mode(mode): + return False, "Invalid eFP mode." + + # Maybe this part could be improved + if efp == "efp_arabidopsis": + # Validate gene ids + if not BARUtils.is_arabidopsis_gene_valid(gene_1): + return False, "Gene 1 is invalid." + + if mode == "Compare": + if not BARUtils.is_arabidopsis_gene_valid(gene_2): + return False, "Gene 2 is invalid." + + if efp == "efp_cannabis": + # Validate gene ids + if not BARUtils.is_cannabis_gene_valid(gene_1): + return False, "Gene 1 is invalid." + + if mode == "Compare": + if not BARUtils.is_cannabis_gene_valid(gene_2): + return False, "Gene 2 is invalid." + + if efp == "efp_arachis": + # Validate gene ids + if not BARUtils.is_arachis_gene_valid(gene_1): + return False, "Gene 1 is invalid." + + if mode == "Compare": + if not BARUtils.is_arachis_gene_valid(gene_2): + return False, "Gene 2 is invalid." + + if efp == "efp_soybean": + # Validate gene ids + if not BARUtils.is_soybean_gene_valid(gene_1): + return False, "Gene 1 is invalid." + + if mode == "Compare": + if not BARUtils.is_soybean_gene_valid(gene_2): + return False, "Gene 2 is invalid." + + if efp == "efp_maize": + # Validate gene ids + if not BARUtils.is_maize_gene_valid(gene_1): + return False, "Gene 1 is invalid." + + if mode == "Compare": + if not BARUtils.is_maize_gene_valid(gene_2): + return False, "Gene 2 is invalid." + + # In compare mode gene1 != gene2 + if mode == "Compare" and gene_1 == gene_2: + return False, "In compare mode, both genes should be different." + + # Assuming all check have passed + return True, None diff --git a/requirements.txt b/requirements.txt index bf3c5a25..f6e5910e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ aniso8601==9.0.1 appdirs==1.4.4 -attrs==21.2.0 +attrs==21.4.0 black==20.8b1 certifi==2021.10.8 cffi==1.15.0 @@ -8,14 +8,14 @@ chardet==4.0.0 charset-normalizer==2.0.9 click==8.0.3 coverage==6.2 -cryptography==36.0.0 +cryptography==36.0.1 Deprecated==1.2.13 docopt==0.6.2 flake8==4.0.1 Flask==2.0.2 Flask-Caching==1.10.1 Flask-Cors==3.0.10 -Flask-Limiter==2.0.2 +Flask-Limiter==2.0.4 flask-marshmallow==0.14.0 flask-restx==0.5.1 Flask-SQLAlchemy==2.5.1 @@ -24,17 +24,17 @@ idna==3.3 iniconfig==1.1.1 itsdangerous==2.0.1 Jinja2==3.0.3 -jsonschema==4.2.1 -limits==2.0.3 +jsonschema==4.3.2 +limits==2.1.0 MarkupSafe==2.0.1 marshmallow==3.14.1 mccabe==0.6.1 more-itertools==8.12.0 mypy-extensions==0.4.3 mysqlclient==2.1.0 -numpy==1.21.4 +numpy==1.21.5 packaging==21.3 -pandas==1.3.4 +pandas==1.3.5 pathspec==0.9.0 pluggy==1.0.0 py==1.11.0 @@ -46,12 +46,12 @@ pyrsistent==0.18.0 pytest==6.2.5 python-dateutil==2.8.2 pytz==2021.3 -redis==4.0.2 +redis==4.1.0 regex==2021.11.10 requests==2.26.0 scour==0.38.2 six==1.16.0 -SQLAlchemy==1.4.28 +SQLAlchemy==1.4.29 toml==0.10.2 typed-ast==1.5.1 typing_extensions==4.0.1 diff --git a/tests/resources/test_efp_image.py b/tests/resources/test_efp_image.py index 97bc30a2..0b77e550 100644 --- a/tests/resources/test_efp_image.py +++ b/tests/resources/test_efp_image.py @@ -11,7 +11,16 @@ def test_get_efp_image_list(self): :return: """ response = self.app_client.get("/efp_image/") - expected = {"wasSuccessful": True, "data": ["efp_arabidopsis"]} + expected = { + "wasSuccessful": True, + "data": [ + "efp_arabidopsis", + "efp_cannabis", + "efp_arachis", + "efp_soybean", + "efp_maize", + ], + } self.assertEqual(response.json, expected) def test_get_efp_image(self): @@ -20,16 +29,16 @@ def test_get_efp_image(self): """ # Test absolute modes in the beginning # A very basic test for Arabidopsis requests - # https://bar.utoronto.ca/api/efp_image/efp_arabidopsis/Developmental_Map/Absolute/At1g01010 + # https://bar.utoronto.ca/api/efp_image/efp_cannabis/Cannabis_Atlas/Absolute/AGQN03000001 response = self.app_client.get( - "/efp_image/efp_arabidopsis/Developmental_Map/Absolute/At1g01010" + "/efp_image/efp_cannabis/Cannabis_Atlas/Absolute/AGQN03000001" ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content_type, "image/png") - # Now rerun for Cached requests. A image should be return from the cache + # Now rerun for Cached requests. An image should be return from the cache response = self.app_client.get( - "/efp_image/efp_arabidopsis/Developmental_Map/Absolute/At1g01010" + "/efp_image/efp_cannabis/Cannabis_Atlas/Absolute/AGQN03000001" ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content_type, "image/png") @@ -69,25 +78,9 @@ def test_get_efp_image(self): } self.assertEqual(response.json, expected) - # Test for gene 2 using Arabidopsis + # Test for gene 1 using Arabidopsis response = self.app_client.get( "/efp_image/efp_arabidopsis/Root/Compare/At1g01010/Abc" ) expected = {"wasSuccessful": False, "error": "Gene 2 is invalid."} self.assertEqual(response.json, expected) - - # Test compare modes in the end - # A very basic test for Arabidopsis requests - # https://bar.utoronto.ca/api/efp_image/efp_arabidopsis/Developmental_Map/Compare/At1g01010/At1g01030 - response = self.app_client.get( - "/efp_image/efp_arabidopsis/Developmental_Map/Compare/At1g01010/At1g01030" - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content_type, "image/png") - - # Rerun for cached request. An image should be returned - response = self.app_client.get( - "/efp_image/efp_arabidopsis/Developmental_Map/Compare/At1g01010/At1g01030" - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content_type, "image/png") diff --git a/tests/utils/test_efp_utils.py b/tests/utils/test_efp_utils.py new file mode 100644 index 00000000..d3bc1a7a --- /dev/null +++ b/tests/utils/test_efp_utils.py @@ -0,0 +1,169 @@ +from unittest import TestCase +from api.utils.efp_utils import eFPUtils + + +class UtilsUnitTest(TestCase): + def test_is_efp_input_valid(self): + """Tests for eFP input data""" + + # eFP Arabidopsis compare mode + result = eFPUtils.is_efp_input_valid( + "efp_arabidopsis", "Root", "Compare", "At1g01010", "At1g01030" + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_arabidopsis", "Root", "Compare", "At1g01010", "Abc" + ) + expected = "Gene 2 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # eFP Cannabis gene1 + result = eFPUtils.is_efp_input_valid( + "efp_cannabis", "Cannabis_Atlas", "Absolute", "AGQN03000001" + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_cannabis", "Cannabis_Atlas", "Absolute", "Abc" + ) + expected = "Gene 1 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # eFP Cannabis gene2 + result = eFPUtils.is_efp_input_valid( + "efp_cannabis", "Cannabis_Atlas", "Compare", "AGQN03000001", "AGQN03000012" + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_cannabis", "Cannabis_Atlas", "Compare", "AGQN03000001", "Abc" + ) + expected = "Gene 2 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # eFP Arachis Absolute + result = eFPUtils.is_efp_input_valid( + "efp_arachis", "Arachis_Atlas", "Absolute", "Adur10002_comp0_c0_seq1" + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_arachis", "Arachis_Atlas", "Absolute", "Abc" + ) + expected = "Gene 1 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # eFP Arachis gene 2 + result = eFPUtils.is_efp_input_valid( + "efp_arachis", + "Arachis_Atlas", + "Compare", + "Adur10002_comp0_c0_seq1", + "Adur10002_comp0_c0_seq11", + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_arachis", "Arachis_Atlas", "Compare", "Adur10002_comp0_c0_seq1", "Abc" + ) + expected = "Gene 2 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # eFP Soybean Absolute + result = eFPUtils.is_efp_input_valid( + "efp_soybean", "soybean", "Absolute", "Glyma06g47400" + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_soybean", "soybean", "Absolute", "Abc" + ) + expected = "Gene 1 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # eFP Soybean gene 2 + result = eFPUtils.is_efp_input_valid( + "efp_soybean", + "soybean", + "Compare", + "Glyma06g47400", + "Glyma06g47390", + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_soybean", "soybean", "Compare", "Glyma06g47400", "Abc" + ) + expected = "Gene 2 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # Test if both gene are the same in eFP Comare mode + result = eFPUtils.is_efp_input_valid( + "efp_arachis", + "Arachis_Atlas", + "Compare", + "Adur10002_comp0_c0_seq1", + "Adur10002_comp0_c0_seq1", + ) + expected = "In compare mode, both genes should be different." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # eFP Maize Absolute + result = eFPUtils.is_efp_input_valid( + "efp_maize", "maize_iplant", "Absolute", "Zm00001d046170" + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_maize", "maize_iplant", "Absolute", "Abc" + ) + expected = "Gene 1 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # eFP Maize gene 2 + result = eFPUtils.is_efp_input_valid( + "efp_maize", + "maize_iplant", + "Compare", + "Zm00001d046170", + "Zm00001d014297", + ) + self.assertTrue(result[0]) + self.assertIsNone(result[1]) + + result = eFPUtils.is_efp_input_valid( + "efp_maize", "maize_iplant", "Compare", "Zm00001d046170", "Abc" + ) + expected = "Gene 2 is invalid." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected) + + # Test if both gene are the same in eFP Comare mode + result = eFPUtils.is_efp_input_valid( + "efp_arachis", + "Arachis_Atlas", + "Compare", + "Adur10002_comp0_c0_seq1", + "Adur10002_comp0_c0_seq1", + ) + expected = "In compare mode, both genes should be different." + self.assertFalse(result[0]) + self.assertEqual(result[1], expected)