[FEATURE] --match flag to only run subset of subscriptions (#881)
Implements https://github.com/jmbannon/ytdl-sub/issues/880 and maybe fixes https://github.com/jmbannon/ytdl-sub/issues/827 Can now do `ytdl-sub sub --match SubA SubB` or `ytdl-sub sub --match SubA --match SubB`, which will only run subscriptions that contain `SubA` or `SubB` in their names
This commit is contained in:
parent
d80a63631a
commit
9382be5c42
7 changed files with 117 additions and 25 deletions
|
|
@ -67,7 +67,11 @@ def _maybe_write_subscription_log_file(
|
|||
|
||||
|
||||
def _download_subscriptions_from_yaml_files(
|
||||
config: ConfigFile, subscription_paths: List[str], update_with_info_json: bool, dry_run: bool
|
||||
config: ConfigFile,
|
||||
subscription_paths: List[str],
|
||||
subscription_matches: List[str],
|
||||
update_with_info_json: bool,
|
||||
dry_run: bool,
|
||||
) -> List[Subscription]:
|
||||
"""
|
||||
Downloads all subscriptions from one or many subscription yaml files.
|
||||
|
|
@ -78,6 +82,8 @@ def _download_subscriptions_from_yaml_files(
|
|||
Configuration file
|
||||
subscription_paths
|
||||
Path to subscription files to download
|
||||
subscription_matches
|
||||
Optional list of substrings to match subscription names to (only run if matched)
|
||||
update_with_info_json
|
||||
Whether to actually download or update using existing info json
|
||||
dry_run
|
||||
|
|
@ -98,6 +104,12 @@ def _download_subscriptions_from_yaml_files(
|
|||
for path in subscription_paths:
|
||||
subscriptions += Subscription.from_file_path(config=config, subscription_path=path)
|
||||
|
||||
if subscriptions and subscription_matches:
|
||||
logger.info("Filtering subscriptions by name based on --match arguments")
|
||||
subscriptions = [
|
||||
sub for sub in subscriptions if any(match in sub.name for match in subscription_matches)
|
||||
]
|
||||
|
||||
for subscription in subscriptions:
|
||||
with subscription.exception_handling():
|
||||
logger.info(
|
||||
|
|
@ -119,7 +131,7 @@ def _download_subscriptions_from_yaml_files(
|
|||
exception=subscription.exception,
|
||||
)
|
||||
|
||||
Logger.cleanup(cleanup_error_log=False)
|
||||
Logger.cleanup(has_error=False)
|
||||
gc.collect() # Garbage collect after each subscription download
|
||||
|
||||
return subscriptions
|
||||
|
|
@ -225,6 +237,7 @@ def main() -> List[Subscription]:
|
|||
subscriptions = _download_subscriptions_from_yaml_files(
|
||||
config=config,
|
||||
subscription_paths=args.subscription_paths,
|
||||
subscription_matches=args.match,
|
||||
update_with_info_json=args.update_with_info_json,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ class MainArguments:
|
|||
long="--suppress-transaction-log",
|
||||
is_positional=True,
|
||||
)
|
||||
MATCH = CLIArgument(
|
||||
short="-m",
|
||||
long="--match",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> List[CLIArgument]:
|
||||
|
|
@ -54,6 +58,7 @@ class MainArguments:
|
|||
cls.LOG_LEVEL,
|
||||
cls.TRANSACTION_LOG,
|
||||
cls.SUPPRESS_TRANSACTION_LOG,
|
||||
cls.MATCH,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
|
@ -124,6 +129,16 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults
|
|||
help="do not output transaction logs to console or file",
|
||||
default=argparse.SUPPRESS if suppress_defaults else False,
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
MainArguments.MATCH.short,
|
||||
MainArguments.MATCH.long,
|
||||
dest="match",
|
||||
nargs="+",
|
||||
action="extend",
|
||||
type=str,
|
||||
help="match subscription names to one or more substrings, and only run those subscriptions",
|
||||
default=argparse.SUPPRESS if suppress_defaults else [],
|
||||
)
|
||||
|
||||
|
||||
###################################################################################################
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def main():
|
|||
"""
|
||||
try:
|
||||
return_code = _main()
|
||||
Logger.cleanup(cleanup_error_log=return_code == 0)
|
||||
Logger.cleanup(has_error=return_code != 0)
|
||||
sys.exit(return_code)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
Logger.log_exception(exception=exc)
|
||||
|
|
|
|||
|
|
@ -210,6 +210,14 @@ class Logger:
|
|||
finally:
|
||||
redirect_stream.flush()
|
||||
|
||||
@classmethod
|
||||
def _append_to_error_log(cls):
|
||||
# Any time an exception occurs, dump all debug logs into the error log
|
||||
with open(cls.debug_log_filename(), mode="r", encoding="utf-8") as debug_logs, open(
|
||||
cls.error_log_filename(), mode="a", encoding="utf-8"
|
||||
) as error_logs:
|
||||
error_logs.writelines(debug_logs.readlines())
|
||||
|
||||
@classmethod
|
||||
def log_exception(cls, exception: Exception, log_filepath: Optional[Path] = None):
|
||||
"""
|
||||
|
|
@ -248,14 +256,10 @@ class Logger:
|
|||
log_filepath if log_filepath else Logger.error_log_filename(),
|
||||
)
|
||||
|
||||
# Any time an exception occurs, dump all debug logs into the error log
|
||||
with open(cls.debug_log_filename(), mode="r", encoding="utf-8") as debug_logs, open(
|
||||
cls.error_log_filename(), mode="a", encoding="utf-8"
|
||||
) as error_logs:
|
||||
error_logs.writelines(debug_logs.readlines())
|
||||
cls._append_to_error_log()
|
||||
|
||||
@classmethod
|
||||
def cleanup(cls, cleanup_error_log: bool = False):
|
||||
def cleanup(cls, has_error: bool = False):
|
||||
"""
|
||||
Cleans up debug log file left behind
|
||||
"""
|
||||
|
|
@ -263,9 +267,11 @@ class Logger:
|
|||
for handler in logger.handlers:
|
||||
handler.close()
|
||||
|
||||
cls._DEBUG_LOGGER_FILE.close()
|
||||
FileHandler.delete(cls.debug_log_filename())
|
||||
|
||||
if cleanup_error_log:
|
||||
if has_error:
|
||||
cls._append_to_error_log()
|
||||
else:
|
||||
cls._ERROR_LOG_FILE.close()
|
||||
FileHandler.delete(cls.error_log_filename())
|
||||
|
||||
cls._DEBUG_LOGGER_FILE.close()
|
||||
FileHandler.delete(cls.debug_log_filename())
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import re
|
|||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -22,6 +23,7 @@ from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled
|
|||
@pytest.mark.parametrize("dry_run", [True, False])
|
||||
@pytest.mark.parametrize("mock_success_output", [True, False])
|
||||
@pytest.mark.parametrize("keep_successful_logs", [True, False])
|
||||
@pytest.mark.parametrize("match", [[], ["Rick", "Michael"]])
|
||||
def test_subscription_logs_write_to_file(
|
||||
persist_logs_directory: str,
|
||||
persist_logs_config_factory: Callable,
|
||||
|
|
@ -30,8 +32,11 @@ def test_subscription_logs_write_to_file(
|
|||
dry_run: bool,
|
||||
mock_success_output: bool,
|
||||
keep_successful_logs: bool,
|
||||
match: List[str],
|
||||
):
|
||||
subscripton_names = ["Rick Astley", "Michael Jackson", "Eric Clapton"]
|
||||
subscription_names = ["Rick Astley", "Michael Jackson", "Eric Clapton"]
|
||||
if match:
|
||||
subscription_names = ["Rick Astley", "Michael Jackson"]
|
||||
num_runs = 2
|
||||
|
||||
config = persist_logs_config_factory(keep_successful_logs=keep_successful_logs)
|
||||
|
|
@ -47,6 +52,7 @@ def test_subscription_logs_write_to_file(
|
|||
_download_subscriptions_from_yaml_files(
|
||||
config=config,
|
||||
subscription_paths=subscription_paths,
|
||||
subscription_matches=match,
|
||||
update_with_info_json=False,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
|
@ -61,8 +67,8 @@ def test_subscription_logs_write_to_file(
|
|||
return
|
||||
# If not success, expect 2 log files for both sub errors
|
||||
elif not mock_success_output:
|
||||
assert len(log_directory_files) == (num_runs * len(subscripton_names))
|
||||
for log_path, subscription_name in zip(log_directory_files, subscripton_names):
|
||||
assert len(log_directory_files) == (num_runs * len(subscription_names))
|
||||
for log_path, subscription_name in zip(log_directory_files, subscription_names):
|
||||
subscription_log_file_name = subscription_name.lower().replace(" ", "_")
|
||||
|
||||
assert bool(re.match(rf"\d\.{subscription_log_file_name}\.error\.log", log_path.name))
|
||||
|
|
@ -74,9 +80,9 @@ def test_subscription_logs_write_to_file(
|
|||
)
|
||||
# If success and success logging, expect 3 log files
|
||||
else:
|
||||
assert len(log_directory_files) == (num_runs * len(subscripton_names))
|
||||
assert len(log_directory_files) == (num_runs * len(subscription_names))
|
||||
for log_file_path, subscription_name in zip(
|
||||
log_directory_files, subscripton_names * num_runs
|
||||
log_directory_files, subscription_names * num_runs
|
||||
):
|
||||
subscription_log_file_name = subscription_name.lower().replace(" ", "_")
|
||||
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ def test_main_exit_code(mock_sys_exit, return_code: int):
|
|||
main()
|
||||
|
||||
assert mock_logger_cleanup.call_count == 1
|
||||
assert mock_logger_cleanup.call_args.kwargs["cleanup_error_log"] == (
|
||||
True if return_code == 0 else False
|
||||
assert mock_logger_cleanup.call_args.kwargs["has_error"] == (
|
||||
True if return_code != 0 else False
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -107,6 +107,58 @@ def test_args_after_sub_work(mock_sys_exit, tv_show_config_path):
|
|||
assert mock_sub.call_count == 1
|
||||
assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"]
|
||||
assert mock_sub.call_args.kwargs["config"]._name == tv_show_config_path
|
||||
assert mock_sub.call_args.kwargs["subscription_matches"] == []
|
||||
assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE
|
||||
|
||||
|
||||
def test_sub_match_arguments_before(mock_sys_exit, tv_show_config_path):
|
||||
with mock_sys_exit(expected_exit_code=0), patch.object(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"ytdl-sub",
|
||||
"--match",
|
||||
"testA",
|
||||
"testB",
|
||||
"-c",
|
||||
tv_show_config_path,
|
||||
"sub",
|
||||
"--log-level",
|
||||
"verbose",
|
||||
],
|
||||
), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub:
|
||||
main()
|
||||
|
||||
assert mock_sub.call_count == 1
|
||||
assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"]
|
||||
assert mock_sub.call_args.kwargs["config"]._name == tv_show_config_path
|
||||
assert mock_sub.call_args.kwargs["subscription_matches"] == ["testA", "testB"]
|
||||
assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE
|
||||
|
||||
|
||||
def test_sub_match_arguments_after_many(mock_sys_exit, tv_show_config_path):
|
||||
with mock_sys_exit(expected_exit_code=0), patch.object(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"ytdl-sub",
|
||||
"-c",
|
||||
tv_show_config_path,
|
||||
"sub",
|
||||
"--log-level",
|
||||
"verbose",
|
||||
"--match",
|
||||
"testA",
|
||||
"--match",
|
||||
"testB",
|
||||
],
|
||||
), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub:
|
||||
main()
|
||||
|
||||
assert mock_sub.call_count == 1
|
||||
assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"]
|
||||
assert mock_sub.call_args.kwargs["config"]._name == tv_show_config_path
|
||||
assert mock_sub.call_args.kwargs["subscription_matches"] == ["testA", "testB"]
|
||||
assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ class TestLogger:
|
|||
Logger.cleanup()
|
||||
assert not os.path.isfile(Logger._DEBUG_LOGGER_FILE.name)
|
||||
|
||||
@pytest.mark.parametrize("clean_error_log", [True, False])
|
||||
def test_logger_can_be_cleaned_during_execution(self, clean_error_log: bool):
|
||||
@pytest.mark.parametrize("has_error", [True, False])
|
||||
def test_logger_can_be_cleaned_during_execution(self, has_error: bool):
|
||||
Logger._LOGGER_LEVEL = LoggerLevels.INFO
|
||||
logger = Logger.get(name="name_test")
|
||||
|
||||
|
|
@ -133,11 +133,11 @@ class TestLogger:
|
|||
except ValueError as exc:
|
||||
Logger.log_exception(exception=exc)
|
||||
|
||||
Logger.cleanup(cleanup_error_log=clean_error_log)
|
||||
Logger.cleanup(has_error=has_error)
|
||||
assert not os.path.isfile(Logger.debug_log_filename())
|
||||
|
||||
assert clean_error_log == (not os.path.isfile(Logger.error_log_filename()))
|
||||
if not clean_error_log:
|
||||
assert not has_error == (not os.path.isfile(Logger.error_log_filename()))
|
||||
if has_error:
|
||||
with open(Logger.error_log_filename(), mode="r", encoding="utf-8") as err_file:
|
||||
err_logs = err_file.readlines()
|
||||
expected = [
|
||||
|
|
|
|||
Loading…
Reference in a new issue