from typing import ( # noqa: F401 # pylint: disable=unused-import
Any,
AnyStr,
Dict,
Optional,
Tuple,
Union,
)
from jsonschema import validate
from pykechain.enums import PropertyVTypes, ValidatorEffectTypes
from pykechain.models.validators.validator_schemas import (
effects_jsonschema_stub,
validator_jsonschema_stub,
)
[docs]
class BaseValidator:
"""Base class for all Validators.
This is the base implementation for both the :class:`PropertyValidator` as well as the :class:`ValidatorEffect`.
.. versionadded:: 2.2
:cvar jsonschema: jsonschema to validate the json representation of the Validator
:type jsonschema: dict
:cvar accuracy: default value used in comparison of floats, normally 1E-6
:type accuracy: float
"""
jsonschema: Union[Dict, None] = None
accuracy = 1e-6
def __init__(self, json=None, *args, **kwargs):
"""Construct a base validator."""
self._json = json or dict(config=dict())
self._config = self._json.get("config", dict())
[docs]
def as_json(self) -> Dict:
"""Parse the validator to a proper validator json."""
return self._json
[docs]
def validate_json(self) -> Any:
"""Validate the json representation of the validator against the validator jsonschema."""
return validate(self._json, self.jsonschema)
[docs]
@classmethod
def parse(cls, json: Dict) -> Any:
"""Parse a json dict and return the correct subclass."""
raise NotImplementedError # pragma: no cover
[docs]
class PropertyValidator(BaseValidator):
"""Base class for all property validators.
If json is provided, the validator is instantiated based on that json.
.. versionadded:: 2.2
:cvar vtype: Validator type, one of :class:`pykechain.enums.PropertyVTypes`
:type vtype: basestring
:cvar jsonschema: jsonschema to validate the structure of the json representation of the effect against
:type jsonschema: dict
"""
vtype: str = PropertyVTypes.NONEVALIDATOR
jsonschema = validator_jsonschema_stub
def __init__(self, json=None, *args, **kwargs):
"""Construct a Property Validator."""
super().__init__(json=json, *args, **kwargs)
self._json = json or {"vtype": self.vtype, "config": {}}
self._validation_result = None
self._validation_reason = None
self._value = None
if self._config.get("on_valid"):
self.on_valid = self._parse_effects(self._config.get("on_valid"))
else:
self.on_valid = kwargs.get("on_valid") or []
if self._config.get("on_invalid"):
self.on_invalid = self._parse_effects(self._config.get("on_invalid"))
else:
self.on_invalid = kwargs.get("on_invalid") or []
@staticmethod
def _parse_effects(effects_json: Optional[Dict] = None) -> Any:
"""Parse multiple effects from an effects(list) json."""
if isinstance(effects_json, list):
return [ValidatorEffect.parse(effect) for effect in effects_json]
elif isinstance(effects_json, dict):
return ValidatorEffect.parse(effects_json)
else:
raise Exception(
"The provided json, should be a list of valid effects, "
"or a single effect. Got '{}'".format(effects_json)
)
[docs]
@classmethod
def parse(cls, json: Dict) -> "PropertyValidator":
"""Parse a json dict and return the correct subclass of :class:`PropertyValidator`.
It uses the 'effect' key to determine which :class:`PropertyValidator` to instantiate.
Please refer to :class:`pykechain.enums.PropertyVTypes` for the supported effects.
:param json: dictionary containing the specific keys to parse into a :class:`PropertyValidator`
:type json: dict
:returns: the instantiated subclass of :class:`PropertyValidator`
:rtype: :class:`PropertyValidator` or subclass thereof
"""
if "vtype" in json:
vtype = json.get("vtype")
if vtype not in PropertyVTypes.values():
raise Exception(f"Validator unknown, incorrect json: '{json}'")
from pykechain.models.validators import validators
vtype_implementation_classname = f"{vtype[0].upper()}{vtype[1:]}" # type: ignore
if hasattr(validators, vtype_implementation_classname):
return getattr(validators, vtype_implementation_classname)(json=json)
else:
raise Exception("unknown vtype in json")
raise Exception(f"Validator unknown, incorrect json: '{json}'")
[docs]
def as_json(self) -> Dict:
"""JSON representation of the effect.
:returns: a python dictionary, serializable as json of the effect
:rtype: dict
"""
new_json = dict(vtype=self.vtype, config=self._config)
if self.on_valid:
new_json["config"]["on_valid"] = [
effect.as_json() for effect in self.on_valid
]
if self.on_invalid:
new_json["config"]["on_invalid"] = [
effect.as_json() for effect in self.on_invalid
]
self._json = new_json
return self._json
def __call__(self, value: Any) -> bool:
"""Trigger the validation of the validator.
The reason may retrieved by the :func:`get_reason()` method.
:param value: The value to check against
:type value: Any
:return: bool
"""
self._validation_result, self._validation_reason = self._logic(value)
if self._validation_result is not None and self._validation_result:
for effect in self.on_valid:
effect()
elif self._validation_result is not None and not self._validation_result:
for effect in self.on_invalid:
effect()
return self._validation_result
[docs]
def is_valid(self, value: Any) -> bool:
"""Check if the validation against a value, returns a boolean.
This is the logical inverse of the :func:`is_invalid()` method.
:param value: The value to check against
:type value: Any
:return: True if valid, False if invalid
:rtype: bool
"""
return self.__call__(value)
[docs]
def is_invalid(self, value: Any) -> bool:
"""Check if the validation against a value, returns a boolean.
This is the logical inverse of the :func:`is_valid()` method.
:param value: The value to check against
:type value: Any
:return: True if INvalid, False if valid
:rtype: bool
"""
return not self.is_valid(value)
[docs]
def get_reason(self) -> AnyStr:
"""Retrieve the reason of the (in)validation.
:return: reason text
:rtype: basestring
"""
return self._validation_reason
def _logic(self, value: Optional[Any] = None) -> Tuple[Optional[bool], str]:
"""Process the inner logic of the validator.
The validation results are returned as tuple (boolean (true/false), reasontext)
"""
self._validation_result, self._validation_reason = None, "No reason"
return self._validation_result, self._validation_reason
[docs]
class ValidatorEffect(BaseValidator):
"""
A Validator Effect.
This is an effect that can be associated with the :attr:`PropertyValidator.on_valid` or
:attr:`PropertyValidator.on_invalid`. The effects associated are called based on the results of the
validation of the :class:`PropertyValidator`.
.. versionadded:: 2.2
:cvar effect: Effect type, one of :class:`pykechain.enums.ValidatorEffectTypes`
:cvar jsonschema: jsonschema to validate the structure of the json representation of the effect against
"""
effect = ValidatorEffectTypes.NONE_EFFECT
jsonschema = effects_jsonschema_stub
def __init__(self, json=None, *args, **kwargs):
"""Construct a Validator Effect."""
super().__init__(json=json, *args, **kwargs)
self._json = json or {"effect": self.effect, "config": {}}
def __call__(self, **kwargs):
"""Execute the effect."""
return True
[docs]
@classmethod
def parse(cls, json: Dict) -> "ValidatorEffect":
"""Parse a json dict and return the correct subclass of :class:`ValidatorEffect`.
It uses the 'effect' key to determine which :class:`ValidatorEffect` to instantiate.
Please refer to :class:`enums.ValidatorEffectTypes` for the supported effects.
:param json: dictionary containing the specific keys to parse into a :class:`ValidatorEffect`
:type json: dict
:returns: the instantiated subclass of :class:`ValidatorEffect`
:rtype: :class:`ValidatorEffect` or subclass
"""
effect = json.get("effect")
if effect:
from pykechain.models.validators import effects
effect_implementation_classname = effect[0].upper() + effect[1:]
if hasattr(effects, effect_implementation_classname):
return getattr(effects, effect_implementation_classname)(json=json)
else:
raise Exception("unknown effect in json")
raise Exception(f"Effect unknown, incorrect json: '{json}'")
[docs]
def as_json(self) -> Dict:
"""JSON representation of the effect.
:returns: a python dictionary, serializable as json of the effect
:rtype: dict
"""
self._json = dict(effect=self.effect, config=self._config)
return self._json