|
"""Event schema objects.""" |
|
from __future__ import annotations |
|
|
|
import json |
|
from pathlib import Path, PurePath |
|
from typing import Any, Dict, Union |
|
|
|
from jsonschema import FormatChecker, validators |
|
from referencing import Registry |
|
from referencing.jsonschema import DRAFT7 |
|
|
|
try: |
|
from jsonschema.protocols import Validator |
|
except ImportError: |
|
Validator = Any |
|
|
|
from . import yaml |
|
from .validators import draft7_format_checker, validate_schema |
|
|
|
|
|
class EventSchemaUnrecognized(Exception): |
|
"""An error for an unrecognized event schema.""" |
|
|
|
|
|
class EventSchemaLoadingError(Exception): |
|
"""An error for an event schema loading error.""" |
|
|
|
|
|
class EventSchemaFileAbsent(Exception): |
|
"""An error for an absent event schema file.""" |
|
|
|
|
|
SchemaType = Union[Dict[str, Any], str, PurePath] |
|
|
|
|
|
class EventSchema: |
|
"""A validated schema that can be used. |
|
|
|
On instantiation, validate the schema against |
|
Jupyter Event's metaschema. |
|
|
|
Parameters |
|
---------- |
|
schema: dict or str |
|
JSON schema to validate against Jupyter Events. |
|
|
|
validator_class: jsonschema.validators |
|
The validator class from jsonschema used to validate instances |
|
of this event schema. The schema itself will be validated |
|
against Jupyter Event's metaschema to ensure that |
|
any schema registered here follows the expected form |
|
of Jupyter Events. |
|
|
|
registry: |
|
Registry for nested JSON schema references. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
schema: SchemaType, |
|
validator_class: type[Validator] = validators.Draft7Validator, |
|
format_checker: FormatChecker = draft7_format_checker, |
|
registry: Registry[Any] | None = None, |
|
): |
|
"""Initialize an event schema.""" |
|
_schema = self._load_schema(schema) |
|
|
|
validate_schema(_schema) |
|
|
|
if registry is None: |
|
registry = DRAFT7.create_resource(_schema) @ Registry() |
|
|
|
|
|
self._validator = validator_class(_schema, registry=registry, format_checker=format_checker) |
|
self._schema = _schema |
|
|
|
def __repr__(self) -> str: |
|
"""A string repr for an event schema.""" |
|
return json.dumps(self._schema, indent=2) |
|
|
|
@staticmethod |
|
def _ensure_yaml_loaded(schema: SchemaType, was_str: bool = False) -> None: |
|
"""Ensures schema was correctly loaded into a dictionary. Raises |
|
EventSchemaLoadingError otherwise.""" |
|
if isinstance(schema, dict): |
|
return |
|
|
|
error_msg = "Could not deserialize schema into a dictionary." |
|
|
|
def intended_as_path(schema: str) -> bool: |
|
path = Path(schema) |
|
return path.match("*.yml") or path.match("*.yaml") or path.match("*.json") |
|
|
|
|
|
|
|
if was_str and intended_as_path(schema): |
|
error_msg += " Paths to schema files must be explicitly wrapped in a Pathlib object." |
|
else: |
|
error_msg += " Double check the schema and ensure it is in the proper form." |
|
|
|
raise EventSchemaLoadingError(error_msg) |
|
|
|
@staticmethod |
|
def _load_schema(schema: SchemaType) -> dict[str, Any]: |
|
"""Load a JSON schema from different sources/data types. |
|
|
|
`schema` could be a dictionary or serialized string representing the |
|
schema itself or a Pathlib object representing a schema file on disk. |
|
|
|
Returns a dictionary with schema data. |
|
""" |
|
|
|
|
|
if isinstance(schema, dict): |
|
return schema |
|
|
|
|
|
if isinstance(schema, PurePath): |
|
if not Path(schema).exists(): |
|
msg = f'Schema file not present at path "{schema}".' |
|
raise EventSchemaFileAbsent(msg) |
|
|
|
loaded_schema = yaml.load(schema) |
|
EventSchema._ensure_yaml_loaded(loaded_schema) |
|
return loaded_schema |
|
|
|
|
|
if isinstance(schema, str): |
|
|
|
loaded_schema = yaml.loads(schema) |
|
EventSchema._ensure_yaml_loaded(loaded_schema, was_str=True) |
|
return loaded_schema |
|
|
|
msg = f"Expected a dictionary, string, or PurePath, but instead received {schema.__class__.__name__}." |
|
raise EventSchemaUnrecognized(msg) |
|
|
|
@property |
|
def id(self) -> str: |
|
"""Schema $id field.""" |
|
return self._schema["$id"] |
|
|
|
@property |
|
def version(self) -> int: |
|
"""Schema's version.""" |
|
return self._schema["version"] |
|
|
|
@property |
|
def properties(self) -> dict[str, Any]: |
|
return self._schema["properties"] |
|
|
|
def validate(self, data: dict[str, Any]) -> None: |
|
"""Validate an incoming instance of this event schema.""" |
|
self._validator.validate(data) |
|
|