Validator major refactory
This commit is contained in:
parent
a0187cab41
commit
623f495b60
17 changed files with 351 additions and 265 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ytdl_subscribe.utils.formatter_validator import FormatterValidator
|
from ytdl_subscribe.validators.base.string_formatter_validator import (
|
||||||
|
StringFormatterValidator,
|
||||||
|
)
|
||||||
|
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -13,42 +16,21 @@ def error_message_unequal_regex_matches_str():
|
||||||
return "{variable_names} should only contain lowercase letters and underscores with a single open and close bracket."
|
return "{variable_names} should only contain lowercase letters and underscores with a single open and close bracket."
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def error_message_prefix_generator():
|
|
||||||
def _error_message_prefix_generator(format_string):
|
|
||||||
return f"Format string '{format_string}' is invalid: "
|
|
||||||
|
|
||||||
return _error_message_prefix_generator
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def error_message_unequal_brackets_generator(
|
|
||||||
error_message_prefix_generator, error_message_unequal_brackets_str
|
|
||||||
):
|
|
||||||
def _error_message_unequal_brackets_generator(format_string):
|
|
||||||
return f"{error_message_prefix_generator(format_string)}{error_message_unequal_brackets_str}"
|
|
||||||
|
|
||||||
return _error_message_unequal_brackets_generator
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def error_message_unequal_regex_matches_generator(
|
|
||||||
error_message_prefix_generator, error_message_unequal_regex_matches_str
|
|
||||||
):
|
|
||||||
def _error_message_unequal_regex_matches_generator(format_string):
|
|
||||||
return f"{error_message_prefix_generator(format_string)}{error_message_unequal_regex_matches_str}"
|
|
||||||
|
|
||||||
return _error_message_unequal_regex_matches_generator
|
|
||||||
|
|
||||||
|
|
||||||
class TestEntryFormatter(object):
|
class TestEntryFormatter(object):
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
format_string = "Here is my {var_one} and {var_two} 💩"
|
format_string = "Here is my {var_one} and {var_two} 💩"
|
||||||
assert FormatterValidator(format_string).parse() == ["var_one", "var_two"]
|
assert StringFormatterValidator(
|
||||||
|
name="test_format_variables", format_string=format_string
|
||||||
|
).format_variables == ["var_one", "var_two"]
|
||||||
|
|
||||||
def test_parse_no_variables(self):
|
def test_parse_no_variables(self):
|
||||||
format_string = "No vars 💩"
|
format_string = "No vars 💩"
|
||||||
assert FormatterValidator(format_string).parse() == []
|
assert (
|
||||||
|
StringFormatterValidator(
|
||||||
|
name="test_format_variables_empty", format_string=format_string
|
||||||
|
).format_variables
|
||||||
|
== []
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"format_string",
|
"format_string",
|
||||||
|
|
@ -60,12 +42,14 @@ class TestEntryFormatter(object):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_parse_fail_uneven_brackets(
|
def test_parse_fail_uneven_brackets(
|
||||||
self, format_string, error_message_unequal_brackets_generator
|
self, format_string, error_message_unequal_brackets_str
|
||||||
):
|
):
|
||||||
expected_error_msg = error_message_unequal_brackets_generator(format_string)
|
expected_error_msg = (
|
||||||
|
f"Validation error in fail: {error_message_unequal_brackets_str}"
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=expected_error_msg):
|
with pytest.raises(ValidationException, match=expected_error_msg):
|
||||||
assert FormatterValidator(format_string).parse()
|
_ = StringFormatterValidator(name="fail", format_string=format_string)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"format_string",
|
"format_string",
|
||||||
|
|
@ -79,11 +63,11 @@ class TestEntryFormatter(object):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_parse_fail_variable(
|
def test_parse_fail_variable(
|
||||||
self, format_string, error_message_unequal_regex_matches_generator
|
self, format_string, error_message_unequal_regex_matches_str
|
||||||
):
|
):
|
||||||
expected_error_msg = error_message_unequal_regex_matches_generator(
|
expected_error_msg = (
|
||||||
format_string
|
f"Validation error in fail: {error_message_unequal_regex_matches_str}"
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=expected_error_msg):
|
with pytest.raises(ValidationException, match=expected_error_msg):
|
||||||
assert FormatterValidator(format_string).parse()
|
_ = StringFormatterValidator(name="fail", format_string=format_string)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ from typing import Optional
|
||||||
|
|
||||||
from sanitize_filename import sanitize
|
from sanitize_filename import sanitize
|
||||||
|
|
||||||
from ytdl_subscribe.utils.formatter_validator import FormatterValidator
|
from ytdl_subscribe.validators.base.string_formatter_validator import (
|
||||||
|
StringFormatterValidator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Entry:
|
class Entry:
|
||||||
|
|
@ -104,7 +106,9 @@ class Entry:
|
||||||
if overrides:
|
if overrides:
|
||||||
entry_dict = dict(entry_dict, **overrides)
|
entry_dict = dict(entry_dict, **overrides)
|
||||||
|
|
||||||
field_names = FormatterValidator(format_string).parse()
|
field_names = StringFormatterValidator(
|
||||||
|
"TODO: UPDATE", format_string
|
||||||
|
).format_variables
|
||||||
|
|
||||||
for field_name in field_names:
|
for field_name in field_names:
|
||||||
if field_name not in entry_dict:
|
if field_name not in entry_dict:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ class Subscription(object):
|
||||||
options: dict
|
options: dict
|
||||||
Dictionary of ytdl options, specific to the source type
|
Dictionary of ytdl options, specific to the source type
|
||||||
ytdl_opts: dict
|
ytdl_opts: dict
|
||||||
Dictionary of options passed directly to ytdl. See `youtube_dl.YoutubeDL.YoutubeDL` for options.
|
Dictionary of options passed directly to ytdl.
|
||||||
|
See `youtube_dl.YoutubeDL.YoutubeDL` for options.
|
||||||
post_process: dict
|
post_process: dict
|
||||||
Dictionary of ytdl-subscribe post processing options
|
Dictionary of ytdl-subscribe post processing options
|
||||||
overrides: dict
|
overrides: dict
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import re
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
|
|
||||||
class FormatterValidator:
|
|
||||||
"""
|
|
||||||
Ensures user-created formatter strings are valid
|
|
||||||
"""
|
|
||||||
|
|
||||||
FIELDS_VALIDATOR = re.compile(r"{([a-z_]+?)}")
|
|
||||||
|
|
||||||
def __init__(self, format_string: str):
|
|
||||||
self.format_string = format_string
|
|
||||||
self._error_prefix = f"Format string '{self.format_string}' is invalid:"
|
|
||||||
|
|
||||||
def parse(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
List of fields to format
|
|
||||||
"""
|
|
||||||
open_bracket_count = self.format_string.count("{")
|
|
||||||
close_bracket_count = self.format_string.count("}")
|
|
||||||
|
|
||||||
if open_bracket_count != close_bracket_count:
|
|
||||||
raise ValueError(
|
|
||||||
f"{self._error_prefix} Brackets are reserved for {{variable_names}} "
|
|
||||||
f"and should contain a single open and close bracket."
|
|
||||||
)
|
|
||||||
|
|
||||||
parsed_fields = re.findall(
|
|
||||||
FormatterValidator.FIELDS_VALIDATOR, self.format_string
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(parsed_fields) != open_bracket_count:
|
|
||||||
raise ValueError(
|
|
||||||
f"{self._error_prefix} {{variable_names}} should only contain "
|
|
||||||
f"lowercase letters and underscores with a single open and close bracket."
|
|
||||||
)
|
|
||||||
|
|
||||||
return sorted(parsed_fields)
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class BaseValidator:
|
|
||||||
def __init__(self, name: str, value: Any):
|
|
||||||
self.name = name
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
def validate(self) -> "BaseValidator":
|
|
||||||
return self
|
|
||||||
11
ytdl_subscribe/validators/base/bool_validator.py
Normal file
11
ytdl_subscribe/validators/base/bool_validator.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from ytdl_subscribe.validators.base.validator import Validator
|
||||||
|
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
|
class BoolValidator(Validator):
|
||||||
|
expected_value_type: Type = bool
|
||||||
|
expected_value_type_name = "boolean"
|
||||||
95
ytdl_subscribe/validators/base/dict_validator.py
Normal file
95
ytdl_subscribe/validators/base/dict_validator.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Set
|
||||||
|
from typing import Type
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from ytdl_subscribe.validators.base.validator 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 __validate_required_fields_are_present(self):
|
||||||
|
"""
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValidationException
|
||||||
|
If the required fields are not present in the dict
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
def __validate_extra_fields(self):
|
||||||
|
"""
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValidationException
|
||||||
|
If allow_extra_fields=False and non-required/options fields are present
|
||||||
|
"""
|
||||||
|
if not self.allow_extra_fields:
|
||||||
|
for object_key in self.object_keys:
|
||||||
|
if (
|
||||||
|
object_key not in self.required_fields
|
||||||
|
and object_key not in self.optional_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 __init__(self, name, value):
|
||||||
|
super().__init__(name, value)
|
||||||
|
self.__validate_required_fields_are_present()
|
||||||
|
self.__validate_extra_fields()
|
||||||
|
|
||||||
|
def validate_dict_value(
|
||||||
|
self, dict_value_name: str, validator: Type[T], default: Optional[Any] = None
|
||||||
|
) -> T:
|
||||||
|
value = self.get(object_key=dict_value_name, default=default)
|
||||||
|
if value is None:
|
||||||
|
raise self._validation_exception(
|
||||||
|
f"{dict_value_name} is missing when it should be present."
|
||||||
|
)
|
||||||
|
|
||||||
|
return validator(
|
||||||
|
name=f"{self.name}.{dict_value_name}",
|
||||||
|
value=self.get(object_key=dict_value_name, 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 object_keys(self):
|
||||||
|
return sorted(list(self.dict.keys()))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def object_items(self):
|
||||||
|
return self.dict.items()
|
||||||
|
|
||||||
|
def get(self, object_key: str, default: Optional[Any] = None) -> Any:
|
||||||
|
return self.dict.get(object_key, default)
|
||||||
|
|
||||||
|
|
||||||
|
class DictWithExtraFieldsValidator(DictValidator):
|
||||||
|
allow_extra_fields = True
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
from typing import Any
|
|
||||||
from typing import Optional
|
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
from ytdl_subscribe.validators.base.base_validator import BaseValidator
|
|
||||||
from ytdl_subscribe.validators.exceptions import ValidationException
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectValidator(BaseValidator):
|
|
||||||
|
|
||||||
required_fields: Set[str] = []
|
|
||||||
optional_fields: Set[str] = []
|
|
||||||
allow_extra_fields = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def allowed_fields(self):
|
|
||||||
return sorted(self.required_fields.union(self.optional_fields))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def object_keys(self):
|
|
||||||
return sorted(list(self.value.keys()))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def object_items(self):
|
|
||||||
return self.value.items()
|
|
||||||
|
|
||||||
def get(self, object_key: str, default: Optional[Any] = None) -> Any:
|
|
||||||
return self.value.get(object_key, default)
|
|
||||||
|
|
||||||
def validate(self) -> "ObjectValidator":
|
|
||||||
super().validate()
|
|
||||||
|
|
||||||
# Ensure the value is an object
|
|
||||||
if not isinstance(self.value, dict):
|
|
||||||
error_msg = f"'{self.name}' must be an object"
|
|
||||||
if self.required_fields:
|
|
||||||
error_msg = f"{error_msg} with the required field: {', '.join(self.required_fields)}"
|
|
||||||
raise ValidationException(error_msg)
|
|
||||||
|
|
||||||
# Ensure the object contains all the required fields
|
|
||||||
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 extra fields are not allowed, ensure we only have required or optional keys
|
|
||||||
if not self.allow_extra_fields:
|
|
||||||
for object_key in self.object_keys:
|
|
||||||
if (
|
|
||||||
object_key not in self.required_fields
|
|
||||||
and object_key not in self.optional_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)
|
|
||||||
|
|
||||||
return self
|
|
||||||
64
ytdl_subscribe/validators/base/string_formatter_validator.py
Normal file
64
ytdl_subscribe/validators/base/string_formatter_validator.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import re
|
||||||
|
from keyword import iskeyword
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ytdl_subscribe.validators.base.string_validator import StringValidator
|
||||||
|
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
|
class StringFormatterValidator(StringValidator):
|
||||||
|
"""
|
||||||
|
Ensures user-created formatter strings are valid
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_value_type = str
|
||||||
|
expected_value_type_name = "format string"
|
||||||
|
|
||||||
|
FIELDS_VALIDATOR = re.compile(r"{([a-z_]+?)}")
|
||||||
|
|
||||||
|
def __validate_and_get_format_variables(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[str]
|
||||||
|
List of format variables in the format string
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValidationException
|
||||||
|
If the format string contains invalid variable formatting
|
||||||
|
"""
|
||||||
|
open_bracket_count = self.format_string.count("{")
|
||||||
|
close_bracket_count = self.format_string.count("}")
|
||||||
|
|
||||||
|
if open_bracket_count != close_bracket_count:
|
||||||
|
raise self._validation_exception(
|
||||||
|
error_message="Brackets are reserved for {variable_names} and should contain a single open and close bracket."
|
||||||
|
)
|
||||||
|
|
||||||
|
format_variables: List[str] = list(
|
||||||
|
re.findall(StringFormatterValidator.FIELDS_VALIDATOR, self.format_string)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(format_variables) != open_bracket_count:
|
||||||
|
raise self._validation_exception(
|
||||||
|
"{variable_names} should only contain "
|
||||||
|
"lowercase letters and underscores with a single open and close bracket."
|
||||||
|
)
|
||||||
|
|
||||||
|
for variable in format_variables:
|
||||||
|
if iskeyword(variable):
|
||||||
|
raise self._validation_exception(
|
||||||
|
f"'{variable}' is a Python keyword and cannot be used as a variable."
|
||||||
|
)
|
||||||
|
|
||||||
|
return format_variables
|
||||||
|
|
||||||
|
def __init__(self, name, format_string: str):
|
||||||
|
super().__init__(name=name, value=format_string)
|
||||||
|
self.format_variables = self.__validate_and_get_format_variables()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def format_string(self) -> str:
|
||||||
|
return self._value
|
||||||
15
ytdl_subscribe/validators/base/string_validator.py
Normal file
15
ytdl_subscribe/validators/base/string_validator.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from ytdl_subscribe.validators.base.validator import Validator
|
||||||
|
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
|
class StringValidator(Validator):
|
||||||
|
expected_value_type: Type = str
|
||||||
|
expected_value_type_name = "string"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> str:
|
||||||
|
return self._value
|
||||||
41
ytdl_subscribe/validators/base/validator.py
Normal file
41
ytdl_subscribe/validators/base/validator.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
|
class Validator:
|
||||||
|
# The python type that value should be
|
||||||
|
expected_value_type: Type = object
|
||||||
|
|
||||||
|
# When raising an error, call the type this value instead of its python name
|
||||||
|
expected_value_type_name: Optional[str] = None
|
||||||
|
|
||||||
|
def __validate_value(self):
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Validation exception to raise when the value's type is not the expected type
|
||||||
|
"""
|
||||||
|
if not isinstance(self._value, self.expected_value_type):
|
||||||
|
expected_value_type_name = self.expected_value_type_name or str(
|
||||||
|
self.expected_value_type
|
||||||
|
)
|
||||||
|
raise self._validation_exception(
|
||||||
|
error_message=f"should be of type {expected_value_type_name}."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, name: str, value: Any):
|
||||||
|
self.name = name
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
self.__validate_value()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> object:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def _validation_exception(self, error_message: str):
|
||||||
|
prefix = f"Validation error in {self.name}: "
|
||||||
|
return ValidationException(f"{prefix}{error_message}")
|
||||||
|
|
@ -1,32 +1,21 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from ytdl_subscribe.validators.base.object_validator import ObjectValidator
|
from ytdl_subscribe.validators.base.dict_validator import DictValidator
|
||||||
from ytdl_subscribe.validators.config.preset_validator import PresetValidator
|
from ytdl_subscribe.validators.base.dict_validator import DictWithExtraFieldsValidator
|
||||||
|
from ytdl_subscribe.validators.base.string_validator import StringValidator
|
||||||
|
|
||||||
|
|
||||||
class ConfigValidator(ObjectValidator):
|
class ConfigValidator(DictValidator):
|
||||||
required_fields = {"working_directory", "presets"}
|
required_fields = {"working_directory", "presets"}
|
||||||
|
|
||||||
def __init__(self, name: str, value: Any):
|
def __init__(self, name: str, value: Any):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self.working_directory: Optional[str] = None
|
self.working_directory = self.validate_dict_value(
|
||||||
self.presets: Optional[Dict[str, PresetValidator]] = None
|
"working_directory", StringValidator
|
||||||
|
)
|
||||||
def validate(self) -> "ConfigValidator":
|
self.presets = self.validate_dict_value("presets", DictWithExtraFieldsValidator)
|
||||||
super().validate()
|
|
||||||
|
|
||||||
self.working_directory = self.get("working_directory")
|
|
||||||
self.presets = {}
|
|
||||||
for preset_name, preset_obj in self.get("presets").items():
|
|
||||||
self.presets[preset_name] = PresetValidator(
|
|
||||||
name=f"{self.name}.presets.{preset_name}", value=preset_obj
|
|
||||||
).validate()
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file_path(cls, config_path) -> "ConfigValidator":
|
def from_file_path(cls, config_path) -> "ConfigValidator":
|
||||||
|
|
@ -34,4 +23,4 @@ class ConfigValidator(ObjectValidator):
|
||||||
with open(config_path, "r", encoding="utf-8") as file:
|
with open(config_path, "r", encoding="utf-8") as file:
|
||||||
config_dict = yaml.safe_load(file)
|
config_dict = yaml.safe_load(file)
|
||||||
|
|
||||||
return ConfigValidator(name="config", value=config_dict).validate()
|
return ConfigValidator(name="config", value=config_dict)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from typing import Any
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ytdl_subscribe.utils.enums import SubscriptionSourceName
|
from ytdl_subscribe.utils.enums import SubscriptionSourceName
|
||||||
from ytdl_subscribe.validators.base.object_validator import ObjectValidator
|
from ytdl_subscribe.validators.base.dict_validator import DictValidator
|
||||||
from ytdl_subscribe.validators.config.sources.base_source_validator import (
|
from ytdl_subscribe.validators.config.sources.base_source_validator import (
|
||||||
BaseSourceValidator,
|
BaseSourceValidator,
|
||||||
)
|
)
|
||||||
|
|
@ -15,9 +15,14 @@ from ytdl_subscribe.validators.config.sources.youtube_source_validator import (
|
||||||
from ytdl_subscribe.validators.exceptions import ValidationException
|
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
class PresetValidator(ObjectValidator):
|
class PresetValidator(DictValidator):
|
||||||
required_fields = {"post_process"}
|
required_fields = {"post_process"}
|
||||||
optional_fields = {"ytdl_options", "output_path", *SubscriptionSourceName.all()}
|
optional_fields = {
|
||||||
|
"ytdl_options",
|
||||||
|
"output_path",
|
||||||
|
"overrides",
|
||||||
|
*SubscriptionSourceName.all(),
|
||||||
|
}
|
||||||
allow_extra_fields = False
|
allow_extra_fields = False
|
||||||
|
|
||||||
def __init__(self, name: str, value: Any):
|
def __init__(self, name: str, value: Any):
|
||||||
|
|
@ -25,8 +30,6 @@ class PresetValidator(ObjectValidator):
|
||||||
self.subscription_source: Optional[BaseSourceValidator] = None
|
self.subscription_source: Optional[BaseSourceValidator] = None
|
||||||
self.subscription_source_name: Optional[str] = None
|
self.subscription_source_name: Optional[str] = None
|
||||||
|
|
||||||
def _validate_subscription_source(self):
|
|
||||||
# Make sure we only have a single subscription source
|
|
||||||
for object_key, object_value in self.object_items:
|
for object_key, object_value in self.object_items:
|
||||||
if object_key in SubscriptionSourceName.all() and self.subscription_source:
|
if object_key in SubscriptionSourceName.all() and self.subscription_source:
|
||||||
raise ValidationException(
|
raise ValidationException(
|
||||||
|
|
@ -35,22 +38,19 @@ class PresetValidator(ObjectValidator):
|
||||||
|
|
||||||
if object_key == SubscriptionSourceName.SOUNDCLOUD:
|
if object_key == SubscriptionSourceName.SOUNDCLOUD:
|
||||||
self.subscription_source_name = SubscriptionSourceName.SOUNDCLOUD
|
self.subscription_source_name = SubscriptionSourceName.SOUNDCLOUD
|
||||||
self.subscription_source = SoundcloudSourceValidator(
|
self.subscription_source = self.validate_dict_value(
|
||||||
name=f"{self.name}{object_key}", value=object_value
|
dict_value_name=object_key,
|
||||||
).validate()
|
validator=SoundcloudSourceValidator,
|
||||||
|
)
|
||||||
elif object_key == SubscriptionSourceName.YOUTUBE:
|
elif object_key == SubscriptionSourceName.YOUTUBE:
|
||||||
self.subscription_source_name = SubscriptionSourceName.YOUTUBE
|
self.subscription_source_name = SubscriptionSourceName.YOUTUBE
|
||||||
self.subscription_source = YoutubeSourceValidator(
|
self.subscription_source = self.validate_dict_value(
|
||||||
name=f"{self.name}{object_key}", value=object_value
|
dict_value_name=object_key,
|
||||||
).validate()
|
validator=YoutubeSourceValidator,
|
||||||
|
)
|
||||||
|
|
||||||
# If subscription source was not set, error
|
# If subscription source was not set, error
|
||||||
if not self.subscription_source:
|
if not self.subscription_source:
|
||||||
raise ValidationException(
|
raise ValidationException(
|
||||||
f"'{self.name} must have one of the following sources: {SubscriptionSourceName.pretty_all()}"
|
f"'{self.name} must have one of the following sources: {SubscriptionSourceName.pretty_all()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self) -> "PresetValidator":
|
|
||||||
super().validate()
|
|
||||||
self._validate_subscription_source()
|
|
||||||
return self
|
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,29 @@ from typing import Any
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
from ytdl_subscribe.validators.base.object_validator import ObjectValidator
|
from ytdl_subscribe.validators.base.dict_validator import DictValidator
|
||||||
|
from ytdl_subscribe.validators.base.string_validator import StringValidator
|
||||||
from ytdl_subscribe.validators.exceptions import ValidationException
|
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
class BaseSourceValidator(ObjectValidator):
|
class BaseSourceValidator(DictValidator):
|
||||||
|
# All media sources must define a download strategy
|
||||||
required_fields = {"download_strategy"}
|
required_fields = {"download_strategy"}
|
||||||
download_strategies: Set[str] = {}
|
download_strategies: Set[str] = {}
|
||||||
|
|
||||||
def __init__(self, name: str, value: Any):
|
def _validate_and_get_download_strategy(self) -> str:
|
||||||
super().__init__(name=name, value=value)
|
download_strategy = self.validate_dict_value(
|
||||||
self.download_strategy: Optional[str] = None
|
dict_value_name="download_strategy",
|
||||||
|
validator=StringValidator,
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self) -> "BaseSourceValidator":
|
if download_strategy.value not in self.download_strategies:
|
||||||
super().validate()
|
raise self._validation_exception(
|
||||||
|
f"download_strategy must be one of the following: {', '.join(self.download_strategies)}"
|
||||||
if self.value["download_strategy"] not in self.download_strategies:
|
|
||||||
raise ValidationException(
|
|
||||||
f"'download_strategy' must be one of the following: {', '.join(self.download_strategies)}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self
|
return download_strategy.value
|
||||||
|
|
||||||
|
def __init__(self, name: str, value: Any):
|
||||||
|
super().__init__(name=name, value=value)
|
||||||
|
self.download_strategy = self._validate_and_get_download_strategy()
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,27 @@
|
||||||
from typing import Any
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from ytdl_subscribe.validators.base.bool_validator import BoolValidator
|
||||||
|
from ytdl_subscribe.validators.base.string_validator import StringValidator
|
||||||
from ytdl_subscribe.validators.config.sources.base_source_validator import (
|
from ytdl_subscribe.validators.config.sources.base_source_validator import (
|
||||||
BaseSourceValidator,
|
BaseSourceValidator,
|
||||||
)
|
)
|
||||||
from ytdl_subscribe.validators.exceptions import ValidationException
|
|
||||||
|
|
||||||
|
|
||||||
class SoundcloudSourceValidator(BaseSourceValidator):
|
class SoundcloudSourceValidator(BaseSourceValidator):
|
||||||
optional_fields = {"username", "skip_premiere_tracks"}
|
optional_fields = {"username", "skip_premiere_tracks"}
|
||||||
download_strategies = {"albums_and_singles"}
|
download_strategies = {"albums_and_singles"}
|
||||||
|
|
||||||
def __init__(self, name: str, value: Any):
|
def __validate_and_set_download_strategy_fields(self):
|
||||||
super().__init__(name=name, value=value)
|
|
||||||
self.username: Optional[str] = None
|
|
||||||
self.skip_premiere_tracks: Optional[bool] = None
|
|
||||||
|
|
||||||
def validate(self) -> "SoundcloudSourceValidator":
|
|
||||||
super().validate()
|
|
||||||
|
|
||||||
self.skip_premiere_tracks = self.get("skip_premiere_tracks", True)
|
|
||||||
if self.download_strategy == "albums_and_singles":
|
if self.download_strategy == "albums_and_singles":
|
||||||
self.username = self.value.get("username")
|
self.username = self.validate_dict_value(
|
||||||
if not self.username or not isinstance(self.username, str):
|
dict_value_name="username",
|
||||||
raise ValidationException(
|
validator=StringValidator,
|
||||||
"Soundcloud download_strategy 'albums_and_singles' requires the field 'username'"
|
).value
|
||||||
)
|
|
||||||
|
|
||||||
return self
|
def __init__(self, name: str, value: dict):
|
||||||
|
super().__init__(name=name, value=value)
|
||||||
|
self.skip_premiere_tracks = self.validate_dict_value(
|
||||||
|
"skip_premiere_tracks", BoolValidator, default=True
|
||||||
|
).value
|
||||||
|
self.username: Optional[str] = None
|
||||||
|
self.__validate_and_set_download_strategy_fields()
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
from ytdl_subscribe.validators.base.string_validator import StringValidator
|
||||||
from ytdl_subscribe.validators.config.sources.base_source_validator import (
|
from ytdl_subscribe.validators.config.sources.base_source_validator import (
|
||||||
BaseSourceValidator,
|
BaseSourceValidator,
|
||||||
)
|
)
|
||||||
from ytdl_subscribe.validators.exceptions import ValidationException
|
|
||||||
|
|
||||||
|
|
||||||
class YoutubeSourceValidator(BaseSourceValidator):
|
class YoutubeSourceValidator(BaseSourceValidator):
|
||||||
|
|
@ -13,16 +12,4 @@ class YoutubeSourceValidator(BaseSourceValidator):
|
||||||
|
|
||||||
def __init__(self, name: str, value: Any):
|
def __init__(self, name: str, value: Any):
|
||||||
super().__init__(name=name, value=value)
|
super().__init__(name=name, value=value)
|
||||||
self.playlist_id: Optional[str] = None
|
self.playlist_id = self.validate_dict_value("playlist_id", StringValidator)
|
||||||
|
|
||||||
def validate(self) -> "YoutubeSourceValidator":
|
|
||||||
super().validate()
|
|
||||||
|
|
||||||
if self.download_strategy == "playlist":
|
|
||||||
self.playlist_id = self.value.get("playlist_id")
|
|
||||||
if not self.playlist_id or not isinstance(self.playlist_id, str):
|
|
||||||
raise ValidationException(
|
|
||||||
"Youtube download_strategy 'playlist' requires the field 'playlist_id' to be a string"
|
|
||||||
)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from mergedeep import mergedeep
|
from mergedeep import mergedeep
|
||||||
|
|
@ -10,64 +8,72 @@ from ytdl_subscribe.subscriptions.soundcloud import SoundcloudSubscription
|
||||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
from ytdl_subscribe.subscriptions.subscription import Subscription
|
||||||
from ytdl_subscribe.subscriptions.youtube import YoutubeSubscription
|
from ytdl_subscribe.subscriptions.youtube import YoutubeSubscription
|
||||||
from ytdl_subscribe.utils.enums import SubscriptionSourceName
|
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.string_validator import StringValidator
|
||||||
from ytdl_subscribe.validators.config.config_validator import ConfigValidator
|
from ytdl_subscribe.validators.config.config_validator import ConfigValidator
|
||||||
from ytdl_subscribe.validators.config.preset_validator import PresetValidator
|
from ytdl_subscribe.validators.config.preset_validator import PresetValidator
|
||||||
from ytdl_subscribe.validators.exceptions import ValidationException
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionValidator(PresetValidator):
|
class SubscriptionValidator(DictValidator):
|
||||||
"""
|
"""
|
||||||
A Subscription is a preset but overrides it with specific values
|
A Subscription is a preset but overrides it with specific values
|
||||||
"""
|
"""
|
||||||
|
|
||||||
required_fields = {"preset"}
|
required_fields = {"preset"}
|
||||||
optional_fields = {"overrides"}.union(
|
optional_fields = PresetValidator.required_fields.union(
|
||||||
PresetValidator.required_fields, PresetValidator.optional_fields
|
PresetValidator.optional_fields
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, config: ConfigValidator, name: str, value: Any):
|
def __init__(self, config: ConfigValidator, name: str, value: Any):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.overrides: Optional[Dict] = None
|
self.overrides = self.validate_dict_value(
|
||||||
|
dict_value_name="overrides",
|
||||||
|
validator=DictWithExtraFieldsValidator,
|
||||||
|
default={},
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self) -> "SubscriptionValidator":
|
preset_name = self.validate_dict_value(
|
||||||
# If a preset is present, update the
|
dict_value_name="preset",
|
||||||
preset_name = self.get("preset")
|
validator=StringValidator,
|
||||||
available_presets = sorted(list(self.config.presets.keys()))
|
).value
|
||||||
|
|
||||||
|
available_presets = self.config.presets.object_keys
|
||||||
if preset_name not in available_presets:
|
if preset_name not in available_presets:
|
||||||
raise ValidationException(
|
raise self._validation_exception(
|
||||||
f"'{self.name}.preset' does not exist in the provided config. "
|
f"'preset '{preset_name}' does not exist in the provided config. "
|
||||||
f"Available presets: {', '.join(available_presets)}"
|
f"Available presets: {', '.join(available_presets)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.overrides = self.get("overrides", {})
|
|
||||||
|
|
||||||
# A little hacky, we will override the preset with the contents of this subscription, then validate it
|
# A little hacky, we will override the preset with the contents of this subscription, then validate it
|
||||||
self.value = mergedeep.merge(
|
preset_dict = mergedeep.merge(
|
||||||
self.config.presets[preset_name].value,
|
self.config.presets.dict[preset_name],
|
||||||
self.value,
|
self.dict,
|
||||||
strategy=mergedeep.Strategy.REPLACE,
|
strategy=mergedeep.Strategy.REPLACE,
|
||||||
)
|
)
|
||||||
|
del preset_dict["preset"]
|
||||||
|
|
||||||
_ = super().validate()
|
self.preset = PresetValidator(
|
||||||
return self
|
name=self.name,
|
||||||
|
value=preset_dict,
|
||||||
|
)
|
||||||
|
|
||||||
def to_subscription(self) -> Subscription:
|
def to_subscription(self) -> Subscription:
|
||||||
subscription_class = None
|
if self.preset.subscription_source_name == SubscriptionSourceName.SOUNDCLOUD:
|
||||||
if self.subscription_source_name == SubscriptionSourceName.SOUNDCLOUD:
|
|
||||||
subscription_class = SoundcloudSubscription
|
subscription_class = SoundcloudSubscription
|
||||||
elif self.subscription_source_name == SubscriptionSourceName.YOUTUBE:
|
elif self.preset.subscription_source_name == SubscriptionSourceName.YOUTUBE:
|
||||||
subscription_class = YoutubeSubscription
|
subscription_class = YoutubeSubscription
|
||||||
else:
|
else:
|
||||||
raise ValueError("subscription source class not found")
|
raise ValueError("subscription source class not found")
|
||||||
|
|
||||||
return subscription_class(
|
return subscription_class(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
options=self.get(self.subscription_source_name),
|
options=self.preset.get(self.preset.subscription_source_name),
|
||||||
ytdl_opts=self.get("ytdl_options"),
|
ytdl_opts=self.preset.get("ytdl_options"),
|
||||||
post_process=self.get("post_process"),
|
post_process=self.preset.get("post_process"),
|
||||||
overrides=self.get("overrides"),
|
overrides=self.preset.get("overrides"),
|
||||||
output_path=self.get("output_path"),
|
output_path=self.preset.get("output_path"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -83,7 +89,7 @@ class SubscriptionValidator(PresetValidator):
|
||||||
subscriptions.append(
|
subscriptions.append(
|
||||||
SubscriptionValidator(
|
SubscriptionValidator(
|
||||||
config=config, name=subscription_key, value=subscription_object
|
config=config, name=subscription_key, value=subscription_object
|
||||||
).validate()
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return subscriptions
|
return subscriptions
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue