"""
The ``linkml.validator`` package contains the LinkML validation framework.
"""
import os
from pathlib import Path
from typing import Any
from linkml.validator.loaders import default_loader_for_file
from linkml.validator.plugins import JsonschemaValidationPlugin
from linkml.validator.report import ValidationReport
from linkml.validator.validator import Validator
from linkml_runtime.linkml_model import SchemaDefinition
from linkml_runtime.loaders import yaml_loader
def _get_default_validator(
schema: str | dict | Path | SchemaDefinition,
*,
strict: bool = False,
closed: bool = True,
config: str | Path | dict | None = None,
) -> Validator:
"""Build the default :class:`Validator` for a schema.
:param schema: The schema to validate against.
:param strict: Stop after the first validation failure when ``True``.
:param closed: When ``True`` (default), the bundled
:class:`JsonschemaValidationPlugin` rejects undeclared slots
(closed-world validation). Set to ``False`` to allow extra
properties. Ignored if ``config`` provides an explicit ``plugins``
section.
:param config: Optional project-level validation config - either a path
to a ``linkml-validate``-style YAML file or a pre-parsed mapping.
When the mapping contains a ``plugins:`` key, those plugins are
used in place of the default and ``closed`` is ignored.
"""
try:
if isinstance(schema, Path):
schema = str(schema)
if isinstance(schema, dict):
schema = SchemaDefinition(**schema)
elif isinstance(schema, str):
schema = yaml_loader.load(schema, target_class=SchemaDefinition)
if not isinstance(schema, SchemaDefinition):
raise ValueError(f"Schema could not be loaded from {schema}")
except ValueError as e:
raise ValueError(f"Invalid schema: {schema}") from e
validation_plugins = _resolve_default_plugins(closed=closed, config=config)
return Validator(schema, validation_plugins=validation_plugins, strict=strict)
def _resolve_default_plugins(
*,
closed: bool = True,
config: str | Path | dict | None = None,
) -> list:
"""Resolve the list of plugins for :func:`_get_default_validator`.
Honors an optional project-level ``linkml-validate``-style config that
may override the default ``JsonschemaValidationPlugin(closed=...)``.
"""
if config is not None:
# Lazy import to avoid a circular dependency with "linkml.validator.cli".
import yaml
from linkml.validator.cli import Config as _CliConfig
from linkml.validator.cli import _resolve_plugins
if isinstance(config, str | Path):
with open(config) as cfg_file:
config_data = yaml.safe_load(cfg_file) or {}
else:
config_data = dict(config)
# ``Config.schema`` is optional; drop ``data_sources`` if present
# since it has no meaning here.
config_data.pop("data_sources", None)
cfg = _CliConfig(**config_data)
if cfg.plugins:
return _resolve_plugins(cfg.plugins)
return [JsonschemaValidationPlugin(closed=closed)]
[docs]
def validate(
instance: Any,
schema: str | dict | SchemaDefinition,
target_class: str | None = None,
*,
strict: bool = False,
) -> ValidationReport:
"""Validate a data instance against a schema
This function provides a simple interface to do basic validation performed by a JSON Schema
validator on a single instance. To have more control over the type of validation performed,
see the :class:`Validator` class.
:param instance: The instance to validate
:param schema: The schema used to validate the instance. If a string is
it will be interpreted as a path, URL, or other loadable location.
If it is a dict it should be compatible with ``SchemaDefinition``,
otherwise it should be a ``SchemaDefinition`` instance.
:param target_class: Name of the class within the schema to validate
against. If ``None``, the class will be inferred from the schema by
looking for a class with ``tree_root: true``. Defaults to ``None``.
:param strict: If ``True``, validation will stop after the first validation
error is found, Otherwise all validation problems will be reported.
Defaults to ``False``.
:raises ValidationError: If requested to raise and validation errors are found.
:return: A validation report
:rtype: ValidationReport
"""
validator = _get_default_validator(schema, strict=strict)
return validator.validate(instance, target_class)
[docs]
def validate_file(
file: str | bytes | os.PathLike,
schema: str | dict | SchemaDefinition,
target_class: str | None = None,
*,
strict: bool = False,
) -> ValidationReport:
"""Validate instances loaded from a file against a schema
This function provides a simple interface to do basic validation performed by a JSON Schema
validator on instances loaded from a file. Loading is done according to the file's extension.
Accepted file extensions are: ``.csv``, ``.tsv``, ``.yaml``, ``.yml``, and ``.json``.
Individual rows of CSV and TSV files are treated as instances to validate. Each document
within a YAML file is treated as an individual instance to validate. If the top-level of a
JSON file is an array, each element of the array is treated as an instance to validate.
Otherwise, if the top-level is an object it is treated as a single instance to validate.
To have more control over the type of validation performed, see the :class:`Validator` class.
:param file: Path-like object of the file to be read
:param schema: The schema used to validate the instance. If a string is
it will be interpreted as a path, URL, or other loadable location.
If it is a dict it should be compatible with ``SchemaDefinition``,
otherwise it should be a ``SchemaDefinition`` instance.
:param target_class: Name of the class within the schema to validate
against. If ``None``, the class will be inferred from the schema by
looking for a class with ``tree_root: true``. Defaults to ``None``.
:param strict: If ``True``, validation will stop after the first validation
error is found, Otherwise all validation problems will be reported.
Defaults to ``False``.
:return: A validation report
:rtype: ValidationReport
"""
schema_path = schema if isinstance(schema, str | Path) else None
loader = default_loader_for_file(file, schema_path=schema_path, target_class=target_class)
validator = _get_default_validator(schema, strict=strict)
return validator.validate_source(loader, target_class)
__all__ = ["Validator", "validate", "validate_file"]