From d3c2c1445207366de34f24213ad81c156852a525 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Sun, 23 Oct 2022 23:12:50 -0700 Subject: [PATCH] wip --- src/ytdl_sub/cli/download_args_parser.py | 2 +- src/ytdl_sub/config/config_file.py | 90 +----------------- src/ytdl_sub/config/config_validator.py | 92 +++++++++++++++++++ src/ytdl_sub/config/preset.py | 26 ++++-- src/ytdl_sub/config/preset_options.py | 1 - .../subscriptions/base_subscription.py | 2 +- src/ytdl_sub/validators/nfo_validators.py | 1 - .../validators/strict_dict_validator.py | 1 - tests/unit/cli/test_download_args_parser.py | 2 +- 9 files changed, 120 insertions(+), 97 deletions(-) create mode 100644 src/ytdl_sub/config/config_validator.py diff --git a/src/ytdl_sub/cli/download_args_parser.py b/src/ytdl_sub/cli/download_args_parser.py index 95d086ef..938bed5e 100644 --- a/src/ytdl_sub/cli/download_args_parser.py +++ b/src/ytdl_sub/cli/download_args_parser.py @@ -8,7 +8,7 @@ from typing import Tuple from mergedeep import mergedeep from ytdl_sub.cli.main_args_parser import MainArgs -from ytdl_sub.config.config_file import ConfigOptions +from ytdl_sub.config.config_validator import ConfigOptions from ytdl_sub.utils.exceptions import InvalidDlArguments diff --git a/src/ytdl_sub/config/config_file.py b/src/ytdl_sub/config/config_file.py index d8f48920..197a82ce 100644 --- a/src/ytdl_sub/config/config_file.py +++ b/src/ytdl_sub/config/config_file.py @@ -1,99 +1,19 @@ import os from typing import Any -from typing import Dict -from typing import Optional -import mergedeep - -from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS +from ytdl_sub.config.config_validator import ConfigValidator +from ytdl_sub.config.preset import Preset from ytdl_sub.utils.yaml import load_yaml -from ytdl_sub.validators.strict_dict_validator import StrictDictValidator -from ytdl_sub.validators.validators import LiteralDictValidator -from ytdl_sub.validators.validators import StringValidator -class ConfigOptions(StrictDictValidator): - _required_keys = {"working_directory"} - _optional_keys = {"umask", "dl_aliases"} - - def __init__(self, name: str, value: Any): - super().__init__(name, value) - - self._working_directory = self._validate_key( - key="working_directory", validator=StringValidator - ) - self._umask = self._validate_key_if_present( - key="umask", validator=StringValidator, default="022" - ) - self._dl_aliases = self._validate_key_if_present( - key="dl_aliases", validator=LiteralDictValidator - ) - - @property - def working_directory(self) -> str: - """ - The directory to temporarily store downloaded files before moving them into their final - directory. - """ - return self._working_directory.value - - @property - def umask(self) -> Optional[str]: - """ - Umask (octal format) to apply to every created file. Defaults to "022". - """ - return self._umask.value - - @property - def dl_aliases(self) -> Optional[Dict[str, str]]: - """ - Alias definitions to shorten ``ytdl-sub dl`` arguments. For example, - - .. code-block:: yaml - - configuration: - dl_aliases: - mv: "--preset yt_music_video" - v: "--youtube.video_url" - - Simplifies - - .. code-block:: bash - - ytdl-sub dl --preset "yt_music_video" --youtube.video_url "youtube.com/watch?v=a1b2c3" - - to - - .. code-block:: bash - - ytdl-sub dl --mv --v "youtube.com/watch?v=a1b2c3" - """ - if self._dl_aliases: - return self._dl_aliases.dict - return {} - - -class ConfigFile(StrictDictValidator): +class ConfigFile(ConfigValidator): _required_keys = {"configuration", "presets"} def __init__(self, name: str, value: Any): super().__init__(name, value) - self.config_options = self._validate_key("configuration", ConfigOptions) - prebuilt_presets = PREBUILT_PRESETS - - # Make sure presets is a dictionary. Will be validated in `PresetValidator` - self.presets = self._validate_key("presets", LiteralDictValidator) - - # Ensure custom presets do not collide with prebuilt presets - for preset_name in self.presets.keys: - if preset_name in prebuilt_presets: - raise self._validation_exception( - f"preset name '{preset_name}' conflicts with a prebuilt preset" - ) - - # Merge prebuilt presets into the config so custom presets can use them - mergedeep.merge(self.presets._value, prebuilt_presets) + for preset_name, preset_dict in self.presets.dict.items(): + Preset.preset_partial_validate(config=self, name=preset_name, value=preset_dict) def initialize(self): """ diff --git a/src/ytdl_sub/config/config_validator.py b/src/ytdl_sub/config/config_validator.py new file mode 100644 index 00000000..31a15b04 --- /dev/null +++ b/src/ytdl_sub/config/config_validator.py @@ -0,0 +1,92 @@ +from typing import Any +from typing import Dict +from typing import Optional + +from mergedeep import mergedeep + +from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS +from ytdl_sub.validators.strict_dict_validator import StrictDictValidator +from ytdl_sub.validators.validators import LiteralDictValidator +from ytdl_sub.validators.validators import StringValidator + + +class ConfigOptions(StrictDictValidator): + _required_keys = {"working_directory"} + _optional_keys = {"umask", "dl_aliases"} + + def __init__(self, name: str, value: Any): + super().__init__(name, value) + + self._working_directory = self._validate_key( + key="working_directory", validator=StringValidator + ) + self._umask = self._validate_key_if_present( + key="umask", validator=StringValidator, default="022" + ) + self._dl_aliases = self._validate_key_if_present( + key="dl_aliases", validator=LiteralDictValidator + ) + + @property + def working_directory(self) -> str: + """ + The directory to temporarily store downloaded files before moving them into their final + directory. + """ + return self._working_directory.value + + @property + def umask(self) -> Optional[str]: + """ + Umask (octal format) to apply to every created file. Defaults to "022". + """ + return self._umask.value + + @property + def dl_aliases(self) -> Optional[Dict[str, str]]: + """ + Alias definitions to shorten ``ytdl-sub dl`` arguments. For example, + + .. code-block:: yaml + + configuration: + dl_aliases: + mv: "--preset yt_music_video" + v: "--youtube.video_url" + + Simplifies + + .. code-block:: bash + + ytdl-sub dl --preset "yt_music_video" --youtube.video_url "youtube.com/watch?v=a1b2c3" + + to + + .. code-block:: bash + + ytdl-sub dl --mv --v "youtube.com/watch?v=a1b2c3" + """ + if self._dl_aliases: + return self._dl_aliases.dict + return {} + + +class ConfigValidator(StrictDictValidator): + _required_keys = {"configuration", "presets"} + + def __init__(self, name: str, value: Any): + super().__init__(name, value) + self.config_options = self._validate_key("configuration", ConfigOptions) + + # Make sure presets is a dictionary. Will be validated in `PresetValidator` + self.presets = self._validate_key("presets", LiteralDictValidator) + + # Ensure custom presets do not collide with prebuilt presets + for preset_name in self.presets.keys: + if preset_name in PREBUILT_PRESETS: + raise self._validation_exception( + f"preset name '{preset_name}' conflicts with a prebuilt preset" + ) + + # Merge prebuilt presets into the config so custom presets can use them + mergedeep.merge(self.presets._value, PREBUILT_PRESETS) diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index 04cd96ef..0ee5b1ee 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -11,7 +11,7 @@ from typing import Union from mergedeep import mergedeep -from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.config.config_validator import ConfigValidator from ytdl_sub.config.preset_class_mappings import DownloadStrategyMapping from ytdl_sub.config.preset_class_mappings import PluginMapping from ytdl_sub.config.preset_options import OutputOptions @@ -33,6 +33,7 @@ from ytdl_sub.validators.validators import ListValidator from ytdl_sub.validators.validators import StringListValidator from ytdl_sub.validators.validators import StringValidator from ytdl_sub.validators.validators import Validator +from ytdl_sub.validators.validators import validation_exception PRESET_KEYS = { "preset", @@ -129,7 +130,11 @@ class Preset(StrictDictValidator): _optional_keys = PRESET_KEYS @classmethod - def preset_partial_validate(cls, config: ConfigFile, name: str, value: Any) -> None: + def preset_partial_validate(cls, config: ConfigValidator, name: str, value: Any) -> None: + # Ensure value is a dict + _ = DictValidator(name=name, value=value) + assert isinstance(value, dict) + cls._partial_validate_key(name, value, "output_options", OutputOptions) cls._partial_validate_key(name, value, "ytdl_options", YTDLOptions) cls._partial_validate_key(name, value, "overrides", Overrides) @@ -141,6 +146,15 @@ class Preset(StrictDictValidator): validator=PluginMapping.get(plugin_name).plugin_options_type, ) + parent_presets = StringListValidator(name=f"{name}.preset", value=value.get("preset", [])) + for parent_preset_name in parent_presets.list: + if parent_preset_name.value not in config.presets.keys: + raise validation_exception( + name=f"{name}.preset", + error_message=f"preset '{parent_preset_name.value}' does not exist in the " + f"provided config. Available presets: {', '.join(config.presets.keys)}", + ) + @property def _source_variables(self) -> List[str]: return Entry.source_variables() @@ -291,7 +305,7 @@ class Preset(StrictDictValidator): self.__validate_override_string_formatter_validator(validator_value) def _get_presets_to_merge( - self, parent_presets: str | List[str], seen_presets: List[str], config: ConfigFile + self, parent_presets: str | List[str], seen_presets: List[str], config: ConfigValidator ) -> List[Dict]: presets_to_merge: List[Dict] = [] @@ -326,7 +340,7 @@ class Preset(StrictDictValidator): return presets_to_merge - def __merge_parent_preset_dicts_if_present(self, config: ConfigFile): + def __merge_parent_preset_dicts_if_present(self, config: ConfigValidator): parent_preset_validator = self._validate_key_if_present( key="preset", validator=StringListValidator ) @@ -345,7 +359,7 @@ class Preset(StrictDictValidator): mergedeep.merge({}, *reversed(presets_to_merge), strategy=mergedeep.Strategy.ADDITIVE) ) - def __init__(self, config: ConfigFile, name: str, value: Any): + def __init__(self, config: ConfigValidator, name: str, value: Any): super().__init__(name=name, value=value) # Perform the merge of parent presets before validating any keys @@ -380,7 +394,7 @@ class Preset(StrictDictValidator): return self._name @classmethod - def from_dict(cls, config: ConfigFile, preset_name: str, preset_dict: Dict) -> "Preset": + def from_dict(cls, config: ConfigValidator, preset_name: str, preset_dict: Dict) -> "Preset": """ Parameters ---------- diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index d09c50fa..886ec379 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -13,7 +13,6 @@ from ytdl_sub.validators.string_formatter_validators import DictFormatterValidat from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.validators import BoolValidator -from ytdl_sub.validators.validators import DictValidator from ytdl_sub.validators.validators import LiteralDictValidator diff --git a/src/ytdl_sub/subscriptions/base_subscription.py b/src/ytdl_sub/subscriptions/base_subscription.py index 3b1405c3..d3a2cf06 100644 --- a/src/ytdl_sub/subscriptions/base_subscription.py +++ b/src/ytdl_sub/subscriptions/base_subscription.py @@ -2,7 +2,7 @@ from abc import ABC from pathlib import Path from typing import Type -from ytdl_sub.config.config_file import ConfigOptions +from ytdl_sub.config.config_validator import ConfigOptions from ytdl_sub.config.preset import Preset from ytdl_sub.config.preset import PresetPlugins from ytdl_sub.config.preset_options import OutputOptions diff --git a/src/ytdl_sub/validators/nfo_validators.py b/src/ytdl_sub/validators/nfo_validators.py index 8f1e9f38..d5cdd819 100644 --- a/src/ytdl_sub/validators/nfo_validators.py +++ b/src/ytdl_sub/validators/nfo_validators.py @@ -1,6 +1,5 @@ from abc import ABC from collections import defaultdict -from typing import Any from typing import Dict from typing import List diff --git a/src/ytdl_sub/validators/strict_dict_validator.py b/src/ytdl_sub/validators/strict_dict_validator.py index 3e93a2cf..e76f4eec 100644 --- a/src/ytdl_sub/validators/strict_dict_validator.py +++ b/src/ytdl_sub/validators/strict_dict_validator.py @@ -1,4 +1,3 @@ -from typing import Any from typing import List from typing import Set diff --git a/tests/unit/cli/test_download_args_parser.py b/tests/unit/cli/test_download_args_parser.py index 1388e7dc..d528c6c1 100644 --- a/tests/unit/cli/test_download_args_parser.py +++ b/tests/unit/cli/test_download_args_parser.py @@ -9,7 +9,7 @@ import pytest from ytdl_sub.cli.download_args_parser import DownloadArgsParser from ytdl_sub.cli.main_args_parser import MainArgs from ytdl_sub.cli.main_args_parser import parser -from ytdl_sub.config.config_file import ConfigOptions +from ytdl_sub.config.config_validator import ConfigOptions from ytdl_sub.utils.exceptions import InvalidDlArguments