parent
f318cf6fcb
commit
119a0c212f
4 changed files with 214 additions and 3 deletions
|
|
@ -43,13 +43,28 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
||||||
# TODO pass yaml snake case name in the class somewhere, and use it for the logger
|
# TODO pass yaml snake case name in the class somewhere, and use it for the logger
|
||||||
self._logger = Logger.get(self.__class__.__name__)
|
self._logger = Logger.get(self.__class__.__name__)
|
||||||
|
|
||||||
def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]:
|
def modify_entry(self, entry: Entry) -> Optional[Entry]:
|
||||||
"""
|
"""
|
||||||
For each file downloaded, apply post processing to it.
|
For each entry downloaded, modify the entry in some way before sending it to
|
||||||
|
post-processing.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
entry:
|
entry
|
||||||
|
Entry to modify
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The entry or None, indicating not to move it to the output directory
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]:
|
||||||
|
"""
|
||||||
|
For each entry downloaded, apply post processing to it.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
entry
|
||||||
Entry to post process
|
Entry to post process
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
|
|
|
||||||
101
src/ytdl_sub/validators/regex_validator.py
Normal file
101
src/ytdl_sub/validators/regex_validator.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import re
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ytdl_sub.validators.validators import ListValidator
|
||||||
|
from ytdl_sub.validators.validators import StringValidator
|
||||||
|
|
||||||
|
|
||||||
|
class RegexValidator(StringValidator):
|
||||||
|
_expected_value_type_name = "regex"
|
||||||
|
|
||||||
|
def __init__(self, name, value):
|
||||||
|
super().__init__(name, value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._compiled_regex = re.compile(self.value)
|
||||||
|
except Exception as exc:
|
||||||
|
raise self._validation_exception(
|
||||||
|
error_message=f"invalid regex: '{self.value}'"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_capture_groups(self) -> int:
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Number of capture groups in the regex
|
||||||
|
"""
|
||||||
|
return self._compiled_regex.groups
|
||||||
|
|
||||||
|
def is_match(self, input_str: str) -> bool:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_str
|
||||||
|
String to match against the regex
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
True if input_str matches. False otherwise.
|
||||||
|
"""
|
||||||
|
return self._compiled_regex.search(input_str) is not None
|
||||||
|
|
||||||
|
def capture(self, input_str: str) -> Optional[List[str]]:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_str
|
||||||
|
String to try to regex capture from
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List of captures (will always be >= 1). None if there are no captures.
|
||||||
|
"""
|
||||||
|
if match := self._compiled_regex.search(input_str):
|
||||||
|
if len(to_return := list(match.groups())) > 0:
|
||||||
|
return to_return
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class RegexListValidator(ListValidator[RegexValidator]):
|
||||||
|
_expected_value_type_name = "regex list"
|
||||||
|
_inner_list_type = RegexValidator
|
||||||
|
|
||||||
|
def __init__(self, name, value):
|
||||||
|
super().__init__(name, value)
|
||||||
|
|
||||||
|
if len(set(reg.num_capture_groups for reg in self._list)) > 1:
|
||||||
|
raise self._validation_exception(
|
||||||
|
"each regex in a list must have the same number of capture groups"
|
||||||
|
)
|
||||||
|
|
||||||
|
def matches_any(self, input_str: str) -> bool:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_str
|
||||||
|
String to match against any regexes in the list
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
True if at least one matches. False if none match
|
||||||
|
"""
|
||||||
|
return any(reg.is_match(input_str) for reg in self._list)
|
||||||
|
|
||||||
|
def capture_any(self, input_str: str) -> Optional[List[str]]:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_str
|
||||||
|
String to try to regex capture from any of the regexes in the list
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List of captures (will always be >= 1) on the first regex that matches. None if
|
||||||
|
no regexes match.
|
||||||
|
"""
|
||||||
|
for reg in self._list:
|
||||||
|
if maybe_capture := reg.capture(input_str):
|
||||||
|
return maybe_capture
|
||||||
|
return None
|
||||||
|
|
@ -2,6 +2,7 @@ import copy
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import Generic
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
@ -94,6 +95,24 @@ class StringValidator(Validator):
|
||||||
T = TypeVar("T", bound=Validator)
|
T = TypeVar("T", bound=Validator)
|
||||||
|
|
||||||
|
|
||||||
|
class ListValidator(Validator, ABC, Generic[T]):
|
||||||
|
"""
|
||||||
|
Validates a list of objects to validate
|
||||||
|
"""
|
||||||
|
|
||||||
|
_expected_value_type = list
|
||||||
|
_expected_value_type_name = "list"
|
||||||
|
|
||||||
|
_inner_list_type: Type[T]
|
||||||
|
|
||||||
|
def __init__(self, name, value):
|
||||||
|
super().__init__(name, value)
|
||||||
|
self._list: List[T] = [
|
||||||
|
self._inner_list_type(name=f"{name}.{i+1}", value=val)
|
||||||
|
for i, val in enumerate(self._value)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class DictValidator(Validator):
|
class DictValidator(Validator):
|
||||||
"""
|
"""
|
||||||
Validates dictionary-based fields. Errors to them as 'object's since this could be validating
|
Validates dictionary-based fields. Errors to them as 'object's since this could be validating
|
||||||
|
|
|
||||||
76
tests/unit/validators/test_regex_validator.py
Normal file
76
tests/unit/validators/test_regex_validator.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ytdl_sub.utils.exceptions import ValidationException
|
||||||
|
from ytdl_sub.validators.regex_validator import RegexListValidator
|
||||||
|
from ytdl_sub.validators.regex_validator import RegexValidator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"regex_value, input_str, matches, captures",
|
||||||
|
[
|
||||||
|
("^match this$", "match this", True, None),
|
||||||
|
("^my (.+) cap", "my first cap", True, ["first"]),
|
||||||
|
(".* (.+) - (.+) two", "my other - capped two", True, ["other", "capped"]),
|
||||||
|
("failed match", "nope", False, None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_regex_validator(regex_value, input_str, matches, captures):
|
||||||
|
regex_validator = RegexValidator(name="good_regex_validator", value=regex_value)
|
||||||
|
assert regex_validator._name == "good_regex_validator"
|
||||||
|
assert regex_validator._value == regex_value
|
||||||
|
|
||||||
|
assert regex_validator.is_match(input_str=input_str) is matches
|
||||||
|
assert regex_validator.capture(input_str=input_str) == captures
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("regex_value", ["(", "??"])
|
||||||
|
def test_regex_validator_fails_bad_value(regex_value):
|
||||||
|
with pytest.raises(ValidationException, match="Validation error in fail: invalid regex"):
|
||||||
|
_ = RegexValidator(name="fail", value=regex_value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_regex_list_validator():
|
||||||
|
regex_list_validator_raw_value = ["try matching this", "try matching that", "how about this"]
|
||||||
|
|
||||||
|
regex_list = RegexListValidator(name="list val", value=regex_list_validator_raw_value)
|
||||||
|
|
||||||
|
for raw_val in regex_list_validator_raw_value:
|
||||||
|
assert regex_list.matches_any(raw_val)
|
||||||
|
assert regex_list.capture_any(raw_val) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_regex_list_validator_capture():
|
||||||
|
regex_list_validator_raw_value = ["try (.+) this", "try (.+) that", "how (.+) this"]
|
||||||
|
|
||||||
|
regex_list = RegexListValidator(name="list val", value=regex_list_validator_raw_value)
|
||||||
|
captures_test = [
|
||||||
|
("try first this", "first"),
|
||||||
|
("try second that", "second"),
|
||||||
|
("how third this", "third"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for capture_str, expected_capture in captures_test:
|
||||||
|
assert regex_list.matches_any(capture_str)
|
||||||
|
assert regex_list.capture_any(capture_str) == [expected_capture]
|
||||||
|
|
||||||
|
|
||||||
|
def test_regex_list_validator_invalid_regex():
|
||||||
|
with pytest.raises(ValidationException, match="Validation error in fail\.2: invalid regex"):
|
||||||
|
_ = RegexListValidator(name="fail", value=["first is good", "second ( bad", "third okay"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_regex_list_validator_inconsistent_capture_groups():
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationException,
|
||||||
|
match=(
|
||||||
|
"Validation error in fail: "
|
||||||
|
"each regex in a list must have the same number of capture groups"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
_ = RegexListValidator(
|
||||||
|
name="fail",
|
||||||
|
value=[
|
||||||
|
"first with one (.+) cap group",
|
||||||
|
"two (.+) has two (.) cap groups",
|
||||||
|
],
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue