Proper pythonic way to structure class hierarchy which tries to validate input and load into dataclasses_jsonschema schema

51 views Asked by At

I have various classes that try to load different schemas from an input, and these classes can pose an additional input value validation. I'm wondering what the proper pythonic (and mypy satisfactory) way to structure this is. Here's what I have so far:

from dataclasses import dataclass
from abc import ABC, abstractproperty
from typing import Union

from dataclasses_jsonschema import JsonSchemaMixin
from jsonschema import exceptions as jsonschexc, validate


""" Validator parent class """
class BaseValidator(ABC):
    @abstractproperty
    def schema(self) -> JsonSchemaMixin:
        ...

    @classmethod
    def validate_data(cls, instance: dict):
        # by default, there is no additional data validation
        return True

    @classmethod
    def instance_is_valid(cls, instance: dict, raise_exception: bool = False) -> bool:
        try:
            # perform json_schema loading validation
            validate(instance=instance, schema=cls.schema.json_schema())  # type: ignore

            # perform optional additional validation with override "validate_data" method
            is_valid = cls.validate_data(instance)

            if raise_exception and not is_valid:
                raise ValueError()
        except jsonschexc.ValidationError:
            if raise_exception:
                raise ValueError()

    @classmethod
    def get_object(cls, instance: dict, validate=True) -> JsonSchemaMixin:
        if validate:
            cls.instance_is_valid(instance, raise_exception=True)  # type: ignore
        return cls.schema.from_dict(instance)  # type: ignore

""" App parent class """
class BaseApp(ABC):
    @abstractproperty
    def validator(self) -> BaseValidator:
        ...

    def __init__(self, msg: dict):
        self.validated_msg = self.validator.get_object(msg)


""" Below is a specific example for a handler which
expects to extract "foo" and "bar" from the input,
and tries to validate the input foo==42
"""
@dataclass
class SomeDataModel(JsonSchemaMixin):
    foo: int
    bar: str


class HandlerAppValidator(BaseValidator):
    schema = SomeDataModel  # type: ignore

    @classmethod
    def validate_data(cls, instance: dict):  # type: ignore
        # make specific validation checks for the input instance,
        # even after the schema is successfully loaded
        return instance["foo"] == 42

class HandlerApp(BaseApp):
    validator = HandlerAppValidator  # type: ignore

    def __init__(self, input_: dict):
        self.instance_is_valid = True
        try:
            super().__init__(input_)
        except ValueError:
            self.instance_is_valid = False

    def execute(self, *args, **kwargs):
        # if data model could not be read from sb message, exit
        if not self.instance_is_valid:
            return
        ... # do something with the self.validated_msg
        self.validated_msg

The concept is that many different HandlerApps can use this same structure, where each of them have their own Validator with their own Schema.

I feel like there are improvements to be made here as there are a few places where the mypy linter is unhappy (hence the # type: ignore). I'm wondering if there is some best practice approach when multiple applications are built on the same core validation structure

0

There are 0 answers