Added --report option to CLI, implements #253

This commit is contained in:
Rhet Turnbull
2020-11-28 09:24:16 -08:00
parent adf2ba7678
commit d22eaf39ed
5 changed files with 256 additions and 27 deletions

View File

@@ -373,6 +373,8 @@ Options:
work with iTerm2 (use with Terminal.app).
This is faster and more reliable than the
default AppleScript interface.
--report REPORTNAME.CSV Write a CSV formatted report of all files
that were exported.
-h, --help Show this message and exit.
** Export **

View File

@@ -1413,6 +1413,12 @@ def query(
"Highly experimental alpha feature; does not work with iTerm2 (use with Terminal.app). "
"This is faster and more reliable than the default AppleScript interface.",
)
@click.option(
"--report",
metavar="REPORTNAME.CSV",
help="Write a CSV formatted report of all files that were exported.",
type=click.Path(),
)
@DB_ARGUMENT
@click.argument("dest", nargs=1, type=click.Path(exists=True))
@click.pass_obj
@@ -1505,6 +1511,7 @@ def export(
deleted_only,
use_photos_export,
use_photokit,
report,
):
""" Export photos from the Photos database.
Export path DEST is required.
@@ -1522,7 +1529,12 @@ def export(
VERBOSE = bool(verbose_)
if not os.path.isdir(dest):
sys.exit(f"DEST {dest} must be valid path")
click.echo(f"DEST {dest} must be valid path", err=True)
raise click.Abort()
if report and os.path.isdir(report):
click.echo(f"report is a directory, must be file name", err=True)
raise click.Abort()
# sanity check input args
exclusive = [
@@ -1729,8 +1741,11 @@ def export(
results_skipped = []
results_exif_updated = []
results_touched = []
results_sidecar_json = []
results_sidecar_xmp = []
results_converted = []
results_sidecar_json_written = []
results_sidecar_json_skipped = []
results_sidecar_xmp_written = []
results_sidecar_xmp_skipped = []
if verbose_:
for p in photos:
results = export_photo(
@@ -1774,8 +1789,11 @@ def export(
results_skipped.extend(results.skipped)
results_exif_updated.extend(results.exif_updated)
results_touched.extend(results.touched)
results_sidecar_json.extend(results.sidecar_json)
results_sidecar_xmp.extend(results.sidecar_xmp)
results_converted.extend(results.converted_to_jpeg)
results_sidecar_json_written.extend(results.sidecar_json_written)
results_sidecar_json_skipped.extend(results.sidecar_json_skipped)
results_sidecar_xmp_written.extend(results.sidecar_xmp_written)
results_sidecar_xmp_skipped.extend(results.sidecar_xmp_skipped)
# if convert_to_jpeg and p.isphoto and p.uti != "public.jpeg":
# for photo_file in set(
@@ -1828,8 +1846,11 @@ def export(
results_skipped.extend(results.skipped)
results_exif_updated.extend(results.exif_updated)
results_touched.extend(results.touched)
results_sidecar_json.extend(results.sidecar_json)
results_sidecar_xmp.extend(results.sidecar_xmp)
results_converted.extend(results.converted_to_jpeg)
results_sidecar_json_written.extend(results.sidecar_json_written)
results_sidecar_json_skipped.extend(results.sidecar_json_skipped)
results_sidecar_xmp_written.extend(results.sidecar_xmp_written)
results_sidecar_xmp_skipped.extend(results.sidecar_xmp_skipped)
stop_time = time.perf_counter()
# print summary results
@@ -1839,9 +1860,27 @@ def export(
# print(f"results_skipped: {results_skipped}")
# print(f"results_exif_updated: {results_exif_updated}")
# print(f"results_touched: {results_touched}")
# print(f"results_converted: {results_converted}")
# print(f"results_sidecar_json: {results_sidecar_json}")
# print(f"results_sidecar_xmp: {results_sidecar_xmp}")
if report:
verbose(f"Writing export report to {report}")
write_export_report(
report,
results_exported=results_exported,
results_new=results_new,
results_updated=results_updated,
results_skipped=results_skipped,
results_exif_updated=results_exif_updated,
results_touched=results_touched,
results_converted=results_converted,
results_sidecar_json_written=results_sidecar_json_written,
results_sidecar_json_skipped=results_sidecar_json_skipped,
results_sidecar_xmp_written=results_sidecar_xmp_written,
results_sidecar_xmp_skipped=results_sidecar_xmp_skipped,
)
if update:
photo_str_new = "photos" if len(results_new) != 1 else "photo"
photo_str_updated = "photos" if len(results_updated) != 1 else "photo"
@@ -2391,19 +2430,19 @@ def export_photo(
if photo.ismissing:
space = " " if not verbose_ else ""
verbose(f"{space}Skipping missing photo {photo.original_filename}")
return ExportResults([], [], [], [], [], [], [], [])
return ExportResults([], [], [], [], [], [], [], [], [], [], [])
elif photo.path is None:
space = " " if not verbose_ else ""
verbose(
f"{space}WARNING: photo {photo.original_filename} ({photo.uuid}) is missing but ismissing=False, "
f"skipping {photo.original_filename}"
)
return ExportResults([], [], [], [], [], [], [], [])
return ExportResults([], [], [], [], [], [], [], [], [], [], [])
elif photo.ismissing and not photo.iscloudasset and not photo.incloud:
verbose(
f"Skipping missing {photo.original_filename}: not iCloud asset or missing from cloud"
)
return ExportResults([], [], [], [], [], [], [], [])
return ExportResults([], [], [], [], [], [], [], [], [], [], [])
results_exported = []
results_new = []
@@ -2411,8 +2450,11 @@ def export_photo(
results_skipped = []
results_exif_updated = []
results_touched = []
results_sidecar_json = []
results_sidecar_xmp = []
results_converted = []
results_sidecar_json_written = []
results_sidecar_json_skipped = []
results_sidecar_xmp_written = []
results_sidecar_xmp_skipped = []
export_original = not (skip_original_if_edited and photo.hasadjustments)
@@ -2507,8 +2549,11 @@ def export_photo(
results_skipped.extend(export_results.skipped)
results_exif_updated.extend(export_results.exif_updated)
results_touched.extend(export_results.touched)
results_sidecar_json.extend(export_results.sidecar_json)
results_sidecar_xmp.extend(export_results.sidecar_xmp)
results_converted.extend(export_results.converted_to_jpeg)
results_sidecar_json_written.extend(export_results.sidecar_json_written)
results_sidecar_json_skipped.extend(export_results.sidecar_json_skipped)
results_sidecar_xmp_written.extend(export_results.sidecar_xmp_written)
results_sidecar_xmp_skipped.extend(export_results.sidecar_xmp_skipped)
if verbose_:
for exported in export_results.exported:
@@ -2580,8 +2625,19 @@ def export_photo(
results_skipped.extend(export_results_edited.skipped)
results_exif_updated.extend(export_results_edited.exif_updated)
results_touched.extend(export_results_edited.touched)
results_sidecar_json.extend(export_results_edited.sidecar_json)
results_sidecar_xmp.extend(export_results_edited.sidecar_xmp)
results_converted.extend(export_results_edited.converted_to_jpeg)
results_sidecar_json_written.extend(
export_results_edited.sidecar_json_written
)
results_sidecar_json_skipped.extend(
export_results_edited.sidecar_json_skipped
)
results_sidecar_xmp_written.extend(
export_results_edited.sidecar_xmp_written
)
results_sidecar_xmp_skipped.extend(
export_results_edited.sidecar_xmp_skipped
)
if verbose_:
for exported in export_results_edited.exported:
@@ -2602,8 +2658,11 @@ def export_photo(
results_skipped,
results_exif_updated,
results_touched,
results_sidecar_json,
results_sidecar_xmp,
results_converted,
results_sidecar_json_written,
results_sidecar_json_skipped,
results_sidecar_xmp_written,
results_sidecar_xmp_skipped,
)
@@ -2755,5 +2814,111 @@ def load_uuid_from_file(filename):
return uuid
def write_export_report(
report_file,
results_exported,
results_new,
results_updated,
results_skipped,
results_exif_updated,
results_touched,
results_converted,
results_sidecar_json_written,
results_sidecar_json_skipped,
results_sidecar_xmp_written,
results_sidecar_xmp_skipped,
):
""" write CSV report with results from export """
# Collect results for reporting
# TODO: pull this in a separate write_report function
all_results = {
result: {
"filename": result,
"exported": 0,
"new": 0,
"updated": 0,
"skipped": 0,
"exif_updated": 0,
"touched": 0,
"converted_to_jpeg": 0,
"sidecar_xmp": 0,
"sidecar_json": 0,
}
for result in results_exported
+ results_new
+ results_updated
+ results_skipped
+ results_exif_updated
+ results_touched
+ results_converted
+ results_sidecar_json_written
+ results_sidecar_json_skipped
+ results_sidecar_xmp_written
+ results_sidecar_xmp_skipped
}
for result in results_exported:
all_results[result]["exported"] = 1
for result in results_new:
all_results[result]["new"] = 1
for result in results_updated:
all_results[result]["updated"] = 1
for result in results_skipped:
all_results[result]["skipped"] = 1
for result in results_exif_updated:
all_results[result]["exif_updated"] = 1
for result in results_touched:
all_results[result]["touched"] = 1
for result in results_converted:
all_results[result]["converted_to_jpeg"] = 1
for result in results_sidecar_xmp_written:
all_results[result]["sidecar_xmp"] = 1
all_results[result]["exported"] = 1
for result in results_sidecar_xmp_skipped:
all_results[result]["sidecar_xmp"] = 1
all_results[result]["skipped"] = 1
for result in results_sidecar_json_written:
all_results[result]["sidecar_json"] = 1
all_results[result]["exported"] = 1
for result in results_sidecar_json_skipped:
all_results[result]["sidecar_json"] = 1
all_results[result]["skipped"] = 1
report_columns = [
"filename",
"exported",
"new",
"updated",
"skipped",
"exif_updated",
"touched",
"converted_to_jpeg",
"sidecar_xmp",
"sidecar_json",
]
try:
with open(report_file, "w") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=report_columns)
writer.writeheader()
for data in [result for result in all_results.values()]:
writer.writerow(data)
except IOError:
click.echo("Could not open output file for writing", err=True)
raise click.Abort()
if __name__ == "__main__":
cli() # pylint: disable=no-value-for-parameter

View File

@@ -1,4 +1,4 @@
""" version info """
__version__ = "0.37.0"
__version__ = "0.37.1"

View File

@@ -53,8 +53,11 @@ ExportResults = namedtuple(
"skipped",
"exif_updated",
"touched",
"sidecar_json",
"sidecar_xmp",
"converted_to_jpeg",
"sidecar_json_written",
"sidecar_json_skipped",
"sidecar_xmp_written",
"sidecar_xmp_skipped",
],
)
@@ -425,6 +428,9 @@ def export2(
# list of all files with utime touched (touch_file = True)
touched_files = []
# list of all files convereted to jpeg
converted_to_jpeg_files = []
# check edited and raise exception trying to export edited version of
# photo that hasn't been edited
if edited and not self.hasadjustments:
@@ -597,6 +603,7 @@ def export2(
update_updated_files = results.updated
update_skipped_files = results.skipped
touched_files = results.touched
converted_to_jpeg_files = results.converted_to_jpeg
# copy live photo associated .mov if requested
if live_photo and self.live_photo:
@@ -622,6 +629,7 @@ def export2(
update_updated_files.extend(results.updated)
update_skipped_files.extend(results.skipped)
touched_files.extend(results.touched)
converted_to_jpeg_files.extend(results.converted_to_jpeg)
# copy associated RAW image if requested
if raw_photo and self.has_raw:
@@ -648,6 +656,7 @@ def export2(
update_updated_files.extend(results.updated)
update_skipped_files.extend(results.skipped)
touched_files.extend(results.touched)
converted_to_jpeg_files.extend(results.converted_to_jpeg)
else:
# use_photo_export
exported = []
@@ -748,10 +757,10 @@ def export2(
)
# export metadata
sidecar_json_files = []
sidecar_json_files_skipped = []
sidecar_json_files_written = []
if sidecar_json:
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
sidecar_json_files.append(str(sidecar_filename))
sidecar_str = self._exiftool_json_sidecar(
use_albums_as_keywords=use_albums_as_keywords,
use_persons_as_keywords=use_persons_as_keywords,
@@ -774,6 +783,7 @@ def export2(
)
if write_sidecar:
verbose(f"Writing exiftool JSON sidecar {sidecar_filename}")
sidecar_json_files_written.append(str(sidecar_filename))
if not dry_run:
self._write_sidecar(sidecar_filename, sidecar_str)
export_db.set_sidecar_for_file(
@@ -783,11 +793,12 @@ def export2(
)
else:
verbose(f"Skipped up to date exiftool JSON sidecar {sidecar_filename}")
sidecar_json_files_skipped.append(str(sidecar_filename))
sidecar_xmp_files = []
sidecar_xmp_files_skipped = []
sidecar_xmp_files_written = []
if sidecar_xmp:
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.xmp")
sidecar_xmp_files.append(str(sidecar_filename))
sidecar_str = self._xmp_sidecar(
use_albums_as_keywords=use_albums_as_keywords,
use_persons_as_keywords=use_persons_as_keywords,
@@ -810,6 +821,7 @@ def export2(
)
if write_sidecar:
verbose(f"Writing XMP sidecar {sidecar_filename}")
sidecar_xmp_files_written.append(str(sidecar_filename))
if not dry_run:
self._write_sidecar(sidecar_filename, sidecar_str)
export_db.set_sidecar_for_file(
@@ -819,6 +831,7 @@ def export2(
)
else:
verbose(f"Skipped up to date XMP sidecar {sidecar_filename}")
sidecar_xmp_files_skipped.append(str(sidecar_filename))
# if exiftool, write the metadata
if update:
@@ -918,8 +931,11 @@ def export2(
update_skipped_files,
exif_files_updated,
touched_files,
sidecar_json_files,
sidecar_xmp_files,
converted_to_jpeg_files,
sidecar_json_files_written,
sidecar_json_files_skipped,
sidecar_xmp_files_written,
sidecar_xmp_files_skipped,
)
return results
@@ -976,6 +992,7 @@ def _export_photo(
update_new_files = []
update_skipped_files = []
touched_files = []
converted_to_jpeg_files = []
dest_str = str(dest)
dest_exists = dest.exists()
@@ -1065,6 +1082,7 @@ def _export_photo(
# use convert_to_jpeg to export the file
fileutil.convert_to_jpeg(src, dest_str, compression_quality=jpeg_quality)
converted_stat = fileutil.file_sig(dest_str)
converted_to_jpeg_files.append(dest_str)
else:
fileutil.copy(src, dest_str, norsrc=no_xattr)
@@ -1090,6 +1108,9 @@ def _export_photo(
update_skipped_files,
[],
touched_files,
converted_to_jpeg_files,
[],
[],
[],
[],
)

View File

@@ -3763,3 +3763,44 @@ def test_persons():
json_got = json.loads(result.output)
assert json_got == PERSONS_JSON
def test_export_report():
""" test export with --report option """
import glob
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V", "--report", "report.csv"],
)
assert result.exit_code == 0
assert "Writing export report" in result.output
assert os.path.exists("report.csv")
def test_export_report_not_a_file():
""" test export with --report option and bad report value """
import glob
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V", "--report", "."]
)
assert result.exit_code != 0
assert "Aborted!" in result.output