From 3eca67101b0e0dede62829c0056ee5c0b60cbf16 Mon Sep 17 00:00:00 2001 From: jbannon Date: Sun, 3 Apr 2022 18:02:22 +0000 Subject: [PATCH] More validator refactoring --- .../validators/base/dict_validator.py | 77 ------------------- .../validators/base/strict_dict_validator.py | 41 ++++++++++ ytdl_subscribe/validators/base/validators.py | 34 ++++++++ .../validators/config/config_validator.py | 10 +-- .../validators/config/preset_validator.py | 8 +- .../config/sources/soundcloud_validators.py | 4 +- .../config/sources/source_validator.py | 14 ++-- .../config/sources/youtube_validators.py | 2 +- .../config/subscription_validator.py | 14 ++-- 9 files changed, 101 insertions(+), 103 deletions(-) delete mode 100644 ytdl_subscribe/validators/base/dict_validator.py create mode 100644 ytdl_subscribe/validators/base/strict_dict_validator.py diff --git a/ytdl_subscribe/validators/base/dict_validator.py b/ytdl_subscribe/validators/base/dict_validator.py deleted file mode 100644 index 6aae19fb..00000000 --- a/ytdl_subscribe/validators/base/dict_validator.py +++ /dev/null @@ -1,77 +0,0 @@ -from typing import Any -from typing import Dict -from typing import Optional -from typing import Set -from typing import Type -from typing import TypeVar - -from ytdl_subscribe.validators.base.validators import Validator -from ytdl_subscribe.validators.exceptions import ValidationException - -T = TypeVar("T", bound=Validator) - - -class DictValidator(Validator): - - expected_value_type = dict - expected_value_type_name = "object" # for non-python users - - required_fields: Set[str] = set() - optional_fields: Set[str] = set() - - _allow_extra_fields = False - - def __init__(self, name, value): - super().__init__(name, value) - - # Ensure all required keys are present - for required_key in self.required_fields: - if required_key not in self.value: - error_msg = ( - f"'{self.name}' is missing the required field '{required_key}'" - ) - raise ValidationException(error_msg) - - # If no extra fields are allowed, ensure all fields are either - # required or optional fields - if not self._allow_extra_fields: - for object_key in self.keys: - if object_key not in self.allowed_fields: - error_msg = ( - f"'{self.name}' contains the field '{object_key}' which is not allowed. " - f"Allowed fields: {', '.join(self.allowed_fields)}" - ) - raise ValidationException(error_msg) - - def validate_key( - self, key: str, validator: Type[T], default: Optional[Any] = None - ) -> T: - value = self.get(key=key, default=default) - if value is None: - raise self._validation_exception( - f"{key} is missing when it should be present." - ) - - return validator( - name=f"{self.name}.{key}", - value=self.get(key=key, default=default), - ) - - @property - def dict(self) -> dict: - return self._value - - @property - def allowed_fields(self): - return sorted(self.required_fields.union(self.optional_fields)) - - @property - def keys(self): - return sorted(list(self.dict.keys())) - - def get(self, key: str, default: Optional[Any] = None) -> Any: - return self.dict.get(key, default) - - -class DictWithExtraFieldsValidator(DictValidator): - _allow_extra_fields = True diff --git a/ytdl_subscribe/validators/base/strict_dict_validator.py b/ytdl_subscribe/validators/base/strict_dict_validator.py new file mode 100644 index 00000000..a7506ce4 --- /dev/null +++ b/ytdl_subscribe/validators/base/strict_dict_validator.py @@ -0,0 +1,41 @@ +from typing import Set + +from ytdl_subscribe.validators.base.validators import DictValidator +from ytdl_subscribe.validators.exceptions import ValidationException + + +class StrictDictValidator(DictValidator): + required_keys: Set[str] = set() + optional_keys: Set[str] = set() + + allow_extra_fields = False + + def __init__(self, name, value): + super().__init__(name, value) + + if len(self.required_keys) == 0: + raise ValueError( + "No required fields when using a StrictDictValidator. Should be using DictValidator instead." + ) + + # Ensure all required keys are present + for required_key in self.required_keys: + if required_key not in self.value: + error_msg = ( + f"'{self.name}' is missing the required field '{required_key}'" + ) + raise ValidationException(error_msg) + + # Ensure all keys are either required or optional keys if no extra field are allowed + if not self.allow_extra_fields: + for object_key in self.keys: + if object_key not in self.allowed_keys: + error_msg = ( + f"'{self.name}' contains the field '{object_key}' which is not allowed. " + f"Allowed fields: {', '.join(self.allowed_keys)}" + ) + raise ValidationException(error_msg) + + @property + def allowed_keys(self): + return sorted(self.required_keys.union(self.optional_keys)) diff --git a/ytdl_subscribe/validators/base/validators.py b/ytdl_subscribe/validators/base/validators.py index 46bb7f7e..893e791a 100644 --- a/ytdl_subscribe/validators/base/validators.py +++ b/ytdl_subscribe/validators/base/validators.py @@ -1,6 +1,7 @@ from typing import Any from typing import Optional from typing import Type +from typing import TypeVar from ytdl_subscribe.validators.exceptions import ValidationException @@ -57,3 +58,36 @@ class StringValidator(Validator): @property def value(self) -> str: return self._value + + +T = TypeVar("T", bound=Validator) + + +class DictValidator(Validator): + expected_value_type = dict + expected_value_type_name = "object" # for non-python users + + @property + def dict(self) -> dict: + return self._value + + @property + def keys(self): + return sorted(list(self.dict.keys())) + + def get(self, key: str, default: Optional[Any] = None) -> Any: + return self.dict.get(key, default) + + def validate_key( + self, key: str, validator: Type[T], default: Optional[Any] = None + ) -> T: + value = self.get(key=key, default=default) + if value is None: + raise self._validation_exception( + f"{key} is missing when it should be present." + ) + + return validator( + name=f"{self.name}.{key}", + value=self.get(key=key, default=default), + ) diff --git a/ytdl_subscribe/validators/config/config_validator.py b/ytdl_subscribe/validators/config/config_validator.py index ccbb8ccb..34923aaf 100644 --- a/ytdl_subscribe/validators/config/config_validator.py +++ b/ytdl_subscribe/validators/config/config_validator.py @@ -2,18 +2,18 @@ from typing import Any import yaml -from ytdl_subscribe.validators.base.dict_validator import DictValidator -from ytdl_subscribe.validators.base.dict_validator import DictWithExtraFieldsValidator +from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator +from ytdl_subscribe.validators.base.validators import DictValidator from ytdl_subscribe.validators.base.validators import StringValidator -class ConfigValidator(DictValidator): - required_fields = {"working_directory", "presets"} +class ConfigValidator(StrictDictValidator): + required_keys = {"working_directory", "presets"} def __init__(self, name: str, value: Any): super().__init__(name, value) self.working_directory = self.validate_key("working_directory", StringValidator) - self.presets = self.validate_key("presets", DictWithExtraFieldsValidator) + self.presets = self.validate_key("presets", DictValidator) @classmethod def from_file_path(cls, config_path) -> "ConfigValidator": diff --git a/ytdl_subscribe/validators/config/preset_validator.py b/ytdl_subscribe/validators/config/preset_validator.py index 7d98a9fd..f8d347dd 100644 --- a/ytdl_subscribe/validators/config/preset_validator.py +++ b/ytdl_subscribe/validators/config/preset_validator.py @@ -2,7 +2,7 @@ from typing import Any from typing import Optional from ytdl_subscribe.utils.enums import SubscriptionSourceName -from ytdl_subscribe.validators.base.dict_validator import DictValidator +from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator from ytdl_subscribe.validators.config.sources.soundcloud_validators import ( SoundcloudSourceValidator, ) @@ -13,9 +13,9 @@ from ytdl_subscribe.validators.config.sources.youtube_validators import ( from ytdl_subscribe.validators.exceptions import ValidationException -class PresetValidator(DictValidator): - required_fields = {"post_process"} - optional_fields = { +class PresetValidator(StrictDictValidator): + required_keys = {"post_process"} + optional_keys = { "ytdl_options", "output_path", "overrides", diff --git a/ytdl_subscribe/validators/config/sources/soundcloud_validators.py b/ytdl_subscribe/validators/config/sources/soundcloud_validators.py index 49dc9c63..806f95cd 100644 --- a/ytdl_subscribe/validators/config/sources/soundcloud_validators.py +++ b/ytdl_subscribe/validators/config/sources/soundcloud_validators.py @@ -7,7 +7,7 @@ from ytdl_subscribe.validators.config.sources.source_validator import SourceVali class SoundcloudAlbumsAndSinglesDownloadValidator(DownloadStrategyValidator): - required_fields = {"username"} + required_keys = {"username"} def __init__(self, name, value): super().__init__(name, value) @@ -17,7 +17,7 @@ class SoundcloudAlbumsAndSinglesDownloadValidator(DownloadStrategyValidator): class SoundcloudSourceValidator(SourceValidator): - optional_fields = {"skip_premiere_tracks"} + optional_keys = {"skip_premiere_tracks"} download_strategy_validator_mapping = { "albums_and_singles": SoundcloudAlbumsAndSinglesDownloadValidator diff --git a/ytdl_subscribe/validators/config/sources/source_validator.py b/ytdl_subscribe/validators/config/sources/source_validator.py index e73ed989..2ada750c 100644 --- a/ytdl_subscribe/validators/config/sources/source_validator.py +++ b/ytdl_subscribe/validators/config/sources/source_validator.py @@ -3,18 +3,20 @@ from typing import Any from typing import Dict from typing import Type -from ytdl_subscribe.validators.base.dict_validator import DictValidator -from ytdl_subscribe.validators.base.dict_validator import DictWithExtraFieldsValidator +from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator from ytdl_subscribe.validators.base.validators import StringValidator -class DownloadStrategyValidator(DictValidator): +class DownloadStrategyValidator(StrictDictValidator): pass -class SourceValidator(DictWithExtraFieldsValidator): +class SourceValidator(StrictDictValidator): # All media sources must define a download strategy - required_fields = {"download_strategy"} + required_keys = {"download_strategy"} + + # Extra fields will be strict-validated using other StictDictValidators + allow_extra_fields = True download_strategy_validator_mapping: Dict[str, Type[DownloadStrategyValidator]] = {} @@ -32,7 +34,7 @@ class SourceValidator(DictWithExtraFieldsValidator): # Remove all non-download strategy keys before passing the dict to the validator download_strategy_dict = copy.deepcopy(self.dict) - for key_to_delete in self.allowed_fields: + for key_to_delete in self.allowed_keys: del download_strategy_dict[key_to_delete] download_strategy_class = self.download_strategy_validator_mapping[ diff --git a/ytdl_subscribe/validators/config/sources/youtube_validators.py b/ytdl_subscribe/validators/config/sources/youtube_validators.py index 7f5c3468..53115bc3 100644 --- a/ytdl_subscribe/validators/config/sources/youtube_validators.py +++ b/ytdl_subscribe/validators/config/sources/youtube_validators.py @@ -8,7 +8,7 @@ from ytdl_subscribe.validators.config.sources.source_validator import SourceVali class YoutubePlaylistDownloadValidator(DownloadStrategyValidator): - required_fields = {"playlist_id"} + required_keys = {"playlist_id"} def __init__(self, name, value): super().__init__(name, value) diff --git a/ytdl_subscribe/validators/config/subscription_validator.py b/ytdl_subscribe/validators/config/subscription_validator.py index 16464868..38597911 100644 --- a/ytdl_subscribe/validators/config/subscription_validator.py +++ b/ytdl_subscribe/validators/config/subscription_validator.py @@ -8,29 +8,27 @@ from ytdl_subscribe.subscriptions.soundcloud import SoundcloudSubscription from ytdl_subscribe.subscriptions.subscription import Subscription from ytdl_subscribe.subscriptions.youtube import YoutubeSubscription from ytdl_subscribe.utils.enums import SubscriptionSourceName -from ytdl_subscribe.validators.base.dict_validator import DictValidator -from ytdl_subscribe.validators.base.dict_validator import DictWithExtraFieldsValidator +from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator +from ytdl_subscribe.validators.base.validators import DictValidator from ytdl_subscribe.validators.base.validators import StringValidator from ytdl_subscribe.validators.config.config_validator import ConfigValidator from ytdl_subscribe.validators.config.preset_validator import PresetValidator -class SubscriptionValidator(DictValidator): +class SubscriptionValidator(StrictDictValidator): """ A Subscription is a preset but overrides it with specific values """ - required_fields = {"preset"} - optional_fields = PresetValidator.required_fields.union( - PresetValidator.optional_fields - ) + required_keys = {"preset"} + optional_keys = PresetValidator.required_keys.union(PresetValidator.optional_keys) def __init__(self, config: ConfigValidator, name: str, value: Any): super().__init__(name, value) self.config = config self.overrides = self.validate_key( key="overrides", - validator=DictWithExtraFieldsValidator, + validator=DictValidator, default={}, )