You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
154 lines
5.3 KiB
Python
154 lines
5.3 KiB
Python
"""Event schema objects."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path, PurePath
|
|
from typing import Any, 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 # type:ignore[assignment, misc]
|
|
|
|
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, # type:ignore[assignment]
|
|
format_checker: FormatChecker = draft7_format_checker,
|
|
registry: Registry[Any] | None = None,
|
|
):
|
|
"""Initialize an event schema."""
|
|
_schema = self._load_schema(schema)
|
|
# Validate the schema against Jupyter Events metaschema.
|
|
validate_schema(_schema)
|
|
|
|
if registry is None:
|
|
registry = DRAFT7.create_resource(_schema) @ Registry()
|
|
|
|
# Create a validator for this schema
|
|
self._validator = validator_class(_schema, registry=registry, format_checker=format_checker) # type: ignore[call-arg]
|
|
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")
|
|
|
|
# detect whether the user specified a string but intended a PurePath to
|
|
# generate a more helpful error message
|
|
if was_str and intended_as_path(schema): # type:ignore[arg-type]
|
|
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 schema is already a dictionary, return it
|
|
if isinstance(schema, dict):
|
|
return schema
|
|
|
|
# if schema is PurePath, ensure file exists at path and then load from file
|
|
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 # type:ignore[no-any-return]
|
|
|
|
# finally, if schema is string, attempt to deserialize and return the output
|
|
if isinstance(schema, str):
|
|
# note the diff b/w load v.s. loads
|
|
loaded_schema = yaml.loads(schema)
|
|
EventSchema._ensure_yaml_loaded(loaded_schema, was_str=True)
|
|
return loaded_schema # type:ignore[no-any-return]
|
|
|
|
msg = f"Expected a dictionary, string, or PurePath, but instead received {schema.__class__.__name__}." # type:ignore[unreachable]
|
|
raise EventSchemaUnrecognized(msg)
|
|
|
|
@property
|
|
def id(self) -> str:
|
|
"""Schema $id field."""
|
|
return self._schema["$id"] # type:ignore[no-any-return]
|
|
|
|
@property
|
|
def version(self) -> int:
|
|
"""Schema's version."""
|
|
return self._schema["version"] # type:ignore[no-any-return]
|
|
|
|
@property
|
|
def properties(self) -> dict[str, Any]:
|
|
return self._schema["properties"] # type:ignore[no-any-return]
|
|
|
|
def validate(self, data: dict[str, Any]) -> None:
|
|
"""Validate an incoming instance of this event schema."""
|
|
self._validator.validate(data)
|