reasoner-validator 4.2.0__tar.gz → 4.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/PKG-INFO +1 -1
  2. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/validation_codes_dictionary.md +6 -0
  3. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/pyproject.toml +1 -1
  4. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/codes.yaml +8 -0
  5. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/utils/__init__.py +6 -6
  6. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/validator.py +99 -48
  7. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_ontology.py +27 -1
  8. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_response_validator.py +42 -3
  9. reasoner_validator-4.2.2/tests/test_utils.py +77 -0
  10. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/LICENSE +0 -0
  11. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/README.md +0 -0
  12. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/.nojekyll +0 -0
  13. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/Makefile +0 -0
  14. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/conf.py +0 -0
  15. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/index.rst +0 -0
  16. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/make.bat +0 -0
  17. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/reasoner_validator.biolink.rst +0 -0
  18. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/reasoner_validator.report.rst +0 -0
  19. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/reasoner_validator.rst +0 -0
  20. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/reasoner_validator.trapi.mapping.rst +0 -0
  21. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/reasoner_validator.trapi.rst +0 -0
  22. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/reasoner_validator.validation_codes.rst +0 -0
  23. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/docs/reasoner_validator.versioning.rst +0 -0
  24. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/__init__.py +0 -0
  25. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/biolink/__init__.py +0 -0
  26. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/biolink/ontology.py +0 -0
  27. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/github.py +0 -0
  28. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/message.py +0 -0
  29. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/report.py +0 -0
  30. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/trapi/__init__.py +0 -0
  31. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/trapi/mapping.py +0 -0
  32. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/utils/http.py +0 -0
  33. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/validation_codes.py +0 -0
  34. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/versioning.py +0 -0
  35. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/reasoner_validator/versions.yaml +0 -0
  36. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/__init__.py +0 -0
  37. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/conftest.py +0 -0
  38. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_biolink_compliance_validation.py +0 -0
  39. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_data/patched_trapi_schema_v1.4.0-beta5.yaml +0 -0
  40. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_semver.py +0 -0
  41. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_trapi_versioning.py +0 -0
  42. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_validate.py +0 -0
  43. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_validation_report.py +0 -0
  44. {reasoner_validator-4.2.0 → reasoner_validator-4.2.2}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reasoner-validator
3
- Version: 4.2.0
3
+ Version: 4.2.2
4
4
  Summary: Validation tools for Reasoner API
5
5
  Home-page: https://github.com/NCATSTranslator
6
6
  License: MIT
@@ -902,6 +902,12 @@
902
902
 
903
903
  **Description:** An empty Knowledge Graph is allowed but merits a boundary response warning?
904
904
 
905
+ ### warning.trapi.response.message.knowledge_graph.node.category.imprecise
906
+
907
+ **Message:** The category of the knowledge graph node matching an input node identifier is more generic than expected.
908
+
909
+ **Context:** identifier, expected_category, observed_categories
910
+
905
911
  ### warning.trapi.response.message.results.empty
906
912
 
907
913
  **Message:** Response returned empty Message.results?
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "reasoner-validator"
3
- version = "4.2.0"
3
+ version = "4.2.2"
4
4
  description = "Validation tools for Reasoner API"
5
5
  authors = [
6
6
  "Richard Bruskiewich <richard.bruskiewich@delphinai.com>",
@@ -694,6 +694,14 @@ warning:
694
694
  empty:
695
695
  $message: "Response returned an empty Message Knowledge Graph?"
696
696
  $description: "An empty Knowledge Graph is allowed but merits a boundary response warning?"
697
+ node:
698
+ category:
699
+ imprecise:
700
+ $message: "The category of the knowledge graph node matching an input node identifier is more generic than expected."
701
+ $context:
702
+ - identifier
703
+ - expected_category
704
+ - observed_categories
697
705
  results:
698
706
  empty:
699
707
  $message: "Response returned empty Message.results?"
@@ -79,13 +79,9 @@ def get_aliases(identifier: str) -> List[str]:
79
79
  # preferred_curie = preferred_id["identifier"]
80
80
  # preferred_name = preferred_id["label"]
81
81
  if "equivalent_identifiers" in clique.keys():
82
+ # Sanity check: returned aliases
83
+ # are all converted to upper case
82
84
  aliases = [entry["identifier"] for entry in clique["equivalent_identifiers"]]
83
- #
84
- # Decided that it was a bad idea to remove
85
- # the original identifier from the aliases...
86
- # aliases.remove(identifier)
87
- #
88
- # print(dumps(aliases, indent=2))
89
85
  else:
90
86
  logging.warning(
91
87
  f"get_aliases(): missing the 'equivalent identifiers' for the '{identifier}' clique?"
@@ -103,6 +99,10 @@ def get_aliases(identifier: str) -> List[str]:
103
99
  # Logging various errors but always
104
100
  # return the identifier as its own alias
105
101
  aliases = [identifier]
102
+ elif identifier not in aliases:
103
+ # If the identifier itself is missing in the aliases, then
104
+ # could just be a letter case mismatch? See if you can correct for this...
105
+ aliases = [identifier if str(i).upper() == identifier.upper() else i for i in aliases]
106
106
 
107
107
  return aliases
108
108
 
@@ -1,4 +1,4 @@
1
- from typing import Optional, Tuple, List, Dict
1
+ from typing import Optional, List, Dict, Set
2
2
 
3
3
  from reasoner_validator.biolink import (
4
4
  BiolinkValidator,
@@ -66,7 +66,7 @@ class TRAPIResponseValidator(BiolinkValidator):
66
66
 
67
67
  def is_trapi_1_4_or_later(self) -> bool:
68
68
  assert self.trapi_version
69
- try: # try block ... Sanity check: in case the trapi_version is somehow invalid?
69
+ try: # try block ... Sanity check: in testcase the trapi_version is somehow invalid?
70
70
  target_major_version: SemVer = \
71
71
  SemVer.from_string(self.trapi_version, core_fields=['major', 'minor'], ext_fields=[])
72
72
  self._is_trapi_1_4_or_later = target_major_version >= TRAPI_1_4_0_SEMVER
@@ -402,14 +402,14 @@ class TRAPIResponseValidator(BiolinkValidator):
402
402
  # there should be in the output
403
403
 
404
404
  # object_ids = [r['node_bindings'][output_node_binding][0]['id'] for r in results_sample]
405
- # if case[output_element] not in object_ids:
405
+ # if testcase[output_element] not in object_ids:
406
406
  # # The 'get_aliases' method uses the Translator NodeNormalizer to check if any of
407
- # # the aliases of the case[output_element] identifier are in the object_ids list
408
- # output_aliases = get_aliases(case[output_element])
407
+ # # the aliases of the testcase[output_element] identifier are in the object_ids list
408
+ # output_aliases = get_aliases(testcase[output_element])
409
409
  # if not any([alias == object_id for alias in output_aliases for object_id in object_ids]):
410
410
  # validator.report(
411
411
  # code=error.results.missing_bindings,
412
- # identifier=case[output_element],
412
+ # identifier=testcase[output_element],
413
413
  # output_node_binding=output_node_binding
414
414
  # )
415
415
  # # data_dump=f"Resolved aliases:\n{','.join(output_aliases)}\n" +
@@ -418,16 +418,39 @@ class TRAPIResponseValidator(BiolinkValidator):
418
418
  # Only 'error' but not 'info' nor 'warning' messages invalidate the overall Message
419
419
  return False if self.has_errors() else True
420
420
 
421
- def case_node_found(self, target: str, identifiers: List[str], case: Dict, nodes: Dict) -> bool:
421
+ def category_matched(self, source_categories: List[str], target_categories: List[str]) -> bool:
422
422
  """
423
- Check for presence of the target identifier,
424
- with expected categories, in the "nodes" catalog.
423
+ For each 'source' Biolink Model category given (list of CURIEs as strings?),
424
+ first get the union set of all parent (ancestral) categories, then check if
425
+ at least one of these categories is matched to the list of target categories.
426
+
427
+ :param source_categories: List[str], list of 'source' categories whose category hierarchy is to be matched.
428
+ :param target_categories: List[str], list of 'target' categories to be matched against 'source'
429
+ (or 'source parent' categories
430
+ :return: bool, True if a category match is found (as described above)
431
+ """
432
+ source_category_set: Set = set()
433
+ # gather all the possible exact and ancestor (parent)
434
+ # categories of source_categories to match...
435
+ for source_category in source_categories:
436
+ source_category_set.update(self.bmt.get_ancestors(source_category, formatted=True, mixin=False))
437
+ # ...then check all the target categories against that source category set
438
+ return any([c in target_categories for c in source_category_set])
439
+
440
+ def testcase_node_found(self, target: str, identifiers: List[str], testcase: Dict, nodes: Dict) -> bool:
441
+ """
442
+ Check for presence of the target identifier, with expected categories, in the "nodes" catalog.
443
+ If the identifier is found, and at least one KG node category is the expected category or a proper subclass
444
+ category of the test testcase category, then return True; if the node is found but the testcase category is not
445
+ the expected category but is a subclass category of the KG node categories (i.e. KG node categories
446
+ are too general), then return False. If the identifier is NOT found in the nodes list or
447
+ there is no overlap in the (expected or parent) testcase and node categories, then return False.
425
448
 
426
449
  :param target: 'subject' or 'object'
427
450
  :param identifiers: List of node (CURIE) identifiers to be checked in the "nodes" catalog
428
- :param case: Dict, full test case (to access the target node 'category')
451
+ :param testcase: Dict, full test testcase (to access the target node 'category')
429
452
  :param nodes: Dict, nodes category indexed by node identifiers.
430
- :return:
453
+ :return: bool, True if feasible node identifier and category match; False otherwise
431
454
  """
432
455
  #
433
456
  # "nodes": {
@@ -440,25 +463,53 @@ class TRAPIResponseValidator(BiolinkValidator):
440
463
  assert target in ["subject", "object"]
441
464
  for identifier in identifiers:
442
465
  if identifier in nodes.keys():
443
- # Found the target node identifier,
444
- # but is the expected category present?
466
+ # Found the target node identifier, but is the expected category present?
467
+ # For this comparison, we assume that a specified node category plus all its
468
+ # parent categories, may be used to match the test testcase specified category.
445
469
  node_details = nodes[identifier]
470
+ test_case_category: str = testcase[f"{target}_category"]
446
471
  if "categories" in node_details:
447
- category = case[f"{target}_category"]
448
- categories: List[str] = self.bmt.get_ancestors(category, formatted=True, mixin=False)
449
- if any([c in node_details["categories"] for c in categories]):
472
+ if self.category_matched(
473
+ source_categories=node_details["categories"],
474
+ target_categories=[test_case_category]
475
+ ):
476
+ # The 'identifier' was present in the list of KG nodes, plus there was a match of the target
477
+ # testcase category either exactly to a specified one of the indicated KG node categories or to
478
+ # an ancestral ("parent") category of one of the node categories. This is a completely regular
479
+ # result. For example, if a KG node return is "biolink:Gene", but the testcase query is only
480
+ # expecting to see a "biolink:BiologicalEntity", however, since the identifier is matched
481
+ # exactly, since the node match is good with greater categorical precision than expected.
482
+ return True
483
+ elif self.category_matched(
484
+ source_categories=[test_case_category],
485
+ target_categories=node_details["categories"]
486
+ ):
487
+ # The 'identifier' was present in the list of KG nodes;
488
+ # however, there was likely only a more general ("parent-level") category match
489
+ # of at least one of the KG node categories, to a parent category of the testcase category
490
+ # (since the 'exact match' match scenario was handled above). This is a less precise
491
+ # node match, hence we issue a warning. For example, maybe the KG node returned is only
492
+ # tagged as "biolink:NamedThing" but we are looking for a testcase with "biolink:Gene".
493
+ # Since the testcase identifier was matched exactly, we assume that the node is matched,
494
+ # but just with less than desired data type categorical precision.
495
+ self.report(
496
+ code="warning.trapi.response.message.knowledge_graph.node.category.imprecise",
497
+ identifier=identifier,
498
+ expected_category=test_case_category,
499
+ observed_categories=",".join(node_details["categories"])
500
+ )
450
501
  return True
451
502
 
452
- # Target node identifier or categories is missing,
453
- # or not annotated with the expected category?
503
+ # Target node identifier is missing from the list of KG nodes or categories is either missing
504
+ # or not annotated with any compatible category, hence, we deem the node effectively missing.
454
505
  return False
455
506
 
456
507
  @staticmethod
457
- def case_edge_bindings(q_edge_ids: Tuple[str], target_edge_id: str, data: Dict) -> bool:
508
+ def testcase_edge_bindings(q_edge_ids: List[str], target_edge_id: str, data: Dict) -> bool:
458
509
  """
459
510
  Check if target query edge id and knowledge graph edge id are in specified edge_bindings.
460
511
  :rtype: object
461
- :param q_edge_ids: Tuple[str], expected query edge identifiers in a matching result
512
+ :param q_edge_ids: List[str], expected query edge identifiers in a matching result
462
513
  :param target_edge_id: str, expected knowledge edge identifier in a matching result
463
514
  :param data: TRAPI version-specific Response context from which the 'edge_bindings' may be retrieved
464
515
  :return: True, if found
@@ -474,7 +525,7 @@ class TRAPIResponseValidator(BiolinkValidator):
474
525
  return True
475
526
  return False
476
527
 
477
- def case_result_found(
528
+ def testcase_result_found(
478
529
  self,
479
530
  query_graph: Dict,
480
531
  subject_id: str,
@@ -483,17 +534,17 @@ class TRAPIResponseValidator(BiolinkValidator):
483
534
  results: List
484
535
  ) -> bool:
485
536
  """
486
- Validate that test case S--P->O edge is found bound to the Results?
537
+ Validate that test testcase S--P->O edge is found bound to the Results?
487
538
  :param query_graph: Dict, query graph to which the results pertain
488
539
  :param subject_id: str, subject node (CURIE) identifier
489
540
  :param object_id: str, subject node (CURIE) identifier
490
541
  :param edge_id: str, subject node (CURIE) identifier
491
542
  :param results: List of (TRAPI-version specific) Result objects
492
- :return: bool, True if case S-P-O edge was found in the results
543
+ :return: bool, True if testcase S-P-O edge was found in the results
493
544
  """
494
- assert query_graph, "case_result_found() encountered an empty query graph"
545
+ assert query_graph, "testcase_result_found() encountered an empty query graph"
495
546
  q_edges: Dict = query_graph["edges"]
496
- q_edge_ids = tuple(q_edges.keys())
547
+ q_edge_ids = list(q_edges.keys())
497
548
 
498
549
  result_found: bool = False
499
550
  result: Dict
@@ -573,7 +624,7 @@ class TRAPIResponseValidator(BiolinkValidator):
573
624
  # the "analysis" key is at least present plus the objects themselves are 'well-formed'
574
625
  analyses: List = result["analyses"]
575
626
  for analysis in analyses:
576
- edge_id_found: bool = self.case_edge_bindings(q_edge_ids, edge_id, analysis)
627
+ edge_id_found: bool = self.testcase_edge_bindings(q_edge_ids, edge_id, analysis)
577
628
  if edge_id_found:
578
629
  break
579
630
 
@@ -599,7 +650,7 @@ class TRAPIResponseValidator(BiolinkValidator):
599
650
  # }
600
651
  # ]
601
652
  #
602
- edge_id_found: bool = self.case_edge_bindings(q_edge_ids, edge_id, result)
653
+ edge_id_found: bool = self.testcase_edge_bindings(q_edge_ids, edge_id, result)
603
654
 
604
655
  if subject_id_found and object_id_found and edge_id_found:
605
656
  result_found = True
@@ -607,9 +658,9 @@ class TRAPIResponseValidator(BiolinkValidator):
607
658
 
608
659
  return result_found
609
660
 
610
- def case_input_found_in_response(
661
+ def testcase_input_found_in_response(
611
662
  self,
612
- case: Dict,
663
+ testcase: Dict,
613
664
  response: Dict
614
665
  ) -> bool:
615
666
  """
@@ -617,17 +668,17 @@ class TRAPIResponseValidator(BiolinkValidator):
617
668
  in the Knowledge Graph of the TRAPI Response Message. This method assumes
618
669
  that the TRAPI response is already generally validated as well-formed.
619
670
 
620
- :param case: Dict, input data test case
671
+ :param testcase: Dict, input data test case
621
672
  :param response: Dict, TRAPI Response whose message ought to contain the test case edge
622
673
  :return: True if test case edge found; False otherwise
623
674
  """
624
675
  # sanity checks
625
- assert case, "case_input_found_in_response(): Empty or missing test case data!"
626
- assert response, "case_input_found_in_response(): Empty or missing TRAPI Response!"
627
- assert "message" in response, "case_input_found_in_response(): TRAPI Response missing its Message component!"
676
+ assert testcase, "testcase_input_found_in_response(): Empty or missing test testcase data!"
677
+ assert response, "testcase_input_found_in_response(): Empty or missing TRAPI Response!"
678
+ assert "message" in response, "testcase_input_found_in_response(): TRAPI Response missing its Message component!"
628
679
 
629
680
  #
630
- # case: Dict parameter contains something like:
681
+ # testcase: Dict parameter contains something like:
631
682
  #
632
683
  # idx: 0,
633
684
  # subject_category: 'biolink:SmallMolecule',
@@ -644,7 +695,7 @@ class TRAPIResponseValidator(BiolinkValidator):
644
695
  # Another sanity check - unlikely to be a problem since
645
696
  # TRAPI schema validation should have picked it up since
646
697
  # the TRAPI Message is "nullable: false" in the schema
647
- assert message, "case_input_found_in_response(): Empty or missing TRAPI message component!"
698
+ assert message, "testcase_input_found_in_response(): Empty or missing TRAPI message component!"
648
699
 
649
700
  message_found: bool = False
650
701
  if "query_graph" not in message:
@@ -723,12 +774,12 @@ class TRAPIResponseValidator(BiolinkValidator):
723
774
  # "CHEBI:6801": {"name": "metformin", "categories": ["biolink:Drug"]}
724
775
  # }
725
776
  #
726
- # Check for case 'subject_id' and 'object_id',
777
+ # Check for testcase 'subject_id' and 'object_id',
727
778
  # with expected categories, in nodes catalog
728
779
  nodes: Dict = knowledge_graph["nodes"]
729
- subject_id = case["subject_id"] if "subject_id" in case else case["subject"]
780
+ subject_id: str = testcase["subject_id"] if "subject_id" in testcase else testcase["subject"]
730
781
  subject_aliases = get_aliases(subject_id)
731
- if not self.case_node_found("subject", subject_aliases, case, nodes):
782
+ if not self.testcase_node_found("subject", subject_aliases, testcase, nodes):
732
783
  self.report(
733
784
  code="error.trapi.response.message.knowledge_graph.node.missing",
734
785
  identifier=subject_id,
@@ -736,9 +787,9 @@ class TRAPIResponseValidator(BiolinkValidator):
736
787
  )
737
788
  return False
738
789
 
739
- object_id = case["object_id"] if "object_id" in case else case["object"]
790
+ object_id: str = testcase["object_id"] if "object_id" in testcase else testcase["object"]
740
791
  object_aliases = get_aliases(object_id)
741
- if not self.case_node_found("object", object_aliases, case, nodes):
792
+ if not self.testcase_node_found("object", object_aliases, testcase, nodes):
742
793
  self.report(
743
794
  code="error.trapi.response.message.knowledge_graph.node.missing",
744
795
  identifier=object_id,
@@ -757,10 +808,10 @@ class TRAPIResponseValidator(BiolinkValidator):
757
808
  # }
758
809
  #
759
810
  # Check in the edges catalog for an edge containing
760
- # the case 'subject_id', 'predicate' and 'object_id'
811
+ # the testcase 'subject_id', 'predicate' and 'object_id'
761
812
  edges: Dict = knowledge_graph["edges"]
762
813
 
763
- predicate = case["predicate"] if "predicate" in case else case["predicate_id"]
814
+ predicate = testcase["predicate"] if "predicate" in testcase else testcase["predicate_id"]
764
815
  predicate_descendants: List[str]
765
816
  inverse_predicate_descendants: List[str] = list() # may sometimes remain empty...
766
817
  if self.validate_biolink():
@@ -769,7 +820,7 @@ class TRAPIResponseValidator(BiolinkValidator):
769
820
  if inverse_predicate:
770
821
  inverse_predicate_descendants = self.bmt.get_descendants(inverse_predicate, formatted=True)
771
822
  else:
772
- # simpler case in which we are ignoring deep Biolink Model validation
823
+ # simpler testcase in which we are ignoring deep Biolink Model validation
773
824
  predicate_descendants = [predicate]
774
825
 
775
826
  edge_id_match: Optional[str] = None
@@ -794,10 +845,10 @@ class TRAPIResponseValidator(BiolinkValidator):
794
845
  break
795
846
 
796
847
  edge_id: str = \
797
- f"{case['idx']}|" +\
798
- f"({case['subject_id']}#{case['subject_category']})" + \
848
+ f"{testcase['idx']}|" +\
849
+ f"({testcase['subject_id']}#{testcase['subject_category']})" + \
799
850
  f"-[{predicate}]->" + \
800
- f"({case['object_id']}#{case['object_category']})"
851
+ f"({testcase['object_id']}#{testcase['object_category']})"
801
852
 
802
853
  if edge_id_match is None:
803
854
  self.report(
@@ -807,13 +858,13 @@ class TRAPIResponseValidator(BiolinkValidator):
807
858
  return False
808
859
 
809
860
  results: List = message["results"]
810
- if not self.case_result_found(query_graph, subject_match, object_match, edge_id_match, results):
861
+ if not self.testcase_result_found(query_graph, subject_match, object_match, edge_id_match, results):
811
862
  self.report(
812
863
  code="error.trapi.response.message.result.missing",
813
864
  identifier=edge_id
814
865
  )
815
866
  return False
816
867
 
817
- # By this point, the case data assumed to be
868
+ # By this point, the testcase data assumed to be
818
869
  # successfully validated in the TRAPI Response?
819
870
  return True
@@ -7,7 +7,8 @@ import pytest
7
7
  from reasoner_validator.utils.http import post_query
8
8
  from reasoner_validator.biolink.ontology import (
9
9
  ONTOLOGY_KP_TRAPI_SERVER,
10
- NODE_NORMALIZER_SERVER
10
+ NODE_NORMALIZER_SERVER,
11
+ get_parent_concept
11
12
  )
12
13
 
13
14
  pytest_plugins = ('pytest_asyncio',)
@@ -77,3 +78,28 @@ async def test_post_query_to_node_normalization(curie: str, category: str):
77
78
  assert "equivalent_identifiers" in result[curie]
78
79
  assert len(result[curie]["equivalent_identifiers"])
79
80
  assert category in result[curie]["type"]
81
+
82
+
83
+ @pytest.mark.parametrize(
84
+ "curie,category,result",
85
+ [
86
+ ( # Query 0 - chemical compounds are NOT in an ontology hierarchy
87
+ "CHEMBL.COMPOUND:CHEMBL2333026",
88
+ "biolink:SmallMolecule",
89
+ None
90
+ ),
91
+ ( # Query 1 - MONDO disease terms are in an ontology term hierarchy
92
+ "MONDO:0011027",
93
+ "biolink:Disease",
94
+ "MONDO:0015967"
95
+ ),
96
+ ( # Query 2 - HP phenotype terms are in an ontology term hierarchy
97
+ "HP:0040068", # "Abnormality of limb bone"
98
+ "biolink:PhenotypicFeature",
99
+ "HP:0000924" # Abnormality of the skeletal system
100
+ )
101
+ ]
102
+ )
103
+ def test_get_parent_concept(curie: str, category: str, result: Optional[str]):
104
+ # Just use default Biolink Model release for this test
105
+ assert get_parent_concept(curie=curie, category=category, biolink_version=None) == result
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Unit tests for the generic (shared) components of the SRI Testing Framework
3
3
  """
4
- from typing import Dict, Optional
4
+ from typing import List, Dict, Optional
5
5
  from sys import stderr
6
6
 
7
7
  import logging
@@ -1205,6 +1205,45 @@ def test_dump_report_of_biolink_model_compliance_of_trapi_response_with_errors(r
1205
1205
  print("\n"+"="*80+"\n", file=stderr)
1206
1206
 
1207
1207
 
1208
+ @pytest.mark.parametrize(
1209
+ "source_categories,target_categories,outcome",
1210
+ [
1211
+ ( # query 0 - empty lists don't have anything to match?
1212
+ [], [], False
1213
+ ),
1214
+ ( # query 1 - exact matches should be true
1215
+ ["biolink:Gene"], ["biolink:Gene"], True
1216
+ ),
1217
+ ( # query 2 - biolink:Protein is **not** directly matched in the
1218
+ # 'source' hierarchy (even though its parents could overlap)
1219
+ ["biolink:Gene"], ["biolink:Drug"], False
1220
+ ),
1221
+ ( # query 3 - but a 'target' category list with at least one exact
1222
+ # match of category to source will also be matched,
1223
+ # even if other target entries don't match
1224
+ ["biolink:Gene"], ["biolink:Gene", "biolink:Drug"], True
1225
+ ),
1226
+ ( # query 4 - 'target' category matches at least one
1227
+ # parent of the list of 'source' categories
1228
+ ["biolink:Gene"], ["biolink:BiologicalEntity"], True
1229
+ ),
1230
+ ( # query 5 - a 'target' category list matches at least
1231
+ # one match to a parent of the list of the
1232
+ # 'source' categories, will be matched,
1233
+ # even if other target entries don't match
1234
+ #
1235
+ ["biolink:Gene"], ["biolink:BiologicalEntity", "biolink:Drug"], True
1236
+ )
1237
+ ]
1238
+ )
1239
+ def test_category_matched(source_categories: List[str], target_categories: List[str], outcome: bool):
1240
+ validator = TRAPIResponseValidator()
1241
+ assert validator.category_matched(
1242
+ source_categories,
1243
+ target_categories,
1244
+ ) is outcome
1245
+
1246
+
1208
1247
  @pytest.mark.parametrize(
1209
1248
  "case,response",
1210
1249
  [
@@ -1215,7 +1254,7 @@ def test_dump_report_of_biolink_model_compliance_of_trapi_response_with_errors(r
1215
1254
  def test_empty_case_input_found_in_response(case, response):
1216
1255
  validator = TRAPIResponseValidator()
1217
1256
  with pytest.raises(AssertionError):
1218
- validator.case_input_found_in_response(case, response)
1257
+ validator.testcase_input_found_in_response(case, response)
1219
1258
 
1220
1259
 
1221
1260
  #
@@ -1458,5 +1497,5 @@ SAMPLE_TEST_INCOMPLETE_RESULTS = [
1458
1497
  )
1459
1498
  def test_case_input_found_in_response(case, response, code):
1460
1499
  validator = TRAPIResponseValidator()
1461
- assert not validator.case_input_found_in_response(case, response) if code else True
1500
+ assert not validator.testcase_input_found_in_response(case, response) if code else True
1462
1501
  check_messages(validator, code)
@@ -0,0 +1,77 @@
1
+ """
2
+ Unit tests against reasoner-validator utils
3
+ """
4
+ from typing import List, Optional
5
+ import pytest
6
+
7
+ from reasoner_validator.utils import is_curie, get_aliases, get_reference
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ "identifier,outcome",
12
+ [
13
+ (None, False),
14
+ ("foo:bar", True),
15
+ ("not-a-curie", False),
16
+ ]
17
+ )
18
+ def test_is_curie(identifier: str, outcome: bool):
19
+ assert is_curie(identifier) is outcome
20
+
21
+
22
+ @pytest.mark.parametrize(
23
+ "identifier",
24
+ [
25
+ None,
26
+ ""
27
+ ]
28
+ )
29
+ def test_get_aliases_of_empty_identifier(identifier):
30
+ with pytest.raises(RuntimeError):
31
+ get_aliases(identifier)
32
+
33
+
34
+ @pytest.mark.parametrize(
35
+ "identifier,one_alias",
36
+ [
37
+ ( # Special test for a CURIE for which
38
+ # NodeNormalization is known to
39
+ # return a lower case namespace
40
+ "ORPHANET:33110",
41
+ "MONDO:0011096"
42
+ ),
43
+ ( # Test that the alias search is also
44
+ # input namespace case-insensitive:
45
+ # A modification of case of the input
46
+ # identifier is still properly returned.
47
+ "Orphanet:33110",
48
+ "MONDO:0011096"
49
+ ),
50
+ ( # only CURIEs can be resolved to aliases
51
+ # but just logs a warning but returns the identifier
52
+ "not-a-curie",
53
+ ""
54
+ ),
55
+ ]
56
+ )
57
+ def test_get_aliases(identifier: str, one_alias: str):
58
+ aliases: List[str] = get_aliases(identifier)
59
+ assert identifier in aliases
60
+ assert one_alias in aliases if one_alias in aliases else True
61
+
62
+
63
+ @pytest.mark.parametrize(
64
+ "identifier,reference",
65
+ [
66
+ ( # Valid CURIE has a reference
67
+ "ORPHANET:33110",
68
+ "33110"
69
+ ),
70
+ ( # Only CURIEs can be resolved to references
71
+ "not-a-curie",
72
+ None
73
+ ),
74
+ ]
75
+ )
76
+ def test_get_reference(identifier: str, reference: str):
77
+ assert get_reference(identifier) == reference