[FEATURE] Toggleable plugin field enable for all dict-based plugins

This commit is contained in:
Jesse Bannon 2024-01-10 20:48:38 -08:00
parent 2076de3074
commit 13dcfb9c10
11 changed files with 145 additions and 10 deletions

View file

@ -1,5 +1,6 @@
from abc import ABC from abc import ABC
from abc import abstractmethod from abc import abstractmethod
from functools import cached_property
from typing import Dict from typing import Dict
from typing import Generic from typing import Generic
from typing import List from typing import List
@ -8,9 +9,11 @@ from typing import Tuple
from typing import Type from typing import Type
from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.overrides import Overrides
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
from ytdl_sub.config.validators.options import TOptionsValidator from ytdl_sub.config.validators.options import TOptionsValidator
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
@ -40,6 +43,17 @@ class Plugin(BasePlugin[TOptionsValidator], Generic[TOptionsValidator], ABC):
Class to define the new plugin functionality Class to define the new plugin functionality
""" """
@cached_property
def is_enabled(self) -> bool:
"""
Returns True if enabled, False if disabled.
"""
if isinstance(self.plugin_options, ToggleableOptionsDictValidator):
return ScriptUtils.bool_formatter_output(
self.overrides.apply_formatter(self.plugin_options.enable)
)
return True
def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]: def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]:
""" """
Returns Returns

View file

@ -6,6 +6,7 @@ from typing import TypeVar
from ytdl_sub.config.plugin.plugin_operation import PluginOperation from ytdl_sub.config.plugin.plugin_operation import PluginOperation
from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator
from ytdl_sub.validators.validators import Validator from ytdl_sub.validators.validators import Validator
# pylint: disable=no-self-use # pylint: disable=no-self-use
@ -57,3 +58,25 @@ TOptionsValidator = TypeVar("TOptionsValidator", bound=OptionsValidator)
class OptionsDictValidator(StrictDictValidator, OptionsValidator, ABC): class OptionsDictValidator(StrictDictValidator, OptionsValidator, ABC):
pass pass
class ToggleableOptionsDictValidator(OptionsDictValidator):
_optional_keys = {"enable"}
def __init__(self, name, value):
assert "enable" in self._optional_keys, ""
super().__init__(name, value)
self._enable = self._validate_key(
key="enable", validator=OverridesBooleanFormatterValidator, default="False"
)
@property
def enable(self) -> OverridesBooleanFormatterValidator:
"""
:expected type: Optional[Boolean]
:description:
Whether to enable or disable this plugin when it is defined in a config. Defaults
to enabled (True).
"""
return self._enable

View file

@ -1,4 +1,3 @@
import json
from typing import Dict from typing import Dict
from typing import Optional from typing import Optional
@ -8,6 +7,7 @@ from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingException
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
@ -53,7 +53,9 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
return entry return entry
for formatter in self.plugin_options.list: for formatter in self.plugin_options.list:
out = json.loads(self.overrides.apply_formatter(formatter=formatter, entry=entry)) out = ScriptUtils.bool_formatter_output(
self.overrides.apply_formatter(formatter=formatter, entry=entry)
)
if bool(out): if bool(out):
logger.info( logger.info(
"Filtering '%s' from the filter %s evaluating to True", "Filtering '%s' from the filter %s evaluating to True",

View file

@ -1,4 +1,3 @@
import json
from typing import Dict from typing import Dict
from typing import Optional from typing import Optional
@ -8,6 +7,7 @@ from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingException
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
@ -61,7 +61,9 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
return entry return entry
for formatter in self.plugin_options.list: for formatter in self.plugin_options.list:
out = json.loads(self.overrides.apply_formatter(formatter=formatter, entry=entry)) out = ScriptUtils.bool_formatter_output(
self.overrides.apply_formatter(formatter=formatter, entry=entry)
)
if not bool(out): if not bool(out):
logger.info( logger.info(
"Filtering '%s' from the filter %s evaluating to False", "Filtering '%s' from the filter %s evaluating to False",

View file

@ -6,7 +6,7 @@ from typing import Tuple
from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.overrides import Overrides
from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.plugin.plugin import Plugin
from ytdl_sub.config.validators.options import OptionsDictValidator from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
@ -59,7 +59,7 @@ class RandomizedRangeValidator(StrictDictValidator):
return int(self.randomized_float()) return int(self.randomized_float())
class ThrottleProtectionOptions(OptionsDictValidator): class ThrottleProtectionOptions(ToggleableOptionsDictValidator):
""" """
Provides options to make ytdl-sub look more 'human-like' to protect from throttling. For Provides options to make ytdl-sub look more 'human-like' to protect from throttling. For
range-based values, a random number will be chosen within the range to avoid sleeps looking range-based values, a random number will be chosen within the range to avoid sleeps looking
@ -84,7 +84,7 @@ class ThrottleProtectionOptions(OptionsDictValidator):
subscription_download_probability: 1.0 subscription_download_probability: 1.0
""" """
_optional_keys = { _optional_keys = ToggleableOptionsDictValidator._optional_keys | {
"sleep_per_download_s", "sleep_per_download_s",
"sleep_per_subscription_s", "sleep_per_subscription_s",
"max_downloads_per_subscription", "max_downloads_per_subscription",

View file

@ -190,7 +190,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
------- -------
List of plugins defined in the subscription, initialized and ready to use. List of plugins defined in the subscription, initialized and ready to use.
""" """
return [ plugins = [
plugin_type( plugin_type(
options=plugin_options, options=plugin_options,
overrides=self.overrides, overrides=self.overrides,
@ -198,6 +198,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
) )
for plugin_type, plugin_options in self.plugins.zipped() for plugin_type, plugin_options in self.plugins.zipped()
] ]
return [plugin for plugin in plugins if plugin.is_enabled]
@classmethod @classmethod
def _cleanup_entry_files(cls, entry: Entry): def _cleanup_entry_files(cls, entry: Entry):

View file

@ -38,3 +38,15 @@ class ScriptUtils:
out = f"{{%from_json('''{dumped_json}''')}}" out = f"{{%from_json('''{dumped_json}''')}}"
return out return out
@classmethod
def bool_formatter_output(cls, output: str) -> bool:
"""
Translate formatter output to a boolean
"""
if not output or output.lower() == "false":
return False
try:
return bool(json.loads(output))
except Exception: # pylint: disable=broad-except
return True

View file

@ -92,7 +92,7 @@ class OverridesStringFormatterValidator(StringFormatterValidator):
# pylint: enable=line-too-long # pylint: enable=line-too-long
class OverridesIntegerFormatterValidator(StringFormatterValidator): class OverridesIntegerFormatterValidator(OverridesStringFormatterValidator):
_expected_value_type_name = "integer" _expected_value_type_name = "integer"
def post_process(self, resolved: str) -> str: def post_process(self, resolved: str) -> str:
@ -106,6 +106,10 @@ class OverridesIntegerFormatterValidator(StringFormatterValidator):
return resolved return resolved
class OverridesBooleanFormatterValidator(OverridesStringFormatterValidator):
_expected_value_type_name = "boolean"
class ListFormatterValidator(ListValidator[StringFormatterValidator]): class ListFormatterValidator(ListValidator[StringFormatterValidator]):
_inner_list_type = StringFormatterValidator _inner_list_type = StringFormatterValidator

View file

@ -113,7 +113,7 @@ def assert_logs(
for call_args in patched_debug.call_args_list: for call_args in patched_debug.call_args_list:
occurrences += int(expected_message in call_args.args[0]) occurrences += int(expected_message in call_args.args[0])
if expected_occurrences: if expected_occurrences is not None:
assert ( assert (
occurrences == expected_occurrences occurrences == expected_occurrences
), f"{expected_message} was expected {expected_occurrences} times, got {occurrences}" ), f"{expected_message} was expected {expected_occurrences} times, got {occurrences}"

View file

@ -1,3 +1,4 @@
import pytest
from conftest import assert_logs from conftest import assert_logs
from ytdl_sub.plugins.throttle_protection import logger as throttle_protection_logger from ytdl_sub.plugins.throttle_protection import logger as throttle_protection_logger
@ -57,3 +58,59 @@ class TestThrottleProtectionPlugin:
expected_occurrences=1, expected_occurrences=1,
): ):
_ = subscription.download(dry_run=False) _ = subscription.download(dry_run=False)
@pytest.mark.parametrize(
"disable_value",
[
"",
False,
"{tp_bool_string}",
"{tp_empty_string}",
],
)
def test_disabled(
self,
config,
subscription_name,
output_directory,
mock_download_collection_entries,
disable_value,
):
preset_dict = {
"preset": [
"Kodi Music Videos",
],
"overrides": {
"url": "https://your.name.here",
"music_video_directory": output_directory,
"tp_bool_string": "{ %bool(False) }",
"tp_empty_string": "",
},
"throttle_protection": {
"enable": disable_value,
"sleep_per_download_s": {
"min": 0.01,
"max": 0.01,
},
"sleep_per_subscription_s": {
"min": 0.02,
"max": 0.02,
},
},
}
subscription = Subscription.from_dict(
config=config,
preset_name=subscription_name,
preset_dict=preset_dict,
)
with mock_download_collection_entries(
is_youtube_channel=False, num_urls=1, is_extracted_audio=False
), assert_logs(
logger=throttle_protection_logger,
expected_message="Sleeping between downloads for %0.2f seconds",
log_level="debug",
expected_occurrences=0,
):
_ = subscription.download(dry_run=False)

View file

@ -1,5 +1,6 @@
import copy import copy
import pytest
from unit.script.conftest import single_variable_output from unit.script.conftest import single_variable_output
from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.utils.script import ScriptUtils
@ -31,3 +32,22 @@ class TestScriptUtils:
output = single_variable_output(ScriptUtils.to_script(json_dict)) output = single_variable_output(ScriptUtils.to_script(json_dict))
assert output == expected_output assert output == expected_output
@pytest.mark.parametrize(
"input_str, expected_output",
[
("", False),
("true", True),
("false", False),
("[ ]", False),
("{ }", False),
("True", True),
("False", False),
("lol not False", True),
("0", False),
("-1", True),
("1", True),
],
)
def test_bool_formatter_output(self, input_str: str, expected_output: bool):
assert ScriptUtils.bool_formatter_output(input_str) == expected_output