parent
02d772c921
commit
e39776b51e
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user