Skip to content

Commit

Permalink
Merge pull request #143 from x-atlas-consortia/jas_code_term
Browse files Browse the repository at this point in the history
New endpoints for codes and concepts paths
  • Loading branch information
yuanzhou authored Aug 2, 2024
2 parents 3e0fefa + f6a698f commit ceafafb
Show file tree
Hide file tree
Showing 9 changed files with 817 additions and 3 deletions.
139 changes: 139 additions & 0 deletions dd-api-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,38 @@ paths:
description: no Concept nodes for the specified Code
'5XX':
description: Unknown error
/codes/{code_id}/terms:
get:
operationId: codes_code_id_terms_get
summary: Returns the set of Term nodes that link to the specified Code node, subject to constraints specified in parameters.
parameters:
- name: code_id
in: path
required: true
description: The CodeID for a Code node, in format SAB:CODE.
schema:
type: string
example: SNOMEDCT_US:254837009
- name: term_type
in: query
required: false
description: optional term type for the term. Can be either a list of values delimited with commas (e.g., ?term_type=PT,SY,FN) or with individual key-value pairs (e.g., ?term_type=PT&term_type=SY&term_type=FN)
schema:
type: string
example: PT
responses:
'200':
description: An array of Term nodes that have relationships with the specified Code node
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CodeTerms'
'404':
description: no Term nodes for the specified Code
'5XX':
description: Unknown error
/concepts/{concept_id}/codes:
get:
operationId: concepts_concept_id_codes_get
Expand Down Expand Up @@ -372,6 +404,91 @@ paths:
description: No subgraph for specified relationship types.
'5XX':
description: Unknown error
/concepts/paths/subgraph/sequential:
get:
operationId: concepts_paths_subgraph_sequential_get
summary: Returns a set of paths with a specified sequence of relationships, subject to constraints specified in parameters.
parameters:
- name: relsequence
in: query
required: true
description: A sequential set of relationships that specifies the pattern of relationships in the paths in the subgraph. Each element in the set should be in format SAB:relationship type. SAB corresponds to the source in which the relationship was asserted--e.g., "NCI:is_marked_by_gene_product" corresponds to the "is_marked_by_gene_product" relationship asserted in NCI. The set can be specified with a list of values delimited with commas (e.g., ?relsequence=SAB1:relationshiptype1,SAB2:relationshiptype2) or with individual key-value pairs (e.g., ?relsequence=SAB1:relationship_type1&relsequence=SAB2:relationshiptype2).
schema:
type: string
example: NCI:is_marked_by_gene_product,NCI:3Agene_product_encoded_by_gene
- name: skip
in: query
required: false
description: the number of paths to skip in the returned set. The value must be non-negative. The default value is 0.
schema:
type: string
example: 0
- name: limit
in: query
required: false
description: the maximum number of paths to return. The value must be non-negative. The default value is the maximum number of rows specified by configuration.
schema:
type: string
example: 10
responses:
'200':
description: Return the graph of all Concepts in paths with specified sequential relationships, subject to constraints. The schema of the response corresponds to the Table result frame of the endpoint query in the neo4j browser.
content:
application/json:
schema:
$ref: '#/components/schemas/ConceptPaths'
'400':
description: invalid parameter name; missing required parameter name; non-numeric parameter value; negative value; mindepth > maxdepth
'404':
description: No Concepts with paths originating from the specified concept and set of parameters. Includes responses from queries with execution times that exceed the maximum.
'5XX':
description: Unknown error
/concepts/{concept_id}/paths/subgraph/sequential:
get:
operationId: concepts_concept_id_paths_subgraph_sequential_get
summary: Returns a set of paths that originate from the specified Concept with a specified sequence of relationships, subject to constraints specified in parameters.
parameters:
- name: concept_id
in: path
required: true
description: The identifier (also known as Concept Unique Identifier, or CUI) for the Concept from which paths originate.
schema:
type: string
example: C0006142
- name: relsequence
in: query
required: true
description: A sequential set of relationships that specifies the pattern of relationships in the paths in the subgraph. Each element in the set should be in format SAB:relationship type. SAB corresponds to the source in which the relationship was asserted--e.g., "NCI:is_marked_by_gene_product" corresponds to the "is_marked_by_gene_product" relationship asserted in NCI. The set can be specified with a list of values delimited with commas (e.g., ?relsequence=SAB1:relationshiptype1,SAB2:relationshiptype2) or with individual key-value pairs (e.g., ?relsequence=SAB1:relationship_type1&relsequence=SAB2:relationshiptype2).
schema:
type: string
example: NCI:is_marked_by_gene_product,NCI:3Agene_product_encoded_by_gene
- name: skip
in: query
required: false
description: the number of paths to skip in the returned set. The value must be non-negative. The default value is 0.
schema:
type: string
example: 0
- name: limit
in: query
required: false
description: the maximum number of paths to return. The value must be non-negative. The default value is the maximum number of rows specified by configuration.
schema:
type: string
example: 10
responses:
'200':
description: Return the graph of all Concepts in paths that originate with the specified Concept node with specified sequential relationships, subject to constraints. The schema of the response corresponds to the Table result frame of the endpoint query in the neo4j browser.
content:
application/json:
schema:
$ref: '#/components/schemas/ConceptPaths'
'400':
description: invalid parameter name; missing required parameter name; non-numeric parameter value; negative value; mindepth > maxdepth
'404':
description: No Concepts with paths originating from the specified concept and set of parameters. Includes responses from queries with execution times that exceed the maximum.
'5XX':
description: Unknown error
/concepts/{origin_concept_id}/paths/shortestpath/{terminus_concept_id}:
get:
operationId: concepts_shortestpath_get
Expand Down Expand Up @@ -909,6 +1026,28 @@ components:
description: an identifier for the source of the code. Corresponds to the Source Abbreviaion (SAB) of a Code in the UMLS.
type: string
example: CCF
CodeTerms: # Schema name
description: Term nodes that link to the specified Code.
type: object
properties:
code:
description: the Code to which Terms link
type: string
example: SNOMEDCT_US:254837009
terms:
description: terms linked to the code, subject to parameters
type: array
items:
type: object
properties:
term:
type: string
description: term string
example: Malignant tumor of breast
term_type:
type: string
description: term type for the term - an acronym of usually two or three letters
example: PT
ConceptDetail: # Schema name
type: object
description: Information on a Concept node.
Expand Down
29 changes: 28 additions & 1 deletion src/ubkg_api/common_routes/codes/codes_controller.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from flask import Blueprint, jsonify, current_app, make_response #, request
from ..common_neo4j_logic import codes_code_id_codes_get_logic, codes_code_id_concepts_get_logic
from ..common_neo4j_logic import (codes_code_id_codes_get_logic, codes_code_id_concepts_get_logic,
codes_code_id_terms_get_logic)
from utils.http_error_string import get_404_error_string, validate_query_parameter_names, \
validate_parameter_value_in_enum
from utils.http_parameter import parameter_as_list
Expand Down Expand Up @@ -54,3 +55,29 @@ def codes_code_id_concepts_get(code_id):
return make_response(err, 404)

return jsonify(result)
@codes_blueprint.route('/<code_id>/terms', methods=['GET'])
def codes_code_id_terms_get(code_id):
"""
Returns an array of terms linked to the specified Code node.
:param code_id: the code identifier
"""

# Validate parameters.
# Validate sab parameter.
err = validate_query_parameter_names(parameter_name_list=['term_type'])
if err != 'ok':
return make_response(err, 400)

# Obtain a list of sab parameter values.
term_type = parameter_as_list(param_name='term_type')

neo4j_instance = current_app.neo4jConnectionHelper.instance()
result = codes_code_id_terms_get_logic(neo4j_instance, code_id=code_id,term_type=term_type)

if result is None or result == []:
# Empty result
err = get_404_error_string(prompt_string='No Terms linked to the Code specified',
custom_request_path=f'CodeID = {code_id}')
return make_response(err, 404)

return jsonify(result)
85 changes: 84 additions & 1 deletion src/ubkg_api/common_routes/common_neo4j_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,5 +1082,88 @@ def sources_get_logic(neo4j_instance, sab=None, context=None) -> dict:
except KeyError:
pass

# The query has a single record.
return source

def codes_code_id_terms_get_logic(neo4j_instance,code_id: str, term_type=None) -> dict:
"""
Obtains information on terms that link to a code.
The return from the query is simple, and there is no need for a model class.
:param neo4j_instance: neo4j connection
:param code_id: a UBKG Code in format SAB:CodeId
:param term_type: an optional list of acronyms for a code type
"""
terms: [dict] = []

# Load and parameterize query.
querytxt = loadquerystring('code_code_id_terms.cypher')

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

# Filter by code SAB.
if len(term_type) == 0:
querytxt = querytxt.replace('$termtype_filter', '')
else:
querytxt = querytxt.replace('$termtype_filter', f" AND TYPE(r) IN {term_type}")

# Set timeout for query based on value in app.cfg.
query = neo4j.Query(text=querytxt, timeout=neo4j_instance.timeout)
with neo4j_instance.driver.session() as session:
recds: neo4j.Result = session.run(query)
for record in recds:
term = record.get('response')
try:
terms.append(term)

except KeyError:
pass

# The query has either zero records or one record
if len(terms) == 1:
return term
else:
return terms

def concepts_subgraph_sequential_get_logic(neo4j_instance, startCUI=None, reltypes=None, relsabs=None, skip=None,
limit=None) -> List[ConceptGraph]:

"""
Obtains a subset of paths that originate from the concept with CUI=startCUI, in a sequence of relationships
specified by reltypes and relsab, limited by skip and limit parameters.
:param neo4j_instance: UBKG connection
:param startCUI: CUI of concept from which to expand paths
:param reltypes: sequential list of relationship types
:param relsabs: sequential list of relationship SABs
:param skip: paths to skip
:param limit: maximum number of paths to return
For example, reltypes=["isa","part_of"] and relsabs=["UBERON","PATO"] results in a query for paths that match
the pattern
(startCUI: Concept)-[r1:isa]-(c1:Concept)-[r2:has_part]->(c2:Concept)
where r1.SAB = "UBERON" and r2.SAB="PATO"
"""

conceptgraphs: [ConceptGraph] = []
conceptgraph: ConceptGraph = {}

# Load query string and associate parameter values to variables.
querytxt = loadquerystring(filename='concepts_subgraph_sequential.cypher')
querytxt = querytxt.replace('$startCUI', f'"{startCUI}"')

sabjoin = format_list_for_query(listquery=reltypes, doublequote=True)
querytxt = querytxt.replace('$reltypes', sabjoin)
reljoin = format_list_for_query(listquery=relsabs, doublequote=True)
querytxt = querytxt.replace('$relsabs', reljoin)
querytxt = querytxt.replace('$skip', str(skip))
querytxt = querytxt.replace('$limit', str(limit))

# Set timeout for query based on value in app.cfg.
query = neo4j.Query(text=querytxt, timeout=neo4j_instance.timeout)

# MAY 2024 - bug fix - changed argument from querytxt to query
return get_graph(neo4j_instance, query=query)
92 changes: 91 additions & 1 deletion src/ubkg_api/common_routes/concepts/concepts_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..common_neo4j_logic import concepts_concept_id_codes_get_logic, concepts_concept_id_concepts_get_logic,\
concepts_concept_id_definitions_get_logic, concepts_expand_get_logic,\
concepts_shortestpath_get_logic, concepts_trees_get_logic, concepts_subgraph_get_logic, \
concepts_identfier_node_get_logic
concepts_identfier_node_get_logic, concepts_subgraph_sequential_get_logic
# Functions to validate query parameters
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, \
Expand Down Expand Up @@ -423,3 +423,93 @@ def concepts_concept_identifier_nodes_get(search):

dict_result = {'nodeobjects': result}
return jsonify(dict_result)

@concepts_blueprint.route('/paths/subgraph/sequential', methods=['GET'])
def concepts_paths_subgraphs_sequential_get_endpoint():
return concepts_paths_subraphs_sequential_get(concept_id=None)

@concepts_blueprint.route('<concept_id>/paths/subgraph/sequential', methods=['GET'])
def concepts_paths_subgraphs_name_sequential_get_endpoint(concept_id):
return concepts_paths_subraphs_sequential_get(concept_id=concept_id)

def concepts_paths_subraphs_sequential_get(concept_id=None):

"""
Returns the set of paths that begins with the concept <concept_id> and has relationships in a specified
sequence.
If no concept_id is specified, then return all paths that begin with the first specified relationship.
Response is in neo4j graph format ({nodes, paths, edges}).
The relsequence request parameter is an ordered list that specifies a sequence of relationships in a path.
The format of each element in relsequence is <SAB>:<relationship type>.
For example, ['UBERON:isa','PATO:has_part'] specifies the set of paths that start from the concept with CUI
<concept_id> with relationships that match the pattern
(concept_id: Concept)-[r1:isa]-(c1:Concept)-[r2:has_part]->(c2:Concept)
in which r1.SAB = 'UBERON' and r2.SAB = 'PATO'
"""

neo4j_instance = current_app.neo4jConnectionHelper.instance()

# Validate parameters.
# Check for invalid parameter names.
err = validate_query_parameter_names(parameter_name_list=['relsequence', 'skip', 'limit'])
if err != 'ok':
return make_response(err, 400)

# Check for required parameters.
err = validate_required_parameters(required_parameter_list=['relsequence'])
if err != 'ok':
return make_response(err, 400)

# Check that the non-default skip is non-negative.
skip = request.args.get('skip')
err = validate_parameter_is_nonnegative(param_name='skip', param_value=skip)
if err != 'ok':
return make_response(err, 400)

# Set default mininum.
skip = set_default_minimum(param_value=skip, default=0)

# Check that non-default limit is non-negative.
limit = request.args.get('limit')
err = validate_parameter_is_nonnegative(param_name='limit', param_value=limit)
if err != 'ok':
return make_response(err, 400)
# Set default row limit, based on the app configuration.
limit = set_default_maximum(param_value=limit, default=neo4j_instance.rowlimit)

# Get remaining parameter values from the path or query string.
relsequence = parameter_as_list(param_name='relsequence')
reltypes = []
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)

relsabs.append(rs.split(':')[0].upper())
reltypes.append(rs.split(':')[1])


result = concepts_subgraph_sequential_get_logic(neo4j_instance, startCUI=concept_id, reltypes=reltypes, relsabs=relsabs,
skip=skip, limit=limit)

iserr = result is None or result == {}

if iserr:
err = get_404_error_string(prompt_string=f"No Concepts in paths with specified parameters",
custom_request_path=f"startCUI='{concept_id}'",
timeout=neo4j_instance.timeout)
return make_response(err, 404)

# Limit the size of the payload, based on the app configuration.
err = check_payload_size(payload=result, max_payload_size=neo4j_instance.payloadlimit)
if err != "ok":
return make_response(err, 400)

return jsonify(result)
Loading

0 comments on commit ceafafb

Please sign in to comment.