Feature post command options 1142 (#1145)
* Added --post-command-break/catch * Added --post-command-break/catch * Added --post-command-error and tests * Fixed help text for --post-command-error
This commit is contained in:
parent
8fb47d9c40
commit
2875b45d6e
@ -1,5 +1,7 @@
|
|||||||
"""export command for osxphotos CLI"""
|
"""export command for osxphotos CLI"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
@ -9,7 +11,7 @@ import shlex
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Iterable, List, Optional, Tuple
|
from typing import Any, Callable, Iterable, List, Literal, Optional, Tuple
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
@ -647,7 +649,7 @@ from .verbose import get_verbose_console, verbose_print
|
|||||||
"If present, this file will be read after the export is completed and any rules found in the file "
|
"If present, this file will be read after the export is completed and any rules found in the file "
|
||||||
"will be added to the list of rules to keep. "
|
"will be added to the list of rules to keep. "
|
||||||
"This file uses the same format as a .gitignore file and should contain one rule per line; "
|
"This file uses the same format as a .gitignore file and should contain one rule per line; "
|
||||||
"lines starting with a `#` will be ignored. "
|
"lines starting with a `#` will be ignored. ",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--add-exported-to-album",
|
"--add-exported-to-album",
|
||||||
@ -683,11 +685,22 @@ from .verbose import get_verbose_console, verbose_print
|
|||||||
"COMMAND is an osxphotos template string, for example: '--post-command exported \"echo {filepath|shell_quote} >> {export_dir}/exported.txt\"', "
|
"COMMAND is an osxphotos template string, for example: '--post-command exported \"echo {filepath|shell_quote} >> {export_dir}/exported.txt\"', "
|
||||||
"which appends the full path of all exported files to the file 'exported.txt'. "
|
"which appends the full path of all exported files to the file 'exported.txt'. "
|
||||||
"You can run more than one command by repeating the '--post-command' option with different arguments. "
|
"You can run more than one command by repeating the '--post-command' option with different arguments. "
|
||||||
|
"See also --post-command-error and --post-function."
|
||||||
"See Post Command below.",
|
"See Post Command below.",
|
||||||
type=click.Tuple(
|
type=click.Tuple(
|
||||||
[click.Choice(POST_COMMAND_CATEGORIES, case_sensitive=False), TemplateString()]
|
[click.Choice(POST_COMMAND_CATEGORIES, case_sensitive=False), TemplateString()]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--post-command-error",
|
||||||
|
metavar="ACTION",
|
||||||
|
help="Specify either `continue` or `break` for ACTION to control behavior when a post-command fails. "
|
||||||
|
"If `continue`, osxphotos will log the error and continue processing. "
|
||||||
|
"If `break`, osxphotos will stop processing any additional --post-command commands for the current photo "
|
||||||
|
"but will continue with the export. "
|
||||||
|
"Without --post-command-error, osxphotos will abort the export if a post-command encounters an error. ",
|
||||||
|
type=click.Choice(["continue", "break"], case_sensitive=False),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--post-function",
|
"--post-function",
|
||||||
metavar="filename.py::function",
|
metavar="filename.py::function",
|
||||||
@ -910,6 +923,7 @@ def export(
|
|||||||
place,
|
place,
|
||||||
portrait,
|
portrait,
|
||||||
post_command,
|
post_command,
|
||||||
|
post_command_error,
|
||||||
post_function,
|
post_function,
|
||||||
preview,
|
preview,
|
||||||
preview_if_missing,
|
preview_if_missing,
|
||||||
@ -1138,6 +1152,7 @@ def export(
|
|||||||
place = cfg.place
|
place = cfg.place
|
||||||
portrait = cfg.portrait
|
portrait = cfg.portrait
|
||||||
post_command = cfg.post_command
|
post_command = cfg.post_command
|
||||||
|
post_command_error = cfg.post_command_error
|
||||||
post_function = cfg.post_function
|
post_function = cfg.post_function
|
||||||
preview = cfg.preview
|
preview = cfg.preview
|
||||||
preview_if_missing = cfg.preview_if_missing
|
preview_if_missing = cfg.preview_if_missing
|
||||||
@ -1575,7 +1590,7 @@ def export(
|
|||||||
export_dir=dest,
|
export_dir=dest,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
exiftool_path=exiftool_path,
|
exiftool_path=exiftool_path,
|
||||||
export_db=export_db,
|
on_error=post_command_error,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2590,7 +2605,7 @@ def collect_files_to_keep(
|
|||||||
KEEP_RULEs = []
|
KEEP_RULEs = []
|
||||||
|
|
||||||
# parse .osxphotos_keep file if it exists
|
# parse .osxphotos_keep file if it exists
|
||||||
keep_file : pathlib.Path = export_dir / ".osxphotos_keep"
|
keep_file: pathlib.Path = export_dir / ".osxphotos_keep"
|
||||||
if keep_file.is_file():
|
if keep_file.is_file():
|
||||||
for line in keep_file.read_text().splitlines():
|
for line in keep_file.read_text().splitlines():
|
||||||
line = line.rstrip("\r\n")
|
line = line.rstrip("\r\n")
|
||||||
@ -2604,10 +2619,10 @@ def collect_files_to_keep(
|
|||||||
KEEP_RULEs.append(k.replace(export_dir_str, ""))
|
KEEP_RULEs.append(k.replace(export_dir_str, ""))
|
||||||
else:
|
else:
|
||||||
KEEP_RULEs.append(k)
|
KEEP_RULEs.append(k)
|
||||||
|
|
||||||
if not KEEP_RULEs:
|
if not KEEP_RULEs:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
# have some rules to apply
|
# have some rules to apply
|
||||||
matcher = osxphotos.gitignorefile.parse_pattern_list(KEEP_RULEs, export_dir)
|
matcher = osxphotos.gitignorefile.parse_pattern_list(KEEP_RULEs, export_dir)
|
||||||
keepers = []
|
keepers = []
|
||||||
@ -2841,16 +2856,18 @@ def write_extended_attributes(
|
|||||||
|
|
||||||
|
|
||||||
def run_post_command(
|
def run_post_command(
|
||||||
photo,
|
photo: osxphotos.PhotoInfo,
|
||||||
post_command,
|
post_command: tuple[tuple[str, str]],
|
||||||
export_results,
|
export_results: ExportResults,
|
||||||
export_dir,
|
export_dir: str | pathlib.Path,
|
||||||
dry_run,
|
dry_run: bool,
|
||||||
exiftool_path,
|
exiftool_path: str,
|
||||||
export_db,
|
on_error: Literal["break", "continue"] | None,
|
||||||
verbose,
|
verbose: Callable[[Any], None],
|
||||||
):
|
):
|
||||||
|
"""Run --post-command commands"""
|
||||||
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
|
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
|
||||||
|
|
||||||
for category, command_template in post_command:
|
for category, command_template in post_command:
|
||||||
files = getattr(export_results, category)
|
files = getattr(export_results, category)
|
||||||
for f in files:
|
for f in files:
|
||||||
@ -2864,7 +2881,6 @@ def run_post_command(
|
|||||||
if command:
|
if command:
|
||||||
verbose(f'Running command: "{command}"')
|
verbose(f'Running command: "{command}"')
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
args = shlex.split(command)
|
|
||||||
cwd = pathlib.Path(f).parent
|
cwd = pathlib.Path(f).parent
|
||||||
run_error = None
|
run_error = None
|
||||||
run_results = None
|
run_results = None
|
||||||
@ -2873,11 +2889,18 @@ def run_post_command(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
run_error = e
|
run_error = e
|
||||||
finally:
|
finally:
|
||||||
run_error = run_error or run_results.returncode
|
returncode = run_results.returncode if run_results else None
|
||||||
if run_error:
|
if run_error or returncode:
|
||||||
rich_echo_error(
|
# there was an error running the command
|
||||||
f'[error]Error running command "{command}": {run_error}'
|
error_str = f'Error running command "{command}": return code: {returncode}, exception: {run_error}'
|
||||||
)
|
rich_echo_error(f"[error]{error_str}[/]")
|
||||||
|
if not on_error:
|
||||||
|
# no error handling specified, raise exception
|
||||||
|
raise RuntimeError(error_str)
|
||||||
|
if on_error == "break":
|
||||||
|
# break out of loop and return
|
||||||
|
return
|
||||||
|
# else on_error must be continue
|
||||||
|
|
||||||
|
|
||||||
def render_and_validate_report(report: str, exiftool_path: str, export_dir: str) -> str:
|
def render_and_validate_report(report: str, exiftool_path: str, export_dir: str) -> str:
|
||||||
|
|||||||
@ -8644,14 +8644,67 @@ def test_export_post_command_bad_command():
|
|||||||
".",
|
".",
|
||||||
"--post-command",
|
"--post-command",
|
||||||
"exported",
|
"exported",
|
||||||
"foobar {filepath.name|shell_quote} >> {export_dir}/exported.txt",
|
"false",
|
||||||
"--name",
|
"--name",
|
||||||
"Park",
|
"Park",
|
||||||
"--skip-original-if-edited",
|
"--skip-original-if-edited",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
assert result.exit_code != 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_post_command_bad_command_continue():
|
||||||
|
"""Test --post-command with bad command with --post-command-error=continue"""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
cli_main,
|
||||||
|
[
|
||||||
|
"export",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||||
|
".",
|
||||||
|
"--post-command",
|
||||||
|
"exported",
|
||||||
|
"false",
|
||||||
|
"--post-command-error",
|
||||||
|
"continue",
|
||||||
|
"--name",
|
||||||
|
"wedding",
|
||||||
|
],
|
||||||
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert 'Error running command "foobar' in result.output
|
assert result.output.count("Error running command") == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_post_command_bad_command_break():
|
||||||
|
"""Test --post-command with bad command with --post-command-error=break"""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
cli_main,
|
||||||
|
[
|
||||||
|
"export",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||||
|
".",
|
||||||
|
"--post-command",
|
||||||
|
"exported",
|
||||||
|
"false",
|
||||||
|
"--post-command-error",
|
||||||
|
"break",
|
||||||
|
"--name",
|
||||||
|
"wedding",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.count("Error running command") == 1
|
||||||
|
|
||||||
|
|
||||||
def test_export_post_command_bad_option_1():
|
def test_export_post_command_bad_option_1():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user