diff --git a/dd-api-spec.yaml b/dd-api-spec.yaml index dd0d2c0..31069b4 100644 --- a/dd-api-spec.yaml +++ b/dd-api-spec.yaml @@ -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 @@ -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 @@ -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. diff --git a/src/ubkg_api/common_routes/codes/codes_controller.py b/src/ubkg_api/common_routes/codes/codes_controller.py index e224133..373204f 100644 --- a/src/ubkg_api/common_routes/codes/codes_controller.py +++ b/src/ubkg_api/common_routes/codes/codes_controller.py @@ -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 @@ -54,3 +55,29 @@ def codes_code_id_concepts_get(code_id): return make_response(err, 404) return jsonify(result) +@codes_blueprint.route('//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) \ No newline at end of file diff --git a/src/ubkg_api/common_routes/common_neo4j_logic.py b/src/ubkg_api/common_routes/common_neo4j_logic.py index 2978695..52b90c6 100644 --- a/src/ubkg_api/common_routes/common_neo4j_logic.py +++ b/src/ubkg_api/common_routes/common_neo4j_logic.py @@ -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) \ No newline at end of file diff --git a/src/ubkg_api/common_routes/concepts/concepts_controller.py b/src/ubkg_api/common_routes/concepts/concepts_controller.py index 83c87b5..fe3efda 100644 --- a/src/ubkg_api/common_routes/concepts/concepts_controller.py +++ b/src/ubkg_api/common_routes/concepts/concepts_controller.py @@ -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, \ @@ -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('/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 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 :. + For example, ['UBERON:isa','PATO:has_part'] specifies the set of paths that start from the concept with CUI + 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 :' + 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) \ No newline at end of file diff --git a/src/ubkg_api/cypher/code_code_id_terms.cypher b/src/ubkg_api/cypher/code_code_id_terms.cypher new file mode 100644 index 0000000..d2854a4 --- /dev/null +++ b/src/ubkg_api/cypher/code_code_id_terms.cypher @@ -0,0 +1,19 @@ +// Used in the codes/{code_id}/terms endpoint +// This query returns the list of Term nodes +// that link to the specified code. +// The list can be filtered to return only those terms of a particular "term type"--which, in the +// UBKG, corresponds to the type of relationship between the term and the code. + +// The function that loads this query will replace values for code_id and termtype_filter. + +CALL +{ + WITH $code_id AS query + MATCH (p:Concept)-[:CODE]->(c:Code)-[r]->(t:Term) + WHERE c.CodeID = query + AND r.CUI = p.CUI + $termtype_filter + RETURN DISTINCT c.CodeID AS code, type(r) as term_type, t.name as term +} +WITH code, COLLECT(DISTINCT {term_type: term_type, term: term}) AS terms +RETURN {code:code,terms:terms} AS response \ No newline at end of file diff --git a/src/ubkg_api/cypher/concepts_subgraph_sequential.cypher b/src/ubkg_api/cypher/concepts_subgraph_sequential.cypher new file mode 100644 index 0000000..419d4a2 --- /dev/null +++ b/src/ubkg_api/cypher/concepts_subgraph_sequential.cypher @@ -0,0 +1,83 @@ +// Used by the concepts/paths/subgraph/sequential endpoint. +// Find all paths that start with Concept associated with the specified code and have relationships that are defined by specified SABs +// in the specified sequence. +// For example, the query will return all paths that start with the concept linked to UBERON:0004548 (left eye) and then have relationships: +// 1st level - "isa" relationship from UBERON +// 2nd level - "has_part" relationship from PATO + +// SELECTION PARAMETERS +// Supplied by the calling function. +// 1. CUI corresponding to the starting concept of the expansion. +// If no CUI was supplied, query for all CUIs that have the first relationship. +WITH $startCUI AS initCUI, +// 2. Sequence of relationship types +//['isa','only_in_taxon','has_part'] as reltypes, +[$reltypes] AS reltypes, +// 3. Sequence of relationship SABs +//['CL','MP','EFO'] as relsabs, +[$relsabs] AS relsabs + +// SELECTION OF STARTING CUIS +// Obtain either the CUI provided by the parameter OR the CUIs for all concepts that have the first relationship +// of the sequence. +CALL{ + WITH initCUI,reltypes, relsabs + MATCH (cStart:Concept)-[rStart]->(c1:Concept) + WHERE TYPE(rStart) = reltypes[0] + AND rStart.SAB = relsabs[0] + AND CASE WHEN initCUI <> "None" THEN cStart.CUI=initCUI ELSE 1=1 END + RETURN DISTINCT cStart.CUI AS startCUI + ORDER BY cStart.CUI +} + +// FILTERING +// Two independent filters are required: +// 1. sequential relationship types (e.g., ["isa","has_part"]) +// 2. sequential SAB properties (e.g. ["UBERON","PATO"]) + +// FIRST FILTER: sequence of relationship types. +// Expand from the starting concept, obtaining only those paths that exactly match the specified sequence of relationship types. +// Because the relationshipFilter specifies a sequence, relevant paths have length equal to the number of specified sequential relationships. +CALL +{ +WITH startCUI,reltypes +MATCH (cStart:Concept {CUI:startCUI}) +CALL apoc.path.expandConfig(cStart, + {relationshipFilter:apoc.text.join([x IN reltypes | x], ">,"), beginSequenceAtStart: true,minLevel: 1, maxLevel: size(reltypes)}) +YIELD path +return path +} +WITH path,reltypes,relsabs +WHERE length(path) = size(reltypes) + +// SECOND FILTER: sequence of relationship SABs. +// Filter paths to those in which the SAB properties of relationships occur in the same order as the the specified sequence. +CALL +{ +WITH path,relsabs +UNWIND(path) as path_check +UNWIND(relationships(path_check)) AS path_check_rels +RETURN COLLECT(path_check_rels.SAB) AS path_rel_sabs +} + +WITH path,path_rel_sabs +WHERE path_rel_sabs=relsabs +WITH path +SKIP $skip LIMIT $limit +//For the filtered set of paths, + +// 1. Obtain an "edges" object with information on all relationships in all paths. +// 2. Obtain a "paths" object with path information on all paths. + +UNWIND(relationships(path)) AS r +WITH path,collect({type:type(r),SAB:r.SAB,source:startNode(r).CUI,target:endNode(r).CUI}) AS path_r +WITH collect(path) as paths, apoc.coll.toSet(apoc.coll.flatten(COLLECT(path_r))) AS edges + +// 3. Obtain a "nodes" object for all Concept nodes in all paths +UNWIND(paths) AS path +UNWIND(nodes(path)) AS n +// Obtain preferred terms for Concept nodes. +OPTIONAL MATCH (n)-[:PREF_TERM]->(t:Term) + +WITH paths,edges,collect(DISTINCT{id:n.CUI,name:t.name}) AS nodes +RETURN {nodes:nodes, paths:paths, edges:edges} AS graph diff --git a/tests/test_dd_api.sh b/tests/test_dd_api.sh index 58beb09..b7196ab 100755 --- a/tests/test_dd_api.sh +++ b/tests/test_dd_api.sh @@ -58,6 +58,7 @@ case "$env" in esac # UBKG_URL=$UBKG_URL_LOCAL +echo "DATA DISTILLERY API" | tee test.out echo "Using UBKG at: ${UBKG_URL}" | tee test.out echo "Only the first 60 characters of output from HTTP 200 returns displayed." @@ -130,6 +131,33 @@ curl --request GET \ echo | tee -a test.out echo | tee -a test.out +#-------------------------------------------- +echo "TESTS FOR: codes//terms" | tee -a test.out +echo "SIGNATURE: /codes//terms" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "1. codes/SNOMEDCT_US%3A254837009X/terms => no match; should return custom 404" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/codes/SNOMEDCT_US%3A254837009X/terms" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "2. codes/SNOMEDCT_US%3A254837009/terms?term_typex=PT => invalid parameter; should return custom 404" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/codes/SNOMEDCT_US%3A254837009/terms?term_typex=PT" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "3. codes/SNOMEDCT_US%3A254837009/terms?term_type=PT => valid; should return 200" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/codes/SNOMEDCT_US%3A254837009/terms?term_type=PT" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + #-------------------------------------------- echo "TESTS FOR: concepts//codes GET" | tee -a test.out echo "SIGNATURE: /concepts//codes"| tee -a test.out @@ -283,6 +311,99 @@ curl --request GET \ echo | tee -a test.out echo | tee -a test.out +#-------------------------------------------- +echo "TESTS FOR: concepts//paths/subgraph/sequential GET" | tee -a test.out +echo "SIGNATURE: /conepts//paths/subgraph/sequential?relsequence=&limit=" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "1. concepts/C0006142/paths/subgraph/sequential?test=x => invalid parameter; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?test=x" \ + --header "Accept: application/json" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "2. concepts/C0006142/paths/subgraph/sequential?relsequence=x&skip=0&limit=5 => invalid relsequence format; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=x&skip=0&limit=5" \ + --header "Accept: application/json" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "3. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=X&limit=5 => skip non-numeric; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product&skip=X&limit=5" \ + --header "Accept: application/json" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "4. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=-1&limit=5 => skip negative; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product&skip=X&limit=5" \ + --header "Accept: application/json" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "5. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=0&limit=x => limit non-numeric; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product&skip=0&limit=x" \ + --header "Accept: application/json" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "6. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=0&limit=-1 => limit negative; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=0&limit=-1" \ + --header "Accept: application/json" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "7. concepts/C0006142X/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product,NCI%3Agene_product_encoded_by_gene&skip=0&limit=5 => invalid CUI; should return custom 404" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142X/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product,NCI%3Agene_product_encoded_by_gene&skip=0&limit=5" \ + --header "Accept: application/json" \ + --header "Authorization: UMLS-Key $umlskey" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "8. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product,NCI%3Agene_product_encoded_by_gene&skip=0&limit=5 => valid, with list; should return 200" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product,NCI%3Agene_product_encoded_by_gene&skip=0&limit=5" \ + --header "Accept: application/json" \ + --header "Authorization: UMLS-Key $umlskey" | cut -c1-60 | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "9. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&relsequence=NCI%3Agene_product_encoded_by_gene&skip=0&limit=5 => valid, with individual; should return 200" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&relsequence=NCI%3Agene_product_encoded_by_gene&skip=0&limit=5" \ + --header "Authorization: UMLS-Key $umlskey" | cut -c1-60 | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +#-------------------------------------------- +# The /concepts/paths/subgraph/sequential endpoint uses the same code as the +# /concepts//paths/subgraph/sequential endpoint. + +echo "TEST FOR: concepts/paths/subgraph/sequential GET" | tee -a test.out +echo "SIGNATURE: /conepts/paths/subgraph/sequential?relsequence=&limit=" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "concepts/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&relsequence=NCI%3Agene_product_encoded_by_gene&skip=0&limit=5 => valid, with individual; should return 200" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&relsequence=NCI%3Agene_product_encoded_by_gene&skip=0&limit=5" \ + --header "Authorization: UMLS-Key $umlskey" | cut -c1-60 | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + #-------------------------------------------- echo "TESTS FOR: concepts//nodeobjects GET" | tee -a test.out echo "SIGNATURE: /conepts//nodeobjects" | tee -a test.out diff --git a/tests/test_ubkg_api.sh b/tests/test_ubkg_api.sh index e5cd0e1..44a58bb 100755 --- a/tests/test_ubkg_api.sh +++ b/tests/test_ubkg_api.sh @@ -117,6 +117,34 @@ curl --request GET \ echo | tee -a test.out echo | tee -a test.out +#-------------------------------------------- +echo "TESTS FOR: codes//terms" | tee -a test.out +echo "SIGNATURE: /codes//terms" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "1. codes/SNOMEDCT_US%3A254837009X/terms => no match; should return custom 404" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/codes/SNOMEDCT_US%3A254837009X/terms" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "2. codes/SNOMEDCT_US%3A254837009/terms?term_typex=PT => invalid parameter; should return custom 404" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/codes/SNOMEDCT_US%3A254837009/terms?term_typex=PT" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "3. codes/SNOMEDCT_US%3A254837009/terms?term_type=PT => valid; should return 200" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/codes/SNOMEDCT_US%3A254837009/terms?term_type=PT" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + + #-------------------------------------------- echo "TESTS FOR: concepts//codes GET" | tee -a test.out echo "SIGNATURE: /concepts//codes"| tee -a test.out @@ -254,6 +282,90 @@ curl --request GET \ echo | tee -a test.out echo | tee -a test.out +#-------------------------------------------- +echo "TESTS FOR: concepts//paths/subgraph/sequential GET" | tee -a test.out +echo "SIGNATURE: /conepts//paths/subgraph/sequential?relsequence=&limit=" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "1. concepts/C0006142/paths/subgraph/sequential?test=x => invalid parameter; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?test=x" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "2. concepts/C0006142/paths/subgraph/sequential?relsequence=x&skip=0&limit=5 => invalid relsequence format; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=x&skip=0&limit=5" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "3. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=X&limit=5 => skip non-numeric; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product&skip=X&limit=5" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "4. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=-1&limit=5 => skip negative; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=X&limit=5" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "5. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=0&limit=x => limit non-numeric; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product&skip=0&limit=x" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "6. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&skip=0&limit=-1 => limit negative; should return custom 400" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product&skip=0&limit=-1" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "7. concepts/C0006142X/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product,NCI%3Agene_product_encoded_by_gene&skip=0&limit=5 => invalid CUI; should return custom 404" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142X/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product,NCI:gene_product_encoded_by_gene&skip=0&limit=5" \ + --header "Accept: application/json" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "8. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product,NCI%3Agene_product_encoded_by_gene&skip=0&limit=5 => valid, with list; should return 200" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product,NCI:gene_product_encoded_by_gene&skip=0&limit=5" \ + --header "Accept: application/json" | cut -c1-60 | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +echo "9. concepts/C0006142/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&relsequence=NCI%3Agene_product_encoded_by_gene&skip=0&limit=5 => valid, with individual; should return 200" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/C0006142/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product&relsequence=NCI:gene_product_encoded_by_gene&skip=0&limit=5" \ + --header "Accept: application/json" | cut -c1-60 | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + +#-------------------------------------------- +# The /concepts/paths/subgraph/sequential endpoint uses the same code as the +# /concepts//paths/subgraph/sequential endpoint. +#-------------------------------------------- +echo "TESTS FOR: concepts//paths/subgraph/sequential GET" | tee -a test.out +echo "SIGNATURE: /conepts//paths/subgraph/sequential?relsequence=&limit=" | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out +echo "1. concepts/paths/subgraph/sequential?relsequence=NCI%3Ais_marked_by_gene_product&relsequence=NCI%3Agene_product_encoded_by_gene&skip=0&limit=5 => valid, with individual; should return 200" | tee -a test.out +curl --request GET \ + --url "${UBKG_URL}/concepts/paths/subgraph/sequential?relsequence=NCI:is_marked_by_gene_product&relsequence=NCI:gene_product_encoded_by_gene&skip=0&limit=5" \ + --header "Accept: application/json" | cut -c1-60 | tee -a test.out +echo | tee -a test.out +echo | tee -a test.out + #-------------------------------------------- echo "TESTS FOR: concepts//nodeobjects GET" | tee -a test.out echo "SIGNATURE: /conepts//nodeobjects" | tee -a test.out diff --git a/ubkg-api-spec.yaml b/ubkg-api-spec.yaml index 4196b1e..699411f 100644 --- a/ubkg-api-spec.yaml +++ b/ubkg-api-spec.yaml @@ -76,6 +76,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 @@ -370,6 +402,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 @@ -901,6 +1018,29 @@ 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.