Skip to content
Draft
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ The request body consists of JSON data structure with two top level tag:

### Running the Web Service Directly

First install the web-specific dependencies, if not already done (e.g. by **`--all-extras`** above):
First, install the web-specific dependencies, if not already done:

```bash
poetry install --with web
Expand Down Expand Up @@ -286,7 +286,7 @@ To stop the service:
docker-compose down
```

Of course, the above docker-compose commands may be customized by the user to suit their needs. Note that the docker implementation assumes the use of uvicorn
Of course, the user may customize the above docker-compose commands to suit their needs. Note that the docker implementation assumes the use of uvicorn

## Change Log

Expand Down
2,669 changes: 1,812 additions & 857 deletions poetry.lock

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "reasoner-validator"
version = "5.0.0"
version = "5.0.1"
description = "Validation tools for Reasoner API"
authors = [
"Richard Bruskiewich <[email protected]>",
Expand Down Expand Up @@ -40,9 +40,10 @@ include = [
]

[tool.poetry.dependencies]
python = ">=3.9,<3.13"
bmt = "^1.4.5"
bioregistry = "^0.12.33"
python = ">=3.10,<3.13"
bmt = "==1.4.6"
biolink-model = "^4.3.6"
bioregistry = "^0.12.46"
jsonschema = "^4.24.0"
dictdiffer = "^0.9.0"
PyYAML = "^6.0"
Expand All @@ -57,9 +58,9 @@ poetry-core = "^2.1.3"

[tool.poetry.group.dev.dependencies]
setuptools = "^78.1.1"
pytest = "^7.4.4"
pytest-cov = "^4.1.0"
pytest-asyncio = "^0.23.3"
pytest = "^8.4.1"
pytest-cov = "^7.0.0"
pytest-asyncio = "^1.3.0"

[tool.poetry.group.docs.dependencies]
numpydoc = "^1.5.0"
Expand Down
68 changes: 34 additions & 34 deletions reasoner_validator/biolink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from typing import Optional, Any, Dict, List, Tuple
from numbers import Number
from functools import lru_cache
from importlib.resources import files
import re
from urllib.error import HTTPError
from pprint import PrettyPrinter

from linkml_runtime.utils.schemaview import SchemaView

from bmt import Toolkit, utils
from linkml_runtime.linkml_model import ClassDefinition, Element
Expand Down Expand Up @@ -63,8 +65,9 @@ def get_reference(curie: str) -> Optional[str]:
return reference


def _get_biolink_model_schema(biolink_version: Optional[str] = None) -> Optional[str]:
# Get Biolink Model Schema
@lru_cache(maxsize=3)
def get_biolink_schema(biolink_version: Optional[str] = None) -> SchemaView:
"""Get cached Biolink schema, loading it if not already cached."""
if biolink_version:
try:
svm = SemVer.from_string(biolink_version)
Expand All @@ -84,49 +87,46 @@ def _get_biolink_model_schema(biolink_version: Optional[str] = None) -> Optional
biolink_version = "v" + str(svm)
else:
biolink_version = str(svm)
schema = f"https://raw.githubusercontent.com/biolink/biolink-model/{biolink_version}/biolink-model.yaml"
return schema

schema_view = SchemaView(f"https://raw.githubusercontent.com/biolink/biolink-model/{biolink_version}/biolink-model.yaml")
logger.debug("Successfully loaded user-specified Biolink schema from URL")
return schema_view
else:
return None
# Try to load from the local Biolink Model package
# from the locally installed distribution
try:
schema_path = files("biolink_model.schema").joinpath("biolink_model.yaml")
schema_view = SchemaView(str(schema_path))
logger.debug("Successfully loaded locally-specified Biolink schema from local file")
return schema_view
except Exception as e:
logger.warning(f"Failed to load local Biolink schema: {e}")
# Fallback to loading from official URL
schema_view = SchemaView("https://w3id.org/biolink/biolink-model.yaml")
logger.debug("Successfully loaded current release Biolink schema from URL")
return schema_view


# At any given time, only a modest number of Biolink Model versions
# are expected to be active targets for SRI Test validations?
@lru_cache(maxsize=3)
def get_biolink_model_toolkit(biolink_version: Optional[str] = None) -> Toolkit:
"""
Return Biolink Model Toolkit corresponding to specified version of the model (Default: current 'latest' version).
def get_current_biolink_version(biolink_version: Optional[str] = None) -> str:
return get_biolink_schema(biolink_version).schema.version

:param biolink_version: Optional[str], caller specified Biolink Model version (default: None)
:type biolink_version: Optional[str] or None
:return: Biolink Model Toolkit.
:rtype: Toolkit

"""
if biolink_version:
# If errors occur while instantiating non-default Toolkit;
# then log the error but just use default as a workaround?
try:
biolink_schema = _get_biolink_model_schema(biolink_version=biolink_version)
bmt = Toolkit(biolink_schema)
return bmt
except (TypeError, HTTPError) as ex:
logger.error(str(ex))

# 'latest' default Biolink Model
# version of given Toolkit returned
return Toolkit()
@lru_cache(maxsize=3)
def get_biolink_model_toolkit(biolink_version: Optional[str] = None) -> Toolkit:
"""Get a Biolink Model Toolkit configured with the expected project Biolink Model schema."""
return Toolkit(schema=get_biolink_schema(biolink_version).schema)


class BMTWrapper:
def __init__(self, biolink_version: Optional[str] = None):
self.bmt: Optional[Toolkit] = None
self.default_biolink: bool = False
if biolink_version != "suppress":
# Here, the Biolink Model version is validated, and the relevant Toolkit pulled.
# Here, the Biolink Model version is validated,
# and the relevant Toolkit pulled.
if biolink_version is None:
self.default_biolink = True
self.bmt = get_biolink_model_toolkit(biolink_version=biolink_version)
self.bmt = get_biolink_model_toolkit(biolink_version)
self.biolink_version = self.bmt.get_model_version()
else:
self.biolink_version = "suppress"
Expand Down Expand Up @@ -205,14 +205,14 @@ def __init__(
):
"""
Biolink Validator constructor.
:param default_test: Optional[str] = None, initial default test context of the BiolinkValidator messages
:param default_target: Optional[str] = None, initial default target context of the BiolinkValidator,
:param default_test: Optional[str] = None, initial default test context of the BiolinkValidator messages
:param default_target: Optional[str] = None, initial default target context of the BiolinkValidator,
also used as a prefix in validation messages.
:param trapi_version: Optional[str], caller specified Biolink Model version (default: None, use TRAPI 'latest')
:param biolink_version: Optional[str], caller specified Biolink Model version (default: None, use BMT 'latest')
Note that a special biolink_version value string "suppress" disables full Biolink Model
validation by the validator (i.e. limits validation to superficial validation).
:param target_provenance: Optional[Dict[str,str]], Dictionary of context ARA and KP for provenance validation
:param target_provenance: Optional[Dict[str, str]], Dictionary of context ARA and KP for provenance validation
:param strict_validation: Optional[bool] = None, if True, some tests validate as 'error'; False, simply issues
'info' message; A value of 'None' uses the default value for specific graph contexts.

Expand Down
19 changes: 8 additions & 11 deletions reasoner_validator/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,9 @@ def label(self) -> str:
class ValidationReporter:
"""
General wrapper for managing validation status messages: information, warnings, errors and 'critical' (errors).
The TRAPI version and Biolink Model versions are also tracked for convenience at this abstract level
The TRAPI version and Biolink Model versions are also tracked for convenience at this abstract level,
although their application is within specific pertinent subclasses.
"""

# Default major version resolves to latest TRAPI OpenAPI release,
# specifically 1.3.0, as of September 1st, 2022
DEFAULT_TRAPI_VERSION = "1"

def __init__(
Expand All @@ -67,11 +64,11 @@ def __init__(
strict_validation: Optional[bool] = None
):
"""
:param default_test: Optional[str] = None, initial default test context of the Validator messages
:param default_test: Optional[str] = None, initial default test context of the Validator messages
Default "global" if not provided.
:param default_target: Optional[str] = None, initial default target context of the Validator,
:param default_target: Optional[str] = None, initial default target context of the Validator,
also used as a prefix in validation messages. Default "global" if not provided.
:param strict_validation: Optional[bool] = None, if True, some tests validate as 'error'; False, simply issues
:param strict_validation: Optional[bool] = None, if True, some tests validate as 'error'; False, simply issues
'info' message; A value of 'None' uses the default value for specific graph contexts.
"""
self.default_test: str = default_test if default_test else "Test"
Expand Down Expand Up @@ -112,7 +109,7 @@ def get_default_target(self) -> str:
def is_strict_validation(self, graph_type: TRAPIGraphType, ignore_graph_type: bool = False) -> bool:
"""
Predicate to test if strict validation is to be applied. If the internal
'strict_validation' flag is not set (i.e. None), then graph_type is
'strict_validation' flag is not set (i.e., None), then graph_type is
to resolve strictness based on TRAPI graph type context.

:param graph_type: TRAPIGraphType, type of TRAPI graph component being validated
Expand Down Expand Up @@ -632,8 +629,8 @@ def report_header(self, title: Optional[str] = "", compact_format: bool = True)
:param title: Optional[str], if the title is None, then only the 'reasoner-validator' version is printed out
in the header. If the title is an empty string (the default), then 'Validation Report' used.
:param compact_format: bool, whether to print the header in compact format (default: True).
Extra line feeds are otherwise provided to provide space around header
and control characters are output to underline the header.
Extra line feeds are otherwise provided to provide space around the header.
Control characters are also output to underline the header.
:return: str, generated header.
"""
header: str = ""
Expand All @@ -657,7 +654,7 @@ def report_header(self, title: Optional[str] = "", compact_format: bool = True)
try:
# This only works if the reasoner-validator
# is locally installed as a package into the environment
version = f" version '{metadata.version('reasoner-validator')}'"
version = f"version '{metadata.version('reasoner-validator')}'"
except metadata.PackageNotFoundError:
#
version = ""
Expand Down
10 changes: 5 additions & 5 deletions reasoner_validator/trapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,11 @@ def __init__(
):
"""
TRAPI Validator constructor.
:param default_test: Optional[str] = None, initial default test context of the TRAPISchemaValidator messages
:param default_target: Optional[str] = None, initial default target context of the TRAPISchemaValidator,
also used as a prefix in validation messages.
:param trapi_version: str, version of component to validate against
:param strict_validation: Optional[bool] = None, if True, some tests validate as 'error'; False, simply issues
:param default_test: Optional[str] = None, initial default test context of the TRAPISchemaValidator messages
:param default_target: Optional[str] = None, initial default target context of the TRAPISchemaValidator,
also used as a prefix in validation messages.
:param trapi_version: Str, version of the component to validate against
:param strict_validation: Optional[bool] = None, if True, some tests validate as 'error'; False, simply issues
'info' message; A value of 'None' uses the default value for specific graph contexts.

"""
Expand Down
2 changes: 1 addition & 1 deletion reasoner_validator/versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def get_latest_version(release_tag: Optional[str]) -> Optional[str]:
"""
Return the latest TRAPI version corresponding to the release tag given.
Note that if the release tag looks like a YAML file, then it is assumed
to be a direct schema specification. If a Git branch name in the schema
to be a direct schema specification. If a Git branch name is in the schema
repository, the branch name is also passed on.

:param release_tag: (possibly partial) SemVer string, Git branch name,
Expand Down
15 changes: 9 additions & 6 deletions scripts/trapi_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ async def direct_trapi_request(
print(f"Submitting TRAPI Response file to endpoint '{endpoint}'")

# Make the TRAPI call to the Case targeted
# endpoint with specified TRAPI request
# endpoint with a specified TRAPI request
result = await call_trapi(endpoint, trapi_request)

# Was the web service (HTTP) call successful?
Expand Down Expand Up @@ -308,6 +308,7 @@ def main():

# Retrieve the TRAPI JSON Response result to be validated
if args.endpoint:
# Response directly retrieved from a target endpoint
if args.local_request:
# The internal TRAPI call is a coroutine
# from which the results in a global variable
Expand All @@ -328,18 +329,21 @@ def main():

elif args.arax_id:
retrieve_arax_result(args.arax_id)

elif args.ars_response_id:
if isfile(args.ars_response_id):
# The response identifier can just be a local file...
with open(args.ars_response_id) as infile:
trapi_response = json.load(infile)
else:
# ... unless, it is an ARS PK
# ... unless it is an ARS PK
retrieve_ars_result(response_id=args.ars_response_id, verbose=args.verbose)

else:
print("Need to specify either an --endpoint/--local_request or a --response_id input argument to proceed!")
print(
"Need to specify either an --endpoint plus --local_request, "
"--arax_id or --ars_response_id input argument to proceed!"
)

if not trapi_response:
print("TRAPI Response JSON is unavailable for validation?")
Expand All @@ -348,9 +352,8 @@ def main():
# OK, we have something to validate here...
validator.check_compliance_of_trapi_response(response=trapi_response)

# Print out the outcome of the main validation of the TRAPI Response
# Print out the outcome of the TRAPI Response main validation
validation_report(validator, args)


if __name__ == "__main__":
main()
Loading