From e39776b51e19fcb7947ddcaa6a24eae3ea2e9ebf Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sat, 27 Aug 2022 21:05:57 -0700 Subject: [PATCH] Added --field to dump and query, #777 (#779) --- osxphotos/cli/common.py | 14 ++++++++ osxphotos/cli/dump.py | 60 +++++++++++++++++++++---------- osxphotos/cli/print_photo_info.py | 44 ++++++++++++++++++++++- osxphotos/cli/query.py | 10 ++++-- tests/test_cli.py | 53 +++++++++++++++++++++++++++ tests/test_cli_dump.py | 54 ++++++++++++++++++++++++++++ 6 files changed, 213 insertions(+), 22 deletions(-) diff --git a/osxphotos/cli/common.py b/osxphotos/cli/common.py index 9f47737f..2951605e 100644 --- a/osxphotos/cli/common.py +++ b/osxphotos/cli/common.py @@ -35,6 +35,7 @@ __all__ = [ "DB_OPTION", "DEBUG_OPTIONS", "DELETED_OPTIONS", + "FIELD_OPTION", "JSON_OPTION", "QUERY_OPTIONS", "THEME_OPTION", @@ -116,6 +117,19 @@ JSON_OPTION = click.option( help="Print output in JSON format.", ) +FIELD_OPTION = click.option( + "--field", + "-f", + metavar="FIELD TEMPLATE", + multiple=True, + nargs=2, + help="Output only specified custom fields. " + "FIELD is the name of the field and TEMPLATE is the template to use as the field value. " + "May be repeated to output multiple fields. " + "For example, to output photo uuid, name, and title: " + '`--field uuid "{uuid}" --field name "{original_name}" --field title "{title}"`.', +) + def DELETED_OPTIONS(f): o = click.option diff --git a/osxphotos/cli/dump.py b/osxphotos/cli/dump.py index 2162fe5b..731c8bd3 100644 --- a/osxphotos/cli/dump.py +++ b/osxphotos/cli/dump.py @@ -12,9 +12,16 @@ from osxphotos.phototemplate import RenderOptions from osxphotos.queryoptions import QueryOptions from .color_themes import get_default_theme -from .common import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, JSON_OPTION, get_photos_db +from .common import ( + DB_ARGUMENT, + DB_OPTION, + DELETED_OPTIONS, + FIELD_OPTION, + JSON_OPTION, + get_photos_db, +) from .list import _list_libraries -from .print_photo_info import print_photo_info +from .print_photo_info import print_photo_fields, print_photo_info from .verbose import get_verbose_console @@ -22,7 +29,7 @@ from .verbose import get_verbose_console @DB_OPTION @JSON_OPTION @DELETED_OPTIONS -@DB_ARGUMENT +@FIELD_OPTION @click.option( "--print", "print_template", @@ -35,10 +42,19 @@ from .verbose import get_verbose_console "and only the rendered TEMPLATE values are printed. " "May be repeated to print multiple template strings. ", ) +@DB_ARGUMENT @click.pass_obj @click.pass_context def dump( - ctx, cli_obj, db, json_, deleted, deleted_only, photos_library, print_template + ctx, + cli_obj, + db, + deleted, + deleted_only, + field, + json_, + photos_library, + print_template, ): """Print list of all photos & associated info from the Photos library.""" @@ -71,22 +87,28 @@ def dump( if not deleted_only: photos += photosdb.photos(movies=True) - if not print_template: + if not print_template and not field: # just dump and be done print_photo_info(photos, cli_json or json_) return - # have print template(s) - options = RenderOptions() - for p in photos: - for template in print_template: - rendered_templates, unmatched = p.render_template( - template, - options, - ) - if unmatched: - rich_click_echo(f"[warning]Unmatched template field: {unmatched}[/]") - for rendered_template in rendered_templates: - if not rendered_template: - continue - print(rendered_template) + if field: + print_photo_fields(photos, field, cli_json or json_) + + if print_template: + # have print template(s) + options = RenderOptions() + for p in photos: + for template in print_template: + rendered_templates, unmatched = p.render_template( + template, + options, + ) + if unmatched: + rich_click_echo( + f"[warning]Unmatched template field: {unmatched}[/]" + ) + for rendered_template in rendered_templates: + if not rendered_template: + continue + print(rendered_template) diff --git a/osxphotos/cli/print_photo_info.py b/osxphotos/cli/print_photo_info.py index 55aa2d0b..43bfea13 100644 --- a/osxphotos/cli/print_photo_info.py +++ b/osxphotos/cli/print_photo_info.py @@ -1,8 +1,9 @@ """print_photo_info function to print PhotoInfo objects""" import csv +import json import sys -from typing import Callable, List +from typing import Callable, List, Tuple from osxphotos.photoinfo import PhotoInfo @@ -110,3 +111,44 @@ def print_photo_info( ) for row in dump: csv_writer.writerow(row) + + +def print_photo_fields( + photos: List[PhotoInfo], fields: Tuple[Tuple[str]], json_format: bool +): + """Output custom field templates from PhotoInfo objects + + Args: + photos: List of PhotoInfo objects + fields: Tuple of Tuple of field names/field templates to output""" + keys = [f[0] for f in fields] + data = [] + for p in photos: + record = {} + for field in fields: + rendered_value, unmatched = p.render_template(field[1]) + if unmatched: + raise ValueError( + f"Unmatched template variables in field {field[0]}: {field[1]}" + ) + field_value = ( + rendered_value[0] + if len(rendered_value) == 1 + else ",".join(rendered_value) + if not json_format + else rendered_value + ) + record[field[0]] = field_value + data.append(record) + + if json_format: + print(json.dumps(data, indent=4)) + else: + # dump as CSV + csv_writer = csv.writer( + sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL + ) + # add headers + csv_writer.writerow(keys) + for record in data: + csv_writer.writerow(record.values()) diff --git a/osxphotos/cli/query.py b/osxphotos/cli/query.py index 024deb71..08d35a0a 100644 --- a/osxphotos/cli/query.py +++ b/osxphotos/cli/query.py @@ -20,6 +20,7 @@ from .common import ( DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, + FIELD_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN, QUERY_OPTIONS, @@ -27,7 +28,7 @@ from .common import ( load_uuid_from_file, ) from .list import _list_libraries -from .print_photo_info import print_photo_info +from .print_photo_info import print_photo_fields, print_photo_info from .verbose import get_verbose_console @@ -76,6 +77,7 @@ from .verbose import get_verbose_console help="Quiet output; doesn't actually print query results. " "Useful with --print and --add-to-album if you don't want to see the actual query results.", ) +@FIELD_OPTION @click.option( "--print", "print_template", @@ -113,6 +115,7 @@ def query( exif, external_edit, favorite, + field, folder, from_date, from_time, @@ -399,6 +402,9 @@ def query( err=True, ) + if field: + print_photo_fields(photos, field, cli_json or json_) + if print_template: options = RenderOptions() for p in photos: @@ -416,5 +422,5 @@ def query( continue print(rendered_template) - if not quiet: + if not quiet and not field: print_photo_info(photos, cli_json or json_, print_func=click.echo) diff --git a/tests/test_cli.py b/tests/test_cli.py index 7dd860da..6dbda7cd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8363,3 +8363,56 @@ def test_query_print_quiet(): ) assert result.exit_code == 0 assert result.output.strip() == f"uuid: {UUID_FAVORITE}" + + +def test_query_field(): + """test query --field""" + + runner = CliRunner() + cwd = os.getcwd() + with runner.isolated_filesystem(): + result = runner.invoke( + query, + [ + "--db", + os.path.join(cwd, PHOTOS_DB_15_7), + "--field", + "uuid", + "{uuid}", + "--field", + "name", + "{photo.original_filename}", + "--uuid", + UUID_FAVORITE, + ], + ) + assert result.exit_code == 0 + assert result.output.strip() == f"uuid,name\n{UUID_FAVORITE},{FILE_FAVORITE}" + + +def test_query_field_json(): + """test query --field --json""" + + runner = CliRunner() + cwd = os.getcwd() + with runner.isolated_filesystem(): + result = runner.invoke( + query, + [ + "--db", + os.path.join(cwd, PHOTOS_DB_15_7), + "--field", + "uuid", + "{uuid}", + "--field", + "name", + "{photo.original_filename}", + "--uuid", + UUID_FAVORITE, + "--json", + ], + ) + assert result.exit_code == 0 + json_results = json.loads(result.output) + assert json_results[0]["uuid"] == UUID_FAVORITE + assert json_results[0]["name"] == FILE_FAVORITE diff --git a/tests/test_cli_dump.py b/tests/test_cli_dump.py index 63e05dc5..fc280266 100644 --- a/tests/test_cli_dump.py +++ b/tests/test_cli_dump.py @@ -69,3 +69,57 @@ def test_dump_print(photos): assert result.exit_code == 0 for photo in photos: assert f"{photo.uuid}\t{photo.original_filename}" in result.output + + +def test_dump_field(photos): + """Test osxphotos dump --field""" + runner = CliRunner() + cwd = os.getcwd() + db_path = os.path.join(cwd, CLI_PHOTOS_DB) + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + dump, + [ + "--db", + db_path, + "--deleted", + "--field", + "uuid", + "{uuid}", + "--field", + "name", + "{photo.original_filename}", + ], + ) + assert result.exit_code == 0 + for photo in photos: + assert f"{photo.uuid},{photo.original_filename}" in result.output + +def test_dump_field_json(photos): + """Test osxphotos dump --field --jso""" + runner = CliRunner() + cwd = os.getcwd() + db_path = os.path.join(cwd, CLI_PHOTOS_DB) + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + dump, + [ + "--db", + db_path, + "--deleted", + "--field", + "uuid", + "{uuid}", + "--field", + "name", + "{photo.original_filename}", + "--json", + ], + ) + assert result.exit_code == 0 + json_data = {record["uuid"]: record for record in json.loads(result.output)} + for photo in photos: + assert photo.uuid in json_data + assert json_data[photo.uuid]["name"] == photo.original_filename