From e8273c97521c881056aeca0f85fec224cd404538 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Tue, 31 Mar 2020 20:39:13 -0700 Subject: [PATCH] Added places, --place, --no-place to CLI, closes #87, #88 --- README.md | 13 +++-- osxphotos/__main__.py | 62 +++++++++++++++++++++- osxphotos/_version.py | 2 +- tests/test_cli.py | 116 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d93787d5..d290fe46 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ Example: `osxphotos help export` ``` Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST +Usage: __main__.py export [OPTIONS] [PHOTOS_LIBRARY]... DEST Export photos from the Photos database. Export path DEST is required. Optionally, query the Photos database using 1 or more search options; if @@ -119,11 +120,15 @@ Options: --no-title Search for photos with no title. --description DESC Search for DESC in description of photo. --no-description Search for photos with no description. + --place PLACE Search for PLACE in photo's reverse + geolocation info + --no-place Search for photos with no associated place + name info (no reverse geolocation info) --uti UTI Search for photos whose uniform type identifier (UTI) matches UTI - -i, --ignore-case Case insensitive search for title or - description. Does not apply to keyword, - person, or album. + -i, --ignore-case Case insensitive search for title, + description, or place. Does not apply to + keyword, person, or album. --edited Search for photos that have been edited. --external-edit Search for photos edited in external editor. --favorite Search for photos marked favorite. @@ -296,6 +301,8 @@ Substitution Description padded) {place.name} Place name from the photo's reverse geolocation data, as displayed in Photos +{place.country_code} The ISO country code from the photo's + reverse geolocation data {place.name.country} Country name from the photo's reverse geolocation data {place.name.state_province} State or province name from the photo's diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 9c16e069..7909ab28 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -214,6 +214,18 @@ def query_options(f): is_flag=True, help="Search for photos with no description.", ), + o( + "--place", + metavar="PLACE", + default=None, + multiple=True, + help="Search for PLACE in photo's reverse geolocation info", + ), + o( + "--no-place", + is_flag=True, + help="Search for photos with no associated place name info (no reverse geolocation info)", + ), o( "--uti", metavar="UTI", @@ -225,7 +237,7 @@ def query_options(f): "-i", "--ignore-case", is_flag=True, - help="Case insensitive search for title or description. Does not apply to keyword, person, or album.", + help="Case insensitive search for title, description, or place. Does not apply to keyword, person, or album.", ), o("--edited", is_flag=True, help="Search for photos that have been edited."), o( @@ -683,6 +695,8 @@ def query( not_selfie, panorama, not_panorama, + place, + no_place, ): """ Query the Photos database using 1 or more search options; if more than one option is provided, they are treated as "AND" @@ -720,6 +734,7 @@ def query( (hdr, not_hdr), (selfie, not_selfie), (panorama, not_panorama), + (any(place), no_place), ] # print help if no non-exclusive term or a double exclusive term is given if not any(nonexclusive + [b ^ n for b, n in exclusive]): @@ -790,6 +805,8 @@ def query( not_selfie=not_selfie, panorama=panorama, not_panorama=not_panorama, + place=place, + no_place=no_place, ) # below needed for to make CliRunner work for testing @@ -937,6 +954,8 @@ def export( panorama, not_panorama, directory, + place, + no_place, ): """ Export photos from the Photos database. Export path DEST is required. @@ -966,6 +985,7 @@ def export( (selfie, not_selfie), (panorama, not_panorama), (export_by_date, directory), + (any(place), no_place), ] if any([all(bb) for bb in exclusive]): click.echo(cli.commands["export"].get_help(ctx), err=True) @@ -1046,6 +1066,8 @@ def export( not_selfie=not_selfie, panorama=panorama, not_panorama=not_panorama, + place=place, + no_place=no_place, ) if photos: @@ -1258,6 +1280,8 @@ def _query( not_selfie=None, panorama=None, not_panorama=None, + place=None, + no_place=None, ): """ run a query against PhotosDB to extract the photos based on user supply criteria """ """ used by query and export commands """ @@ -1292,7 +1316,7 @@ def _query( if description: # search description field for text - # if more than one, find photos with all name values in description + # if more than one, find photos with all description values in description if ignore_case: # case-insensitive for d in description: @@ -1306,6 +1330,40 @@ def _query( elif no_description: photos = [p for p in photos if not p.description] + if place: + # search place.names for text matching place + # if more than one place, find photos with all place values in description + if ignore_case: + # case-insensitive + for place_name in place: + place_name = place_name.lower() + photos = [ + p + for p in photos + if p.place + and any( + pname + for pname in p.place.names + if any( + pvalue for pvalue in pname if place_name in pvalue.lower() + ) + ) + ] + else: + for place_name in place: + photos = [ + p + for p in photos + if p.place + and any( + pname + for pname in p.place.names + if any(pvalue for pvalue in pname if place_name in pvalue) + ) + ] + elif no_place: + photos = [p for p in photos if not p.place] + if edited: photos = [p for p in photos if p.hasadjustments] diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 0cbbfb41..f5d6d3cd 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.24.3" +__version__ = "0.24.4" diff --git a/tests/test_cli.py b/tests/test_cli.py index 557e118d..56728f30 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,6 +5,7 @@ CLI_PHOTOS_DB = "tests/Test-10.15.1.photoslibrary" LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary/database/photos.db" RAW_PHOTOS_DB = "tests/Test-RAW-10.15.1.photoslibrary" PLACES_PHOTOS_DB = "tests/Test-Places-Catalina-10_15_1.photoslibrary" +PLACES_PHOTOS_DB_13 = "tests/Test-Places-High-Sierra-10.13.6.photoslibrary" CLI_OUTPUT_NO_SUBCOMMAND = [ "Options:", @@ -428,3 +429,118 @@ def test_places(): assert result.exit_code == 0 json_got = json.loads(result.output) assert json_got == json.loads(CLI_PLACES_JSON) + + +def test_place_13(): + # test --place on 10.13 + import json + import os + import os.path + import osxphotos + from osxphotos.__main__ import query + + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + query, + [os.path.join(cwd, PLACES_PHOTOS_DB_13), "--json", "--place", "Adelaide"], + ) + assert result.exit_code == 0 + json_got = json.loads(result.output) + + assert len(json_got) == 1 # single element + assert json_got[0]["uuid"] == "2L6X2hv3ROWRSCU3WRRAGQ" + +def test_no_place_13(): + # test --no-place on 10.13 + import json + import os + import os.path + import osxphotos + from osxphotos.__main__ import query + + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + query, + [os.path.join(cwd, PLACES_PHOTOS_DB_13), "--json", "--no-place"], + ) + assert result.exit_code == 0 + json_got = json.loads(result.output) + + assert len(json_got) == 1 # single element + assert json_got[0]["uuid"] == "pERZk5T1Sb+XcKDFRCsGpA" + + +def test_place_15_1(): + # test --place on 10.15 + import json + import os + import os.path + import osxphotos + from osxphotos.__main__ import query + + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + query, + [os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--place", "Washington"], + ) + assert result.exit_code == 0 + json_got = json.loads(result.output) + + assert len(json_got) == 1 # single element + assert json_got[0]["uuid"] == "128FB4C6-0B16-4E7D-9108-FB2E90DA1546" + +def test_place_15_2(): + # test --place on 10.15 + import json + import os + import os.path + import osxphotos + from osxphotos.__main__ import query + + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + query, + [os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--place", "United States"], + ) + assert result.exit_code == 0 + json_got = json.loads(result.output) + + assert len(json_got) == 2 # single element + uuid = [json_got[x]["uuid"] for x in (0,1)] + assert "128FB4C6-0B16-4E7D-9108-FB2E90DA1546" in uuid + assert "FF7AFE2C-49B0-4C9B-B0D7-7E1F8B8F2F0C" in uuid + + +def test_no_place_15(): + # test --no-place on 10.15 + import json + import os + import os.path + import osxphotos + from osxphotos.__main__ import query + + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + query, + [os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--no-place"], + ) + assert result.exit_code == 0 + json_got = json.loads(result.output) + + assert len(json_got) == 1 # single element + assert json_got[0]["uuid"] == "A9B73E13-A6F2-4915-8D67-7213B39BAE9F" \ No newline at end of file