Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements and bug fixes #165

Merged
merged 6 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions dd-api-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ paths:
- name: term_id
in: path
required: true
description: The string to match. Subject to timeout.
description: The string to match. Subject to timeout. Should be URL-encoded for spaces (%20).
schema:
type: string
example: Breast cancer
Expand All @@ -950,7 +950,7 @@ paths:
- name: term_id
in: path
required: true
description: The string to match
description: The string to match. Should be URL-encoded for spaces (%20).
schema:
type: string
example: Breast cancer
Expand All @@ -974,7 +974,7 @@ paths:
parameters:
- name: sab
in: query
required: true
required: false
description: A source (SAB) to which to limit the response.
schema:
type: string
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ubkg_api"
version = "2.1.7"
version = "2.1.8"
authors = [
{ name="HuBMAP Consortium", email="api-developers@hubmapconsortium.org" },
]
Expand Down
11 changes: 11 additions & 0 deletions src/ubkg_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from common_routes.sabs.sabs_controller import sabs_blueprint
from common_routes.sources.sources_controller import sources_blueprint

from utils.http_error_string import wrap_message

logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s', level=logging.DEBUG,
datefmt='%Y-%m-%d %H:%M:%S')
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -102,6 +104,15 @@ def __init__(self, config, package_base_dir):
def index():
return "Hello! This is UBKG-API service :)"

@self.app.errorhandler(404)
# Custom 404 error handler.
def servererror(error):
return wrap_message(key='message', msg=error.description)

@self.app.errorhandler(500)
# Custom 500 error handler.
def servererror(error):
return wrap_message(key='error', msg=error.description)

####################################################################################################
## For local development/testing
Expand Down
24 changes: 16 additions & 8 deletions src/ubkg_api/common_routes/common_neo4j_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,23 @@ def codes_code_id_codes_get_logic(neo4j_instance, code_id: str, sab: List[str])
def codes_code_id_concepts_get_logic(neo4j_instance, code_id: str) -> List[ConceptDetail]:
conceptdetails: List[ConceptDetail] = []

query: str = \
'WITH [$code_id] AS query' \
' MATCH (a:Code)<-[:CODE]-(b:Concept)' \
' WHERE a.CodeID IN query' \
' OPTIONAL MATCH (b)-[:PREF_TERM]->(c:Term)' \
' RETURN DISTINCT a.CodeID AS Code, b.CUI AS Concept, c.name as Prefterm' \
' ORDER BY Code ASC, Concept'
# Dec 2024 - replace in-line Cypher with loaded file.
# Load Cypher query from file.
query: str = loadquerystring(filename='codes_code_id_concepts.cypher')

# Filter by code_id.
query = query.replace('$code_id', f"'{code_id}'")

#query: str = \
#'WITH [$code_id] AS query' \
#' MATCH (a:Code)<-[:CODE]-(b:Concept)' \
#' WHERE a.CodeID IN query' \
#' OPTIONAL MATCH (b)-[:PREF_TERM]->(c:Term)' \
#' RETURN DISTINCT a.CodeID AS Code, b.CUI AS Concept, c.name as Prefterm' \
#' ORDER BY Code ASC, Concept'

with neo4j_instance.driver.session() as session:
recds: neo4j.Result = session.run(query, code_id=code_id)
recds: neo4j.Result = session.run(query)
for record in recds:
try:
conceptdetail: ConceptDetail = ConceptDetail(record.get('Concept'),
Expand Down
6 changes: 3 additions & 3 deletions src/ubkg_api/common_routes/concepts/concepts_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from utils.http_error_string import get_404_error_string, validate_query_parameter_names, \
validate_parameter_value_in_enum, validate_required_parameters, validate_parameter_is_numeric, \
validate_parameter_is_nonnegative, validate_parameter_range_order, check_payload_size, \
check_neo4j_version_compatibility,check_max_mindepth
check_neo4j_version_compatibility,check_max_mindepth,wrap_message
# Functions to format query parameters for use in Cypher queries
from utils.http_parameter import parameter_as_list, set_default_minimum, set_default_maximum
# Functions common to paths routes
Expand Down Expand Up @@ -489,8 +489,8 @@ def concepts_paths_subraphs_sequential_get(concept_id=None):
relsabs = []
for rs in relsequence:
if not ':' in rs:
err = f'Invalid parameter value: {rs}. Format relationships as <SAB>:<relationship_type>'
return make_response(err, 400)
err = f'Invalid parameter value for \'relsequence\': {rs}. Format relationships as <SAB>:<relationship_type>'
return make_response(wrap_message(key="message", msg=err), 400)

relsabs.append(rs.split(':')[0].upper())
reltypes.append(rs.split(':')[1])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# August 2024
# The validate_application_context logic is specific to the HuBMAP/SenNet UBKG context. The functionality has been
# moved to the code base in the hs-ontology-api repository.

from flask import request, abort, jsonify


Expand Down
9 changes: 9 additions & 0 deletions src/ubkg_api/cypher/codes_code_id_concepts.cypher
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Used in the codes/{code_id}/concepts endpoint

// December 2024 - Changed to return only those concepts with a linked preferred term.

WITH $code_id AS query
MATCH (:Term)<-[d]-(a:Code)<-[:CODE]-(b:Concept)-[:PREF_TERM]->(c:Term)
WHERE ((a.CodeID = query) AND (b.CUI = d.CUI))
RETURN DISTINCT a.CodeID AS Code, b.CUI AS Concept, c.name as Prefterm
ORDER BY Code ASC, Concept
6 changes: 4 additions & 2 deletions src/ubkg_api/cypher/sources.cypher
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ CALL
CALL
{
WITH CUISource
OPTIONAL MATCH (pSource:Concept)-[:has_citation]->(p:Concept)-[:CODE]->(c:Code)-[r:PT]->(t:Term)
MATCH (pSource:Concept)-[:has_citation]->(p:Concept)-[:CODE]->(c:Code)-[r:PT]->(t:Term)
WHERE pSource.CUI=CUISource
AND r.CUI = p.CUI
RETURN COLLECT(DISTINCT {PMID:split(t.name,':')[1], url:'https://pubmed.ncbi.nlm.nih.gov/'+split(t.name,':')[1]}) AS citations
RETURN COLLECT({pmid:CASE WHEN split(t.name,':')[0]='PMID' THEN split(t.name,':')[1] END,
url:CASE WHEN split(t.name,':')[0]<>'PMID' THEN t.name ELSE 'https://pubmed.ncbi.nlm.nih.gov/'+split(t.name,':')[1] END}) AS citations
}

// ETL command
CALL
{
Expand Down
39 changes: 27 additions & 12 deletions src/ubkg_api/utils/http_error_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

from flask import request

def wrap_message(key:str, msg:str) ->dict:
"""
Wraps a return message string in JSON format.
"""

return {key: msg}

def format_request_path(custom_err=None):
"""
Expand Down Expand Up @@ -83,10 +89,10 @@ def get_404_error_string(prompt_string=None, custom_request_path=None, timeout=N
if timeout > 1:
errtimeout = errtimeout + "s"

err = err + f". Note that this endpoint is limited to an execution time of {errtimeout} " \
err = err + f". Note that this endpoint is limited to an execution time of {errtimeout}" \
f" to prevent timeout errors."

return err
return wrap_message(key="message", msg=err)


def get_number_agreement(list_items=None):
Expand Down Expand Up @@ -127,8 +133,9 @@ def validate_query_parameter_names(parameter_name_list=None) -> str:
if req not in parameter_name_list:
namelist = list_as_single_quoted_string(list_elements=parameter_name_list)
prompt = get_number_agreement(list_items=parameter_name_list)
return f"Invalid query parameter: '{req}'. The possible parameter name{prompt}: {namelist}. " \
err = f"Invalid query parameter: '{req}'. The possible parameter name{prompt}: {namelist}. " \
f"Refer to the SmartAPI documentation for this endpoint for more information."
return wrap_message(key="message", msg=err)

return "ok"

Expand All @@ -150,8 +157,9 @@ def validate_required_parameters(required_parameter_list=None) -> str:
if param not in request.args:
namelist = list_as_single_quoted_string(list_elements=required_parameter_list)
prompt = get_number_agreement(list_items=required_parameter_list)
return f"Missing query parameter: '{param}'. The required parameter{prompt}: {namelist}. " \
err = f"Missing query parameter: '{param}'. The required parameter{prompt}: {namelist}. " \
f"Refer to the SmartAPI documentation for this endpoint for more information."
return wrap_message(key="message", msg=err)

return "ok"

Expand All @@ -176,8 +184,9 @@ def validate_parameter_value_in_enum(param_name=None, param_value=None, enum_lis
if param_value not in enum_list:
namelist = list_as_single_quoted_string(list_elements=enum_list)
prompt = get_number_agreement(enum_list)
return f"Invalid value for parameter: '{param_name}'. The possible parameter value{prompt}: {namelist}. " \
err = f"Invalid value for parameter: '{param_name}'. The possible parameter value{prompt}: {namelist}. " \
f"Refer to the SmartAPI documentation for this endpoint for more information."
return wrap_message(key="message", msg=err)

return "ok"

Expand All @@ -193,7 +202,8 @@ def validate_parameter_is_numeric(param_name=None, param_value: str = '') -> str
"""

if not param_value.lstrip('-').isnumeric():
return f"Invalid value ({param_value}) for parameter '{param_name}'. The parameter must be numeric."
err = f"Invalid value ({param_value}) for parameter '{param_name}'. The parameter must be numeric."
return wrap_message(key="message", msg=err)

return "ok"

Expand All @@ -215,8 +225,8 @@ def validate_parameter_is_nonnegative(param_name=None, param_value: str = '') ->
return err

if int(param_value) < 0:
return f"Invalid value ({param_value}) for parameter '{param_name}'. The parameter cannot be negative."

err = f"Invalid value ({param_value}) for parameter '{param_name}'. The parameter cannot be negative."
return wrap_message(key="message", msg=err)
return "ok"


Expand All @@ -233,7 +243,8 @@ def validate_parameter_range_order(min_name: str, min_value: str, max_name: str,
"""

if int(min_value) > int(max_value):
return f"Invalid parameter values: '{min_name}' ({min_value}) greater than '{max_name}' ({max_value}). "
err = f"Invalid parameter values: '{min_name}' ({min_value}) greater than '{max_name}' ({max_value}). "
return wrap_message(key="message", msg=err)

return "ok"

Expand All @@ -247,9 +258,10 @@ def check_payload_size(payload: str, max_payload_size: int) -> str:

payload_size = len(str(payload))
if payload_size > max_payload_size:
return f"The size of the response to the endpoint with the specified parameters " \
err = f"The size of the response to the endpoint with the specified parameters " \
f"({int(payload_size)/1024} MB) exceeds the payload limit" \
f" of {int(max_payload_size)/1024} MB."
return wrap_message(key="message", msg=err)

return "ok"

Expand All @@ -269,7 +281,8 @@ def check_neo4j_version_compatibility(query_version: str, instance_version: str)
int_query_version = int(query_version.replace('.', ''))

if int_instance_version < int_query_version:
return f"This functionality requires at least version {query_version} of neo4j."
err = f"This functionality requires at least version {query_version} of neo4j."
return wrap_message(key="message", msg=err)

return "ok"

Expand All @@ -281,6 +294,8 @@ def check_max_mindepth(mindepth: int, max_mindepth: int) -> str:

"""
if mindepth > max_mindepth:
return f"The maximum value of 'mindepth' for this endpoint is {max_mindepth}. " \
err = f"The maximum value of 'mindepth' for this endpoint is {max_mindepth}. " \
f"Larger values of 'mindepth' result in queries that will exceed the server timeout."
return wrap_message(key="message", msg=err)

return "ok"
6 changes: 3 additions & 3 deletions ubkg-api-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ paths:
- name: term_id
in: path
required: true
description: The string to match. Subject to timeout.
description: The string to match. Subject to timeout. Should be URL-encoded for spaces (%20).
schema:
type: string
example: Breast cancer
Expand All @@ -948,7 +948,7 @@ paths:
- name: term_id
in: path
required: true
description: The string to match
description: The string to match. Should be URL-encoded for spaces (%20).
schema:
type: string
example: Breast cancer
Expand All @@ -972,7 +972,7 @@ paths:
parameters:
- name: sab
in: query
required: true
required: false
description: A source (SAB) to which to limit the response.
schema:
type: string
Expand Down
Loading