From d21eb9678af46ac97736f9f59f2b10e14d250f3c Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 4 Nov 2024 15:58:13 -0800 Subject: [PATCH 01/34] add custom data not found exception handler to all endpoings --- app/main.py | 14 +++++++++++++- app/routers/bioentity.py | 14 +++++++++++--- app/routers/labeler.py | 7 +++++-- app/routers/models.py | 16 +++++++++++++++- app/routers/ontology.py | 26 +++++++++++++++++++++----- app/routers/pathway_widget.py | 4 ++++ app/routers/prefixes.py | 7 +++++++ app/routers/publications.py | 3 +++ app/routers/ribbon.py | 8 ++++++++ app/routers/search.py | 3 +++ app/routers/slimmer.py | 10 +++++++--- app/routers/users_and_groups.py | 13 +++++++++++++ 12 files changed, 110 insertions(+), 15 deletions(-) diff --git a/app/main.py b/app/main.py index 6d59e34..9a82def 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,7 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse - +from fastapi import HTTPException from app.middleware.logging_middleware import LoggingMiddleware from app.routers import ( bioentity, @@ -24,6 +24,10 @@ logger = logging.getLogger("uvicorn.error") +class DataNotFoundException(HTTPException): + def __init__(self, detail: str = "Data not found"): + super().__init__(status_code=404, detail=detail) + app = FastAPI( title="GO API", description="The Gene Ontology API.\n\n __Source:__ 'https://github.com/geneontology/go-fastapi'", @@ -81,6 +85,14 @@ async def general_exception_handler(request: Request, exc: Exception): content={"message": "An unexpected error occurred. Please try again later."}, ) +@app.exception_handler(DataNotFoundException) +async def data_not_found_exception_handler(request: Request, exc: DataNotFoundException): + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail} + ) + + if __name__ == "__main__": uvicorn.run("main:app", host="127.0.0.1", port=8080, log_level="info", reload=True) diff --git a/app/routers/bioentity.py b/app/routers/bioentity.py index bc6fa9f..4e08024 100644 --- a/app/routers/bioentity.py +++ b/app/routers/bioentity.py @@ -10,7 +10,7 @@ from app.utils.golr_utils import gu_run_solr_text_on from app.utils.settings import ESOLR, ESOLRDoc, get_user_agent - +from app.main import DataNotFoundException from .slimmer import gene_to_uniprot_from_mygene INVOLVED_IN = "involved_in" @@ -96,6 +96,8 @@ async def get_bioentity_by_id( optionals = "&defType=edismax&start=" + str(start) + "&rows=" + str(rows) # id here is passed to solr q parameter, query_filters go to the boost, fields are what's returned bioentity = gu_run_solr_text_on(ESOLR.GOLR, ESOLRDoc.BIOENTITY, id, query_filters, fields, optionals, False) + if not bioentity: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return bioentity @@ -170,7 +172,8 @@ async def get_annotations_by_goterm_id( optionals = "&defType=edismax&start=" + str(start) + "&rows=" + str(rows) + evidence data = gu_run_solr_text_on(ESOLR.GOLR, ESOLRDoc.ANNOTATION, id, query_filters, fields, optionals, False) - + if not data: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return data @@ -273,6 +276,8 @@ async def get_genes_by_goterm_id( url=ESOLR.GOLR, rows=rows, ) + if not association_return: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return {"associations": association_return.get("associations")} @@ -342,7 +347,8 @@ async def get_taxon_by_goterm_id( optionals = "&defType=edismax&start=" + str(start) + "&rows=" + str(rows) + evidence + taxon_restrictions data = gu_run_solr_text_on(ESOLR.GOLR, ESOLRDoc.ANNOTATION, id, query_filters, fields, optionals, False) - + if not data: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return data @@ -437,4 +443,6 @@ async def get_annotations_by_gene_id( for asc in pr_assocs["associations"]: logger.info(asc) assocs["associations"].append(asc) + if not assocs or assocs["associations"] == 0: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return {"associations": assocs.get("associations")} diff --git a/app/routers/labeler.py b/app/routers/labeler.py index 5a172f8..ab5e7e9 100644 --- a/app/routers/labeler.py +++ b/app/routers/labeler.py @@ -2,7 +2,7 @@ import logging from typing import List - +from app.main import DataNotFoundException from fastapi import APIRouter, Query from app.utils.ontology_utils import batch_fetch_labels @@ -22,4 +22,7 @@ async def expand_curie( ): """Fetches a map from IDs to labels e.g. GO:0003677.""" logger.info("fetching labels for IDs") - return batch_fetch_labels(id) + labels = batch_fetch_labels(id) + if not labels: + raise DataNotFoundException(detail=f"Item with ID {id} not found") + return labels diff --git a/app/routers/models.py b/app/routers/models.py index 95b9626..b7259dd 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -2,7 +2,7 @@ import logging from typing import List - +from app.main import DataNotFoundException import requests from fastapi import APIRouter, Path, Query from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation @@ -258,6 +258,8 @@ async def get_gocam_models( query += "\nOFFSET " + str(start) results = si._sparql_query(query) results = transform_array(results, ["orcids", "names", "groupids", "groupnames"]) + if not results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return results @@ -374,6 +376,8 @@ async def get_goterms_by_model_id( collated["definitions"] = [result["definitions"].get("value")] collated["gocam"] = result["gocam"].get("value") collated_results.append(collated) + if not collated_results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results @@ -461,6 +465,8 @@ async def get_geneproducts_by_model_id( """ results = si._sparql_query(query) results = transform_array(results, ["gpids", "gpnames"]) + if not results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return results @@ -531,6 +537,8 @@ async def get_pmid_by_model_id( for result in results: collated = {"gocam": result["gocam"].get("value"), "sources": result["sources"].get("value")} collated_results.append(collated) + if not collated_results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results @@ -558,6 +566,8 @@ async def get_model_details_by_model_id_json( path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % replaced_id response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code + if not response.json(): + raise DataNotFoundException(detail=f"Item with ID {id} not found") return response.json() @@ -598,6 +608,8 @@ async def get_term_details_by_model_id( "object": result["object"].get("value"), } collated_results.append(collated) + if not collated_results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results @@ -643,4 +655,6 @@ async def get_term_details_by_taxon_id( for result in results: collated = {"gocam": result["gocam"].get("value")} collated_results.append(collated) + if not collated_results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 16d4776..03a6dfc 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -10,6 +10,7 @@ from oaklib.resource import OntologyResource import app.utils.ontology_utils as ontology_utils +from app.main import DataNotFoundException from app.utils.golr_utils import gu_run_solr_text_on, run_solr_on from app.utils.prefix_utils import get_prefixes from app.utils.settings import ESOLR, ESOLRDoc, get_sparql_endpoint, get_user_agent @@ -46,6 +47,8 @@ async def get_term_metadata_by_id( cmaps = get_prefixes("go") converter = Converter.from_prefix_map(cmaps, strict=False) transformed_result["goid"] = converter.compress(transformed_result["goid"]) + if not transformed_result: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return transformed_result @@ -62,7 +65,8 @@ async def get_term_graph_by_id( data = run_solr_on(ESOLR.GOLR, ESOLRDoc.ONTOLOGY, id, graph_type) # step required as these graphs are made into strings in the json data[graph_type] = json.loads(data[graph_type]) - + if not data[graph_type]: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return data @@ -115,6 +119,8 @@ async def get_subgraph_by_term_id( ancestors.append({"id": parent}) data = {"descendents": descendents, "ancestors": ancestors} + if data.get("descendents") is None and data.get("ancestors") is None: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return data @@ -151,6 +157,8 @@ async def get_ancestors_shared_by_two_terms( if found: shared.append(sub) shared_labels.append(subres["isa_partof_closure_label"][i]) + if shared is None and shared_labels is None: + raise DataNotFoundException(detail=f"Item with ID {subject} and {object} not found") return {"goids": shared, "gonames: ": shared_labels} @@ -248,7 +256,8 @@ async def get_ancestors_shared_between_two_terms( shared_part_of.append(part_of) result = {"sharedIsA": shared_is_a, "sharedPartOf": shared_part_of} - + if result.get("sharedIsA") is None and result.get("sharedPartOf") is None: + raise DataNotFoundException(detail=f"Item with ID {subject} and {object} not found") return result @@ -271,11 +280,13 @@ async def get_go_term_detail_by_go_id( si = SparqlImplementation(ont_r) query = ontology_utils.create_go_summary_sparql(id) results = si._sparql_query(query) - return transform( + transformed_results = transform( results[0], ["synonyms", "relatedSynonyms", "alternativeIds", "xrefs", "subsets"], ) - + if not transformed_results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") + return transformed_results @router.get( "/api/go/{id}/hierarchy", @@ -330,6 +341,8 @@ async def get_go_hierarchy_go_id( collated["label"] = result["label"].get("value") collated["hierarchy"] = result["hierarchy"].get("value") collated_results.append(collated) + if not collated_results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results @@ -370,4 +383,7 @@ async def get_gocam_models_by_go_id( % id ) results = si._sparql_query(query) - return transform_array(results) + transformed_results = transform_array(results) + if not transformed_results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") + return diff --git a/app/routers/pathway_widget.py b/app/routers/pathway_widget.py index f402f6c..a1e0967 100644 --- a/app/routers/pathway_widget.py +++ b/app/routers/pathway_widget.py @@ -7,6 +7,7 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource +from app.main import DataNotFoundException from app.utils.prefix_utils import get_prefixes from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array @@ -165,4 +166,7 @@ async def get_gocams_by_geneproduct_id( % id ) results = si._sparql_query(query) + transformed_results = transform_array(results) + if not transformed_results: + raise DataNotFoundException(detail=f"No models found for gene product {id}") return transform_array(results) diff --git a/app/routers/prefixes.py b/app/routers/prefixes.py index 766bd95..72d6c4a 100644 --- a/app/routers/prefixes.py +++ b/app/routers/prefixes.py @@ -5,6 +5,7 @@ from curies import Converter from fastapi import APIRouter, Path, Query +from app.main import DataNotFoundException from app.utils.prefix_utils import get_prefixes logger = logging.getLogger() @@ -44,6 +45,9 @@ async def get_expand_curie(id: str = Path(..., description="identifier in CURIE # have to set strict to "False" to allow for WB and WormBase as prefixes that # map to the same expanded URI prefix converter = Converter.from_prefix_map(cmaps, strict=False) + expanded = converter.expand(id) + if not expanded: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return converter.expand(id) @@ -61,4 +65,7 @@ async def get_contract_uri(uri: str = Query(..., description="URI of the resourc """ cmaps = get_prefixes("go") converter = Converter.from_prefix_map(cmaps, strict=False) + compressed = converter.compress(uri) + if not compressed: + raise DataNotFoundException(detail=f"Item with URI {uri} not found") return converter.compress(uri) diff --git a/app/routers/publications.py b/app/routers/publications.py index 6d54c53..7c65bf5 100644 --- a/app/routers/publications.py +++ b/app/routers/publications.py @@ -4,6 +4,7 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource +from app.main import DataNotFoundException from app.utils.settings import get_sparql_endpoint, get_user_agent USER_AGENT = get_user_agent() @@ -46,4 +47,6 @@ async def get_model_details_by_pmid( for result in results: collated["gocam"] = result["gocam"].get("value") collated_results.append(collated) + if not collated_results: + return DataNotFoundException(detail=f"Item with ID {id} not found") return results diff --git a/app/routers/ribbon.py b/app/routers/ribbon.py index cd720f6..009f6c4 100644 --- a/app/routers/ribbon.py +++ b/app/routers/ribbon.py @@ -13,6 +13,7 @@ from app.utils.sparql_utils import transform_array from .slimmer import gene_to_uniprot_from_mygene +from ..main import DataNotFoundException logger = logging.getLogger() @@ -38,6 +39,8 @@ async def get_subsets_by_term( query = ontology_utils.get_go_subsets_sparql_query(id) results = si._sparql_query(query) results = transform_array(results, []) + if not results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return results @@ -51,6 +54,8 @@ async def get_subset_by_id( ): """Returns a subset (slim) by its id which is usually a name.""" result = ontology_utils.get_ontology_subsets_by_id(id=id) + if not result: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return result @@ -326,4 +331,7 @@ async def get_ribbon_results( # bioentity,bioentity_label,taxon,taxon_label&fq=bioentity:(%22MGI:MGI:98214%22%20or%20%22RGD:620474%22) result = {"categories": categories, "subjects": subjects} + if result.get("categories") is None and result.get("subjects") is None: + raise DataNotFoundException(detail="No data found for the provided parameters") + return result diff --git a/app/routers/search.py b/app/routers/search.py index f80cbf4..112854d 100644 --- a/app/routers/search.py +++ b/app/routers/search.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Path, Query +from app.main import DataNotFoundException from app.utils.golr_utils import gu_run_solr_text_on from app.utils.settings import ESOLR, ESOLRDoc, get_user_agent @@ -96,4 +97,6 @@ async def autocomplete_term( docs.append(auto_result) result = {"docs": docs} + if result.get("docs") is None: + raise DataNotFoundException(detail=f"No results found for {term}") return result diff --git a/app/routers/slimmer.py b/app/routers/slimmer.py index 1ffa135..78e40d0 100644 --- a/app/routers/slimmer.py +++ b/app/routers/slimmer.py @@ -8,6 +8,7 @@ from fastapi import APIRouter, Query from ontobio.golr.golr_associations import map2slim +from app.main import DataNotFoundException from app.utils.settings import ESOLR, get_user_agent INVOLVED_IN = "involved_in" @@ -95,7 +96,8 @@ async def slimmer_function( checked[protein_id] = gene else: association["subject"]["id"] = checked[protein_id] - + if not results: + raise DataNotFoundException(detail="No results found") return results @@ -132,7 +134,8 @@ def gene_to_uniprot_from_mygene(id: str): uniprot_ids.append(x) except ConnectionError: logging.error("ConnectionError while querying MyGeneInfo with {}".format(id)) - + if not uniprot_ids: + raise DataNotFoundException(detail="No UniProtKB IDs found for {}".format(id)) return uniprot_ids @@ -152,5 +155,6 @@ def uniprot_to_gene_from_mygene(id: str): gene_id = "HGNC:{}".format(gene_id) except ConnectionError: logging.error("ConnectionError while querying MyGeneInfo with {}".format(id)) - + if not gene_id: + raise DataNotFoundException(detail="No HGNC IDs found for {}".format(id)) return [gene_id] diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index 5fa5b52..d5f7fdc 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -6,6 +6,7 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource +from app.main import DataNotFoundException from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array @@ -51,6 +52,8 @@ async def get_users(): """ results = si._sparql_query(query) results = transform_array(results, ["organizations", "affiliations"]) + if not results: + return DataNotFoundException(detail="No users found") return results @@ -147,6 +150,8 @@ async def get_user_by_orcid( collated["bpnames"] = result["bpnames"].get("value") collated["gpids"] = result["gpids"].get("value") collated_results.append(collated) + if not collated_results: + return DataNotFoundException(detail=f"Item with ID {orcid} not found") return collated_results @@ -219,6 +224,8 @@ async def get_models_by_orcid( collated["gpids"] = result["gpids"].get("value") collated["gpnames"] = result["gpnames"].get("value") collated_results.append(collated) + if not collated_results: + return DataNotFoundException(detail=f"Item with ID {orcid} not found") return collated_results @@ -291,6 +298,8 @@ async def get_gp_models_by_orcid( collated["dates"] = result["dates"].get("value") collated["titles"] = result["titles"].get("value") collated_results.append(collated) + if not collated_results: + return DataNotFoundException(detail=f"Item with ID {orcid} not found") return collated_results @@ -323,6 +332,8 @@ async def get_groups(): GROUP BY ?url ?name """ results = si._sparql_query(query) + if not results: + return DataNotFoundException(detail="No groups found") return results @@ -390,4 +401,6 @@ async def get_group_metadata_by_name( collated["gocams"] = result["gocams"].get("value") collated["bps"] = result["bps"].get("value") collated_results.append(collated) + if not collated_results: + return DataNotFoundException(detail=f"Item with ID {name} not found") return collated_results From 9207eec9ae185a3a7ad0073f9fd88708321bad92 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 4 Nov 2024 16:23:47 -0800 Subject: [PATCH 02/34] lint --- app/exceptions/global_exceptions.py | 24 ++++++++++++++++++++++++ app/main.py | 18 ++++++++++-------- app/routers/bioentity.py | 3 ++- app/routers/labeler.py | 3 ++- app/routers/models.py | 3 ++- app/routers/ontology.py | 3 ++- app/routers/pathway_widget.py | 2 +- app/routers/prefixes.py | 2 +- app/routers/publications.py | 2 +- app/routers/ribbon.py | 2 +- app/routers/search.py | 2 +- app/routers/slimmer.py | 2 +- app/routers/users_and_groups.py | 2 +- 13 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 app/exceptions/global_exceptions.py diff --git a/app/exceptions/global_exceptions.py b/app/exceptions/global_exceptions.py new file mode 100644 index 0000000..d3c6680 --- /dev/null +++ b/app/exceptions/global_exceptions.py @@ -0,0 +1,24 @@ +"""Global exception handlers for API endpoints.""" + +from fastapi import HTTPException + + +class DataNotFoundException(HTTPException): + """ + Exception for when data is not found. + + :param detail: The detail message for the exception. + :type detail: str, optional + :returns: A DataNotFoundException object. + + """ + + def __init__(self, detail: str = "Data not found"): + """ + Initialize the DataNotFoundException object. + + :param detail: + :type detail: + :returns: + """ + super().__init__(status_code=404, detail=detail) diff --git a/app/main.py b/app/main.py index 9a82def..245a6ea 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,8 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -from fastapi import HTTPException + +from app.exceptions.global_exceptions import DataNotFoundException from app.middleware.logging_middleware import LoggingMiddleware from app.routers import ( bioentity, @@ -24,9 +25,6 @@ logger = logging.getLogger("uvicorn.error") -class DataNotFoundException(HTTPException): - def __init__(self, detail: str = "Data not found"): - super().__init__(status_code=404, detail=detail) app = FastAPI( title="GO API", @@ -85,13 +83,17 @@ async def general_exception_handler(request: Request, exc: Exception): content={"message": "An unexpected error occurred. Please try again later."}, ) + @app.exception_handler(DataNotFoundException) async def data_not_found_exception_handler(request: Request, exc: DataNotFoundException): - return JSONResponse( - status_code=exc.status_code, - content={"detail": exc.detail} - ) + """ + Global exception handler for DataNotFoundException. + :param request: + :param exc: + :return: + """ + return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) if __name__ == "__main__": diff --git a/app/routers/bioentity.py b/app/routers/bioentity.py index 4e08024..165d8c4 100644 --- a/app/routers/bioentity.py +++ b/app/routers/bioentity.py @@ -8,9 +8,10 @@ from ontobio.config import get_config from ontobio.golr.golr_associations import search_associations +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.golr_utils import gu_run_solr_text_on from app.utils.settings import ESOLR, ESOLRDoc, get_user_agent -from app.main import DataNotFoundException + from .slimmer import gene_to_uniprot_from_mygene INVOLVED_IN = "involved_in" diff --git a/app/routers/labeler.py b/app/routers/labeler.py index ab5e7e9..9887ef4 100644 --- a/app/routers/labeler.py +++ b/app/routers/labeler.py @@ -2,9 +2,10 @@ import logging from typing import List -from app.main import DataNotFoundException + from fastapi import APIRouter, Query +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.ontology_utils import batch_fetch_labels from app.utils.settings import get_user_agent diff --git a/app/routers/models.py b/app/routers/models.py index b7259dd..d3aa5ce 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -2,12 +2,13 @@ import logging from typing import List -from app.main import DataNotFoundException + import requests from fastapi import APIRouter, Path, Query from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 03a6dfc..558d77c 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -10,7 +10,7 @@ from oaklib.resource import OntologyResource import app.utils.ontology_utils as ontology_utils -from app.main import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.golr_utils import gu_run_solr_text_on, run_solr_on from app.utils.prefix_utils import get_prefixes from app.utils.settings import ESOLR, ESOLRDoc, get_sparql_endpoint, get_user_agent @@ -288,6 +288,7 @@ async def get_go_term_detail_by_go_id( raise DataNotFoundException(detail=f"Item with ID {id} not found") return transformed_results + @router.get( "/api/go/{id}/hierarchy", tags=["ontology"], diff --git a/app/routers/pathway_widget.py b/app/routers/pathway_widget.py index a1e0967..b0c57ba 100644 --- a/app/routers/pathway_widget.py +++ b/app/routers/pathway_widget.py @@ -7,7 +7,7 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource -from app.main import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.prefix_utils import get_prefixes from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array diff --git a/app/routers/prefixes.py b/app/routers/prefixes.py index 72d6c4a..a64fcff 100644 --- a/app/routers/prefixes.py +++ b/app/routers/prefixes.py @@ -5,7 +5,7 @@ from curies import Converter from fastapi import APIRouter, Path, Query -from app.main import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.prefix_utils import get_prefixes logger = logging.getLogger() diff --git a/app/routers/publications.py b/app/routers/publications.py index 7c65bf5..8acc05f 100644 --- a/app/routers/publications.py +++ b/app/routers/publications.py @@ -4,7 +4,7 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource -from app.main import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.settings import get_sparql_endpoint, get_user_agent USER_AGENT = get_user_agent() diff --git a/app/routers/ribbon.py b/app/routers/ribbon.py index 009f6c4..8ee3aef 100644 --- a/app/routers/ribbon.py +++ b/app/routers/ribbon.py @@ -8,12 +8,12 @@ from oaklib.resource import OntologyResource import app.utils.ontology_utils as ontology_utils +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.golr_utils import gu_run_solr_text_on from app.utils.settings import ESOLR, ESOLRDoc, get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array from .slimmer import gene_to_uniprot_from_mygene -from ..main import DataNotFoundException logger = logging.getLogger() diff --git a/app/routers/search.py b/app/routers/search.py index 112854d..cc7d170 100644 --- a/app/routers/search.py +++ b/app/routers/search.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Path, Query -from app.main import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.golr_utils import gu_run_solr_text_on from app.utils.settings import ESOLR, ESOLRDoc, get_user_agent diff --git a/app/routers/slimmer.py b/app/routers/slimmer.py index 78e40d0..c4cd9af 100644 --- a/app/routers/slimmer.py +++ b/app/routers/slimmer.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Query from ontobio.golr.golr_associations import map2slim -from app.main import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.settings import ESOLR, get_user_agent INVOLVED_IN = "involved_in" diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index d5f7fdc..9d3a04e 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -6,7 +6,7 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource -from app.main import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array From 1e8ae92e48a9cfe386ae09e85e26b968df56b548 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 4 Nov 2024 16:31:31 -0800 Subject: [PATCH 03/34] fix tests --- app/routers/models.py | 1 + app/routers/ontology.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routers/models.py b/app/routers/models.py index d3aa5ce..d169472 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -259,6 +259,7 @@ async def get_gocam_models( query += "\nOFFSET " + str(start) results = si._sparql_query(query) results = transform_array(results, ["orcids", "names", "groupids", "groupnames"]) + print(results) if not results: raise DataNotFoundException(detail=f"Item with ID {id} not found") return results diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 558d77c..792c1e6 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -387,4 +387,4 @@ async def get_gocam_models_by_go_id( transformed_results = transform_array(results) if not transformed_results: raise DataNotFoundException(detail=f"Item with ID {id} not found") - return + return transformed_results From c55db2839451128eef80ea795399e9297b1c5938 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 4 Nov 2024 16:36:40 -0800 Subject: [PATCH 04/34] fix tests --- app/routers/models.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/routers/models.py b/app/routers/models.py index d169472..47d6262 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -258,11 +258,9 @@ async def get_gocam_models( if start: query += "\nOFFSET " + str(start) results = si._sparql_query(query) - results = transform_array(results, ["orcids", "names", "groupids", "groupnames"]) - print(results) - if not results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") - return results + transformed_results = transform_array(results, ["orcids", "names", "groupids", "groupnames"]) + print(transformed_results) + return transform_array(results, ["orcids", "names", "groupids", "groupnames"]) @router.get("/api/models/go", tags=["models"], description="Returns go term details based on a GO-CAM model ID.") From f17d61f5e66328d403cb1af97c72cb8ffbb83dfb Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Thu, 14 Nov 2024 15:46:26 -0800 Subject: [PATCH 05/34] fix MGI:MGI double handling in ortho API calls --- app/routers/ribbon.py | 15 ++++++++++++--- tests/unit/test_ribbon.py | 17 +++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/routers/ribbon.py b/app/routers/ribbon.py index 8ee3aef..c3cccb8 100644 --- a/app/routers/ribbon.py +++ b/app/routers/ribbon.py @@ -85,11 +85,16 @@ async def get_ribbon_results( ), ): """Fetch the summary of annotations for a given gene or set of genes.""" + mgied_subjects = [] + for sub in subject: if sub.startswith("MGI:"): - subject.remove(sub) - sub = "MGI:" + sub - subject.append(sub) + sub = sub.replace("MGI:", "MGI:MGI:") # Assign the result back to sub + mgied_subjects.append(sub) + else: + mgied_subjects.append(sub) + + subject = mgied_subjects # Step 1: create the categories categories = ontology_utils.get_ontology_subsets_by_id(subset) @@ -144,15 +149,19 @@ async def get_ribbon_results( for s in subject_ids: if "HGNC:" in s or "NCBIGene:" in s or "ENSEMBL:" in s: prots = gene_to_uniprot_from_mygene(s) + print("prots: ", prots) if len(prots) > 0: mapped_ids[s] = prots[0] + print("mapped_ids: ", mapped_ids) reverse_mapped_ids[prots[0]] = s if len(prots) == 0: prots = [s] slimmer_subjects += prots + print("slimmer_subjects: ", slimmer_subjects) else: slimmer_subjects.append(s) + print("slimmer_subjects: ", slimmer_subjects) logger.info("SLIMMER SUBS: %s", slimmer_subjects) subject_ids = slimmer_subjects diff --git a/tests/unit/test_ribbon.py b/tests/unit/test_ribbon.py index d7e1fe6..588cbdc 100644 --- a/tests/unit/test_ribbon.py +++ b/tests/unit/test_ribbon.py @@ -99,7 +99,6 @@ def test_fly_ribbon(self): """Test fly ribbon returns.""" data = {"subset": "goslim_agr", "subject": ["FB:FBgn0051155"]} response = test_client.get("/api/ontology/ribbon/", params=data) - pprint(response.json()) self.assertTrue(len(response.json().get("subjects")) > 0) for subject in response.json().get("subjects"): self.assertTrue(subject.get("label") == "Polr2G") @@ -114,7 +113,6 @@ def test_mgi_ribbon(self): """Test MGI ribbon annotation returns.""" data = {"subset": "goslim_agr", "subject": ["MGI:1917258"]} response = test_client.get("/api/ontology/ribbon/", params=data) - pprint(response.json()) self.assertTrue(len(response.json().get("subjects")) > 0) for subject in response.json().get("subjects"): self.assertTrue(subject.get("label") == "Ace2") @@ -158,6 +156,21 @@ def test_rgd_ribbon(self): self.assertTrue(subject.get("groups").get("GO:0005575").get("ALL").get("nb_annotations") >= 10) self.assertTrue(response.status_code == 200) + + def test_mgi_ortho_ribbon_calls(self): + """Test MGI ortholog ribbon annotations.""" + data = {"subset": "goslim_agr", "subject": ["MGI:88469","Xenbase:XB-GENE-994160", + "HGNC:2227","RGD:2378", + "ZFIN:ZDB-GENE-060606-1","FB:FBgn003185"], + "exclude_PB":"true", + "exclude_IBA":"false", + "cross_aspect":"false" + } + response = test_client.get("/api/ontology/ribbon/", params=data) + self.assertGreater(len(response.json().get("subjects")), 0) + self.assertIn("MGI:88469", [subject.get("id") for subject in response.json().get("subjects")]) + self.assertEqual(response.status_code, 200) + def test_term_subsets_endpoint(self): """Test the endpoint to get the subsets of a Gene Ontology term by its identifier.""" for id in go_ids: From a509e63b715a7fc695f60621082036e76c3ea21c Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 18 Nov 2024 12:07:03 -0800 Subject: [PATCH 06/34] test exception handler wrapper for golr query helper functions --- app/routers/bioentity.py | 23 ++++- app/utils/golr_utils.py | 11 ++- app/utils/ontology_utils.py | 33 ++++++- tests/unit/test_global_exception_handling.py | 91 +++++++++++++++++++- 4 files changed, 148 insertions(+), 10 deletions(-) diff --git a/app/routers/bioentity.py b/app/routers/bioentity.py index 165d8c4..1614840 100644 --- a/app/routers/bioentity.py +++ b/app/routers/bioentity.py @@ -2,6 +2,7 @@ import logging from enum import Enum +from http.client import HTTPException from typing import List from fastapi import APIRouter, Path, Query @@ -13,6 +14,7 @@ from app.utils.settings import ESOLR, ESOLRDoc, get_user_agent from .slimmer import gene_to_uniprot_from_mygene +from ..utils.ontology_utils import is_valid_goid INVOLVED_IN = "involved_in" ACTS_UPSTREAM_OF_OR_WITHIN = "acts_upstream_of_or_within" @@ -144,6 +146,15 @@ async def get_annotations_by_goterm_id( 'start' determines the starting index for fetching results, and 'rows' specifies the number of results to be retrieved per page. """ + try: + is_valid_goid(id) + if is_valid_goid(id) is True: + print("valid") + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + if rows is None: rows = 100000 # dictates the fields to return, annotation_class,aspect @@ -173,6 +184,7 @@ async def get_annotations_by_goterm_id( optionals = "&defType=edismax&start=" + str(start) + "&rows=" + str(rows) + evidence data = gu_run_solr_text_on(ESOLR.GOLR, ESOLRDoc.ANNOTATION, id, query_filters, fields, optionals, False) + print(data) if not data: raise DataNotFoundException(detail=f"Item with ID {id} not found") return data @@ -230,7 +242,12 @@ async def get_genes_by_goterm_id( """ if rows is None: rows = 100000 + + + if not id: # No results from Solr + raise DataNotFoundException(detail=f"Item with ID {id} not found") association_return = {} + if relationship_type == ACTS_UPSTREAM_OF_OR_WITHIN: association_return = search_associations( subject_category="gene", @@ -275,10 +292,8 @@ async def get_genes_by_goterm_id( invert_subject_object=True, user_agent=USER_AGENT, url=ESOLR.GOLR, - rows=rows, - ) - if not association_return: - raise DataNotFoundException(detail=f"Item with ID {id} not found") + rows=rows) + return {"associations": association_return.get("associations")} diff --git a/app/utils/golr_utils.py b/app/utils/golr_utils.py index eb0617f..3e8762a 100644 --- a/app/utils/golr_utils.py +++ b/app/utils/golr_utils.py @@ -2,6 +2,8 @@ import requests +from app.exceptions.global_exceptions import DataNotFoundException + # Respect the method name for run_sparql_on with enums def run_solr_on(solr_instance, category, id, fields): @@ -22,13 +24,14 @@ def run_solr_on(solr_instance, category, id, fields): try: response = requests.get(query, timeout=timeout_seconds) - return response.json()["response"]["docs"][0] # Process the response here - except requests.Timeout: - print("Request timed out") + except IndexError as e: + raise DataNotFoundException(detail=f"Item with ID {id} not found") + except requests.Timeout as e: + print(f"Request timed out: {e}") except requests.RequestException as e: - print(f"Request error: {e}") + print(f"No results found: {e}") # (ESOLR.GOLR, ESOLRDoc.ANNOTATION, q, qf, fields, fq, False) diff --git a/app/utils/ontology_utils.py b/app/utils/ontology_utils.py index 80ee01b..fb10564 100644 --- a/app/utils/ontology_utils.py +++ b/app/utils/ontology_utils.py @@ -2,6 +2,7 @@ import logging +import requests from linkml_runtime.utils.namespaces import Namespaces from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.implementations.sparql.sparql_query import SparqlQuery @@ -10,7 +11,8 @@ from ontobio.ontol_factory import OntologyFactory from ontobio.sparql.sparql_ontol_utils import SEPARATOR -from app.utils.golr_utils import gu_run_solr_text_on +from app.exceptions.global_exceptions import DataNotFoundException +from app.utils.golr_utils import gu_run_solr_text_on, run_solr_on from app.utils.settings import get_golr_config, get_sparql_endpoint cfg = get_golr_config() @@ -352,3 +354,32 @@ def get_go_subsets_sparql_query(goid): } """ ) + + +def is_valid_goid(goid) -> bool: + """ + Check if the provided GO identifier is valid by querying the AmiGO Solr (GOLR) instance. + + :param goid: The GO identifier to be checked. + :type goid: str + :return: True if the GO identifier is valid, False otherwise. + :rtype: bool + """ + # Ensure the GO ID starts with the proper prefix + if not goid.startswith("GO:"): + raise ValueError("Invalid GO ID format") + + fields = "" + + try: + data = run_solr_on(ESOLR.GOLR, ESOLRDoc.ONTOLOGY, goid, fields) + if data: + return True + except DataNotFoundException as e: + # Log the exception if needed + print(f"Exception occurred: {e}") + # Propagate the exception and return False + raise e + + # Default return False if no data is found + return False diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index a43b53a..9192415 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -1,5 +1,12 @@ +from logging import raiseExceptions + from fastapi.testclient import TestClient + +from app.exceptions.global_exceptions import DataNotFoundException from app.main import app +import pytest + +from app.utils.ontology_utils import is_valid_goid test_client = TestClient(app) @@ -11,7 +18,7 @@ def test_value_error_handler(): # Verify that the global exception handler for ValueErrors, rewrites as an internal server error code. assert response.status_code == 400 response = test_client.get(f"/api/gp/P05067/models") - + assert response.status_code == 400 def test_value_error_curie(): response = test_client.get(f"/api/gp/P05067/models") @@ -22,3 +29,85 @@ def test_value_error_curie(): def test_ncbi_taxon_error_handling(): response = test_client.get("/api/taxon/NCBITaxon%3A4896/models") assert response.status_code == 200 + + +@pytest.mark.parametrize("endpoint", [ + "/api/bioentity/FAKE:12345", + "/api/bioentity/function/FAKE:12345", + "/api/bioentity/function/FAKE:12345/taxons", + "/api/bioentity/gene/FAKE:12345/function", + # "/api/ontol/labeler", # Uncomment if this endpoint should be included +]) +def test_get_bioentity_not_found(endpoint): + """ + Test that the DataNotFoundException is raised when the id does not exist. + """ + # Perform the GET request + response = test_client.get(endpoint) + + # Assert the status code is 404 (Not Found) + assert response.status_code == 404, f"Endpoint {endpoint} failed with status code {response.status_code}" + +@pytest.mark.parametrize("endpoint", [ + "/api/bioentity/function/FAKE:12345/genes", +]) +def test_get_bioentity_not_found(endpoint): + """ + Test that the DataNotFoundException is raised when the id does not exist. + """ + # Perform the GET request + response = test_client.get(endpoint) + + # Assert the status code is 404 (Not Found) + assert response.status_code == 400, f"Endpoint {endpoint} failed with status code {response.status_code}" + + +@pytest.mark.parametrize("goid,expected", [ + ("GO:0044598", True), # Valid GO ID + ("GO:zzzzz", False), # Non-existent GO ID + ("INVALID:12345", False), # Invalid format +]) +def test_is_valid_goid(goid, expected): + """ + Test that the is_valid_goid function behaves as expected. + """ + if expected: + assert is_valid_goid(goid) == True + else: + try: + result = is_valid_goid(goid) + assert result == False + except DataNotFoundException: + assert not expected, f"GO ID {goid} raised DataNotFoundException as expected." + except ValueError: + assert not expected, f"GO ID {goid} raised ValueError as expected." + +@pytest.mark.parametrize( + "goid,evidence,start,rows,expected_status,expected_response", + [ + ("GO:0000001", None, 0, 100, 200, {"key": "value"}), # Example valid response + ("INVALID:12345", None, 0, 100, 400, {"detail": "Invalid GO ID format"}), # Invalid format + ("GO:9999999", None, 0, 100, 404, {"detail": "Item with ID GO:9999999 not found"}), # Non-existent GO ID + ], +) +def test_get_annotations_by_goterm_id(goid, evidence, start, rows, expected_status, expected_response): + """ + Test the /api/bioentity/function/{id} endpoint. + + :param goid: The GO term ID to test. + :param evidence: Evidence codes for filtering. + :param start: Pagination start index. + :param rows: Number of results per page. + :param expected_status: Expected HTTP status code. + :param expected_response: Expected JSON response. + """ + # Perform the GET request + response = test_client.get( + f"/api/bioentity/function/{goid}", params={"evidence": evidence, "start": start, "rows": rows} + ) + + # Assert the status code + assert response.status_code == expected_status, f"Unexpected status code for GO ID {goid}" + + # Assert the response body + assert response.json() == expected_response, f"Unexpected response for GO ID {goid}" From 3dace9fd238accdb88ee0771f430dde035591ba3 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 18 Nov 2024 12:32:26 -0800 Subject: [PATCH 07/34] fix test parameters --- app/exceptions/global_exceptions.py | 20 +++++++++++++++++++ app/routers/bioentity.py | 6 ++---- tests/unit/test_global_exception_handling.py | 21 ++++++++------------ 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/exceptions/global_exceptions.py b/app/exceptions/global_exceptions.py index d3c6680..34988c1 100644 --- a/app/exceptions/global_exceptions.py +++ b/app/exceptions/global_exceptions.py @@ -22,3 +22,23 @@ def __init__(self, detail: str = "Data not found"): :returns: """ super().__init__(status_code=404, detail=detail) + +class InvalidIdentifier(HTTPException): + """ + Exception for when data is not found. + + :param detail: The detail message for the exception. + :type detail: str, optional + :returns: A DataNotFoundException object. + + """ + + def __init__(self, detail: str = "Data not found"): + """ + Initialize the DataNotFoundException object. + + :param detail: + :type detail: + :returns: + """ + super().__init__(status_code=400, detail=detail) diff --git a/app/routers/bioentity.py b/app/routers/bioentity.py index 1614840..e2277b5 100644 --- a/app/routers/bioentity.py +++ b/app/routers/bioentity.py @@ -9,7 +9,7 @@ from ontobio.config import get_config from ontobio.golr.golr_associations import search_associations -from app.exceptions.global_exceptions import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException, InvalidIdentifier from app.utils.golr_utils import gu_run_solr_text_on from app.utils.settings import ESOLR, ESOLRDoc, get_user_agent @@ -148,12 +148,10 @@ async def get_annotations_by_goterm_id( """ try: is_valid_goid(id) - if is_valid_goid(id) is True: - print("valid") except DataNotFoundException as e: raise DataNotFoundException(detail=str(e)) except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) + raise InvalidIdentifier(detail=str(e)) if rows is None: rows = 100000 diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index 9192415..274a691 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -63,7 +63,7 @@ def test_get_bioentity_not_found(endpoint): @pytest.mark.parametrize("goid,expected", [ - ("GO:0044598", True), # Valid GO ID + ("GO:0046330", True), # Valid GO ID ("GO:zzzzz", False), # Non-existent GO ID ("INVALID:12345", False), # Invalid format ]) @@ -83,31 +83,26 @@ def test_is_valid_goid(goid, expected): assert not expected, f"GO ID {goid} raised ValueError as expected." @pytest.mark.parametrize( - "goid,evidence,start,rows,expected_status,expected_response", + "goid,expected_status,expected_response", [ - ("GO:0000001", None, 0, 100, 200, {"key": "value"}), # Example valid response - ("INVALID:12345", None, 0, 100, 400, {"detail": "Invalid GO ID format"}), # Invalid format - ("GO:9999999", None, 0, 100, 404, {"detail": "Item with ID GO:9999999 not found"}), # Non-existent GO ID + ("GO:0008150", 200, {"key": "value"}), # Example valid response + ("INVALID:12345", 400, {"detail": "Invalid GO ID format"}), # Invalid format + ("GO:9999999", 404, {"detail": "Item with ID GO:9999999 not found"}), # Non-existent GO ID ], ) -def test_get_annotations_by_goterm_id(goid, evidence, start, rows, expected_status, expected_response): +def test_get_annotations_by_goterm_id(goid, expected_status, expected_response): """ Test the /api/bioentity/function/{id} endpoint. :param goid: The GO term ID to test. - :param evidence: Evidence codes for filtering. - :param start: Pagination start index. - :param rows: Number of results per page. :param expected_status: Expected HTTP status code. :param expected_response: Expected JSON response. """ # Perform the GET request response = test_client.get( - f"/api/bioentity/function/{goid}", params={"evidence": evidence, "start": start, "rows": rows} + f"/api/bioentity/function/{goid}" ) # Assert the status code - assert response.status_code == expected_status, f"Unexpected status code for GO ID {goid}" + assert response.status_code == expected_status - # Assert the response body - assert response.json() == expected_response, f"Unexpected response for GO ID {goid}" From 4e76b48d027dfd08fe4655a5115e3c5a049f753c Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 18 Nov 2024 12:59:18 -0800 Subject: [PATCH 08/34] handle labeler id not found exception -return 404 --- app/utils/ontology_utils.py | 4 ++++ tests/unit/test_global_exception_handling.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/app/utils/ontology_utils.py b/app/utils/ontology_utils.py index fb10564..6a88af8 100644 --- a/app/utils/ontology_utils.py +++ b/app/utils/ontology_utils.py @@ -39,6 +39,8 @@ def batch_fetch_labels(ids): label = goont_fetch_label(id) if label is not None: m[id] = label + else: + raise DataNotFoundException(detail=f"Item with ID {id} not found") return m @@ -58,6 +60,8 @@ def goont_fetch_label(id): si = SparqlImplementation(ont_r) query = SparqlQuery(select=["?label"], where=["<" + iri + "> rdfs:label ?label"]) bindings = si._sparql_query(query.query_str()) + if not bindings: + return None rows = [r["label"]["value"] for r in bindings] return rows[0] diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index 274a691..f77b321 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -106,3 +106,14 @@ def test_get_annotations_by_goterm_id(goid, expected_status, expected_response): # Assert the status code assert response.status_code == expected_status + +def test_labeler_data_not_found_exception(): + """ + Test the labeler endpoint with "GO:0003677". + + :return: None + """ + endpoint = "/api/ontol/labeler" + data = {"id": "GO:zzzz"} + response = test_client.get(endpoint, params=data) + assert response.status_code == 404 \ No newline at end of file From bfc101ef5ebf7a0c2d33d9a738aa624fdab25556 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 18 Nov 2024 16:00:44 -0800 Subject: [PATCH 09/34] add tests for not found GO ids in ontology endpoints --- app/routers/ontology.py | 82 ++++++++++++++++---- tests/unit/test_global_exception_handling.py | 41 +++++++++- 2 files changed, 105 insertions(+), 18 deletions(-) diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 792c1e6..89aaa59 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -10,7 +10,7 @@ from oaklib.resource import OntologyResource import app.utils.ontology_utils as ontology_utils -from app.exceptions.global_exceptions import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException, InvalidIdentifier from app.utils.golr_utils import gu_run_solr_text_on, run_solr_on from app.utils.prefix_utils import get_prefixes from app.utils.settings import ESOLR, ESOLRDoc, get_sparql_endpoint, get_user_agent @@ -36,10 +36,20 @@ async def get_term_metadata_by_id( ), ): """Returns metadata of an ontology term, e.g. GO:0003677.""" + try: + ontology_utils.is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) query = ontology_utils.create_go_summary_sparql(id) results = si._sparql_query(query) + if not results: + raise DataNotFoundException(detail=f"Item with ID {id} not found") + transformed_result = transform( results[0], ["synonyms", "relatedSynonyms", "alternativeIds", "xrefs", "subsets"], @@ -47,8 +57,6 @@ async def get_term_metadata_by_id( cmaps = get_prefixes("go") converter = Converter.from_prefix_map(cmaps, strict=False) transformed_result["goid"] = converter.compress(transformed_result["goid"]) - if not transformed_result: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return transformed_result @@ -60,13 +68,19 @@ async def get_term_graph_by_id( graph_type: GraphType = Query(GraphType.topology_graph), ): """Returns graph of an ontology term, e.g. GO:0003677.""" + + try: + ontology_utils.is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + graph_type = graph_type + "_json" # GOLR field names data = run_solr_on(ESOLR.GOLR, ESOLRDoc.ONTOLOGY, id, graph_type) # step required as these graphs are made into strings in the json data[graph_type] = json.loads(data[graph_type]) - if not data[graph_type]: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return data @@ -90,6 +104,13 @@ async def get_subgraph_by_term_id( :param rows: The number of results to return :return: A is_a/part_of subgraph of the ontology term including the term's ancestors and descendants, label and ID. """ + try: + ontology_utils.is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + if rows is None: rows = 100000 query_filters = "" @@ -119,8 +140,6 @@ async def get_subgraph_by_term_id( ancestors.append({"id": parent}) data = {"descendents": descendents, "ancestors": ancestors} - if data.get("descendents") is None and data.get("ancestors") is None: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return data @@ -139,6 +158,15 @@ async def get_ancestors_shared_by_two_terms( :param subject: 'CURIE identifier of a GO term, e.g. GO:0006259' :param object: 'CURIE identifier of a GO term, e.g. GO:0046483' """ + + try: + ontology_utils.is_valid_goid(subject) + ontology_utils.is_valid_goid(object) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + fields = "isa_partof_closure,isa_partof_closure_label" subres = run_solr_on(ESOLR.GOLR, ESOLRDoc.ONTOLOGY, subject, fields) @@ -157,8 +185,6 @@ async def get_ancestors_shared_by_two_terms( if found: shared.append(sub) shared_labels.append(subres["isa_partof_closure_label"][i]) - if shared is None and shared_labels is None: - raise DataNotFoundException(detail=f"Item with ID {subject} and {object} not found") return {"goids": shared, "gonames: ": shared_labels} @@ -179,6 +205,14 @@ async def get_ancestors_shared_between_two_terms( :param object: 'CURIE identifier of a GO term, e.g. GO:0046483' :param relation: 'relation between two terms' can only be one of two values: shared or closest """ + try: + ontology_utils.is_valid_goid(subject) + ontology_utils.is_valid_goid(object) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + fields = "isa_partof_closure,isa_partof_closure_label" logger.info(relation) if relation == "shared" or relation is None: @@ -256,8 +290,6 @@ async def get_ancestors_shared_between_two_terms( shared_part_of.append(part_of) result = {"sharedIsA": shared_is_a, "sharedPartOf": shared_part_of} - if result.get("sharedIsA") is None and result.get("sharedPartOf") is None: - raise DataNotFoundException(detail=f"Item with ID {subject} and {object} not found") return result @@ -276,6 +308,14 @@ async def get_go_term_detail_by_go_id( please note, this endpoint was migrated from the GO-CAM service api and may not be supported in its current form in the future. """ + + try: + ontology_utils.is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) query = ontology_utils.create_go_summary_sparql(id) @@ -284,8 +324,6 @@ async def get_go_term_detail_by_go_id( results[0], ["synonyms", "relatedSynonyms", "alternativeIds", "xrefs", "subsets"], ) - if not transformed_results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return transformed_results @@ -304,6 +342,13 @@ async def get_go_hierarchy_go_id( please note, this endpoint was migrated from the GO-CAM service api and may not be supported in its current form in the future. """ + try: + ontology_utils.is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + cmaps = get_prefixes("go") ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) @@ -342,8 +387,6 @@ async def get_go_hierarchy_go_id( collated["label"] = result["label"].get("value") collated["hierarchy"] = result["hierarchy"].get("value") collated_results.append(collated) - if not collated_results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results @@ -361,6 +404,13 @@ async def get_gocam_models_by_go_id( :param id: A GO-Term ID(e.g. GO:0005885, GO:0097136 ...) :return: GO-CAM model identifiers for a given GO term ID. """ + try: + ontology_utils.is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + cmaps = get_prefixes("go") ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) @@ -385,6 +435,4 @@ async def get_gocam_models_by_go_id( ) results = si._sparql_query(query) transformed_results = transform_array(results) - if not transformed_results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return transformed_results diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index f77b321..5cb4a3f 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -116,4 +116,43 @@ def test_labeler_data_not_found_exception(): endpoint = "/api/ontol/labeler" data = {"id": "GO:zzzz"} response = test_client.get(endpoint, params=data) - assert response.status_code == 404 \ No newline at end of file + assert response.status_code == 404 + +ontology_endpoints = [f"/api/go/{id}/models", + ] + + +@pytest.mark.parametrize("endpoint", [ + "/api/go/FAKE:12345/models", + "/api/go/FAKE:12345/hierarchy", + "/api/go/FAKE:12345", + "/api/association/between/FAKE:12345/FAKE:12345" + # "/api/ontol/labeler", # Uncomment if this endpoint should be included +]) +def test_ontology_endpoints_not_found_error_handling(endpoint): + """ + Test that the DataNotFoundException is raised when the id does not exist. + """ + # Perform the GET request + response = test_client.get(endpoint) + + # Assert the status code is 404 (Not Found) + assert response.status_code == 400, f"Endpoint {endpoint} failed with status code {response.status_code}" + + +@pytest.mark.parametrize("endpoint", [ + "/api/go/GO:12345/models", + "/api/go/GO:12345/hierarchy", + "/api/go/GO:12345", + "/api/association/between/GO:12345/GO:12345" + # "/api/ontol/labeler", # Uncomment if this endpoint should be included +]) +def test_ontology_endpoints_not_found_error_handling(endpoint): + """ + Test that the DataNotFoundException is raised when the id does not exist. + """ + # Perform the GET request + response = test_client.get(endpoint) + + # Assert the status code is 404 (Not Found) + assert response.status_code == 404, f"Endpoint {endpoint} failed with status code {response.status_code}" \ No newline at end of file From 51f99cfc8dbcf4066892d1797b4955628d909e72 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 18 Nov 2024 16:10:31 -0800 Subject: [PATCH 10/34] add bioentity is a valid identifier check --- app/routers/models.py | 12 -------- app/routers/pathway_widget.py | 2 -- app/routers/prefixes.py | 6 ++++ app/utils/golr_utils.py | 29 ++++++++++++++++++++ tests/unit/test_global_exception_handling.py | 26 +++++++++++++++++- 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/app/routers/models.py b/app/routers/models.py index 47d6262..4f715a3 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -376,8 +376,6 @@ async def get_goterms_by_model_id( collated["definitions"] = [result["definitions"].get("value")] collated["gocam"] = result["gocam"].get("value") collated_results.append(collated) - if not collated_results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results @@ -465,8 +463,6 @@ async def get_geneproducts_by_model_id( """ results = si._sparql_query(query) results = transform_array(results, ["gpids", "gpnames"]) - if not results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return results @@ -537,8 +533,6 @@ async def get_pmid_by_model_id( for result in results: collated = {"gocam": result["gocam"].get("value"), "sources": result["sources"].get("value")} collated_results.append(collated) - if not collated_results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results @@ -566,8 +560,6 @@ async def get_model_details_by_model_id_json( path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % replaced_id response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code - if not response.json(): - raise DataNotFoundException(detail=f"Item with ID {id} not found") return response.json() @@ -608,8 +600,6 @@ async def get_term_details_by_model_id( "object": result["object"].get("value"), } collated_results.append(collated) - if not collated_results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results @@ -655,6 +645,4 @@ async def get_term_details_by_taxon_id( for result in results: collated = {"gocam": result["gocam"].get("value")} collated_results.append(collated) - if not collated_results: - raise DataNotFoundException(detail=f"Item with ID {id} not found") return collated_results diff --git a/app/routers/pathway_widget.py b/app/routers/pathway_widget.py index b0c57ba..b42aa53 100644 --- a/app/routers/pathway_widget.py +++ b/app/routers/pathway_widget.py @@ -167,6 +167,4 @@ async def get_gocams_by_geneproduct_id( ) results = si._sparql_query(query) transformed_results = transform_array(results) - if not transformed_results: - raise DataNotFoundException(detail=f"No models found for gene product {id}") return transform_array(results) diff --git a/app/routers/prefixes.py b/app/routers/prefixes.py index a64fcff..61698c4 100644 --- a/app/routers/prefixes.py +++ b/app/routers/prefixes.py @@ -38,6 +38,12 @@ async def get_expand_curie(id: str = Path(..., description="identifier in CURIE e.g. MGI:3588192, MGI:MGI:3588192, ZFIN:ZDB-GENE-000403-1. """ + + if ":" not in id: + raise ValueError("Invalid CURIE format") + + + if id.startswith("MGI:MGI:"): id = id.replace("MGI:MGI:", "MGI:") diff --git a/app/utils/golr_utils.py b/app/utils/golr_utils.py index 3e8762a..284bb3b 100644 --- a/app/utils/golr_utils.py +++ b/app/utils/golr_utils.py @@ -3,6 +3,7 @@ import requests from app.exceptions.global_exceptions import DataNotFoundException +from app.utils.settings import ESOLRDoc, ESOLR # Respect the method name for run_sparql_on with enums @@ -102,3 +103,31 @@ def gu_run_solr_text_on( print("Request timed out") except requests.RequestException as e: print(f"Request error: {e}") + +def is_valid_bioentity(entity_id) -> bool: + """ + Check if the provided identifier is valid by querying the AmiGO Solr (GOLR) instance. + + :param entity_id: The bioentity identifier + :type entity_id: str + :return: True if the entity identifier is valid, False otherwise. + :rtype: bool + """ + # Ensure the GO ID starts with the proper prefix + if ":" not in entity_id: + raise ValueError("Invalid CURIE format") + + fields = "" + + try: + data = run_solr_on(ESOLR.GOLR, ESOLRDoc.BIOENTITY, entity_id, fields) + if data: + return True + except DataNotFoundException as e: + # Log the exception if needed + print(f"Exception occurred: {e}") + # Propagate the exception and return False + raise e + + # Default return False if no data is found + return False diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index 5cb4a3f..e275f42 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -6,6 +6,7 @@ from app.main import app import pytest +from app.utils.golr_utils import is_valid_bioentity from app.utils.ontology_utils import is_valid_goid test_client = TestClient(app) @@ -51,7 +52,7 @@ def test_get_bioentity_not_found(endpoint): @pytest.mark.parametrize("endpoint", [ "/api/bioentity/function/FAKE:12345/genes", ]) -def test_get_bioentity_not_found(endpoint): +def test_get_bioentity_genes_not_found(endpoint): """ Test that the DataNotFoundException is raised when the id does not exist. """ @@ -82,6 +83,29 @@ def test_is_valid_goid(goid, expected): except ValueError: assert not expected, f"GO ID {goid} raised ValueError as expected." + +@pytest.mark.parametrize("entity_id,expected", [ + ("MGI:MGI:3588192", True), # Valid ID + ("ZFIN:ZDB-GENE-000403-1", True), # Valid ID + ("MGI:zzzzz", False), # Invalid + ("ZFIN:12345", False), # Invalid +]) +def test_is_valid_entity_id(entity_id, expected): + """ + Test that the is_valid_goid function behaves as expected. + """ + if expected: + assert is_valid_bioentity(entity_id) == True + else: + try: + result = is_valid_bioentity(entity_id) + assert result == False + except DataNotFoundException: + assert not expected, f"GO ID {entity_id} raised DataNotFoundException as expected." + except ValueError: + assert not expected, f"GO ID {entity_id} raised ValueError as expected." + + @pytest.mark.parametrize( "goid,expected_status,expected_response", [ From fc9528c3e11bee653266b88493a57ede7ebbcf76 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 18 Nov 2024 16:14:42 -0800 Subject: [PATCH 11/34] add data not found handling to ribbon --- app/routers/pathway_widget.py | 1 - app/routers/ribbon.py | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/routers/pathway_widget.py b/app/routers/pathway_widget.py index b42aa53..9547213 100644 --- a/app/routers/pathway_widget.py +++ b/app/routers/pathway_widget.py @@ -166,5 +166,4 @@ async def get_gocams_by_geneproduct_id( % id ) results = si._sparql_query(query) - transformed_results = transform_array(results) return transform_array(results) diff --git a/app/routers/ribbon.py b/app/routers/ribbon.py index c3cccb8..90e5a44 100644 --- a/app/routers/ribbon.py +++ b/app/routers/ribbon.py @@ -8,7 +8,7 @@ from oaklib.resource import OntologyResource import app.utils.ontology_utils as ontology_utils -from app.exceptions.global_exceptions import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException, InvalidIdentifier from app.utils.golr_utils import gu_run_solr_text_on from app.utils.settings import ESOLR, ESOLRDoc, get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array @@ -34,6 +34,14 @@ async def get_subsets_by_term( ) ): """Returns subsets (slims) associated to an ontology term.""" + + try: + ontology_utils.is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) query = ontology_utils.get_go_subsets_sparql_query(id) From f5ef77dfa983ab80a02de9588ce132e942306837 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 18 Nov 2024 16:23:21 -0800 Subject: [PATCH 12/34] add data not found handling to ribbon --- app/routers/bioentity.py | 36 ++++++++++++++++---- tests/unit/test_global_exception_handling.py | 20 ++++++++--- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/app/routers/bioentity.py b/app/routers/bioentity.py index e2277b5..dbca769 100644 --- a/app/routers/bioentity.py +++ b/app/routers/bioentity.py @@ -10,7 +10,7 @@ from ontobio.golr.golr_associations import search_associations from app.exceptions.global_exceptions import DataNotFoundException, InvalidIdentifier -from app.utils.golr_utils import gu_run_solr_text_on +from app.utils.golr_utils import gu_run_solr_text_on, is_valid_bioentity from app.utils.settings import ESOLR, ESOLRDoc, get_user_agent from .slimmer import gene_to_uniprot_from_mygene @@ -81,6 +81,13 @@ async def get_bioentity_by_id( 'start' determines the starting index for fetching results, and 'rows' specifies the number of results to be retrieved per page. """ + try: + is_valid_bioentity(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + if rows is None: rows = 100000 # special case MGI, sigh @@ -238,14 +245,16 @@ async def get_genes_by_goterm_id( and 'annotation_extension_class_label' associated with the provided GO term. """ + try: + is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + if rows is None: rows = 100000 - - if not id: # No results from Solr - raise DataNotFoundException(detail=f"Item with ID {id} not found") - association_return = {} - if relationship_type == ACTS_UPSTREAM_OF_OR_WITHIN: association_return = search_associations( subject_category="gene", @@ -329,6 +338,14 @@ async def get_taxon_by_goterm_id( :return: A dictionary containing the taxon information for genes annotated to the provided GO term. The dictionary will contain fields such as 'taxon' and 'taxon_label' associated with the genes. """ + + try: + is_valid_goid(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + if rows is None: rows = 100000 fields = "taxon,taxon_label" @@ -414,6 +431,13 @@ async def get_annotations_by_gene_id( scenes for querying. """ + try: + is_valid_bioentity(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) + if rows is None: rows = 100000 diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index e275f42..d370fb9 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -33,13 +33,25 @@ def test_ncbi_taxon_error_handling(): @pytest.mark.parametrize("endpoint", [ - "/api/bioentity/FAKE:12345", "/api/bioentity/function/FAKE:12345", "/api/bioentity/function/FAKE:12345/taxons", - "/api/bioentity/gene/FAKE:12345/function", - # "/api/ontol/labeler", # Uncomment if this endpoint should be included ]) -def test_get_bioentity_not_found(endpoint): +def test_get_bioentity_goid_not_found(endpoint): + """ + Test that the DataNotFoundException is raised when the id does not exist. + """ + # Perform the GET request + response = test_client.get(endpoint) + + # Assert the status code is 400 (Invalid Request) + assert response.status_code == 400, f"Endpoint {endpoint} failed with status code {response.status_code}" + + +@pytest.mark.parametrize("endpoint", [ + "/api/bioentity/FAKE:12345", + "/api/bioentity/gene/FAKE:12345/function" +]) +def test_get_bioentity_entity_id_not_found(endpoint): """ Test that the DataNotFoundException is raised when the id does not exist. """ From b167c553759ca3015f9b34304506f51cbabe252b Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 09:31:14 -0800 Subject: [PATCH 13/34] handle NCBI taxon ids in id checker --- app/routers/ontology.py | 1 + app/utils/golr_utils.py | 6 ++++++ app/utils/ontology_utils.py | 2 +- tests/unit/test_global_exception_handling.py | 2 +- tests/unit/test_models_endpoints.py | 2 +- tests/unit/test_ontology_endpoints.py | 5 +++-- 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 89aaa59..8627f79 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -74,6 +74,7 @@ async def get_term_graph_by_id( except DataNotFoundException as e: raise DataNotFoundException(detail=str(e)) except ValueError as e: + print("triggered value error") raise InvalidIdentifier(detail=str(e)) graph_type = graph_type + "_json" # GOLR field names diff --git a/app/utils/golr_utils.py b/app/utils/golr_utils.py index 284bb3b..48646bd 100644 --- a/app/utils/golr_utils.py +++ b/app/utils/golr_utils.py @@ -117,6 +117,12 @@ def is_valid_bioentity(entity_id) -> bool: if ":" not in entity_id: raise ValueError("Invalid CURIE format") + if "MGI:" in entity_id: + if "MGI:MGI:" in entity_id: + pass + else: + entity_id = entity_id.replace("MGI:", "MGI:MGI:") + fields = "" try: diff --git a/app/utils/ontology_utils.py b/app/utils/ontology_utils.py index 6a88af8..379b83f 100644 --- a/app/utils/ontology_utils.py +++ b/app/utils/ontology_utils.py @@ -370,7 +370,7 @@ def is_valid_goid(goid) -> bool: :rtype: bool """ # Ensure the GO ID starts with the proper prefix - if not goid.startswith("GO:"): + if not goid.startswith("GO:") and not goid.startswith("GO_"): raise ValueError("Invalid GO ID format") fields = "" diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index d370fb9..408e2dc 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -97,7 +97,7 @@ def test_is_valid_goid(goid, expected): @pytest.mark.parametrize("entity_id,expected", [ - ("MGI:MGI:3588192", True), # Valid ID + ("MGI:3588192", True), # Valid ID ("ZFIN:ZDB-GENE-000403-1", True), # Valid ID ("MGI:zzzzz", False), # Invalid ("ZFIN:12345", False), # Invalid diff --git a/tests/unit/test_models_endpoints.py b/tests/unit/test_models_endpoints.py index 3d3ec35..0f02206 100644 --- a/tests/unit/test_models_endpoints.py +++ b/tests/unit/test_models_endpoints.py @@ -93,7 +93,7 @@ def test_get_modelid_by_pmid(self): def test_get_go_term_detail_by_go_id(self): """Test the endpoint to retrieve GO term details by GO ID.""" - response = test_client.get("/api/go/GO_0008150") + response = test_client.get("/api/go/GO:0008150") self.assertIn("goid", response.json()) self.assertIn("label", response.json()) self.assertEqual(response.json()["goid"], "http://purl.obolibrary.org/obo/GO_0008150") diff --git a/tests/unit/test_ontology_endpoints.py b/tests/unit/test_ontology_endpoints.py index 8d8098d..50899c8 100644 --- a/tests/unit/test_ontology_endpoints.py +++ b/tests/unit/test_ontology_endpoints.py @@ -13,7 +13,8 @@ # Test data gene_ids = ["ZFIN:ZDB-GENE-980526-388", "ZFIN:ZDB-GENE-990415-8", "MGI:3588192"] -go_ids = ["GO:0008150", "NCBITaxon:1"] +ontology_ids = ["GO:0008150", "NCBITaxon:1"] +go_ids = ["GO:0008150", "GO:0046330"] subsets = ["goslim_agr"] shared_ancestors = [("GO:0006259", "GO:0046483")] uris = ["http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2FGO_0008150"] @@ -25,7 +26,7 @@ class TestApp(unittest.TestCase): def test_term_id_endpoint(self): """Test the endpoint to get the details of a Gene Ontology term by its identifier.""" - for id in go_ids: + for id in ontology_ids: response = test_client.get(f"/api/ontology/term/{id}") print(response.json()) self.assertEqual(response.status_code, 200) From 9df701d1a5451348f478c80d94ea95de1a301bc7 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 09:36:20 -0800 Subject: [PATCH 14/34] fix NCBI taxon use case --- app/exceptions/global_exceptions.py | 2 ++ app/middleware/logging_middleware.py | 1 + app/routers/bioentity.py | 5 ++-- app/routers/models.py | 1 - app/routers/ontology.py | 6 ++--- app/routers/pathway_widget.py | 1 - app/routers/prefixes.py | 1 - app/routers/ribbon.py | 1 - app/routers/search.py | 1 + app/routers/slimmer.py | 1 + app/utils/golr_utils.py | 4 ++-- app/utils/ontology_utils.py | 35 +++++++++++++++++++++++++++- app/utils/settings.py | 3 +++ 13 files changed, 48 insertions(+), 14 deletions(-) diff --git a/app/exceptions/global_exceptions.py b/app/exceptions/global_exceptions.py index 34988c1..503ae10 100644 --- a/app/exceptions/global_exceptions.py +++ b/app/exceptions/global_exceptions.py @@ -4,6 +4,7 @@ class DataNotFoundException(HTTPException): + """ Exception for when data is not found. @@ -24,6 +25,7 @@ def __init__(self, detail: str = "Data not found"): super().__init__(status_code=404, detail=detail) class InvalidIdentifier(HTTPException): + """ Exception for when data is not found. diff --git a/app/middleware/logging_middleware.py b/app/middleware/logging_middleware.py index 7491af2..7da51df 100644 --- a/app/middleware/logging_middleware.py +++ b/app/middleware/logging_middleware.py @@ -13,6 +13,7 @@ class LoggingMiddleware(BaseHTTPMiddleware): + """Middleware to log requests.""" async def dispatch(self, request: Request, call_next): diff --git a/app/routers/bioentity.py b/app/routers/bioentity.py index dbca769..3a6e397 100644 --- a/app/routers/bioentity.py +++ b/app/routers/bioentity.py @@ -2,7 +2,6 @@ import logging from enum import Enum -from http.client import HTTPException from typing import List from fastapi import APIRouter, Path, Query @@ -13,8 +12,8 @@ from app.utils.golr_utils import gu_run_solr_text_on, is_valid_bioentity from app.utils.settings import ESOLR, ESOLRDoc, get_user_agent -from .slimmer import gene_to_uniprot_from_mygene from ..utils.ontology_utils import is_valid_goid +from .slimmer import gene_to_uniprot_from_mygene INVOLVED_IN = "involved_in" ACTS_UPSTREAM_OF_OR_WITHIN = "acts_upstream_of_or_within" @@ -33,6 +32,7 @@ class RelationshipType(str, Enum): + """ Enumeration for Gene Ontology relationship types used for filtering associations. @@ -338,7 +338,6 @@ async def get_taxon_by_goterm_id( :return: A dictionary containing the taxon information for genes annotated to the provided GO term. The dictionary will contain fields such as 'taxon' and 'taxon_label' associated with the genes. """ - try: is_valid_goid(id) except DataNotFoundException as e: diff --git a/app/routers/models.py b/app/routers/models.py index 4f715a3..63f88a2 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -8,7 +8,6 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource -from app.exceptions.global_exceptions import DataNotFoundException from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 8627f79..fbddc6c 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -24,6 +24,7 @@ class GraphType(str, Enum): + """Enum for the different types of graphs that can be returned.""" topology_graph = "topology_graph" @@ -37,7 +38,7 @@ async def get_term_metadata_by_id( ): """Returns metadata of an ontology term, e.g. GO:0003677.""" try: - ontology_utils.is_valid_goid(id) + ontology_utils.is_golr_recognized_curie(id) except DataNotFoundException as e: raise DataNotFoundException(detail=str(e)) except ValueError as e: @@ -68,7 +69,6 @@ async def get_term_graph_by_id( graph_type: GraphType = Query(GraphType.topology_graph), ): """Returns graph of an ontology term, e.g. GO:0003677.""" - try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: @@ -159,7 +159,6 @@ async def get_ancestors_shared_by_two_terms( :param subject: 'CURIE identifier of a GO term, e.g. GO:0006259' :param object: 'CURIE identifier of a GO term, e.g. GO:0046483' """ - try: ontology_utils.is_valid_goid(subject) ontology_utils.is_valid_goid(object) @@ -309,7 +308,6 @@ async def get_go_term_detail_by_go_id( please note, this endpoint was migrated from the GO-CAM service api and may not be supported in its current form in the future. """ - try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: diff --git a/app/routers/pathway_widget.py b/app/routers/pathway_widget.py index 9547213..f402f6c 100644 --- a/app/routers/pathway_widget.py +++ b/app/routers/pathway_widget.py @@ -7,7 +7,6 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource -from app.exceptions.global_exceptions import DataNotFoundException from app.utils.prefix_utils import get_prefixes from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array diff --git a/app/routers/prefixes.py b/app/routers/prefixes.py index 61698c4..a09b543 100644 --- a/app/routers/prefixes.py +++ b/app/routers/prefixes.py @@ -38,7 +38,6 @@ async def get_expand_curie(id: str = Path(..., description="identifier in CURIE e.g. MGI:3588192, MGI:MGI:3588192, ZFIN:ZDB-GENE-000403-1. """ - if ":" not in id: raise ValueError("Invalid CURIE format") diff --git a/app/routers/ribbon.py b/app/routers/ribbon.py index 90e5a44..e419707 100644 --- a/app/routers/ribbon.py +++ b/app/routers/ribbon.py @@ -34,7 +34,6 @@ async def get_subsets_by_term( ) ): """Returns subsets (slims) associated to an ontology term.""" - try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: diff --git a/app/routers/search.py b/app/routers/search.py index cc7d170..90fb21b 100644 --- a/app/routers/search.py +++ b/app/routers/search.py @@ -16,6 +16,7 @@ class AutocompleteCategory(str, Enum): + """The category of items to retrieve, can be 'gene' or 'term'.""" gene = "gene" diff --git a/app/routers/slimmer.py b/app/routers/slimmer.py index c4cd9af..57a67b6 100644 --- a/app/routers/slimmer.py +++ b/app/routers/slimmer.py @@ -22,6 +22,7 @@ class RelationshipType(str, Enum): + """Relationship type for slimmer.""" acts_upstream_of_or_within = ACTS_UPSTREAM_OF_OR_WITHIN diff --git a/app/utils/golr_utils.py b/app/utils/golr_utils.py index 48646bd..fd45c09 100644 --- a/app/utils/golr_utils.py +++ b/app/utils/golr_utils.py @@ -3,7 +3,7 @@ import requests from app.exceptions.global_exceptions import DataNotFoundException -from app.utils.settings import ESOLRDoc, ESOLR +from app.utils.settings import ESOLR, ESOLRDoc # Respect the method name for run_sparql_on with enums @@ -27,7 +27,7 @@ def run_solr_on(solr_instance, category, id, fields): response = requests.get(query, timeout=timeout_seconds) return response.json()["response"]["docs"][0] # Process the response here - except IndexError as e: + except IndexError: raise DataNotFoundException(detail=f"Item with ID {id} not found") except requests.Timeout as e: print(f"Request timed out: {e}") diff --git a/app/utils/ontology_utils.py b/app/utils/ontology_utils.py index 379b83f..a100161 100644 --- a/app/utils/ontology_utils.py +++ b/app/utils/ontology_utils.py @@ -2,7 +2,6 @@ import logging -import requests from linkml_runtime.utils.namespaces import Namespaces from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.implementations.sparql.sparql_query import SparqlQuery @@ -387,3 +386,37 @@ def is_valid_goid(goid) -> bool: # Default return False if no data is found return False + + +def is_golr_recognized_curie(id) -> bool: + """ + Check if the provided identifier is valid by querying the AmiGO Solr (GOLR) instance. + + :param id: The GO identifier to be checked. + :type id: str + :return: True if the GO identifier is valid, False otherwise. + :rtype: bool + """ + # Ensure the GO ID starts with the proper prefix + if ":" not in id and "_" not in id: + raise ValueError("Invalid CURIE format") + + fields = "" + + try: + data = run_solr_on(ESOLR.GOLR, ESOLRDoc.ONTOLOGY, id, fields) + if data: + return True + else: + data = run_solr_on(ESOLR.GOLR, ESOLRDoc.BIOENTITY, id, fields) + if data: + return True + + except DataNotFoundException as e: + # Log the exception if needed + print(f"Exception occurred: {e}") + # Propagate the exception and return False + raise e + + # Default return False if no data is found + return False \ No newline at end of file diff --git a/app/utils/settings.py b/app/utils/settings.py index 7bd35a2..e412d56 100644 --- a/app/utils/settings.py +++ b/app/utils/settings.py @@ -42,18 +42,21 @@ def get_golr_config(): class ESOLR(Enum): + """Enum for the GOLR URL.""" GOLR = get_golr_config()["solr_url"]["url"] class ESPARQL(Enum): + """Enum for the SPARQL endpoint URL.""" SPARQL = get_sparql_endpoint() class ESOLRDoc(Enum): + """Enum for the GOLR document type.""" ONTOLOGY = "ontology_class" From 00265aaccf7011016bd03f295a2c3bf24ac14baa Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 11:07:57 -0800 Subject: [PATCH 15/34] rework util methods for checking if an id is valid --- app/utils/golr_utils.py | 15 ++++++++++++--- app/utils/ontology_utils.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/utils/golr_utils.py b/app/utils/golr_utils.py index fd45c09..c398acd 100644 --- a/app/utils/golr_utils.py +++ b/app/utils/golr_utils.py @@ -3,6 +3,7 @@ import requests from app.exceptions.global_exceptions import DataNotFoundException +from app.routers.slimmer import gene_to_uniprot_from_mygene from app.utils.settings import ESOLR, ESOLRDoc @@ -131,9 +132,17 @@ def is_valid_bioentity(entity_id) -> bool: return True except DataNotFoundException as e: # Log the exception if needed - print(f"Exception occurred: {e}") - # Propagate the exception and return False - raise e + fix_possible_hgnc_id = gene_to_uniprot_from_mygene(entity_id) + print(fix_possible_hgnc_id) + try: + if fix_possible_hgnc_id: + data = run_solr_on(ESOLR.GOLR, ESOLRDoc.BIOENTITY, fix_possible_hgnc_id[0], fields) + if data: + return True + except DataNotFoundException as e: + print(f"Exception occurred: {e}") + # Propagate the exception and return False + raise e # Default return False if no data is found return False diff --git a/app/utils/ontology_utils.py b/app/utils/ontology_utils.py index a100161..630b4cd 100644 --- a/app/utils/ontology_utils.py +++ b/app/utils/ontology_utils.py @@ -419,4 +419,4 @@ def is_golr_recognized_curie(id) -> bool: raise e # Default return False if no data is found - return False \ No newline at end of file + return False From be57e658c2cee24016fc5353081cfa58e9cd08d5 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 13:48:34 -0800 Subject: [PATCH 16/34] lint --- Makefile | 1 + app/exceptions/global_exceptions.py | 3 +- app/middleware/logging_middleware.py | 1 - app/routers/bioentity.py | 24 ++++---- app/routers/ontology.py | 36 +++++------ app/routers/prefixes.py | 2 - app/routers/ribbon.py | 4 +- app/routers/search.py | 1 - app/routers/slimmer.py | 1 - app/utils/golr_utils.py | 65 +++++++++++++------- app/utils/settings.py | 3 - tests/unit/test_global_exception_handling.py | 7 ++- 12 files changed, 80 insertions(+), 68 deletions(-) diff --git a/Makefile b/Makefile index 79373cd..1358234 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ integration-tests: poetry run pytest tests/integration/step_defs/*.py lint: + poetry run tox -e flake8 poetry run tox -e lint-fix spell: diff --git a/app/exceptions/global_exceptions.py b/app/exceptions/global_exceptions.py index 503ae10..b4c6782 100644 --- a/app/exceptions/global_exceptions.py +++ b/app/exceptions/global_exceptions.py @@ -4,7 +4,6 @@ class DataNotFoundException(HTTPException): - """ Exception for when data is not found. @@ -24,8 +23,8 @@ def __init__(self, detail: str = "Data not found"): """ super().__init__(status_code=404, detail=detail) -class InvalidIdentifier(HTTPException): +class InvalidIdentifier(HTTPException): """ Exception for when data is not found. diff --git a/app/middleware/logging_middleware.py b/app/middleware/logging_middleware.py index 7da51df..7491af2 100644 --- a/app/middleware/logging_middleware.py +++ b/app/middleware/logging_middleware.py @@ -13,7 +13,6 @@ class LoggingMiddleware(BaseHTTPMiddleware): - """Middleware to log requests.""" async def dispatch(self, request: Request, call_next): diff --git a/app/routers/bioentity.py b/app/routers/bioentity.py index 3a6e397..f3c6dd9 100644 --- a/app/routers/bioentity.py +++ b/app/routers/bioentity.py @@ -32,7 +32,6 @@ class RelationshipType(str, Enum): - """ Enumeration for Gene Ontology relationship types used for filtering associations. @@ -84,9 +83,9 @@ async def get_bioentity_by_id( try: is_valid_bioentity(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e if rows is None: rows = 100000 @@ -156,9 +155,9 @@ async def get_annotations_by_goterm_id( try: is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e if rows is None: rows = 100000 @@ -248,9 +247,9 @@ async def get_genes_by_goterm_id( try: is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e if rows is None: rows = 100000 @@ -299,7 +298,8 @@ async def get_genes_by_goterm_id( invert_subject_object=True, user_agent=USER_AGENT, url=ESOLR.GOLR, - rows=rows) + rows=rows, + ) return {"associations": association_return.get("associations")} @@ -341,9 +341,9 @@ async def get_taxon_by_goterm_id( try: is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e if rows is None: rows = 100000 @@ -433,9 +433,9 @@ async def get_annotations_by_gene_id( try: is_valid_bioentity(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e if rows is None: rows = 100000 diff --git a/app/routers/ontology.py b/app/routers/ontology.py index fbddc6c..5ec98be 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -24,7 +24,6 @@ class GraphType(str, Enum): - """Enum for the different types of graphs that can be returned.""" topology_graph = "topology_graph" @@ -38,11 +37,11 @@ async def get_term_metadata_by_id( ): """Returns metadata of an ontology term, e.g. GO:0003677.""" try: - ontology_utils.is_golr_recognized_curie(id) + ontology_utils.is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) @@ -72,10 +71,9 @@ async def get_term_graph_by_id( try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - print("triggered value error") - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e graph_type = graph_type + "_json" # GOLR field names @@ -108,9 +106,9 @@ async def get_subgraph_by_term_id( try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e if rows is None: rows = 100000 @@ -163,9 +161,9 @@ async def get_ancestors_shared_by_two_terms( ontology_utils.is_valid_goid(subject) ontology_utils.is_valid_goid(object) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e fields = "isa_partof_closure,isa_partof_closure_label" @@ -209,9 +207,9 @@ async def get_ancestors_shared_between_two_terms( ontology_utils.is_valid_goid(subject) ontology_utils.is_valid_goid(object) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e fields = "isa_partof_closure,isa_partof_closure_label" logger.info(relation) @@ -311,9 +309,9 @@ async def get_go_term_detail_by_go_id( try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) @@ -344,9 +342,9 @@ async def get_go_hierarchy_go_id( try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e cmaps = get_prefixes("go") ont_r = OntologyResource(url=get_sparql_endpoint()) @@ -406,9 +404,9 @@ async def get_gocam_models_by_go_id( try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e cmaps = get_prefixes("go") ont_r = OntologyResource(url=get_sparql_endpoint()) diff --git a/app/routers/prefixes.py b/app/routers/prefixes.py index a09b543..13039e3 100644 --- a/app/routers/prefixes.py +++ b/app/routers/prefixes.py @@ -41,8 +41,6 @@ async def get_expand_curie(id: str = Path(..., description="identifier in CURIE if ":" not in id: raise ValueError("Invalid CURIE format") - - if id.startswith("MGI:MGI:"): id = id.replace("MGI:MGI:", "MGI:") diff --git a/app/routers/ribbon.py b/app/routers/ribbon.py index e419707..c33d22e 100644 --- a/app/routers/ribbon.py +++ b/app/routers/ribbon.py @@ -37,9 +37,9 @@ async def get_subsets_by_term( try: ontology_utils.is_valid_goid(id) except DataNotFoundException as e: - raise DataNotFoundException(detail=str(e)) + raise DataNotFoundException(detail=str(e)) from e except ValueError as e: - raise InvalidIdentifier(detail=str(e)) + raise InvalidIdentifier(detail=str(e)) from e ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) diff --git a/app/routers/search.py b/app/routers/search.py index 90fb21b..cc7d170 100644 --- a/app/routers/search.py +++ b/app/routers/search.py @@ -16,7 +16,6 @@ class AutocompleteCategory(str, Enum): - """The category of items to retrieve, can be 'gene' or 'term'.""" gene = "gene" diff --git a/app/routers/slimmer.py b/app/routers/slimmer.py index 57a67b6..c4cd9af 100644 --- a/app/routers/slimmer.py +++ b/app/routers/slimmer.py @@ -22,7 +22,6 @@ class RelationshipType(str, Enum): - """Relationship type for slimmer.""" acts_upstream_of_or_within = ACTS_UPSTREAM_OF_OR_WITHIN diff --git a/app/utils/golr_utils.py b/app/utils/golr_utils.py index c398acd..37f1c20 100644 --- a/app/utils/golr_utils.py +++ b/app/utils/golr_utils.py @@ -1,5 +1,7 @@ """golr utils.""" +from zipfile import error + import requests from app.exceptions.global_exceptions import DataNotFoundException @@ -9,7 +11,7 @@ # Respect the method name for run_sparql_on with enums def run_solr_on(solr_instance, category, id, fields): - """Return the result of a solr query on the given solrInstance, for a certain document_category and id.""" + """Return the result of a Solr query.""" query = ( solr_instance.value + 'select?q=*:*&fq=document_category:"' @@ -21,19 +23,29 @@ def run_solr_on(solr_instance, category, id, fields): + "&wt=json&indent=on" ) - print(query) - timeout_seconds = 60 # Set the desired timeout value in seconds + print("Solr query:", query) + timeout_seconds = 60 try: response = requests.get(query, timeout=timeout_seconds) - return response.json()["response"]["docs"][0] - # Process the response here - except IndexError: - raise DataNotFoundException(detail=f"Item with ID {id} not found") + response.raise_for_status() # Raise an error for non-2xx responses + response_json = response.json() + print("Solr response JSON:", response_json) + + docs = response_json.get("response", {}).get("docs", []) + if not docs: + raise DataNotFoundException(detail=f"Item with ID {id} not found") + return docs[0] + + except IndexError as e: + print("IndexError: No docs found, raising DataNotFoundException") + raise DataNotFoundException(detail=f"Item with ID {id} not found") from e except requests.Timeout as e: print(f"Request timed out: {e}") + raise except requests.RequestException as e: - print(f"No results found: {e}") + print(f"Request failed: {e}") + raise # (ESOLR.GOLR, ESOLRDoc.ANNOTATION, q, qf, fields, fq, False) @@ -105,6 +117,7 @@ def gu_run_solr_text_on( except requests.RequestException as e: print(f"Request error: {e}") + def is_valid_bioentity(entity_id) -> bool: """ Check if the provided identifier is valid by querying the AmiGO Solr (GOLR) instance. @@ -130,19 +143,25 @@ def is_valid_bioentity(entity_id) -> bool: data = run_solr_on(ESOLR.GOLR, ESOLRDoc.BIOENTITY, entity_id, fields) if data: return True - except DataNotFoundException as e: - # Log the exception if needed - fix_possible_hgnc_id = gene_to_uniprot_from_mygene(entity_id) - print(fix_possible_hgnc_id) - try: - if fix_possible_hgnc_id: - data = run_solr_on(ESOLR.GOLR, ESOLRDoc.BIOENTITY, fix_possible_hgnc_id[0], fields) - if data: - return True - except DataNotFoundException as e: - print(f"Exception occurred: {e}") - # Propagate the exception and return False - raise e - - # Default return False if no data is found + except DataNotFoundException: + if "HGNC" in entity_id: + try: + fix_possible_hgnc_id = gene_to_uniprot_from_mygene(entity_id) + except DataNotFoundException as e: + print(f"Data Not Found Exception occurred: {e}") + # Propagate the exception and return False + raise e from error + try: + if fix_possible_hgnc_id: + data = run_solr_on(ESOLR.GOLR, ESOLRDoc.BIOENTITY, fix_possible_hgnc_id[0], fields) + if data: + return True + except DataNotFoundException as e: + print(f"Data Not Found Exception occurred: {e}") + print("No results found for the provided entity ID") + # Propagate the exception and return False + raise e from error + except Exception as e: + print(f"Unexpected error in gene_to_uniprot_from_mygene: {e}") + return False return False diff --git a/app/utils/settings.py b/app/utils/settings.py index e412d56..7bd35a2 100644 --- a/app/utils/settings.py +++ b/app/utils/settings.py @@ -42,21 +42,18 @@ def get_golr_config(): class ESOLR(Enum): - """Enum for the GOLR URL.""" GOLR = get_golr_config()["solr_url"]["url"] class ESPARQL(Enum): - """Enum for the SPARQL endpoint URL.""" SPARQL = get_sparql_endpoint() class ESOLRDoc(Enum): - """Enum for the GOLR document type.""" ONTOLOGY = "ontology_class" diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index 408e2dc..1056c0f 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -101,6 +101,7 @@ def test_is_valid_goid(goid, expected): ("ZFIN:ZDB-GENE-000403-1", True), # Valid ID ("MGI:zzzzz", False), # Invalid ("ZFIN:12345", False), # Invalid + ("HGNC:12345", False), # Invalid ]) def test_is_valid_entity_id(entity_id, expected): """ @@ -113,9 +114,11 @@ def test_is_valid_entity_id(entity_id, expected): result = is_valid_bioentity(entity_id) assert result == False except DataNotFoundException: - assert not expected, f"GO ID {entity_id} raised DataNotFoundException as expected." + print("data not found exception") + assert not expected, f"ID {entity_id} raised DataNotFoundException as expected." except ValueError: - assert not expected, f"GO ID {entity_id} raised ValueError as expected." + print("value error") + assert not expected, f"ID {entity_id} raised ValueError as expected." @pytest.mark.parametrize( From b14fae259a50293e40f36a98158eaa86bdd9c817 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 13:55:08 -0800 Subject: [PATCH 17/34] fix linting error and rewire endpoint to look for any valid curie that exists in golr vs. any valid GO term id --- app/routers/ontology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 5ec98be..06dd8ef 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -37,7 +37,7 @@ async def get_term_metadata_by_id( ): """Returns metadata of an ontology term, e.g. GO:0003677.""" try: - ontology_utils.is_valid_goid(id) + ontology_utils.is_golr_recognized_curie(id) except DataNotFoundException as e: raise DataNotFoundException(detail=str(e)) from e except ValueError as e: From 88504c3494512b4d0d5344f612c63c4db5bff436 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 15:08:26 -0800 Subject: [PATCH 18/34] fix tests --- app/routers/bioentity.py | 5 ---- app/routers/models.py | 2 +- app/routers/ribbon.py | 9 +++---- app/utils/golr_utils.py | 26 +++++++++---------- app/utils/ontology_utils.py | 8 +++--- .../step_defs/bioentity_function_steps.py | 5 ++-- tests/integration/step_defs/prefixes_steps.py | 9 +++---- tests/unit/test_bioentity_endpoints.py | 2 -- tests/unit/test_global_exception_handling.py | 5 ++-- tests/unit/test_models_endpoints.py | 2 +- tests/unit/test_ontology_endpoints.py | 6 ++--- tests/unit/test_pathway_widget_endpoints.py | 2 +- tests/unit/test_ribbon.py | 7 ++--- tests/unit/test_slimmer_endpoints.py | 3 +-- 14 files changed, 41 insertions(+), 50 deletions(-) diff --git a/app/routers/bioentity.py b/app/routers/bioentity.py index f3c6dd9..494b2b1 100644 --- a/app/routers/bioentity.py +++ b/app/routers/bioentity.py @@ -100,7 +100,6 @@ async def get_bioentity_by_id( # query_filters is translated to the qf solr parameter # boost fields %5E2 -> ^2, %5E1 -> ^1 query_filters = "bioentity%5E2" - logger.info(id) optionals = "&defType=edismax&start=" + str(start) + "&rows=" + str(rows) # id here is passed to solr q parameter, query_filters go to the boost, fields are what's returned @@ -188,7 +187,6 @@ async def get_annotations_by_goterm_id( optionals = "&defType=edismax&start=" + str(start) + "&rows=" + str(rows) + evidence data = gu_run_solr_text_on(ESOLR.GOLR, ESOLRDoc.ANNOTATION, id, query_filters, fields, optionals, False) - print(data) if not data: raise DataNotFoundException(detail=f"Item with ID {id} not found") return data @@ -453,8 +451,6 @@ async def get_annotations_by_gene_id( rows=rows, slim=slim, ) - logger.info("should be null assocs") - logger.info(assocs) # If there are no associations for the given ID, try other IDs. # Note the AmiGO instance does *not* support equivalent IDs if len(assocs["associations"]) == 0: @@ -478,7 +474,6 @@ async def get_annotations_by_gene_id( num_found = num_found + pr_assocs.get("numFound") assocs["numFound"] = num_found for asc in pr_assocs["associations"]: - logger.info(asc) assocs["associations"].append(asc) if not assocs or assocs["associations"] == 0: raise DataNotFoundException(detail=f"Item with ID {id} not found") diff --git a/app/routers/models.py b/app/routers/models.py index 63f88a2..c1103d3 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -258,7 +258,7 @@ async def get_gocam_models( query += "\nOFFSET " + str(start) results = si._sparql_query(query) transformed_results = transform_array(results, ["orcids", "names", "groupids", "groupnames"]) - print(transformed_results) + logger.info(transformed_results) return transform_array(results, ["orcids", "names", "groupids", "groupnames"]) diff --git a/app/routers/ribbon.py b/app/routers/ribbon.py index c33d22e..056a6be 100644 --- a/app/routers/ribbon.py +++ b/app/routers/ribbon.py @@ -156,20 +156,19 @@ async def get_ribbon_results( for s in subject_ids: if "HGNC:" in s or "NCBIGene:" in s or "ENSEMBL:" in s: prots = gene_to_uniprot_from_mygene(s) - print("prots: ", prots) + logger.info(f"prots: {prots}") if len(prots) > 0: mapped_ids[s] = prots[0] - print("mapped_ids: ", mapped_ids) + logger.info(f"mapped_ids: {mapped_ids}") reverse_mapped_ids[prots[0]] = s if len(prots) == 0: prots = [s] slimmer_subjects += prots - print("slimmer_subjects: ", slimmer_subjects) + logger.info(f"slimmer_subjects: {slimmer_subjects}") else: slimmer_subjects.append(s) - print("slimmer_subjects: ", slimmer_subjects) - logger.info("SLIMMER SUBS: %s", slimmer_subjects) + logger.info(f"SLIMMER_SUBS: {slimmer_subjects}") subject_ids = slimmer_subjects # should remove any undefined subject diff --git a/app/utils/golr_utils.py b/app/utils/golr_utils.py index 37f1c20..b62d0d8 100644 --- a/app/utils/golr_utils.py +++ b/app/utils/golr_utils.py @@ -6,7 +6,7 @@ from app.exceptions.global_exceptions import DataNotFoundException from app.routers.slimmer import gene_to_uniprot_from_mygene -from app.utils.settings import ESOLR, ESOLRDoc +from app.utils.settings import ESOLR, ESOLRDoc, logger # Respect the method name for run_sparql_on with enums @@ -23,14 +23,14 @@ def run_solr_on(solr_instance, category, id, fields): + "&wt=json&indent=on" ) - print("Solr query:", query) + logger.info(f"Solr query: {query}") timeout_seconds = 60 try: response = requests.get(query, timeout=timeout_seconds) response.raise_for_status() # Raise an error for non-2xx responses response_json = response.json() - print("Solr response JSON:", response_json) + logger.info("Solr response JSON:", response_json) docs = response_json.get("response", {}).get("docs", []) if not docs: @@ -38,13 +38,13 @@ def run_solr_on(solr_instance, category, id, fields): return docs[0] except IndexError as e: - print("IndexError: No docs found, raising DataNotFoundException") + logger.info("IndexError: No docs found, raising DataNotFoundException") raise DataNotFoundException(detail=f"Item with ID {id} not found") from e except requests.Timeout as e: - print(f"Request timed out: {e}") + logger.info(f"Request timed out: {e}") raise except requests.RequestException as e: - print(f"Request failed: {e}") + logger.info(f"Request failed: {e}") raise @@ -85,7 +85,7 @@ def gu_run_solr_text_on( + "&wt=json&indent=on" + optionals ) - print(query) + logger.info(query) timeout_seconds = 60 # Set the desired timeout value in seconds try: @@ -113,9 +113,9 @@ def gu_run_solr_text_on( return return_doc # Process the response here except requests.Timeout: - print("Request timed out") + logger.info("Request timed out") except requests.RequestException as e: - print(f"Request error: {e}") + logger.info(f"Request error: {e}") def is_valid_bioentity(entity_id) -> bool: @@ -148,7 +148,7 @@ def is_valid_bioentity(entity_id) -> bool: try: fix_possible_hgnc_id = gene_to_uniprot_from_mygene(entity_id) except DataNotFoundException as e: - print(f"Data Not Found Exception occurred: {e}") + logger.info(f"Data Not Found Exception occurred: {e}") # Propagate the exception and return False raise e from error try: @@ -157,11 +157,11 @@ def is_valid_bioentity(entity_id) -> bool: if data: return True except DataNotFoundException as e: - print(f"Data Not Found Exception occurred: {e}") - print("No results found for the provided entity ID") + logger.info(f"Data Not Found Exception occurred: {e}") + logger.info("No results found for the provided entity ID") # Propagate the exception and return False raise e from error except Exception as e: - print(f"Unexpected error in gene_to_uniprot_from_mygene: {e}") + logger.info(f"Unexpected error in gene_to_uniprot_from_mygene: {e}") return False return False diff --git a/app/utils/ontology_utils.py b/app/utils/ontology_utils.py index 630b4cd..c6053e8 100644 --- a/app/utils/ontology_utils.py +++ b/app/utils/ontology_utils.py @@ -169,7 +169,7 @@ def get_ontology(id): handle = id for c in cfg["ontologies"]: if c["id"] == id: - print("getting handle for id: {} from cfg".format(id)) + logger.info("getting handle for id: {} from cfg".format(id)) handle = c["handle"] if handle not in omap: @@ -179,7 +179,7 @@ def get_ontology(id): else: logging.info("Using cached for {}".format(handle)) - print("handle: " + handle) + logger.info("handle: " + handle) return omap[handle] @@ -380,7 +380,7 @@ def is_valid_goid(goid) -> bool: return True except DataNotFoundException as e: # Log the exception if needed - print(f"Exception occurred: {e}") + logger.info(f"Exception occurred: {e}") # Propagate the exception and return False raise e @@ -414,7 +414,7 @@ def is_golr_recognized_curie(id) -> bool: except DataNotFoundException as e: # Log the exception if needed - print(f"Exception occurred: {e}") + logger.info(f"Exception occurred: {e}") # Propagate the exception and return False raise e diff --git a/tests/integration/step_defs/bioentity_function_steps.py b/tests/integration/step_defs/bioentity_function_steps.py index e15530d..cec7371 100644 --- a/tests/integration/step_defs/bioentity_function_steps.py +++ b/tests/integration/step_defs/bioentity_function_steps.py @@ -1,11 +1,10 @@ """Bioentity Function Steps.""" -from pprint import pprint - from fastapi.testclient import TestClient from pytest_bdd import given, parsers, scenario, then from app.main import app +from app.middleware.logging_middleware import logger EXTRA_TYPES = { "String": str, @@ -109,7 +108,7 @@ def endpoint_first_returns(result, term): data = result.json() found_it = False term = term.replace('"', "") - pprint(data) + logger.info(data) for association in data.get("associations"): if association.get("object").get("id") == term: found_it = True diff --git a/tests/integration/step_defs/prefixes_steps.py b/tests/integration/step_defs/prefixes_steps.py index 715755e..75c5223 100644 --- a/tests/integration/step_defs/prefixes_steps.py +++ b/tests/integration/step_defs/prefixes_steps.py @@ -1,11 +1,10 @@ """Prefix steps.""" -from pprint import pprint - from fastapi.testclient import TestClient from pytest_bdd import given, parsers, scenario, then from app.main import app +from app.middleware.logging_middleware import logger EXTRA_TYPES = { "String": str, @@ -63,10 +62,10 @@ def api_result_second(endpoint, thing): :rtype: TestResponse """ test_client = TestClient(app) - print("") - print(endpoint + thing) + logger.info("") + logger.info(endpoint + thing) response = test_client.get(f"{endpoint}{thing}") - pprint(response.json()) + logger.info(response.json()) return response diff --git a/tests/unit/test_bioentity_endpoints.py b/tests/unit/test_bioentity_endpoints.py index 249e1d5..9f81539 100644 --- a/tests/unit/test_bioentity_endpoints.py +++ b/tests/unit/test_bioentity_endpoints.py @@ -71,7 +71,6 @@ def test_bioentity_gene_endpoints(self): """ for gene_id in gene_ids: response = test_client.get(f"/api/bioentity/gene/{gene_id}/function") - print(response.json()) self.assertEqual(response.status_code, 200) self.assertGreaterEqual(len(response.json().get("associations")), 4) @@ -84,7 +83,6 @@ def test_bioentity_gene_function_endpoints(self): for go_id in go_ids: response = test_client.get(f"/api/bioentity/function/{go_id}/genes") self.assertEqual(response.status_code, 200) - print(len(response.json().get("associations"))) self.assertGreaterEqual(len(response.json().get("associations")), 92) def test_bioentity_gene_function_id_genes_endpoint(self): diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index 1056c0f..b738025 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -6,6 +6,7 @@ from app.main import app import pytest +from app.middleware.logging_middleware import logger from app.utils.golr_utils import is_valid_bioentity from app.utils.ontology_utils import is_valid_goid @@ -114,10 +115,10 @@ def test_is_valid_entity_id(entity_id, expected): result = is_valid_bioentity(entity_id) assert result == False except DataNotFoundException: - print("data not found exception") + logger.info("data not found exception") assert not expected, f"ID {entity_id} raised DataNotFoundException as expected." except ValueError: - print("value error") + logger.info("value error") assert not expected, f"ID {entity_id} raised ValueError as expected." diff --git a/tests/unit/test_models_endpoints.py b/tests/unit/test_models_endpoints.py index 0f02206..b66ef9d 100644 --- a/tests/unit/test_models_endpoints.py +++ b/tests/unit/test_models_endpoints.py @@ -74,7 +74,7 @@ def test_grouplist(self): """Test the endpoint to retrieve the list of groups.""" response = test_client.get("/api/groups") - print(response.json()) + logger.info(response.json()) self.assertGreater(len(response.json()), 15) self.assertEqual(response.status_code, 200) diff --git a/tests/unit/test_ontology_endpoints.py b/tests/unit/test_ontology_endpoints.py index 50899c8..43d0c0b 100644 --- a/tests/unit/test_ontology_endpoints.py +++ b/tests/unit/test_ontology_endpoints.py @@ -28,7 +28,7 @@ def test_term_id_endpoint(self): """Test the endpoint to get the details of a Gene Ontology term by its identifier.""" for id in ontology_ids: response = test_client.get(f"/api/ontology/term/{id}") - print(response.json()) + logger.info(response.json()) self.assertEqual(response.status_code, 200) def test_ontology_ancestors_shared_sub_obj(self): @@ -52,8 +52,8 @@ def test_ontology_ancestors_association_between_sub_obj(self): data = {"relation": "shared"} response = test_client.get(f"/api/association/between/{subject}/{object}", params=data) self.assertIn("GO:0008150", response.json().get("shared")) - print(response.json().get("shared")) - print(response.json().get("shared_labels")) + logger.info(response.json().get("shared")) + logger.info(response.json().get("shared_labels")) self.assertIsNotNone(response.json().get("shared_labels")) self.assertEqual(response.status_code, 200) diff --git a/tests/unit/test_pathway_widget_endpoints.py b/tests/unit/test_pathway_widget_endpoints.py index 2db1dec..877b9e8 100644 --- a/tests/unit/test_pathway_widget_endpoints.py +++ b/tests/unit/test_pathway_widget_endpoints.py @@ -38,7 +38,7 @@ def test_get_gocams_by_geneproduct_id_causal2(self): """ for gid in gene_ids: id = urllib.parse.quote(gid) - print(id) + logger.info(id) data = { "causalmf": 2, } diff --git a/tests/unit/test_ribbon.py b/tests/unit/test_ribbon.py index 588cbdc..daca2e0 100644 --- a/tests/unit/test_ribbon.py +++ b/tests/unit/test_ribbon.py @@ -1,10 +1,10 @@ """Unit tests for the endpoints in the ribbon module.""" import unittest -from pprint import pprint from fastapi.testclient import TestClient from app.main import app +import logging test_client = TestClient(app) @@ -15,6 +15,7 @@ uris = ["http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2FGO_0008150"] +logger = logging.getLogger() class TestOntologyAPI(unittest.TestCase): """Test the ribbon API endpoints.""" @@ -73,7 +74,7 @@ def test_sgd_ribbon_term(self): """Test sgd ribbon with not available annotations.""" data = {"subset": "goslim_agr", "subject": ["SGD:S000002812"]} response = test_client.get("/api/ontology/ribbon/", params=data) - pprint(response.json()) + logger.info(response.json()) self.assertTrue(len(response.json().get("subjects")) > 0) for subject in response.json().get("subjects"): self.assertTrue(subject.get("groups").get("GO:0008219") is None) @@ -141,7 +142,7 @@ def test_rgd_ribbon(self): """Test RGD annotations in the ribbon.""" data = {"subset": "goslim_agr", "subject": ["RGD:70971"]} response = test_client.get("/api/ontology/ribbon/", params=data) - pprint(response.json()) + logger.info(response.json()) self.assertTrue(len(response.json().get("subjects")) > 0) for subject in response.json().get("subjects"): self.assertTrue(subject.get("label") == "Hamp") diff --git a/tests/unit/test_slimmer_endpoints.py b/tests/unit/test_slimmer_endpoints.py index 2a157cf..d914a9b 100644 --- a/tests/unit/test_slimmer_endpoints.py +++ b/tests/unit/test_slimmer_endpoints.py @@ -1,7 +1,6 @@ """Unit tests for the endpoints in the slimmer module.""" import logging import unittest -from pprint import pprint from fastapi.testclient import TestClient @@ -36,7 +35,7 @@ def test_slimmer_endpoint_fgf8a(self): response = test_client.get(endpoint, params=data) self.assertEqual(response.status_code, 200) self.assertGreater(len(response.json()), 2) - pprint(response.json()) + logger.info(response.json()) for item in response.json(): self.assertIn(item.get("slim"), ["GO:0003674", "GO:0008150", "GO:0005575"]) self.assertEqual(item.get("subject"), "ZFIN:ZDB-GENE-980526-388") From f01a04486425494e47f941445ad5026c372502ef Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 16:25:17 -0800 Subject: [PATCH 19/34] fixing manual testing errors --- app/routers/ontology.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 06dd8ef..33337a2 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -8,7 +8,8 @@ from fastapi import APIRouter, Path, Query from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource - +from pydantic import BaseModel +from typing import List import app.utils.ontology_utils as ontology_utils from app.exceptions.global_exceptions import DataNotFoundException, InvalidIdentifier from app.utils.golr_utils import gu_run_solr_text_on, run_solr_on @@ -149,13 +150,13 @@ async def get_subgraph_by_term_id( ) async def get_ancestors_shared_by_two_terms( subject: str = Path(..., description="Identifier of a GO term, e.g. GO:0006259", example="GO:0006259"), - object: str = Path(..., description="Identifier of a GO term, e.g. GO:0046483", example="GO:0046483"), + object: str = Path(..., description="Identifier of a GO term, e.g. GO:0016070", example="GO:0016070"), ): """ Returns the ancestor ontology terms shared by two ontology terms. :param subject: 'CURIE identifier of a GO term, e.g. GO:0006259' - :param object: 'CURIE identifier of a GO term, e.g. GO:0046483' + :param object: 'CURIE identifier of a GO term, e.g. GO:0016070' """ try: ontology_utils.is_valid_goid(subject) @@ -193,14 +194,14 @@ async def get_ancestors_shared_by_two_terms( ) async def get_ancestors_shared_between_two_terms( subject: str = Path(..., description="Identifier of a GO term, e.g. GO:0006259", example="GO:0006259"), - object: str = Path(..., description="Identifier of a GO term, e.g. GO:0046483", example="GO:0046483"), + object: str = Path(..., description="Identifier of a GO term, e.g. GO:0016070", example="GO:0016070"), relation: str = Query(None, description="relation between two terms", example="closest"), ): """ Returns the ancestor ontology terms shared by two ontology terms. :param subject: 'CURIE identifier of a GO term, e.g. GO:0006259' - :param object: 'CURIE identifier of a GO term, e.g. GO:0046483' + :param object: 'CURIE identifier of a GO term, e.g. GO:0016070' :param relation: 'relation between two terms' can only be one of two values: shared or closest """ try: @@ -324,10 +325,17 @@ async def get_go_term_detail_by_go_id( return transformed_results +class GOHierarchyItem(BaseModel): + GO: str + label: str + hierarchy: str + + @router.get( "/api/go/{id}/hierarchy", tags=["ontology"], description="Returns parent and children relationships for a given GO ID, e.g. GO:0005885", + response_model=List[GOHierarchyItem], ) async def get_go_hierarchy_go_id( id: str = Path(..., description="A GO-Term ID, e.g. GO:0097136", example="GO:0008150") @@ -377,12 +385,14 @@ async def get_go_hierarchy_go_id( % id ) results = si._sparql_query(query) + collated_results = [] - collated = {} for result in results: - collated["GO"] = result["GO"].get("value") - collated["label"] = result["label"].get("value") - collated["hierarchy"] = result["hierarchy"].get("value") + collated = { + "GO": result["GO"].get("value"), + "label": result["label"].get("value"), + "hierarchy": result["hierarchy"].get("value"), + } collated_results.append(collated) return collated_results @@ -393,12 +403,12 @@ async def get_go_hierarchy_go_id( description="Returns GO-CAM model identifiers for a given GO term ID, e.g. GO:0008150", ) async def get_gocam_models_by_go_id( - id: str = Path(..., description="A GO-Term ID(e.g. GO:0097136 ...)", example="GO:0097136") + id: str = Path(..., description="A GO-Term ID(e.g. GO:0008150 ...)", example="GO:0008150") ): """ Returns GO-CAM model identifiers for a given GO term ID. - :param id: A GO-Term ID(e.g. GO:0005885, GO:0097136 ...) + :param id: A GO-Term ID(e.g. GO:0008150 ...) :return: GO-CAM model identifiers for a given GO term ID. """ try: @@ -430,6 +440,7 @@ async def get_gocam_models_by_go_id( """ % id ) + logger.info(query) results = si._sparql_query(query) transformed_results = transform_array(results) return transformed_results From 4a34f480aa323e17eedd4577d8126ad701f7d80d Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 16:44:57 -0800 Subject: [PATCH 20/34] fixing up tests --- Makefile | 6 ++++-- app/routers/ontology.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1358234..6a8c0a4 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ install: poetry install help: - @echo "" + @echo "##################################################################################################" @echo "make all -- installs requirements, deploys and starts the site locally" @echo "make install -- install dependencies" @echo "make start -- start the API locally" @@ -47,4 +47,6 @@ help: @echo "make lint -- runs linter in fix mode" @echo "make spell -- runs spell checker" @echo "make help -- show this help" - @echo "" + @echo "make start -- start the API locally at localhost:8080/docs (takes about 10 seconds to start)" + @echo "make start-dev -- start the API locally at localhost:8081/docs (takes about 10 seconds to start)" + @echo "##################################################################################################" diff --git a/app/routers/ontology.py b/app/routers/ontology.py index 33337a2..d9359b9 100644 --- a/app/routers/ontology.py +++ b/app/routers/ontology.py @@ -3,13 +3,14 @@ import json import logging from enum import Enum +from typing import List from curies import Converter from fastapi import APIRouter, Path, Query from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource from pydantic import BaseModel -from typing import List + import app.utils.ontology_utils as ontology_utils from app.exceptions.global_exceptions import DataNotFoundException, InvalidIdentifier from app.utils.golr_utils import gu_run_solr_text_on, run_solr_on @@ -326,6 +327,17 @@ async def get_go_term_detail_by_go_id( class GOHierarchyItem(BaseModel): + """ + A GO Hierarchy return model. + + This helps the hierarchy endpoint render in the swagger interface correctly, + even when a return is missing a component here. + + :param GO: The GO ID. + :param label: The label of the GO ID. + :param hierarchy: The hierarchy of the GO ID. + """ + GO: str label: str hierarchy: str From 2f510b0663168a97bb1eb59dd4c346a03faaa263 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 17:01:39 -0800 Subject: [PATCH 21/34] add model id not found error handling for GO-CAM --- app/routers/models.py | 35 +++++++++++++++++++++++++---- tests/unit/test_models_endpoints.py | 7 ++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/routers/models.py b/app/routers/models.py index c1103d3..becd4e5 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -8,6 +8,7 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource +from app.exceptions.global_exceptions import DataNotFoundException from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array @@ -38,11 +39,12 @@ async def get_gocam_models( "this input gene", ), ): - """Returns metadata of an ontology term, e.g. GO:0003677.""" + """Returns metadata of GO-CAM models, e.g. 59a6110e00000067.""" if last: start = 0 size = last + ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) @@ -278,6 +280,12 @@ async def get_goterms_by_model_id( stripped_ids.append(model_id) else: stripped_ids.append(model_id) + for stripped_id in stripped_ids: + path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % stripped_id + response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) + if response.status_code == 403 or response.status_code == 404: + raise DataNotFoundException("GO-CAM model not found.") + ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) gocam = "" @@ -395,10 +403,16 @@ async def get_geneproducts_by_model_id( stripped_ids = [] for model_id in gocams: if model_id.startswith("gomodel:"): - model_id = id.replace("gomodel:", "") + model_id = model_id.replace("gomodel:", "") stripped_ids.append(model_id) else: stripped_ids.append(model_id) + for stripped_id in stripped_ids: + path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % stripped_id + response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) + if response.status_code == 403 or response.status_code == 404: + raise DataNotFoundException("GO-CAM model not found.") + ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) gocam = "" @@ -477,10 +491,16 @@ async def get_pmid_by_model_id( stripped_ids = [] for model_id in gocams: if model_id.startswith("gomodel:"): - model_id = id.replace("gomodel:", "") + model_id = model_id.replace("gomodel:", "") stripped_ids.append(model_id) else: stripped_ids.append(model_id) + for stripped_id in stripped_ids: + path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % stripped_id + response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) + if response.status_code == 403 or response.status_code == 404: + raise DataNotFoundException("GO-CAM model not found.") + gocam = "" ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) @@ -572,7 +592,14 @@ async def get_term_details_by_model_id( ): """Returns model details based on a GO-CAM model ID.""" if id.startswith("gomodel:"): - id = id.replace("gomodel:", "") + replaced_id = id.replace("gomodel:", "") + else: + replaced_id = id + + path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % replaced_id + response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) + if response.status_code == 403 or response.status_code == 404: + raise DataNotFoundException("GO-CAM model not found.") ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) diff --git a/tests/unit/test_models_endpoints.py b/tests/unit/test_models_endpoints.py index b66ef9d..7763ab0 100644 --- a/tests/unit/test_models_endpoints.py +++ b/tests/unit/test_models_endpoints.py @@ -34,6 +34,13 @@ def test_geneproductmetadata_by_model_ids(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 2) + def test_data_not_found_handling(self): + """Test the endpoint to retrieve gene product metadata by model IDs.""" + data = {"gocams": ["gomodel:fake_id", "fake_id"]} + response = test_client.get("/api/models/gp", params=data) + print(response.json()) + self.assertEqual(response.status_code, 404) + def test_pubmedmetadata_by_model_ids(self): """Test the endpoint to retrieve PubMed metadata by model IDs.""" data = {"gocams": ["59a6110e00000067", "SYNGO_369"]} From b1eaa509956268642e135153f1e1e590491ada5d Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Tue, 19 Nov 2024 17:08:48 -0800 Subject: [PATCH 22/34] add not found error handling and lint --- app/routers/models.py | 1 - poetry.lock | 263 +++++++++++++++++++++++++++++------------- pyproject.toml | 1 + 3 files changed, 182 insertions(+), 83 deletions(-) diff --git a/app/routers/models.py b/app/routers/models.py index becd4e5..20e552d 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -44,7 +44,6 @@ async def get_gocam_models( start = 0 size = last - ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) diff --git a/poetry.lock b/poetry.lock index 9de69c2..0d5f2e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "airium" @@ -26,6 +26,17 @@ files = [ {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "antlr4-python3-runtime" version = "4.9.3" @@ -668,28 +679,26 @@ tox = ["tox"] [[package]] name = "curies" -version = "0.5.7" -description = "Idiomatic conversion between URIs and compact URIs (CURIEs)." +version = "0.9.0" +description = "Idiomatic conversion between URIs and compact URIs (CURIEs)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "curies-0.5.7-py3-none-any.whl", hash = "sha256:74fc743e2c41c3bcd2ecb1178cce72236018f245e970c88faf452b89694b475a"}, - {file = "curies-0.5.7.tar.gz", hash = "sha256:eb984c51dcd04187fec88074a43070ae6f96467887fd502fa5547377722c9a4a"}, + {file = "curies-0.9.0-py3-none-any.whl", hash = "sha256:a4b8d9fff89288190c658ac5941f3099196205cd805cc98fceba1ac5a96daa50"}, + {file = "curies-0.9.0.tar.gz", hash = "sha256:f630fa05b31aff144da66ace18a2c25b30adfa859df36e5fbd8b633b43c80d3a"}, ] [package.dependencies] -pydantic = "<2.0" +pydantic = ">=2.0" pytrie = "*" -requests = "*" [package.extras] -bioregistry = ["bioregistry (>=0.5.136)"] -docs = ["sphinx (<7.0)", "sphinx-autodoc-typehints", "sphinx-automodapi", "sphinx-rtd-theme"] +docs = ["sphinx (>=8)", "sphinx-automodapi", "sphinx-rtd-theme (>=3.0)"] fastapi = ["defusedxml", "fastapi", "httpx", "python-multipart", "uvicorn"] flask = ["defusedxml", "flask"] pandas = ["pandas"] rdflib = ["rdflib"] -tests = ["coverage", "pytest"] +tests = ["coverage", "pytest", "requests"] [[package]] name = "cycler" @@ -882,24 +891,23 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.92.0" +version = "0.115.5" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "fastapi-0.92.0-py3-none-any.whl", hash = "sha256:ae7b97c778e2f2ec3fb3cb4fb14162129411d99907fb71920f6d69a524340ebf"}, - {file = "fastapi-0.92.0.tar.gz", hash = "sha256:023a0f5bd2c8b2609014d3bba1e14a1d7df96c6abea0a73070621c9862b9a4de"}, + {file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"}, + {file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"}, ] [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = ">=0.25.0,<0.26.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.42.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "fastobo" @@ -1049,6 +1057,25 @@ files = [ paramiko = ">=2.11.0" pyyaml = ">=6.0" +[[package]] +name = "gocam" +version = "0.1.0" +description = "GO CAM Data Model (Python)" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "gocam-0.1.0-py3-none-any.whl", hash = "sha256:c780b1ba551f45282ce359067c511656e334458633c34359155ab7ac599deac4"}, + {file = "gocam-0.1.0.tar.gz", hash = "sha256:98bb7a7604a2a3a9abd302d6eca6d0a2f0293f024e85e6f2eb19dab7cbb09b04"}, +] + +[package.dependencies] +click = ">=8,<9" +linkml-runtime = ">=1.1.24,<2.0.0" +ndex2 = ">=3.9.0,<4.0.0" +pydantic = ">=2,<3" +pyyaml = ">=6,<7" +requests = ">=2,<3" + [[package]] name = "graphviz" version = "0.20.1" @@ -1828,13 +1855,13 @@ docs = ["myst-parser[docs] (>=0.18.1,<0.19.0)", "sphinx-autodoc-typehints[docs] [[package]] name = "linkml-runtime" -version = "1.5.5" +version = "1.8.3" description = "Runtime environment for LinkML, the Linked open data modeling language" optional = false -python-versions = ">=3.7.6,<4.0.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "linkml_runtime-1.5.5-py3-none-any.whl", hash = "sha256:92b0148a107f980a75b5608ece692f7d33f77328c6f61640a4d2e51488be4a3c"}, - {file = "linkml_runtime-1.5.5.tar.gz", hash = "sha256:f0275823c659924c032e88d4084e6fdb9699c0a62c6c62d45cb62e27101cbbff"}, + {file = "linkml_runtime-1.8.3-py3-none-any.whl", hash = "sha256:0750920f1348fffa903d99e7b5834ce425a2a538285aff9068dbd96d05caabd1"}, + {file = "linkml_runtime-1.8.3.tar.gz", hash = "sha256:5b7f682eef54aaf0a59c50eeacdb11463b43b124a044caf496cde59936ac05c8"}, ] [package.dependencies] @@ -1847,7 +1874,7 @@ jsonasobj2 = ">=1.0.4,<2.dev0" jsonschema = ">=3.2.0" prefixcommons = ">=0.1.12" prefixmaps = ">=0.1.4" -pydantic = ">=1.10.2,<2.0.0" +pydantic = ">=1.10.2,<3.0.0" pyyaml = "*" rdflib = ">=6.0.0" requests = "*" @@ -2289,13 +2316,13 @@ files = [ [[package]] name = "ndex2" -version = "3.5.0" +version = "3.9.0" description = "Nice CX Python includes a client and a data model." optional = false python-versions = "*" files = [ - {file = "ndex2-3.5.0-py2.py3-none-any.whl", hash = "sha256:256d35140a737c98dc5430181d8f69aefab5c8711df7bb900f555e1ad37ca9b8"}, - {file = "ndex2-3.5.0.tar.gz", hash = "sha256:f8f3041bcb819ddd82c54f794d688d335a40916dc2ce6c47db907e04dbfd5064"}, + {file = "ndex2-3.9.0-py2.py3-none-any.whl", hash = "sha256:168a6ed3209f2c9596752897fe535599b11f87305c10d55446bf8ffef4762283"}, + {file = "ndex2-3.9.0.tar.gz", hash = "sha256:388b2f110b2eb1ba787298bc4210ca0cea821c462ec71a4ec4cb6eb0e1b74f70"}, ] [package.dependencies] @@ -2871,55 +2898,127 @@ files = [ [[package]] name = "pydantic" -version = "1.10.7" -description = "Data validation and settings management using python type hints" +version = "2.9.2" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, - {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, - {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, - {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, - {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, - {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, - {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, - {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, - {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, - {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, - {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, - {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, - {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, - {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, - {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, - {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, - {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, - {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, - {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, - {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, - {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, - {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, - {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, - {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, - {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, - {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, - {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, - {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, - {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, - {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, - {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, - {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, - {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, - {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, - {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, - {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydotplus" @@ -4224,20 +4323,20 @@ mkdocs-mermaid2-plugin = ">=0.6.0,<0.7.0" [[package]] name = "starlette" -version = "0.25.0" +version = "0.41.3" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.25.0-py3-none-any.whl", hash = "sha256:774f1df1983fd594b9b6fb3ded39c2aa1979d10ac45caac0f4255cbe2acb8628"}, - {file = "starlette-0.25.0.tar.gz", hash = "sha256:854c71e73736c429c2bdb07801f2c76c9cba497e7c3cf4988fde5e95fe4cdb3c"}, + {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, + {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, ] [package.dependencies] anyio = ">=3.4.0,<5" [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] [[package]] name = "stringcase" @@ -4345,13 +4444,13 @@ urllib3 = ">=1.26.0" [[package]] name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -4651,4 +4750,4 @@ docs = [] [metadata] lock-version = "2.0" python-versions = "^3.10.1" -content-hash = "8d735ddb93eee233c8804c96352332e7c2ef8aa179a262ff726340a198f005fb" +content-hash = "20d6908ed2e0386d8ad4692d4f589ea6e025d1fa9caba8493ea985f3c496632f" diff --git a/pyproject.toml b/pyproject.toml index f6c3ae9..e181b1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ go-deploy = ">=0.4.1" biothings-client = "^0.3.0" email-validator = "^2.0.0.post2" bmt = "^1.1.2" +gocam = "^0.1.0" [tool.poetry.dev-dependencies] pytest = ">=7.4.0" From e947ee31755a0240c51da7df6f23ac5cb9c668ab Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Thu, 21 Nov 2024 16:25:44 -0800 Subject: [PATCH 23/34] remove gocam --- poetry.lock | 21 +-------------------- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0d5f2e2..1b3cfb1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1057,25 +1057,6 @@ files = [ paramiko = ">=2.11.0" pyyaml = ">=6.0" -[[package]] -name = "gocam" -version = "0.1.0" -description = "GO CAM Data Model (Python)" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "gocam-0.1.0-py3-none-any.whl", hash = "sha256:c780b1ba551f45282ce359067c511656e334458633c34359155ab7ac599deac4"}, - {file = "gocam-0.1.0.tar.gz", hash = "sha256:98bb7a7604a2a3a9abd302d6eca6d0a2f0293f024e85e6f2eb19dab7cbb09b04"}, -] - -[package.dependencies] -click = ">=8,<9" -linkml-runtime = ">=1.1.24,<2.0.0" -ndex2 = ">=3.9.0,<4.0.0" -pydantic = ">=2,<3" -pyyaml = ">=6,<7" -requests = ">=2,<3" - [[package]] name = "graphviz" version = "0.20.1" @@ -4750,4 +4731,4 @@ docs = [] [metadata] lock-version = "2.0" python-versions = "^3.10.1" -content-hash = "20d6908ed2e0386d8ad4692d4f589ea6e025d1fa9caba8493ea985f3c496632f" +content-hash = "8d735ddb93eee233c8804c96352332e7c2ef8aa179a262ff726340a198f005fb" diff --git a/pyproject.toml b/pyproject.toml index e181b1c..f6c3ae9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ go-deploy = ">=0.4.1" biothings-client = "^0.3.0" email-validator = "^2.0.0.post2" bmt = "^1.1.2" -gocam = "^0.1.0" [tool.poetry.dev-dependencies] pytest = ">=7.4.0" From e111882426fafaeafe3a6dc36b1babf66994e8b1 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Thu, 21 Nov 2024 16:37:58 -0800 Subject: [PATCH 24/34] update bioregistry dependency b/c of recent update --- Makefile | 2 +- app/routers/users_and_groups.py | 2 +- poetry.lock | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 6a8c0a4..4720ce0 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ spell: poetry run tox -e codespell unit-tests: - poetry run pytest tests/unit/*.py + poetry run pytest -v tests/unit/*.py export-requirements: poetry export -f requirements.txt --output requirements.txt diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index 9d3a04e..144a627 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -341,7 +341,7 @@ async def get_groups(): "/api/groups/{name}", tags=["users and groups"], deprecated=True, description="Get GO group metadata by name" ) async def get_group_metadata_by_name( - name: str = Path(None, description="The name of the Group (e.g. SynGO, GO Central, MGI, ...)") + name: str = Path(..., description="The name of the Group (e.g. SynGO, GO Central, MGI, ...)") ): """ DEPRECATED. diff --git a/poetry.lock b/poetry.lock index 1b3cfb1..86ffc8a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,33 +202,33 @@ test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "py [[package]] name = "bioregistry" -version = "0.6.99" +version = "0.11.26" description = "Integrated registry of biological databases and nomenclatures" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "bioregistry-0.6.99-py3-none-any.whl", hash = "sha256:7c30edaea11cbed1e13f64ba1debdabc12b65b70c0bc2d904b8b2ed692b9a404"}, - {file = "bioregistry-0.6.99.tar.gz", hash = "sha256:22bc981f39c5aaa1a5430d695728a6dade6edcbfb0abbc2e42b4013a9fe4102b"}, + {file = "bioregistry-0.11.26-py3-none-any.whl", hash = "sha256:d70421daf8b045dfaf3382679d99429b6a0a5bd515820ca1000280a00d235152"}, + {file = "bioregistry-0.11.26.tar.gz", hash = "sha256:299b423644498f5232a7e32f70c90f170d159dccfeae0765f21579792a3398a7"}, ] [package.dependencies] click = "*" -curies = "*" -more-click = ">=0.1.2" +curies = ">=0.7.0" +more_click = ">=0.1.2" pydantic = "*" pystow = ">=0.1.13" requests = "*" tqdm = "*" [package.extras] -align = ["beautifulsoup4", "class-resolver", "defusedxml", "fairsharing-client (>=0.1.0)", "pyyaml", "tabulate"] -charts = ["matplotlib", "matplotlib-venn", "pandas", "seaborn"] -docs = ["autodoc-pydantic", "sphinx", "sphinx-autodoc-typehints (==1.21.1)", "sphinx-automodapi", "sphinx-click", "sphinx-rtd-theme"] +align = ["beautifulsoup4", "class-resolver", "defusedxml", "fairsharing-client (>=0.1.0)", "pandas", "pyyaml", "tabulate"] +charts = ["jinja2", "matplotlib", "matplotlib_venn", "pandas", "seaborn"] +docs = ["autodoc_pydantic", "sphinx (>=8)", "sphinx-click", "sphinx-rtd-theme (>=3.0)", "sphinx_automodapi"] export = ["ndex2", "pyyaml", "rdflib", "rdflib-jsonld"] -gha = ["more-itertools"] -health = ["click-default-group", "pandas", "pyyaml", "tabulate"] -tests = ["coverage", "more-itertools", "pytest"] -web = ["bootstrap-flask (<=2.0.0)", "flasgger", "flask", "markdown", "pyyaml", "rdflib", "rdflib-jsonld"] +gha = ["more_itertools"] +health = ["click_default_group", "jinja2", "pandas", "pyyaml", "tabulate"] +tests = ["coverage", "httpx", "more_itertools", "pytest"] +web = ["a2wsgi", "bootstrap-flask (<=2.0.0)", "curies[fastapi]", "fastapi", "flask (<2.2.4)", "markdown", "pyyaml", "rdflib", "rdflib-endpoint", "rdflib-jsonld", "uvicorn", "werkzeug (<2.3.0)"] [[package]] name = "biothings-client" From fd40c64c9d23435a31bb5415f147278697991a10 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Fri, 22 Nov 2024 09:30:42 -0800 Subject: [PATCH 25/34] not found error handling in api/go-cam/id endpoint --- app/routers/models.py | 20 +++++++++++++------- tests/unit/test_models_endpoints.py | 13 ++++++++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/routers/models.py b/app/routers/models.py index 20e552d..72c85ed 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -570,15 +570,21 @@ async def get_model_details_by_model_id_json( :param id: A GO-CAM identifier (e.g. 581e072c00000820, 581e072c00000295, 5900dc7400000968) :return: model details based on a GO-CAM model ID in JSON format from the S3 bucket. """ + stripped_ids = [] if id.startswith("gomodel:"): - replaced_id = id.replace("gomodel:", "") + model_id = id.replace("gomodel:", "") + stripped_ids.append(model_id) else: - replaced_id = id - - path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % replaced_id - response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) - response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code - return response.json() + stripped_ids.append(id) + for stripped_id in stripped_ids: + path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % stripped_id + response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) + if response.status_code == 403 or response.status_code == 404: + raise DataNotFoundException("GO-CAM model not found.") + else: + response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) + response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code + return response.json() @router.get("/api/models/{id}", tags=["models"], description="Returns model details based on a GO-CAM model ID.") diff --git a/tests/unit/test_models_endpoints.py b/tests/unit/test_models_endpoints.py index 7763ab0..edc89d7 100644 --- a/tests/unit/test_models_endpoints.py +++ b/tests/unit/test_models_endpoints.py @@ -15,7 +15,7 @@ test_client = TestClient(app) gene_ids = ["ZFIN:ZDB-GENE-980526-388", "ZFIN:ZDB-GENE-990415-8", "MGI:3588192"] go_cam_ids = ["59a6110e00000067", "SYNGO_369", "581e072c00000820", "gomodel:59a6110e00000067", "gomodel:SYNGO_369"] - +go_cam_not_found_ids = ["NGO_369", "581e072c000008", "gomodel:59a6110e000000",] class TestApp(unittest.TestCase): """Test the models endpoints.""" @@ -150,6 +150,17 @@ def test_get_model_details_by_model_id_json(self): self.assertGreater(len(response.json().get("individuals")), 0) self.assertGreater(len(response.json().get("facts")), 0) + + def test_get_model_details_by_model_id_not_found_json(self): + """ + Test the endpoint to retrieve model details by model ID in JSON format from the S3 bucket, check for success. + + :return: None + """ + for id in go_cam_not_found_ids: + response = test_client.get(f"/api/go-cam/{id}") + self.assertEqual(response.status_code, 404) + def test_get_model_details_by_model_id_json_failure(self): """ Test the endpoint to retrieve model details by model ID that does not exist, check for failure. From cbffe4546775956075a0e8675db416a1ad669af2 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Fri, 22 Nov 2024 11:37:09 -0800 Subject: [PATCH 26/34] linting and adding taxon tests for 404 --- app/routers/models.py | 15 ++++++++++++--- tests/unit/test_global_exception_handling.py | 7 ++++++- tests/unit/test_models_endpoints.py | 9 ++++----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/routers/models.py b/app/routers/models.py index 72c85ed..f2c3755 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -8,7 +8,8 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource -from app.exceptions.global_exceptions import DataNotFoundException +from app.exceptions.global_exceptions import DataNotFoundException, InvalidIdentifier +from app.utils import ontology_utils from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array @@ -575,7 +576,7 @@ async def get_model_details_by_model_id_json( model_id = id.replace("gomodel:", "") stripped_ids.append(model_id) else: - stripped_ids.append(id) + stripped_ids.append(id) for stripped_id in stripped_ids: path_to_s3 = "https://go-public.s3.amazonaws.com/files/go-cam/%s.json" % stripped_id response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) @@ -583,7 +584,8 @@ async def get_model_details_by_model_id_json( raise DataNotFoundException("GO-CAM model not found.") else: response = requests.get(path_to_s3, timeout=30, headers={"User-Agent": USER_AGENT}) - response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code + response.raise_for_status() # This will raise an HTTPError if the HTTP request returned + # an unsuccessful status code return response.json() @@ -643,6 +645,13 @@ async def get_term_details_by_taxon_id( ) ): """Returns model details based on a NCBI Taxon ID.""" + try: + ontology_utils.is_golr_recognized_curie(taxon) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) from e + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) from e + ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) final_taxon = "http://purl.obolibrary.org/obo/" diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index b738025..55f2d40 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -28,11 +28,16 @@ def test_value_error_curie(): assert response.json() == {"message": "Value error occurred: Invalid CURIE format"} -def test_ncbi_taxon_error_handling(): +def test_ncbi_taxon_success(): response = test_client.get("/api/taxon/NCBITaxon%3A4896/models") assert response.status_code == 200 +def test_ncbi_taxon_error_handling(): + response = test_client.get("/api/taxon/NCBITaxon%3AFAKE/models") + assert response.status_code == 404 + + @pytest.mark.parametrize("endpoint", [ "/api/bioentity/function/FAKE:12345", "/api/bioentity/function/FAKE:12345/taxons", diff --git a/tests/unit/test_models_endpoints.py b/tests/unit/test_models_endpoints.py index edc89d7..c05e303 100644 --- a/tests/unit/test_models_endpoints.py +++ b/tests/unit/test_models_endpoints.py @@ -168,8 +168,8 @@ def test_get_model_details_by_model_id_json_failure(self): :return: None """ - with self.assertRaises(HTTPError): - test_client.get("/api/go-cam/notatallrelevant") + response = test_client.get("/api/go-cam/notatallrelevant") + assert response.status_code == 404 def test_get_model_details_by_model_id_json_failure_id(self): """ @@ -177,9 +177,8 @@ def test_get_model_details_by_model_id_json_failure_id(self): :return: None """ - with self.assertRaises(HTTPError): - test_client.get("/api/go-cam/gocam:59a6110e00000067") - + response = test_client.get("/api/go-cam/gocam:59a6110e00000067") + assert response.status_code == 404 if __name__ == "__main__": unittest.main() From b6362e4c839c72f760149b13831b3858fe734a6d Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Fri, 22 Nov 2024 13:53:25 -0800 Subject: [PATCH 27/34] fix feature steps tests --- app/routers/models.py | 2 +- app/routers/pathway_widget.py | 10 ++++++++-- app/utils/golr_utils.py | 2 ++ tests/integration/features/bioentityfunction.feature | 2 +- tests/unit/test_global_exception_handling.py | 3 ++- tests/unit/test_models_endpoints.py | 3 ++- tests/unit/test_pathway_widget_endpoints.py | 7 +++++++ 7 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/routers/models.py b/app/routers/models.py index f2c3755..7a3492d 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -562,7 +562,7 @@ async def get_model_details_by_model_id_json( id: str = Path( ..., description="A GO-CAM identifier (e.g. 581e072c00000820, 581e072c00000295, 5900dc7400000968)", - example="581e072c00000295", + example="gomodel:66187e4700001573", ) ): """ diff --git a/app/routers/pathway_widget.py b/app/routers/pathway_widget.py index f402f6c..9e7feae 100644 --- a/app/routers/pathway_widget.py +++ b/app/routers/pathway_widget.py @@ -7,6 +7,8 @@ from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation from oaklib.resource import OntologyResource +from app.exceptions.global_exceptions import DataNotFoundException, InvalidIdentifier +from app.utils.golr_utils import is_valid_bioentity from app.utils.prefix_utils import get_prefixes from app.utils.settings import get_sparql_endpoint, get_user_agent from app.utils.sparql_utils import transform_array @@ -39,8 +41,12 @@ async def get_gocams_by_geneproduct_id( (e.g. MGI:3588192, ZFIN:ZDB-GENE-000403-1). """ - if ":" not in id: - raise ValueError("Invalid CURIE format") + try: + is_valid_bioentity(id) + except DataNotFoundException as e: + raise DataNotFoundException(detail=str(e)) from e + except ValueError as e: + raise InvalidIdentifier(detail=str(e)) from e if id.startswith("MGI:MGI:"): id = id.replace("MGI:MGI:", "MGI:") diff --git a/app/utils/golr_utils.py b/app/utils/golr_utils.py index b62d0d8..8e41ea8 100644 --- a/app/utils/golr_utils.py +++ b/app/utils/golr_utils.py @@ -161,6 +161,8 @@ def is_valid_bioentity(entity_id) -> bool: logger.info("No results found for the provided entity ID") # Propagate the exception and return False raise e from error + else: + raise DataNotFoundException(detail=f"Bioentity with ID {entity_id} not found") from error except Exception as e: logger.info(f"Unexpected error in gene_to_uniprot_from_mygene: {e}") return False diff --git a/tests/integration/features/bioentityfunction.feature b/tests/integration/features/bioentityfunction.feature index 706bcc1..25ace40 100644 --- a/tests/integration/features/bioentityfunction.feature +++ b/tests/integration/features/bioentityfunction.feature @@ -13,7 +13,7 @@ Feature: bioentity function (GO) routes work as expected And the response should have an association with object.label of "insulin receptor binding" Scenario: User fetches all GO functional assignments for a human gene using a NCBI ID - Given the "/bioentity/gene/id/function endpoint" is queried with "NCBIGene:6469" + Given the "/bioentity/gene/id/function endpoint" is queried with "HGNC:10848" Then the response status code is "200" And the response contains an association with object.id of "GO:0001755" And the response should have an association with object.label of "neural crest cell migration" diff --git a/tests/unit/test_global_exception_handling.py b/tests/unit/test_global_exception_handling.py index 55f2d40..1cc3dfa 100644 --- a/tests/unit/test_global_exception_handling.py +++ b/tests/unit/test_global_exception_handling.py @@ -25,7 +25,8 @@ def test_value_error_handler(): def test_value_error_curie(): response = test_client.get(f"/api/gp/P05067/models") assert response.status_code == 400 - assert response.json() == {"message": "Value error occurred: Invalid CURIE format"} + print(response.json()) + assert response.json() == {"detail": "Invalid CURIE format"} def test_ncbi_taxon_success(): diff --git a/tests/unit/test_models_endpoints.py b/tests/unit/test_models_endpoints.py index c05e303..9946865 100644 --- a/tests/unit/test_models_endpoints.py +++ b/tests/unit/test_models_endpoints.py @@ -14,7 +14,8 @@ test_client = TestClient(app) gene_ids = ["ZFIN:ZDB-GENE-980526-388", "ZFIN:ZDB-GENE-990415-8", "MGI:3588192"] -go_cam_ids = ["59a6110e00000067", "SYNGO_369", "581e072c00000820", "gomodel:59a6110e00000067", "gomodel:SYNGO_369"] +go_cam_ids = ["gomodel:66187e4700001573", "66187e4700001573", "59a6110e00000067", "SYNGO_369", + "581e072c00000820", "gomodel:59a6110e00000067", "gomodel:SYNGO_369"] go_cam_not_found_ids = ["NGO_369", "581e072c000008", "gomodel:59a6110e000000",] class TestApp(unittest.TestCase): diff --git a/tests/unit/test_pathway_widget_endpoints.py b/tests/unit/test_pathway_widget_endpoints.py index 877b9e8..463e13d 100644 --- a/tests/unit/test_pathway_widget_endpoints.py +++ b/tests/unit/test_pathway_widget_endpoints.py @@ -10,6 +10,7 @@ test_client = TestClient(app) gene_ids = ["WB:WBGene00002147", "FB:FBgn0003731"] # , "SGD:S000003407", "MGI:3588192"] +gene_not_found_ids = ["WB:not_real", "FB:fake", "SGD:FAKE", "MGI:MGI:FAKE"] logging.basicConfig(filename="combined_access_error.log", level=logging.INFO, format="%(asctime)s - %(message)s") logger = logging.getLogger() @@ -28,6 +29,12 @@ def test_get_gocams_by_geneproduct_id(self): self.assertGreater(len(response.json()), 0) self.assertEqual(response.status_code, 200) + def test_get_gocams_by_geneproduct_not_found(self): + for gene_id in gene_not_found_ids: + response = test_client.get(f"/api/gp/{gene_id}/models") + print(response.json()) + self.assertEqual(response.status_code, 404) + def test_get_gocams_by_geneproduct_id_causal2(self): """ Test getting Gene Ontology models associated with a gene product by its ID with causal2. From 7b5e476b769501c28bde975a3b1aba2ea5cd644a Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Fri, 22 Nov 2024 16:39:16 -0800 Subject: [PATCH 28/34] fix models by orcid endpoint to actually return models --- app/routers/users_and_groups.py | 91 ++++++++---------------------- tests/unit/test_users_endpoints.py | 30 ++++++++++ 2 files changed, 52 insertions(+), 69 deletions(-) create mode 100644 tests/unit/test_users_endpoints.py diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index 144a627..8472571 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -66,93 +66,46 @@ async def get_user_by_orcid( ) ): """Returns model details based on a GO-CAM model ID.""" - mod_orcid = f'"http://orcid.org/{orcid}"^^xsd:string' + mod_orcid = f"https://orcid.org/{orcid}" + print(mod_orcid) ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) query = ( """ - PREFIX metago: PREFIX dc: - PREFIX vcard: PREFIX has_affiliation: PREFIX enabled_by: PREFIX BP: PREFIX MF: PREFIX CC: + PREFIX biomacromolecule: - SELECT ?name - (GROUP_CONCAT(distinct ?gocam;separator="@|@") as ?gocams) - (GROUP_CONCAT(distinct ?date;separator="@|@") as ?gocamsDate) - (GROUP_CONCAT(distinct ?title;separator="@|@") as ?gocamsTitle) - (GROUP_CONCAT(distinct ?goid;separator="@|@") as ?bpids) - (GROUP_CONCAT(distinct ?goname;separator="@|@") as ?bpnames) - (GROUP_CONCAT(distinct ?gpid;separator="@|@") as ?gpids) - (GROUP_CONCAT(distinct ?gpname;separator="@|@") as ?gpnames) - WHERE - { - BIND(%s as ?orcid) . - #BIND("SynGO:SynGO-pim"^^xsd:string as ?orcid) . - #BIND("http://orcid.org/0000-0001-7476-6306"^^xsd:string as ?orcid) - #BIND("http://orcid.org/0000-0003-1074-8103"^^xsd:string as ?orcid) . - - BIND(IRI(?orcid) as ?orcidIRI) . - - - # Getting some information on the model - GRAPH ?gocam - { - ?gocam metago:graphType metago:noctuaCam ; - dc:date ?date ; - dc:title ?title ; - dc:contributor ?orcid . - - ?entity rdf:type owl:NamedIndividual . - ?entity rdf:type ?goid . - - ?s enabled_by: ?gpentity . - ?gpentity rdf:type ?gpid . - FILTER(?gpid != owl:NamedIndividual) . - } - - - VALUES ?GO_class { BP: } . - # rdf:type faster then subClassOf+ but require filter - # ?goid rdfs:subClassOf+ ?GO_class . - ?entity rdf:type ?GO_class . - - # Filtering out the root BP, MF & CC terms - filter(?goid != MF: ) - filter(?goid != BP: ) - filter(?goid != CC: ) - - ?goid rdfs:label ?goname . + SELECT distinct ?title ?contributor ?gocam +WHERE { + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam ; + dc:date ?date ; + dc:title ?title ; + dc:contributor ?contributor . - # crash the query for SYNGO user "http://orcid.org/0000-0002-1190-4481"^^xsd:string - optional { - ?gpid rdfs:label ?gpname . - } - BIND(IF(bound(?gpname), ?gpname, ?gpid) as ?gpname) - } - GROUP BY ?name + # Contributor filter + FILTER(?contributor = "%s") + } +} """ % mod_orcid ) - collated_results = [] - collated = {} results = si._sparql_query(query) - for result in results: - collated["gocams"] = result["gocams"].get("value") - collated["gocamsDate"] = result["gocamsDate"].get("value") - collated["gocamsTitle"] = result["gocamsTitle"].get("value") - collated["bpids"] = result["bpids"].get("value") - collated["bpnames"] = result["bpnames"].get("value") - collated["gpids"] = result["gpids"].get("value") - collated_results.append(collated) - if not collated_results: - return DataNotFoundException(detail=f"Item with ID {orcid} not found") - return collated_results + print(query) + if not results: + return DataNotFoundException(detail=f"Item with ID {orcid} not found") + else: + collated_results = [] + for result in results: + collated_results.append({"model_id": result["gocam"].get("value"), "title": result["title"].get("value")}) + return collated_results @router.get("/api/users/{orcid}/models", tags=["models"]) diff --git a/tests/unit/test_users_endpoints.py b/tests/unit/test_users_endpoints.py new file mode 100644 index 0000000..cd24dc5 --- /dev/null +++ b/tests/unit/test_users_endpoints.py @@ -0,0 +1,30 @@ +"""Unit tests for the users endpoints.""" +import logging +from fastapi.testclient import TestClient +from app.main import app + + +logging.basicConfig(filename="combined_access_error.log", level=logging.INFO, format="%(asctime)s - %(message)s") +logger = logging.getLogger() + +test_client = TestClient(app) +gene_ids = ["ZFIN:ZDB-GENE-980526-388", "ZFIN:ZDB-GENE-990415-8", "MGI:3588192"] +go_cam_ids = ["gomodel:66187e4700001573", "66187e4700001573", "59a6110e00000067", "SYNGO_369", + "581e072c00000820", "gomodel:59a6110e00000067", "gomodel:SYNGO_369"] +go_cam_not_found_ids = ["NGO_369", "581e072c000008", "gomodel:59a6110e000000",] +valid_orcids = ["0000-0003-1813-6857"] +invalid_orcids = ["FAKE_ORCID"] + + +def test_get_models_by_orcid(): + for orcid in valid_orcids: + response = test_client.get(f"/api/users/{orcid}") + assert response.status_code == 200 # Verify the status code + data = response.json() + assert len(data) >= 10 # Verify the length of the response + + +def test_invalid_models_by_orcid(): + for orcid in invalid_orcids: + response = test_client.get(f"/api/users/{orcid}") + assert response.status_code == 404 # Verify the status code From f3730748d1a4f27b9b551f97b6c42bda1c08d218 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Fri, 22 Nov 2024 16:39:47 -0800 Subject: [PATCH 29/34] remove print statements --- app/routers/users_and_groups.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index 8472571..be5a7b6 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -67,7 +67,6 @@ async def get_user_by_orcid( ): """Returns model details based on a GO-CAM model ID.""" mod_orcid = f"https://orcid.org/{orcid}" - print(mod_orcid) ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) query = ( @@ -98,7 +97,6 @@ async def get_user_by_orcid( % mod_orcid ) results = si._sparql_query(query) - print(query) if not results: return DataNotFoundException(detail=f"Item with ID {orcid} not found") else: From a22d257ed658dd164455ec2336de8366af379e41 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Fri, 22 Nov 2024 16:45:58 -0800 Subject: [PATCH 30/34] remove print statements --- app/routers/users_and_groups.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index be5a7b6..73e4087 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -96,9 +96,11 @@ async def get_user_by_orcid( """ % mod_orcid ) + results = si._sparql_query(query) + if not results: - return DataNotFoundException(detail=f"Item with ID {orcid} not found") + raise DataNotFoundException(detail=f"Item with ID {orcid} not found") else: collated_results = [] for result in results: From d1f6fa4189ae44ec9dc911ea13dc9d8641dd5830 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Fri, 22 Nov 2024 16:52:48 -0800 Subject: [PATCH 31/34] fix user endpoint --- app/routers/users_and_groups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index 73e4087..aa4ec79 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -57,7 +57,7 @@ async def get_users(): return results -@router.get("/api/users/{orcid}", tags=["models"]) +@router.get("/api/users/{orcid}", tags=["models"], description="Get GO-CAM models by ORCID") async def get_user_by_orcid( orcid: str = Path( ..., @@ -65,7 +65,7 @@ async def get_user_by_orcid( example="0000-0002-7285-027X", ) ): - """Returns model details based on a GO-CAM model ID.""" + """Returns GO-CAM model identifiers for a particular contributor orcid.""" mod_orcid = f"https://orcid.org/{orcid}" ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) From 5d2eaf215f6b4a193f7c225e8f0b3e4885156b37 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 25 Nov 2024 08:26:17 -0800 Subject: [PATCH 32/34] remove print statements --- app/routers/users_and_groups.py | 63 +++++++++++---------------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index aa4ec79..8e47d1b 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -131,55 +131,32 @@ async def get_models_by_orcid( PREFIX CC: PREFIX biomacromolecule: - SELECT ?identifier ?name ?species (count(?name) as ?usages) - (GROUP_CONCAT(?cam;separator="@|@") as ?gocams) - (GROUP_CONCAT(?date;separator="@|@") as ?dates) - (GROUP_CONCAT(?title;separator="@|@") as ?titles) - WHERE - { - #BIND("SynGO:SynGO-pim"^^xsd:string as ?orcid) . - #BIND("http://orcid.org/0000-0001-7476-6306"^^xsd:string as ?orcid) - #BIND("http://orcid.org/0000-0003-1074-8103"^^xsd:string as ?orcid) . - #BIND("http://orcid.org/0000-0001-5259-4945"^^xsd:string as ?orcid) . - - BIND(%s as ?orcid) - BIND(IRI(?orcid) as ?orcidIRI) . + SELECT distinct ?title ?contributor ?gocam +WHERE { + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam ; + dc:date ?date ; + dc:title ?title ; + dc:contributor ?contributor . - # Getting some information on the model - GRAPH ?cam { - ?cam metago:graphType metago:noctuaCam . - ?cam dc:contributor ?orcid . - ?cam dc:title ?title . - ?cam dc:date ?date . - ?s enabled_by: ?id . - ?id rdf:type ?identifier . - FILTER(?identifier != owl:NamedIndividual) . - } - ?identifier rdfs:label ?name . - ?identifier rdfs:subClassOf ?v0 . - ?v0 owl:onProperty . - ?v0 owl:someValuesFrom ?taxon . - ?taxon rdfs:label ?species . - } - GROUP BY ?identifier ?name ?species - ORDER BY DESC(?usages) + # Contributor filter + FILTER(?contributor = "%s") + } +} """ % mod_orcid ) results = si._sparql_query(query) - collated_results = [] - collated = {} - for result in results: - collated["bpids"] = result["bpids"].get("value") - collated["bpnames"] = result["bpnames"].get("value") - collated["gpids"] = result["gpids"].get("value") - collated["gpnames"] = result["gpnames"].get("value") - collated_results.append(collated) - if not collated_results: - return DataNotFoundException(detail=f"Item with ID {orcid} not found") - return collated_results + + if not results: + raise DataNotFoundException(detail=f"Item with ID {orcid} not found") + else: + collated_results = [] + for result in results: + collated_results.append({"model_id": result["gocam"].get("value"), "title": result["title"].get("value")}) + return collated_results @router.get("/api/users/{orcid}/gp", tags=["models"], description="Get GPs by orcid") @@ -190,7 +167,7 @@ async def get_gp_models_by_orcid( example="0000-0002-7285-027X", ) ): - """Returns GP model details based on a orcid.""" + """Returns GO-CAM model identifiers for a particular contributor orcid.""" mod_orcid = f'"http://orcid.org/{orcid}"^^xsd:string' ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) From aa2aec9bd7f3dc8af53d533c870a7e5a7e3a0137 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 25 Nov 2024 13:56:54 -0800 Subject: [PATCH 33/34] reorg deprecated endpoints to make swagger-ui more user friendly, fix tests --- app/main.py | 2 - app/routers/models.py | 585 +++++++++++++++++------------ app/routers/publications.py | 52 --- app/routers/users_and_groups.py | 160 +++----- tests/unit/test_users_endpoints.py | 11 +- 5 files changed, 400 insertions(+), 410 deletions(-) delete mode 100644 app/routers/publications.py diff --git a/app/main.py b/app/main.py index 245a6ea..705daec 100644 --- a/app/main.py +++ b/app/main.py @@ -16,7 +16,6 @@ ontology, pathway_widget, prefixes, - publications, ribbon, search, slimmer, @@ -46,7 +45,6 @@ app.include_router(prefixes.router) app.include_router(labeler.router) app.include_router(search.router) -app.include_router(publications.router) app.include_router(users_and_groups.router) # Logging diff --git a/app/routers/models.py b/app/routers/models.py index 7a3492d..53547ad 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -20,250 +20,6 @@ logger = logging.getLogger() -@router.get( - "/api/models", - tags=["models"], - deprecated=True, - description="Returns metadata of GO-CAM models, e.g. 59a6110e00000067.", -) -async def get_gocam_models( - start: int = Query(None, description="start"), - size: int = Query(None, description="Number of models to look for"), - last: int = Query(None, description="last"), - group: str = Query(None, description="group"), - user: str = Query(None, description="user"), - pmid: str = Query(None, description="pmid"), - causalmf: bool = Query( - False, - description="The model has a chain of at least three functions connected " - "by at least two consecutive causal relation edges. One of these functions is enabled_by " - "this input gene", - ), -): - """Returns metadata of GO-CAM models, e.g. 59a6110e00000067.""" - if last: - start = 0 - size = last - - ont_r = OntologyResource(url=get_sparql_endpoint()) - si = SparqlImplementation(ont_r) - - # support how the model endpoint currently works, better to have one param that controlled user group or pmid - # since this is effectively is an OR at the moment. - by_param = "" - if group: - by_param = group - if user: - by_param = user - - if by_param is not None: - query = ( - """ - PREFIX metago: - PREFIX dc: - PREFIX providedBy: - - SELECT ?gocam ?date ?title (GROUP_CONCAT(distinct ?orcid; separator="@|@") AS ?orcids) - (GROUP_CONCAT(distinct ?name; separator="@|@") AS ?names) - (GROUP_CONCAT(distinct ?providedBy; separator="@|@") AS ?groupids) - (GROUP_CONCAT(distinct ?providedByLabel; separator="@|@") AS ?groupnames) - WHERE - { - { - BIND(" %s " as ?groupName) . - GRAPH ?gocam { - ?gocam metago:graphType metago:noctuaCam . - ?gocam dc:title ?title ; - dc:date ?date ; - dc:contributor ?orcid ; - providedBy: ?providedBy . - - BIND( IRI(?orcid) AS ?orcidIRI ). - BIND( IRI(?providedBy) AS ?providedByIRI ). - } - - optional { - ?providedByIRI rdfs:label ?providedByLabel . - } - - filter(?providedByLabel = ?groupName ) - optional { ?orcidIRI rdfs:label ?name } - BIND(IF(bound(?name), ?name, ?orcid) as ?name) . - } - - } - GROUP BY ?gocam ?date ?title - ORDER BY DESC(?date) - - """ - % by_param - ) - - elif pmid is not None: - if not pmid.startswith("PMID:"): - pmid = "PMID:" + pmid - query = ( - """ - - PREFIX metago: - PREFIX dc: - PREFIX providedBy: - - SELECT ?gocam ?date ?title (GROUP_CONCAT(distinct ?orcid; separator="@|@") AS ?orcids) - (GROUP_CONCAT(distinct ?name; separator="@|@") AS ?names) - (GROUP_CONCAT(distinct ?providedBy; separator="@|@") AS ?groupids) - (GROUP_CONCAT(distinct ?providedByLabel; separator="@|@") AS ?groupnames) - - WHERE - { - GRAPH ?gocam { - ?gocam metago:graphType metago:noctuaCam . - - ?gocam dc:title ?title ; - dc:date ?date ; - dc:contributor ?orcid ; - providedBy: ?providedBy . - - BIND( IRI(?orcid) AS ?orcidIRI ). - BIND( IRI(?providedBy) AS ?providedByIRI ). - - ?s dc:source ?source . - BIND(REPLACE(?source, " ", "") AS ?source) . - FILTER(SAMETERM(?source, "%s"^^xsd:string)) - } - - optional { - ?providedByIRI rdfs:label ?providedByLabel . - } - - optional { - ?orcidIRI rdfs:label ?name - } - BIND(IF(bound(?name), ?name, ?orcid) as ?name) . - - } - GROUP BY ?gocam ?date ?title - ORDER BY DESC(?date) - - """ - % pmid - ) - - elif causalmf is not None: - query = """ - PREFIX pr: - PREFIX metago: - PREFIX dc: - PREFIX providedBy: - - PREFIX MF: - - PREFIX causally_upstream_of_or_within: - PREFIX causally_upstream_of_or_within_negative_effect: - PREFIX causally_upstream_of_or_within_positive_effect: - - PREFIX causally_upstream_of: - PREFIX causally_upstream_of_negative_effect: - PREFIX causally_upstream_of_positive_effect: - - PREFIX regulates: - PREFIX negatively_regulates: - PREFIX positively_regulates: - - PREFIX directly_regulates: - PREFIX directly_positively_regulates: - PREFIX directly_negatively_regulates: - - PREFIX directly_activates: - PREFIX indirectly_activates: - - PREFIX directly_inhibits: - PREFIX indirectly_inhibits: - - PREFIX transitively_provides_input_for: - PREFIX immediately_causally_upstream_of: - PREFIX directly_provides_input_for: - - SELECT ?gocam ?date ?title (GROUP_CONCAT(distinct ?orcid; separator="@|@") AS ?orcids) - (GROUP_CONCAT(distinct ?name; separator="@|@") AS ?names) - (GROUP_CONCAT(distinct ?providedBy; separator="@|@") AS ?groupids) - (GROUP_CONCAT(distinct ?providedByLabel; separator="@|@") AS ?groupnames) - - WHERE - { - ?causal1 rdfs:subPropertyOf* causally_upstream_of_or_within: . - ?causal2 rdfs:subPropertyOf* causally_upstream_of_or_within: . - { - GRAPH ?gocam { - ?gocam metago:graphType metago:noctuaCam . - ?gocam dc:date ?date . - ?gocam dc:title ?title . - ?gocam dc:contributor ?orcid . - ?gocam providedBy: ?providedBy . - BIND( IRI(?orcid) AS ?orcidIRI ). - BIND( IRI(?providedBy) AS ?providedByIRI ). - ?ind1 ?causal1 ?ind2 . - ?ind2 ?causal2 ?ind3 - } - ?ind1 rdf:type MF: . - ?ind2 rdf:type MF: . - ?ind3 rdf:type MF: . - optional { - ?providedByIRI rdfs:label ?providedByLabel . - } - - optional { ?orcidIRI rdfs:label ?name } - BIND(IF(bound(?name), ?name, ?orcid) as ?name) . - } - } - GROUP BY ?gocam ?date ?title - ORDER BY ?gocam - """ - else: - query = """ - PREFIX metago: - PREFIX dc: - PREFIX providedBy: - - SELECT ?gocam ?date ?title (GROUP_CONCAT(distinct ?orcid; separator="@|@") AS ?orcids) - (GROUP_CONCAT(distinct ?name; separator="@|@") AS ?names) - (GROUP_CONCAT(distinct ?providedBy; separator="@|@") AS ?groupids) - (GROUP_CONCAT(distinct ?providedByLabel; separator="@|@") AS ?groupnames) - WHERE - { - { - GRAPH ?gocam { - ?gocam metago:graphType metago:noctuaCam . - ?gocam dc:title ?title ; - dc:date ?date ; - dc:contributor ?orcid ; - providedBy: ?providedBy . - - BIND( IRI(?orcid) AS ?orcidIRI ). - BIND( IRI(?providedBy) AS ?providedByIRI ). - } - - optional { - ?providedByIRI rdfs:label ?providedByLabel . - } - optional { ?orcidIRI rdfs:label ?name } - BIND(IF(bound(?name), ?name, ?orcid) as ?name) . - } - - } - GROUP BY ?gocam ?date ?title - ORDER BY DESC(?date) - """ - if size: - query += "\nLIMIT " + str(size) - if start: - query += "\nOFFSET " + str(start) - results = si._sparql_query(query) - transformed_results = transform_array(results, ["orcids", "names", "groupids", "groupnames"]) - logger.info(transformed_results) - return transform_array(results, ["orcids", "names", "groupids", "groupnames"]) - - @router.get("/api/models/go", tags=["models"], description="Returns go term details based on a GO-CAM model ID.") async def get_goterms_by_model_id( gocams: List[str] = Query( @@ -686,3 +442,344 @@ async def get_term_details_by_taxon_id( collated = {"gocam": result["gocam"].get("value")} collated_results.append(collated) return collated_results + + + +@router.get( + "/api/pmid/{id}/models", + tags=["models"], + description="Returns models for a given publication identifier (PMID).", +) +async def get_model_details_by_pmid( + id: str = Path(..., description="A publication identifier (PMID)" " (e.g. 15314168 or 26954676)") +): + """ + Returns models for a given publication identifier (PMID). + + """ + + ont_r = OntologyResource(url=get_sparql_endpoint()) + si = SparqlImplementation(ont_r) + + query = ( + """ + PREFIX metago: + PREFIX dc: + SELECT distinct ?gocam + WHERE + { + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam . + ?s dc:source ?source . + BIND(REPLACE(?source, " ", "") AS ?source) . + FILTER((CONTAINS(?source, \"""" + + id + + """\"))) + } + } + """ + ) + results = si._sparql_query(query) + collated_results = [] + collated = {} + for result in results: + collated["gocam"] = result["gocam"].get("value") + collated_results.append(collated) + if not collated_results: + return DataNotFoundException(detail=f"Item with ID {id} not found") + return results + + +@router.get("/api/users/{orcid}/models", tags=["models"]) +async def get_models_by_orcid( + orcid: str = Path( + ..., + description="The ORCID of the user (e.g. 0000-0003-1813-6857)", + example="0000-0003-1813-6857", + ) +): + """Returns model details based on an orcid.""" + mod_orcid = f'https://orcid.org/{orcid}' + print(mod_orcid) + ont_r = OntologyResource(url=get_sparql_endpoint()) + si = SparqlImplementation(ont_r) + query = ( + """ + PREFIX metago: + PREFIX dc: + PREFIX has_affiliation: + PREFIX enabled_by: + PREFIX BP: + PREFIX MF: + PREFIX CC: + PREFIX biomacromolecule: + + SELECT distinct ?title ?contributor ?gocam + WHERE { + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam ; + dc:date ?date ; + dc:title ?title ; + dc:contributor ?contributor . + + + # Contributor filter + FILTER(?contributor = "%s") + } + } + """ + % mod_orcid + ) + + results = si._sparql_query(query) + + if not results: + raise DataNotFoundException(detail=f"Item with ID {orcid} not found") + else: + collated_results = [] + for result in results: + collated_results.append({"model_id": result["gocam"].get("value"), "title": result["title"].get("value")}) + return collated_results + +@router.get( + "/api/models", + tags=["models"], + deprecated=True, + description="Returns metadata of GO-CAM models, e.g. 59a6110e00000067.", +) +async def get_gocam_models( + start: int = Query(None, description="start"), + size: int = Query(None, description="Number of models to look for"), + last: int = Query(None, description="last"), + group: str = Query(None, description="group"), + user: str = Query(None, description="user"), + pmid: str = Query(None, description="pmid"), + causalmf: bool = Query( + False, + description="The model has a chain of at least three functions connected " + "by at least two consecutive causal relation edges. One of these functions is enabled_by " + "this input gene", + ), +): + """Returns metadata of GO-CAM models, e.g. 59a6110e00000067.""" + if last: + start = 0 + size = last + + ont_r = OntologyResource(url=get_sparql_endpoint()) + si = SparqlImplementation(ont_r) + + # support how the model endpoint currently works, better to have one param that controlled user group or pmid + # since this is effectively is an OR at the moment. + by_param = "" + if group: + by_param = group + if user: + by_param = user + + if by_param is not None: + query = ( + """ + PREFIX metago: + PREFIX dc: + PREFIX providedBy: + + SELECT ?gocam ?date ?title (GROUP_CONCAT(distinct ?orcid; separator="@|@") AS ?orcids) + (GROUP_CONCAT(distinct ?name; separator="@|@") AS ?names) + (GROUP_CONCAT(distinct ?providedBy; separator="@|@") AS ?groupids) + (GROUP_CONCAT(distinct ?providedByLabel; separator="@|@") AS ?groupnames) + WHERE + { + { + BIND(" %s " as ?groupName) . + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam . + ?gocam dc:title ?title ; + dc:date ?date ; + dc:contributor ?orcid ; + providedBy: ?providedBy . + + BIND( IRI(?orcid) AS ?orcidIRI ). + BIND( IRI(?providedBy) AS ?providedByIRI ). + } + + optional { + ?providedByIRI rdfs:label ?providedByLabel . + } + + filter(?providedByLabel = ?groupName ) + optional { ?orcidIRI rdfs:label ?name } + BIND(IF(bound(?name), ?name, ?orcid) as ?name) . + } + + } + GROUP BY ?gocam ?date ?title + ORDER BY DESC(?date) + + """ + % by_param + ) + + elif pmid is not None: + if not pmid.startswith("PMID:"): + pmid = "PMID:" + pmid + query = ( + """ + + PREFIX metago: + PREFIX dc: + PREFIX providedBy: + + SELECT ?gocam ?date ?title (GROUP_CONCAT(distinct ?orcid; separator="@|@") AS ?orcids) + (GROUP_CONCAT(distinct ?name; separator="@|@") AS ?names) + (GROUP_CONCAT(distinct ?providedBy; separator="@|@") AS ?groupids) + (GROUP_CONCAT(distinct ?providedByLabel; separator="@|@") AS ?groupnames) + + WHERE + { + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam . + + ?gocam dc:title ?title ; + dc:date ?date ; + dc:contributor ?orcid ; + providedBy: ?providedBy . + + BIND( IRI(?orcid) AS ?orcidIRI ). + BIND( IRI(?providedBy) AS ?providedByIRI ). + + ?s dc:source ?source . + BIND(REPLACE(?source, " ", "") AS ?source) . + FILTER(SAMETERM(?source, "%s"^^xsd:string)) + } + + optional { + ?providedByIRI rdfs:label ?providedByLabel . + } + + optional { + ?orcidIRI rdfs:label ?name + } + BIND(IF(bound(?name), ?name, ?orcid) as ?name) . + + } + GROUP BY ?gocam ?date ?title + ORDER BY DESC(?date) + + """ + % pmid + ) + + elif causalmf is not None: + query = """ + PREFIX pr: + PREFIX metago: + PREFIX dc: + PREFIX providedBy: + + PREFIX MF: + + PREFIX causally_upstream_of_or_within: + PREFIX causally_upstream_of_or_within_negative_effect: + PREFIX causally_upstream_of_or_within_positive_effect: + + PREFIX causally_upstream_of: + PREFIX causally_upstream_of_negative_effect: + PREFIX causally_upstream_of_positive_effect: + + PREFIX regulates: + PREFIX negatively_regulates: + PREFIX positively_regulates: + + PREFIX directly_regulates: + PREFIX directly_positively_regulates: + PREFIX directly_negatively_regulates: + + PREFIX directly_activates: + PREFIX indirectly_activates: + + PREFIX directly_inhibits: + PREFIX indirectly_inhibits: + + PREFIX transitively_provides_input_for: + PREFIX immediately_causally_upstream_of: + PREFIX directly_provides_input_for: + + SELECT ?gocam ?date ?title (GROUP_CONCAT(distinct ?orcid; separator="@|@") AS ?orcids) + (GROUP_CONCAT(distinct ?name; separator="@|@") AS ?names) + (GROUP_CONCAT(distinct ?providedBy; separator="@|@") AS ?groupids) + (GROUP_CONCAT(distinct ?providedByLabel; separator="@|@") AS ?groupnames) + + WHERE + { + ?causal1 rdfs:subPropertyOf* causally_upstream_of_or_within: . + ?causal2 rdfs:subPropertyOf* causally_upstream_of_or_within: . + { + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam . + ?gocam dc:date ?date . + ?gocam dc:title ?title . + ?gocam dc:contributor ?orcid . + ?gocam providedBy: ?providedBy . + BIND( IRI(?orcid) AS ?orcidIRI ). + BIND( IRI(?providedBy) AS ?providedByIRI ). + ?ind1 ?causal1 ?ind2 . + ?ind2 ?causal2 ?ind3 + } + ?ind1 rdf:type MF: . + ?ind2 rdf:type MF: . + ?ind3 rdf:type MF: . + optional { + ?providedByIRI rdfs:label ?providedByLabel . + } + + optional { ?orcidIRI rdfs:label ?name } + BIND(IF(bound(?name), ?name, ?orcid) as ?name) . + } + } + GROUP BY ?gocam ?date ?title + ORDER BY ?gocam + """ + else: + query = """ + PREFIX metago: + PREFIX dc: + PREFIX providedBy: + + SELECT ?gocam ?date ?title (GROUP_CONCAT(distinct ?orcid; separator="@|@") AS ?orcids) + (GROUP_CONCAT(distinct ?name; separator="@|@") AS ?names) + (GROUP_CONCAT(distinct ?providedBy; separator="@|@") AS ?groupids) + (GROUP_CONCAT(distinct ?providedByLabel; separator="@|@") AS ?groupnames) + WHERE + { + { + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam . + ?gocam dc:title ?title ; + dc:date ?date ; + dc:contributor ?orcid ; + providedBy: ?providedBy . + + BIND( IRI(?orcid) AS ?orcidIRI ). + BIND( IRI(?providedBy) AS ?providedByIRI ). + } + + optional { + ?providedByIRI rdfs:label ?providedByLabel . + } + optional { ?orcidIRI rdfs:label ?name } + BIND(IF(bound(?name), ?name, ?orcid) as ?name) . + } + + } + GROUP BY ?gocam ?date ?title + ORDER BY DESC(?date) + """ + if size: + query += "\nLIMIT " + str(size) + if start: + query += "\nOFFSET " + str(start) + results = si._sparql_query(query) + transformed_results = transform_array(results, ["orcids", "names", "groupids", "groupnames"]) + logger.info(transformed_results) + return transform_array(results, ["orcids", "names", "groupids", "groupnames"]) \ No newline at end of file diff --git a/app/routers/publications.py b/app/routers/publications.py deleted file mode 100644 index 8acc05f..0000000 --- a/app/routers/publications.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Publication-related endpoints.""" - -from fastapi import APIRouter, Path -from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation -from oaklib.resource import OntologyResource - -from app.exceptions.global_exceptions import DataNotFoundException -from app.utils.settings import get_sparql_endpoint, get_user_agent - -USER_AGENT = get_user_agent() -router = APIRouter() - - -@router.get( - "/api/pmid/{id}/models", - tags=["publications"], - description="Returns models for a given publication identifier (PMID).", -) -async def get_model_details_by_pmid( - id: str = Path(..., description="A publication identifier (PMID)" " (e.g. 15314168 or 26954676)") -): - """Returns models for a given publication identifier (PMID).""" - ont_r = OntologyResource(url=get_sparql_endpoint()) - si = SparqlImplementation(ont_r) - - query = ( - """ - PREFIX metago: - PREFIX dc: - SELECT distinct ?gocam - WHERE - { - GRAPH ?gocam { - ?gocam metago:graphType metago:noctuaCam . - ?s dc:source ?source . - BIND(REPLACE(?source, " ", "") AS ?source) . - FILTER((CONTAINS(?source, \"""" - + id - + """\"))) - } - } - """ - ) - results = si._sparql_query(query) - collated_results = [] - collated = {} - for result in results: - collated["gocam"] = result["gocam"].get("value") - collated_results.append(collated) - if not collated_results: - return DataNotFoundException(detail=f"Item with ID {id} not found") - return results diff --git a/app/routers/users_and_groups.py b/app/routers/users_and_groups.py index 8e47d1b..07fcb09 100644 --- a/app/routers/users_and_groups.py +++ b/app/routers/users_and_groups.py @@ -57,109 +57,7 @@ async def get_users(): return results -@router.get("/api/users/{orcid}", tags=["models"], description="Get GO-CAM models by ORCID") -async def get_user_by_orcid( - orcid: str = Path( - ..., - description="The ORCID of the user (e.g. 0000-0002-7285-027X)", - example="0000-0002-7285-027X", - ) -): - """Returns GO-CAM model identifiers for a particular contributor orcid.""" - mod_orcid = f"https://orcid.org/{orcid}" - ont_r = OntologyResource(url=get_sparql_endpoint()) - si = SparqlImplementation(ont_r) - query = ( - """ - PREFIX metago: - PREFIX dc: - PREFIX has_affiliation: - PREFIX enabled_by: - PREFIX BP: - PREFIX MF: - PREFIX CC: - PREFIX biomacromolecule: - - SELECT distinct ?title ?contributor ?gocam -WHERE { - GRAPH ?gocam { - ?gocam metago:graphType metago:noctuaCam ; - dc:date ?date ; - dc:title ?title ; - dc:contributor ?contributor . - - - # Contributor filter - FILTER(?contributor = "%s") - } -} - """ - % mod_orcid - ) - - results = si._sparql_query(query) - - if not results: - raise DataNotFoundException(detail=f"Item with ID {orcid} not found") - else: - collated_results = [] - for result in results: - collated_results.append({"model_id": result["gocam"].get("value"), "title": result["title"].get("value")}) - return collated_results - - -@router.get("/api/users/{orcid}/models", tags=["models"]) -async def get_models_by_orcid( - orcid: str = Path( - ..., - description="The ORCID of the user (e.g. 0000-0002-7285-027X)", - example="0000-0002-7285-027X", - ) -): - """Returns model details based on an orcid.""" - mod_orcid = f'"http://orcid.org/{orcid}"^^xsd:string' - ont_r = OntologyResource(url=get_sparql_endpoint()) - si = SparqlImplementation(ont_r) - query = ( - """ - PREFIX metago: - PREFIX dc: - PREFIX has_affiliation: - PREFIX enabled_by: - PREFIX BP: - PREFIX MF: - PREFIX CC: - PREFIX biomacromolecule: - - SELECT distinct ?title ?contributor ?gocam -WHERE { - GRAPH ?gocam { - ?gocam metago:graphType metago:noctuaCam ; - dc:date ?date ; - dc:title ?title ; - dc:contributor ?contributor . - - - # Contributor filter - FILTER(?contributor = "%s") - } -} - """ - % mod_orcid - ) - - results = si._sparql_query(query) - - if not results: - raise DataNotFoundException(detail=f"Item with ID {orcid} not found") - else: - collated_results = [] - for result in results: - collated_results.append({"model_id": result["gocam"].get("value"), "title": result["title"].get("value")}) - return collated_results - - -@router.get("/api/users/{orcid}/gp", tags=["models"], description="Get GPs by orcid") +@router.get("/api/users/{orcid}/gp", tags=["models"], description="Get GPs by orcid", deprecated=True) async def get_gp_models_by_orcid( orcid: str = Path( ..., @@ -188,11 +86,6 @@ async def get_gp_models_by_orcid( (GROUP_CONCAT(?title;separator="@|@") as ?titles) WHERE { - #BIND("SynGO:SynGO-pim"^^xsd:string as ?orcid) . - #BIND("http://orcid.org/0000-0001-7476-6306"^^xsd:string as ?orcid) - #BIND("http://orcid.org/0000-0003-1074-8103"^^xsd:string as ?orcid) . - #BIND("http://orcid.org/0000-0001-5259-4945"^^xsd:string as ?orcid) . - BIND(%s as ?orcid) BIND(IRI(?orcid) as ?orcidIRI) . @@ -334,3 +227,54 @@ async def get_group_metadata_by_name( if not collated_results: return DataNotFoundException(detail=f"Item with ID {name} not found") return collated_results + + +@router.get("/api/users/{orcid}", tags=["models"], description="Get GO-CAM models by ORCID", deprecated=True) +async def get_go_cam_models_by_orcid( + orcid: str = Path( + ..., + description="The ORCID of the user (e.g. 0000-0002-7285-027X)", + example="0000-0002-7285-027X", + ) +): + """Returns GO-CAM model identifiers for a particular contributor orcid.""" + mod_orcid = f"https://orcid.org/{orcid}" + ont_r = OntologyResource(url=get_sparql_endpoint()) + si = SparqlImplementation(ont_r) + query = ( + """ + PREFIX metago: + PREFIX dc: + PREFIX has_affiliation: + PREFIX enabled_by: + PREFIX BP: + PREFIX MF: + PREFIX CC: + PREFIX biomacromolecule: + + SELECT distinct ?title ?contributor ?gocam +WHERE { + GRAPH ?gocam { + ?gocam metago:graphType metago:noctuaCam ; + dc:date ?date ; + dc:title ?title ; + dc:contributor ?contributor . + + + # Contributor filter + FILTER(?contributor = "%s") + } +} + """ + % mod_orcid + ) + + results = si._sparql_query(query) + + if not results: + raise DataNotFoundException(detail=f"Item with ID {orcid} not found") + else: + collated_results = [] + for result in results: + collated_results.append({"model_id": result["gocam"].get("value"), "title": result["title"].get("value")}) + return collated_results diff --git a/tests/unit/test_users_endpoints.py b/tests/unit/test_users_endpoints.py index cd24dc5..ff7c14e 100644 --- a/tests/unit/test_users_endpoints.py +++ b/tests/unit/test_users_endpoints.py @@ -18,13 +18,16 @@ def test_get_models_by_orcid(): for orcid in valid_orcids: - response = test_client.get(f"/api/users/{orcid}") + print(orcid) + response = test_client.get(f"/api/users/{orcid}/models") assert response.status_code == 200 # Verify the status code data = response.json() - assert len(data) >= 10 # Verify the length of the response + assert len(data) >= 10 # Verify the length of the response, should be 11 for Pascale def test_invalid_models_by_orcid(): for orcid in invalid_orcids: - response = test_client.get(f"/api/users/{orcid}") - assert response.status_code == 404 # Verify the status code + response = test_client.get(f"/api/users/{orcid}/models") + assert response.status_code == 404 + + From 8f91cc0da5e4814f68d01fcfc4a81f40920c3285 Mon Sep 17 00:00:00 2001 From: Sierra Taylor Moxon Date: Mon, 25 Nov 2024 14:07:14 -0800 Subject: [PATCH 34/34] lint --- app/routers/models.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/routers/models.py b/app/routers/models.py index 53547ad..894380b 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -444,7 +444,6 @@ async def get_term_details_by_taxon_id( return collated_results - @router.get( "/api/pmid/{id}/models", tags=["models"], @@ -453,11 +452,7 @@ async def get_term_details_by_taxon_id( async def get_model_details_by_pmid( id: str = Path(..., description="A publication identifier (PMID)" " (e.g. 15314168 or 26954676)") ): - """ - Returns models for a given publication identifier (PMID). - - """ - + """Returns models for a given publication identifier (PMID).""" ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) @@ -499,12 +494,12 @@ async def get_models_by_orcid( ) ): """Returns model details based on an orcid.""" - mod_orcid = f'https://orcid.org/{orcid}' + mod_orcid = f"https://orcid.org/{orcid}" print(mod_orcid) ont_r = OntologyResource(url=get_sparql_endpoint()) si = SparqlImplementation(ont_r) query = ( - """ + """ PREFIX metago: PREFIX dc: PREFIX has_affiliation: @@ -528,7 +523,7 @@ async def get_models_by_orcid( } } """ - % mod_orcid + % mod_orcid ) results = si._sparql_query(query) @@ -541,6 +536,7 @@ async def get_models_by_orcid( collated_results.append({"model_id": result["gocam"].get("value"), "title": result["title"].get("value")}) return collated_results + @router.get( "/api/models", tags=["models"], @@ -782,4 +778,4 @@ async def get_gocam_models( results = si._sparql_query(query) transformed_results = transform_array(results, ["orcids", "names", "groupids", "groupnames"]) logger.info(transformed_results) - return transform_array(results, ["orcids", "names", "groupids", "groupnames"]) \ No newline at end of file + return transform_array(results, ["orcids", "names", "groupids", "groupnames"])