[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(
|
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]:
|
) -> List[Subscription]:
|
||||||
"""
|
"""
|
||||||
Downloads all subscriptions from one or many subscription yaml files.
|
Downloads all subscriptions from one or many subscription yaml files.
|
||||||
|
|
@ -78,6 +82,8 @@ def _download_subscriptions_from_yaml_files(
|
||||||
Configuration file
|
Configuration file
|
||||||
subscription_paths
|
subscription_paths
|
||||||
Path to subscription files to download
|
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
|
update_with_info_json
|
||||||
Whether to actually download or update using existing info json
|
Whether to actually download or update using existing info json
|
||||||
dry_run
|
dry_run
|
||||||
|
|
@ -98,6 +104,12 @@ def _download_subscriptions_from_yaml_files(
|
||||||
for path in subscription_paths:
|
for path in subscription_paths:
|
||||||
subscriptions += Subscription.from_file_path(config=config, subscription_path=path)
|
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:
|
for subscription in subscriptions:
|
||||||
with subscription.exception_handling():
|
with subscription.exception_handling():
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -119,7 +131,7 @@ def _download_subscriptions_from_yaml_files(
|
||||||
exception=subscription.exception,
|
exception=subscription.exception,
|
||||||
)
|
)
|
||||||
|
|
||||||
Logger.cleanup(cleanup_error_log=False)
|
Logger.cleanup(has_error=False)
|
||||||
gc.collect() # Garbage collect after each subscription download
|
gc.collect() # Garbage collect after each subscription download
|
||||||
|
|
||||||
return subscriptions
|
return subscriptions
|
||||||
|
|
@ -225,6 +237,7 @@ def main() -> List[Subscription]:
|
||||||
subscriptions = _download_subscriptions_from_yaml_files(
|
subscriptions = _download_subscriptions_from_yaml_files(
|
||||||
config=config,
|
config=config,
|
||||||
subscription_paths=args.subscription_paths,
|
subscription_paths=args.subscription_paths,
|
||||||
|
subscription_matches=args.match,
|
||||||
update_with_info_json=args.update_with_info_json,
|
update_with_info_json=args.update_with_info_json,
|
||||||
dry_run=args.dry_run,
|
dry_run=args.dry_run,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ class MainArguments:
|
||||||
long="--suppress-transaction-log",
|
long="--suppress-transaction-log",
|
||||||
is_positional=True,
|
is_positional=True,
|
||||||
)
|
)
|
||||||
|
MATCH = CLIArgument(
|
||||||
|
short="-m",
|
||||||
|
long="--match",
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls) -> List[CLIArgument]:
|
def all(cls) -> List[CLIArgument]:
|
||||||
|
|
@ -54,6 +58,7 @@ class MainArguments:
|
||||||
cls.LOG_LEVEL,
|
cls.LOG_LEVEL,
|
||||||
cls.TRANSACTION_LOG,
|
cls.TRANSACTION_LOG,
|
||||||
cls.SUPPRESS_TRANSACTION_LOG,
|
cls.SUPPRESS_TRANSACTION_LOG,
|
||||||
|
cls.MATCH,
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@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",
|
help="do not output transaction logs to console or file",
|
||||||
default=argparse.SUPPRESS if suppress_defaults else False,
|
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:
|
try:
|
||||||
return_code = _main()
|
return_code = _main()
|
||||||
Logger.cleanup(cleanup_error_log=return_code == 0)
|
Logger.cleanup(has_error=return_code != 0)
|
||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
Logger.log_exception(exception=exc)
|
Logger.log_exception(exception=exc)
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,14 @@ class Logger:
|
||||||
finally:
|
finally:
|
||||||
redirect_stream.flush()
|
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
|
@classmethod
|
||||||
def log_exception(cls, exception: Exception, log_filepath: Optional[Path] = None):
|
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(),
|
log_filepath if log_filepath else Logger.error_log_filename(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Any time an exception occurs, dump all debug logs into the error log
|
cls._append_to_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
|
@classmethod
|
||||||
def cleanup(cls, cleanup_error_log: bool = False):
|
def cleanup(cls, has_error: bool = False):
|
||||||
"""
|
"""
|
||||||
Cleans up debug log file left behind
|
Cleans up debug log file left behind
|
||||||
"""
|
"""
|
||||||
|
|
@ -263,9 +267,11 @@ class Logger:
|
||||||
for handler in logger.handlers:
|
for handler in logger.handlers:
|
||||||
handler.close()
|
handler.close()
|
||||||
|
|
||||||
cls._DEBUG_LOGGER_FILE.close()
|
if has_error:
|
||||||
FileHandler.delete(cls.debug_log_filename())
|
cls._append_to_error_log()
|
||||||
|
else:
|
||||||
if cleanup_error_log:
|
|
||||||
cls._ERROR_LOG_FILE.close()
|
cls._ERROR_LOG_FILE.close()
|
||||||
FileHandler.delete(cls.error_log_filename())
|
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
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import List
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -22,6 +23,7 @@ from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled
|
||||||
@pytest.mark.parametrize("dry_run", [True, False])
|
@pytest.mark.parametrize("dry_run", [True, False])
|
||||||
@pytest.mark.parametrize("mock_success_output", [True, False])
|
@pytest.mark.parametrize("mock_success_output", [True, False])
|
||||||
@pytest.mark.parametrize("keep_successful_logs", [True, False])
|
@pytest.mark.parametrize("keep_successful_logs", [True, False])
|
||||||
|
@pytest.mark.parametrize("match", [[], ["Rick", "Michael"]])
|
||||||
def test_subscription_logs_write_to_file(
|
def test_subscription_logs_write_to_file(
|
||||||
persist_logs_directory: str,
|
persist_logs_directory: str,
|
||||||
persist_logs_config_factory: Callable,
|
persist_logs_config_factory: Callable,
|
||||||
|
|
@ -30,8 +32,11 @@ def test_subscription_logs_write_to_file(
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
mock_success_output: bool,
|
mock_success_output: bool,
|
||||||
keep_successful_logs: 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
|
num_runs = 2
|
||||||
|
|
||||||
config = persist_logs_config_factory(keep_successful_logs=keep_successful_logs)
|
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(
|
_download_subscriptions_from_yaml_files(
|
||||||
config=config,
|
config=config,
|
||||||
subscription_paths=subscription_paths,
|
subscription_paths=subscription_paths,
|
||||||
|
subscription_matches=match,
|
||||||
update_with_info_json=False,
|
update_with_info_json=False,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
)
|
)
|
||||||
|
|
@ -61,8 +67,8 @@ def test_subscription_logs_write_to_file(
|
||||||
return
|
return
|
||||||
# If not success, expect 2 log files for both sub errors
|
# If not success, expect 2 log files for both sub errors
|
||||||
elif not mock_success_output:
|
elif not mock_success_output:
|
||||||
assert len(log_directory_files) == (num_runs * len(subscripton_names))
|
assert len(log_directory_files) == (num_runs * len(subscription_names))
|
||||||
for log_path, subscription_name in zip(log_directory_files, subscripton_names):
|
for log_path, subscription_name in zip(log_directory_files, subscription_names):
|
||||||
subscription_log_file_name = subscription_name.lower().replace(" ", "_")
|
subscription_log_file_name = subscription_name.lower().replace(" ", "_")
|
||||||
|
|
||||||
assert bool(re.match(rf"\d\.{subscription_log_file_name}\.error\.log", log_path.name))
|
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
|
# If success and success logging, expect 3 log files
|
||||||
else:
|
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(
|
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(" ", "_")
|
subscription_log_file_name = subscription_name.lower().replace(" ", "_")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ def test_main_exit_code(mock_sys_exit, return_code: int):
|
||||||
main()
|
main()
|
||||||
|
|
||||||
assert mock_logger_cleanup.call_count == 1
|
assert mock_logger_cleanup.call_count == 1
|
||||||
assert mock_logger_cleanup.call_args.kwargs["cleanup_error_log"] == (
|
assert mock_logger_cleanup.call_args.kwargs["has_error"] == (
|
||||||
True if return_code == 0 else False
|
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_count == 1
|
||||||
assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"]
|
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["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
|
assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,8 @@ class TestLogger:
|
||||||
Logger.cleanup()
|
Logger.cleanup()
|
||||||
assert not os.path.isfile(Logger._DEBUG_LOGGER_FILE.name)
|
assert not os.path.isfile(Logger._DEBUG_LOGGER_FILE.name)
|
||||||
|
|
||||||
@pytest.mark.parametrize("clean_error_log", [True, False])
|
@pytest.mark.parametrize("has_error", [True, False])
|
||||||
def test_logger_can_be_cleaned_during_execution(self, clean_error_log: bool):
|
def test_logger_can_be_cleaned_during_execution(self, has_error: bool):
|
||||||
Logger._LOGGER_LEVEL = LoggerLevels.INFO
|
Logger._LOGGER_LEVEL = LoggerLevels.INFO
|
||||||
logger = Logger.get(name="name_test")
|
logger = Logger.get(name="name_test")
|
||||||
|
|
||||||
|
|
@ -133,11 +133,11 @@ class TestLogger:
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
Logger.log_exception(exception=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 not os.path.isfile(Logger.debug_log_filename())
|
||||||
|
|
||||||
assert clean_error_log == (not os.path.isfile(Logger.error_log_filename()))
|
assert not has_error == (not os.path.isfile(Logger.error_log_filename()))
|
||||||
if not clean_error_log:
|
if has_error:
|
||||||
with open(Logger.error_log_filename(), mode="r", encoding="utf-8") as err_file:
|
with open(Logger.error_log_filename(), mode="r", encoding="utf-8") as err_file:
|
||||||
err_logs = err_file.readlines()
|
err_logs = err_file.readlines()
|
||||||
expected = [
|
expected = [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue