Added --field to dump and query, #777 (#779)

This commit is contained in:
Rhet Turnbull
2022-08-27 21:05:57 -07:00
committed by GitHub
parent 02d772c921
commit e39776b51e
6 changed files with 213 additions and 22 deletions

View File

@@ -35,6 +35,7 @@ __all__ = [
"DB_OPTION", "DB_OPTION",
"DEBUG_OPTIONS", "DEBUG_OPTIONS",
"DELETED_OPTIONS", "DELETED_OPTIONS",
"FIELD_OPTION",
"JSON_OPTION", "JSON_OPTION",
"QUERY_OPTIONS", "QUERY_OPTIONS",
"THEME_OPTION", "THEME_OPTION",
@@ -116,6 +117,19 @@ JSON_OPTION = click.option(
help="Print output in JSON format.", 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): def DELETED_OPTIONS(f):
o = click.option o = click.option

View File

@@ -12,9 +12,16 @@ from osxphotos.phototemplate import RenderOptions
from osxphotos.queryoptions import QueryOptions from osxphotos.queryoptions import QueryOptions
from .color_themes import get_default_theme 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 .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 from .verbose import get_verbose_console
@@ -22,7 +29,7 @@ from .verbose import get_verbose_console
@DB_OPTION @DB_OPTION
@JSON_OPTION @JSON_OPTION
@DELETED_OPTIONS @DELETED_OPTIONS
@DB_ARGUMENT @FIELD_OPTION
@click.option( @click.option(
"--print", "--print",
"print_template", "print_template",
@@ -35,10 +42,19 @@ from .verbose import get_verbose_console
"and only the rendered TEMPLATE values are printed. " "and only the rendered TEMPLATE values are printed. "
"May be repeated to print multiple template strings. ", "May be repeated to print multiple template strings. ",
) )
@DB_ARGUMENT
@click.pass_obj @click.pass_obj
@click.pass_context @click.pass_context
def dump( 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.""" """Print list of all photos & associated info from the Photos library."""
@@ -71,22 +87,28 @@ def dump(
if not deleted_only: if not deleted_only:
photos += photosdb.photos(movies=True) photos += photosdb.photos(movies=True)
if not print_template: if not print_template and not field:
# just dump and be done # just dump and be done
print_photo_info(photos, cli_json or json_) print_photo_info(photos, cli_json or json_)
return return
# have print template(s) if field:
options = RenderOptions() print_photo_fields(photos, field, cli_json or json_)
for p in photos:
for template in print_template: if print_template:
rendered_templates, unmatched = p.render_template( # have print template(s)
template, options = RenderOptions()
options, for p in photos:
) for template in print_template:
if unmatched: rendered_templates, unmatched = p.render_template(
rich_click_echo(f"[warning]Unmatched template field: {unmatched}[/]") template,
for rendered_template in rendered_templates: options,
if not rendered_template: )
continue if unmatched:
print(rendered_template) rich_click_echo(
f"[warning]Unmatched template field: {unmatched}[/]"
)
for rendered_template in rendered_templates:
if not rendered_template:
continue
print(rendered_template)

View File

@@ -1,8 +1,9 @@
"""print_photo_info function to print PhotoInfo objects""" """print_photo_info function to print PhotoInfo objects"""
import csv import csv
import json
import sys import sys
from typing import Callable, List from typing import Callable, List, Tuple
from osxphotos.photoinfo import PhotoInfo from osxphotos.photoinfo import PhotoInfo
@@ -110,3 +111,44 @@ def print_photo_info(
) )
for row in dump: for row in dump:
csv_writer.writerow(row) 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())

View File

@@ -20,6 +20,7 @@ from .common import (
DB_ARGUMENT, DB_ARGUMENT,
DB_OPTION, DB_OPTION,
DELETED_OPTIONS, DELETED_OPTIONS,
FIELD_OPTION,
JSON_OPTION, JSON_OPTION,
OSXPHOTOS_HIDDEN, OSXPHOTOS_HIDDEN,
QUERY_OPTIONS, QUERY_OPTIONS,
@@ -27,7 +28,7 @@ from .common import (
load_uuid_from_file, load_uuid_from_file,
) )
from .list import _list_libraries 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 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. " 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.", "Useful with --print and --add-to-album if you don't want to see the actual query results.",
) )
@FIELD_OPTION
@click.option( @click.option(
"--print", "--print",
"print_template", "print_template",
@@ -113,6 +115,7 @@ def query(
exif, exif,
external_edit, external_edit,
favorite, favorite,
field,
folder, folder,
from_date, from_date,
from_time, from_time,
@@ -399,6 +402,9 @@ def query(
err=True, err=True,
) )
if field:
print_photo_fields(photos, field, cli_json or json_)
if print_template: if print_template:
options = RenderOptions() options = RenderOptions()
for p in photos: for p in photos:
@@ -416,5 +422,5 @@ def query(
continue continue
print(rendered_template) print(rendered_template)
if not quiet: if not quiet and not field:
print_photo_info(photos, cli_json or json_, print_func=click.echo) print_photo_info(photos, cli_json or json_, print_func=click.echo)

View File

@@ -8363,3 +8363,56 @@ def test_query_print_quiet():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output.strip() == f"uuid: {UUID_FAVORITE}" 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

View File

@@ -69,3 +69,57 @@ def test_dump_print(photos):
assert result.exit_code == 0 assert result.exit_code == 0
for photo in photos: for photo in photos:
assert f"{photo.uuid}\t{photo.original_filename}" in result.output 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