Added --uuid-from-file to CLI
This commit is contained in:
@@ -138,6 +138,9 @@ Options:
|
|||||||
searches top level folders (e.g. does not
|
searches top level folders (e.g. does not
|
||||||
look at subfolders)
|
look at subfolders)
|
||||||
--uuid UUID Search for photos with UUID(s).
|
--uuid UUID Search for photos with UUID(s).
|
||||||
|
--uuid-from-file FILE Search for photos with UUID(s) loaded from
|
||||||
|
FILE. Format is a single UUID per line.
|
||||||
|
Lines preceeded with # are ignored.
|
||||||
--title TITLE Search for TITLE in title of photo.
|
--title TITLE Search for TITLE in title of photo.
|
||||||
--no-title Search for photos with no title.
|
--no-title Search for photos with no title.
|
||||||
--description DESC Search for DESC in description of photo.
|
--description DESC Search for DESC in description of photo.
|
||||||
|
|||||||
@@ -298,6 +298,15 @@ def query_options(f):
|
|||||||
multiple=True,
|
multiple=True,
|
||||||
help="Search for photos with UUID(s).",
|
help="Search for photos with UUID(s).",
|
||||||
),
|
),
|
||||||
|
o(
|
||||||
|
"--uuid-from-file",
|
||||||
|
metavar="FILE",
|
||||||
|
default=None,
|
||||||
|
multiple=False,
|
||||||
|
help="Search for photos with UUID(s) loaded from FILE. "
|
||||||
|
"Format is a single UUID per line. Lines preceeded with # are ignored.",
|
||||||
|
type=click.Path(exists=True)
|
||||||
|
),
|
||||||
o(
|
o(
|
||||||
"--title",
|
"--title",
|
||||||
metavar="TITLE",
|
metavar="TITLE",
|
||||||
@@ -891,6 +900,7 @@ def query(
|
|||||||
album,
|
album,
|
||||||
folder,
|
folder,
|
||||||
uuid,
|
uuid,
|
||||||
|
uuid_from_file,
|
||||||
title,
|
title,
|
||||||
no_title,
|
no_title,
|
||||||
description,
|
description,
|
||||||
@@ -954,6 +964,7 @@ def query(
|
|||||||
album,
|
album,
|
||||||
folder,
|
folder,
|
||||||
uuid,
|
uuid,
|
||||||
|
uuid_from_file,
|
||||||
edited,
|
edited,
|
||||||
external_edit,
|
external_edit,
|
||||||
uti,
|
uti,
|
||||||
@@ -998,6 +1009,12 @@ def query(
|
|||||||
if only_photos:
|
if only_photos:
|
||||||
ismovie = False
|
ismovie = False
|
||||||
|
|
||||||
|
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||||
|
if uuid_from_file:
|
||||||
|
uuid_list = list(uuid) # Click option is a tuple
|
||||||
|
uuid_list.extend(load_uuid_from_file(uuid_from_file))
|
||||||
|
uuid = tuple(uuid_list)
|
||||||
|
|
||||||
# below needed for to make CliRunner work for testing
|
# below needed for to make CliRunner work for testing
|
||||||
cli_db = cli_obj.db if cli_obj is not None else None
|
cli_db = cli_obj.db if cli_obj is not None else None
|
||||||
db = get_photos_db(*photos_library, db, cli_db)
|
db = get_photos_db(*photos_library, db, cli_db)
|
||||||
@@ -1247,6 +1264,7 @@ def export(
|
|||||||
album,
|
album,
|
||||||
folder,
|
folder,
|
||||||
uuid,
|
uuid,
|
||||||
|
uuid_from_file,
|
||||||
title,
|
title,
|
||||||
no_title,
|
no_title,
|
||||||
description,
|
description,
|
||||||
@@ -1329,7 +1347,7 @@ def export(
|
|||||||
VERBOSE = True if verbose_ else False
|
VERBOSE = True if verbose_ else False
|
||||||
|
|
||||||
if not os.path.isdir(dest):
|
if not os.path.isdir(dest):
|
||||||
sys.exit("DEST must be valid path")
|
sys.exit(f"DEST {dest} must be valid path")
|
||||||
|
|
||||||
# sanity check input args
|
# sanity check input args
|
||||||
exclusive = [
|
exclusive = [
|
||||||
@@ -1381,6 +1399,12 @@ def export(
|
|||||||
if only_photos:
|
if only_photos:
|
||||||
ismovie = False
|
ismovie = False
|
||||||
|
|
||||||
|
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||||
|
if uuid_from_file:
|
||||||
|
uuid_list = list(uuid) # Click option is a tuple
|
||||||
|
uuid_list.extend(load_uuid_from_file(uuid_from_file))
|
||||||
|
uuid = tuple(uuid_list)
|
||||||
|
|
||||||
# below needed for to make CliRunner work for testing
|
# below needed for to make CliRunner work for testing
|
||||||
cli_db = cli_obj.db if cli_obj is not None else None
|
cli_db = cli_obj.db if cli_obj is not None else None
|
||||||
db = get_photos_db(*photos_library, db, cli_db)
|
db = get_photos_db(*photos_library, db, cli_db)
|
||||||
@@ -2091,6 +2115,12 @@ def export_photo(
|
|||||||
)
|
)
|
||||||
return ExportResults([], [], [], [], [])
|
return ExportResults([], [], [], [], [])
|
||||||
|
|
||||||
|
results_exported = []
|
||||||
|
results_new = []
|
||||||
|
results_updated = []
|
||||||
|
results_skipped = []
|
||||||
|
results_exif_updated = []
|
||||||
|
|
||||||
filenames = get_filenames_from_template(photo, filename_template, original_name)
|
filenames = get_filenames_from_template(photo, filename_template, original_name)
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
verbose(f"Exporting {photo.filename} as {filename}")
|
verbose(f"Exporting {photo.filename} as {filename}")
|
||||||
@@ -2113,11 +2143,6 @@ def export_photo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# export the photo to each path in dest_paths
|
# export the photo to each path in dest_paths
|
||||||
results_exported = []
|
|
||||||
results_new = []
|
|
||||||
results_updated = []
|
|
||||||
results_skipped = []
|
|
||||||
results_exif_updated = []
|
|
||||||
for dest_path in dest_paths:
|
for dest_path in dest_paths:
|
||||||
export_results = photo.export2(
|
export_results = photo.export2(
|
||||||
dest_path,
|
dest_path,
|
||||||
@@ -2338,6 +2363,31 @@ def find_files_in_branch(pathname, filename):
|
|||||||
|
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
def load_uuid_from_file(filename):
|
||||||
|
""" Load UUIDs from file. Does not validate UUIDs.
|
||||||
|
Format is 1 UUID per line, any line beginning with # is ignored.
|
||||||
|
Whitespace is stripped.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
filename: file name of the file containing UUIDs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of UUIDs or empty list of no UUIDs in file
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError if file does not exist
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not pathlib.Path(filename).is_file():
|
||||||
|
raise FileNotFoundError(f"Could not find file {filename}")
|
||||||
|
|
||||||
|
uuid = []
|
||||||
|
with open(filename, "r") as uuid_file:
|
||||||
|
for line in uuid_file:
|
||||||
|
line = line.strip()
|
||||||
|
if len(line) and line[0] != "#":
|
||||||
|
uuid.append(line)
|
||||||
|
return uuid
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli() # pylint: disable=no-value-for-parameter
|
cli() # pylint: disable=no-value-for-parameter
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.31.0"
|
__version__ = "0.31.1"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
""" Test the command line interface (CLI) """
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -14,6 +16,8 @@ PHOTOS_DB_15_4 = "tests/Test-10.15.4.photoslibrary"
|
|||||||
PHOTOS_DB_15_5 = "tests/Test-10.15.5.photoslibrary"
|
PHOTOS_DB_15_5 = "tests/Test-10.15.5.photoslibrary"
|
||||||
PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary"
|
PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary"
|
||||||
|
|
||||||
|
UUID_FILE = "tests/uuid_from_file.txt"
|
||||||
|
|
||||||
CLI_OUTPUT_NO_SUBCOMMAND = [
|
CLI_OUTPUT_NO_SUBCOMMAND = [
|
||||||
"Options:",
|
"Options:",
|
||||||
"--db <Photos database path> Specify Photos database path. Path to Photos",
|
"--db <Photos database path> Specify Photos database path. Path to Photos",
|
||||||
@@ -295,6 +299,23 @@ ALBUMS_JSON = {
|
|||||||
|
|
||||||
PERSONS_JSON = {"persons": {"Katie": 3, "Suzy": 2, "_UNKNOWN_": 1, "Maria": 2}}
|
PERSONS_JSON = {"persons": {"Katie": 3, "Suzy": 2, "_UNKNOWN_": 1, "Maria": 2}}
|
||||||
|
|
||||||
|
UUID_EXPECTED_FROM_FILE = [
|
||||||
|
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||||
|
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
|
||||||
|
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91",
|
||||||
|
]
|
||||||
|
|
||||||
|
UUID_NOT_FROM_FILE = "D79B8D77-BFFC-460B-9312-034F2877D35B"
|
||||||
|
|
||||||
|
CLI_EXPORT_UUID_FROM_FILE_FILENAMES = [
|
||||||
|
"IMG_1994.JPG",
|
||||||
|
"IMG_1994.cr2",
|
||||||
|
"Tulips.jpg",
|
||||||
|
"Tulips_edited.jpeg",
|
||||||
|
"wedding.jpg",
|
||||||
|
"wedding_edited.jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
# determine if exiftool installed so exiftool tests can be skipped
|
# determine if exiftool installed so exiftool tests can be skipped
|
||||||
try:
|
try:
|
||||||
exiftool = get_exiftool_path()
|
exiftool = get_exiftool_path()
|
||||||
@@ -390,6 +411,72 @@ def test_query_uuid():
|
|||||||
assert json_expected[key_] in json_got[key_]
|
assert json_expected[key_] in json_got[key_]
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_uuid_from_file_1():
|
||||||
|
""" Test query with --uuid-from-file """
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--uuid-from-file",
|
||||||
|
UUID_FILE,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# build list of uuids we got from the output JSON
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
uuid_got = []
|
||||||
|
for photo in json_got:
|
||||||
|
uuid_got.append(photo["uuid"])
|
||||||
|
|
||||||
|
assert sorted(UUID_EXPECTED_FROM_FILE) == sorted(uuid_got)
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_uuid_from_file_2():
|
||||||
|
""" Test query with --uuid-from-file and --uuid """
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.__main__ import query
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--json",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
"--uuid-from-file",
|
||||||
|
UUID_FILE,
|
||||||
|
"--uuid",
|
||||||
|
UUID_NOT_FROM_FILE,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# build list of uuids we got from the output JSON
|
||||||
|
json_got = json.loads(result.output)
|
||||||
|
uuid_got = []
|
||||||
|
for photo in json_got:
|
||||||
|
uuid_got.append(photo["uuid"])
|
||||||
|
|
||||||
|
uuid_expected = UUID_EXPECTED_FROM_FILE.copy()
|
||||||
|
uuid_expected.append(UUID_NOT_FROM_FILE)
|
||||||
|
assert sorted(uuid_expected) == sorted(uuid_got)
|
||||||
|
|
||||||
|
|
||||||
def test_export():
|
def test_export():
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
@@ -407,6 +494,33 @@ def test_export():
|
|||||||
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_uuid_from_file():
|
||||||
|
""" Test export with --uuid-from-file """
|
||||||
|
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, PHOTOS_DB_15_5),
|
||||||
|
".",
|
||||||
|
"-V",
|
||||||
|
"--uuid-from-file",
|
||||||
|
os.path.join(cwd, UUID_FILE),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
files = glob.glob("*")
|
||||||
|
assert sorted(files) == sorted(CLI_EXPORT_UUID_FROM_FILE_FILENAMES)
|
||||||
|
|
||||||
|
|
||||||
def test_export_as_hardlink():
|
def test_export_as_hardlink():
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
@@ -2468,7 +2582,7 @@ def test_albums():
|
|||||||
|
|
||||||
|
|
||||||
def test_persons():
|
def test_persons():
|
||||||
"""Test osxphotos albums """
|
"""Test osxphotos persons """
|
||||||
import json
|
import json
|
||||||
import osxphotos
|
import osxphotos
|
||||||
import os
|
import os
|
||||||
|
|||||||
29
tests/test_cli_utils.py
Normal file
29
tests/test_cli_utils.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
""" Test utility functions in __main__.py """
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
UUID_FILE = "tests/uuid_from_file.txt"
|
||||||
|
MISSING_UUID_FILE = "tests/uuid_not_found.txt"
|
||||||
|
|
||||||
|
UUID_EXPECTED_FROM_FILE = [
|
||||||
|
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||||
|
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
|
||||||
|
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_uuid_from_file():
|
||||||
|
"""Test load_uuid_from_file function """
|
||||||
|
from osxphotos.__main__ import load_uuid_from_file
|
||||||
|
|
||||||
|
uuid_got = load_uuid_from_file(UUID_FILE)
|
||||||
|
assert uuid_got == UUID_EXPECTED_FROM_FILE
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_uuid_from_file_filenotfound():
|
||||||
|
"""Test load_uuid_from_file function raises error if file not found"""
|
||||||
|
from osxphotos.__main__ import load_uuid_from_file
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError) as err:
|
||||||
|
uuid_got = load_uuid_from_file(MISSING_UUID_FILE)
|
||||||
|
assert "Could not find" in str(err.value)
|
||||||
7
tests/uuid_from_file.txt
Normal file
7
tests/uuid_from_file.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Test file for load_uuid_from_file
|
||||||
|
E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51
|
||||||
|
6191423D-8DB8-4D4C-92BE-9BBBA308AAC4
|
||||||
|
|
||||||
|
# D79B8D77-BFFC-460B-9312-034F2877D35B
|
||||||
|
A92D9C26-3A50-4197-9388-CB5F7DB9FA91
|
||||||
|
|
||||||
Reference in New Issue
Block a user