Compare commits

..

15 Commits

Author SHA1 Message Date
Rhet Turnbull
4678f15bc8 Version bump 2020-12-17 19:48:23 -08:00
Rhet Turnbull
a7c688cfc2 Fixed issue #296 2020-12-17 19:47:22 -08:00
Rhet Turnbull
880a9b67a1 Added additional test cases for #286, --ignore-signature 2020-12-17 15:21:34 -08:00
Rhet Turnbull
d40b16a456 Updated README.md 2020-12-16 21:56:19 -08:00
Rhet Turnbull
dcd2fde6d0 Updated CHANGELOG.md 2020-12-16 21:52:57 -08:00
Rhet Turnbull
ad860b1500 Add @finestream as a contributor 2020-12-16 21:50:47 -08:00
Rhet Turnbull
7ad4db6c15 Help text update 2020-12-16 21:48:47 -08:00
Rhet Turnbull
0f1cc7cc71 Merge pull request #295 from finestream/master
Documentation fix for #293. Thanks to @finestream
2020-12-16 21:42:06 -08:00
Rhet Turnbull
5e6a6cd5fb Updated CHANGELOG.md 2020-12-16 20:27:10 -08:00
Rhet Turnbull
e394d8e6be Implemented --ignore-signature, issue #286 2020-12-16 20:11:01 -08:00
finestream
8237bc8267 Merge pull request #1 from finestream/patch-1
Patch 1
2020-12-16 21:48:22 +01:00
finestream
e097f3aad5 Update __main__.py
Possible fix of Issue RhetTbull/osxphotos#293
2020-12-16 21:25:52 +01:00
finestream
3155045ec8 Update README.md
Fixed language
2020-12-16 21:17:08 +01:00
finestream
4f64eeb996 Update README.md
Possible documentation improvement to Issue #293
2020-12-16 20:58:04 +01:00
Rhet Turnbull
3c14ace826 Updated CHANGELOG.md 2020-12-13 22:30:14 -08:00
35 changed files with 288 additions and 21 deletions

View File

@@ -109,6 +109,15 @@
"contributions": [
"code"
]
},
{
"login": "finestream",
"name": "finestream",
"avatar_url": "https://avatars1.githubusercontent.com/u/16638513?v=4",
"profile": "https://github.com/finestream",
"contributions": [
"doc"
]
}
],
"contributorsPerLine": 7

View File

@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.38.5](https://github.com/RhetTbull/osxphotos/compare/v0.38.4...v0.38.5)
> 17 December 2020
- Patch 1 [`#1`](https://github.com/RhetTbull/osxphotos/pull/1)
- Implemented --ignore-signature, issue #286 [`e394d8e`](https://github.com/RhetTbull/osxphotos/commit/e394d8e6be7607a1668029bcb37ccb30a4fa792f)
- Update __main__.py [`e097f3a`](https://github.com/RhetTbull/osxphotos/commit/e097f3aad546b5be5eabab529bd2c35ce3056876)
- Update README.md [`4f64eeb`](https://github.com/RhetTbull/osxphotos/commit/4f64eeb996d43953eb90618465d2bd046282c4bb)
- Update README.md [`3155045`](https://github.com/RhetTbull/osxphotos/commit/3155045ec87d83285f2e66210559f4be0a10e3a2)
#### [v0.38.4](https://github.com/RhetTbull/osxphotos/compare/v0.38.3...v0.38.4)
> 14 December 2020
- Fix for issue #263 [`d5730dd`](https://github.com/RhetTbull/osxphotos/commit/d5730dd8ae92bc819b61ab4df9b10ae64e23569f)
#### [v0.38.3](https://github.com/RhetTbull/osxphotos/compare/v0.38.2...v0.38.3)
> 13 December 2020

View File

@@ -3,7 +3,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python package](https://github.com/RhetTbull/osxphotos/workflows/Python%20package/badge.svg)](https://github.com/RhetTbull/osxphotos/workflows/Python%20package/badge.svg)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
- [OSXPhotos](#osxphotos)
@@ -236,6 +236,15 @@ Options:
Deleted' folder.
--update Only export new or updated files. See notes
below on export and --update.
--ignore-signature When used with --update, ignores file
signature when updating files. This is
useful if you have processed or edited
exported photos changing the file signature
(size & modification date). In this case,
--update would normally re-export the
processed files but with --ignore-signature,
files which exist in the export directory
will not be re-exported.
--dry-run Dry run (test) the export but don't actually
export any files; most useful with
--verbose.
@@ -439,7 +448,10 @@ the export folder. If a file is changed in the export folder (for example,
you edited the exported image), osxphotos will detect this as a difference and
re-export the original image from the library thus overwriting the changes.
If using --update, the exported library should be treated as a backup, not a
working copy where you intend to make changes.
working copy where you intend to make changes. If you do edit or process the
exported files and do not want them to be overwritten withsubsequent --update,
use --ignore-signature which will match filename but not file signature when
exporting.
Note: The number of files reported for export and the number actually exported
may differ due to live photos, associated raw images, and edited photos which
@@ -462,6 +474,10 @@ will be rendered to substitute template fields with values from the photo.
For example, '{created.month}' would be replaced with the month name of the
photo creation date. e.g. 'November'.
Some options supporting templates may be repeated e.g., --keyword-template
'{label}' --keyword-template '{media_type}' to add both labels and media
types to the keywords.
The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'. Some
templates have optional modifiers in form
'{[[DELIM]+]TEMPLATE_FIELD[(PATH_SEP)][?VALUE_IF_TRUE][,[DEFAULT]]}'
@@ -729,6 +745,10 @@ Example: export photos to file structure based on 4-digit year and full name of
`osxphotos export ~/Desktop/export --directory "{created.year}/{created.month}"`
Example: export photos to file structure based on 4-digit year of photo's creation date and add keywords for media type and labels (labels are only awailable on Photos 5 and higher):
`osxphotos export ~/Desktop/export --directory "{created.year}" --keyword-template "{label}" --keyword-template "{media_type}"`
Example: export default library using 'country name/year' as output directory (but use "NoCountry/year" if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput
`osxphotos export ~/Desktop/export --directory "{place.name.country,NoCountry}/{created.year}" --person-keyword --album-keyword --keyword-template "{created.year}" --exiftool --update --verbose`
@@ -2230,6 +2250,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/agprimatic"><img src="https://avatars1.githubusercontent.com/u/4685054?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ag Primatic</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=agprimatic" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jstrine"><img src="https://avatars1.githubusercontent.com/u/33943447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonathan Strine</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jstrine" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/finestream"><img src="https://avatars1.githubusercontent.com/u/16638513?v=4?s=100" width="100px;" alt=""/><br /><sub><b>finestream</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=finestream" title="Documentation">📖</a></td>
</tr>
</table>

View File

@@ -146,6 +146,9 @@ class ExportCommand(click.Command):
+ "exported image), osxphotos will detect this as a difference and re-export the original image "
+ "from the library thus overwriting the changes. If using --update, the exported library "
+ "should be treated as a backup, not a working copy where you intend to make changes. "
+ "If you do edit or process the exported files and do not want them to be overwritten with"
+ "subsequent --update, use --ignore-signature which will match filename but not file signature when "
+ "exporting."
)
formatter.write("\n")
formatter.write_text(
@@ -174,6 +177,10 @@ which will be rendered to substitute template fields with values from the photo.
For example, '{created.month}' would be replaced with the month name of the photo creation date.
e.g. 'November'.
\n
Some options supporting templates may be repeated e.g., --keyword-template '{label}'
--keyword-template '{media_type}' to add both labels and media types to the
keywords.
\n
The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'.
Some templates have optional modifiers in form
'{[[DELIM]+]TEMPLATE_FIELD[(PATH_SEP)][?VALUE_IF_TRUE][,[DEFAULT]]}'
@@ -1218,6 +1225,15 @@ def query(
is_flag=True,
help="Only export new or updated files. See notes below on export and --update.",
)
@click.option(
"--ignore-signature",
is_flag=True,
help="When used with --update, ignores file signature when updating files. "
"This is useful if you have processed or edited exported photos changing the "
"file signature (size & modification date). In this case, --update would normally "
"re-export the processed files but with --ignore-signature, files which exist "
"in the export directory will not be re-exported.",
)
@click.option(
"--dry-run",
is_flag=True,
@@ -1496,6 +1512,7 @@ def export(
verbose,
missing,
update,
ignore_signature,
dry_run,
export_as_hardlink,
touch_file,
@@ -1621,6 +1638,7 @@ def export(
verbose = cfg.verbose
missing = cfg.missing
update = cfg.update
ignore_signature = cfg.ignore_signature
dry_run = cfg.dry_run
export_as_hardlink = cfg.export_as_hardlink
touch_file = cfg.touch_file
@@ -1714,6 +1732,7 @@ def export(
dependent_options = [
("missing", ("download_missing", "use_photos_export")),
("jpeg_quality", ("convert_to_jpeg")),
("ignore_signature", ("update")),
]
try:
cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
@@ -1932,6 +1951,7 @@ def export(
export_by_date=export_by_date,
sidecar=sidecar,
update=update,
ignore_signature=ignore_signature,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
export_edited=export_edited,
@@ -1990,6 +2010,7 @@ def export(
export_by_date=export_by_date,
sidecar=sidecar,
update=update,
ignore_signature=ignore_signature,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
export_edited=export_edited,
@@ -2560,6 +2581,7 @@ def export_photo(
export_by_date=None,
sidecar=None,
update=None,
ignore_signature=None,
export_as_hardlink=None,
overwrite=None,
export_edited=None,
@@ -2766,6 +2788,7 @@ def export_photo(
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
@@ -2872,6 +2895,7 @@ def export_photo(
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,

View File

@@ -1,5 +1,5 @@
""" version info """
__version__ = "0.38.4"
__version__ = "0.38.6"

View File

@@ -285,7 +285,8 @@ def export(
when exporting metadata with exiftool or sidecar
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
description_template: string; optional template string that will be rendered for use as photo description
returns: list of photos exported
Returns: list of photos exported
"""
# Implementation note: calls export2 to actually do the work
@@ -333,6 +334,7 @@ def export2(
keyword_template=None,
description_template=None,
update=False,
ignore_signature=False,
export_db=None,
fileutil=FileUtil,
dry_run=False,
@@ -376,6 +378,7 @@ def export2(
description_template: string; optional template string that will be rendered for use as photo description
update: (boolean, default=False); if True export will run in update mode, that is, it will
not export the photo if the current version already exists in the destination
ignore_signature: (bool, default=False), ignore file signature when used with update (look only at filename)
export_db: (ExportDB_ABC); instance of a class that conforms to ExportDB_ABC with methods
for getting/setting data related to exported files to compare update state
fileutil: (FileUtilABC); class that conforms to FileUtilABC with various file utilities
@@ -606,6 +609,7 @@ def export2(
fileutil=fileutil,
edited=edited,
jpeg_quality=jpeg_quality,
ignore_signature=ignore_signature,
)
exported_files = results.exported
update_new_files = results.new
@@ -631,6 +635,7 @@ def export2(
touch_file,
False,
fileutil=fileutil,
ignore_signature=ignore_signature,
)
exported_files.extend(results.exported)
update_new_files.extend(results.new)
@@ -657,6 +662,7 @@ def export2(
convert_to_jpeg,
fileutil=fileutil,
jpeg_quality=jpeg_quality,
ignore_signature=ignore_signature,
)
exported_files.extend(results.exported)
update_new_files.extend(results.new)
@@ -963,6 +969,7 @@ def _export_photo(
fileutil=FileUtil,
edited=False,
jpeg_quality=1.0,
ignore_signature=None,
):
"""Helper function for export()
Does the actual copy or hardlink taking the appropriate
@@ -983,6 +990,7 @@ def _export_photo(
fileutil: FileUtil class that conforms to fileutil.FileUtilABC
edited: bool; set to True if exporting edited version of photo
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
ignore_signature: bool, ignore file signature when used with update (look only at filename)
Returns:
ExportResults
@@ -1008,7 +1016,10 @@ def _export_photo(
cmp_touch, cmp_orig = False, False
if dest_exists:
# update, destination exists, but we might not need to replace it...
if exiftool:
if ignore_signature:
cmp_orig = True
cmp_touch = fileutil.cmp(src, dest, mtime1=int(self.date.timestamp()))
elif exiftool:
sig_exif = export_db.get_stat_exif_for_file(dest_str)
cmp_orig = fileutil.cmp_file_sig(dest_str, sig_exif)
sig_exif = (sig_exif[0], sig_exif[1], int(self.date.timestamp()))

View File

@@ -510,7 +510,7 @@ class PhotoTemplate:
subfield = field[9:]
if not self.photo.path:
values = []
values = [None]
else:
exif = ExifTool(self.photo.path)
exifdict = exif.asdict()
@@ -870,7 +870,7 @@ class PhotoTemplate:
"""
""" return list of values for a multi-valued template field """
values = []
values = []
if field == "album":
values = self.photo.albums
elif field == "keyword":

View File

@@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>464</integer>
<integer>485</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:42Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
<key>BackgroundJobSearch</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:41Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-10-17T23:45:33Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-10-17T23:45:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-10-17T23:45:26Z</date>
<date>2020-12-16T05:41:44Z</date>
<key>SiriPortraitDonation</key>
<date>2020-10-17T23:45:25Z</date>
<date>2020-12-16T05:41:43Z</date>
</dict>
</plist>

View File

@@ -21,7 +21,7 @@ PHOTOS_LIBRARY_PATH = "/Test-10.15.7.photoslibrary"
PHOTOS_DB_LEN = 18
PHOTOS_NOT_IN_TRASH_LEN = 16
PHOTOS_IN_TRASH_LEN = 2
PHOTOS_DB_IMPORT_SESSIONS = 12
PHOTOS_DB_IMPORT_SESSIONS = 13
KEYWORDS = [
"Kids",
@@ -93,7 +93,7 @@ UUID_DICT = {
"not_intrash": "DC99FBDD-7A52-4100-A5BB-344131646C30",
"intrash_person_keywords": "6FD38366-3BF2-407D-81FE-7153EB6125B6",
"import_session": "8846E3E6-8AC8-4857-8448-E3D025784410",
"movie": "2CE332F2-D578-4769-AEFA-7631BB77AA41",
"movie": "D1359D09-1373-4F3B-B0E3-1A4DE573E4A3",
}
UUID_PUMPKIN_FARM = [

View File

@@ -59,6 +59,8 @@ CLI_EXPORT_FILENAMES = [
"wedding_edited.jpeg",
]
CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES = ["Tulips.jpg", "wedding.jpg"]
CLI_EXPORT_FILENAMES_ALBUM = ["Pumkins1.jpg", "Pumkins2.jpg", "Pumpkins3.jpg"]
CLI_EXPORT_FILENAMES_ALBUM_UNICODE = ["IMG_4547.jpg"]
@@ -372,10 +374,10 @@ CLI_EXIFTOOL_QUICKTIME = {
"QuickTime:CreateDate": "2020:01:05 22:13:13",
"QuickTime:ModifyDate": "2020:01:05 22:13:13",
},
"2CE332F2-D578-4769-AEFA-7631BB77AA41": {
"File:FileName": "Jellyfish.mp4",
"D1359D09-1373-4F3B-B0E3-1A4DE573E4A3": {
"File:FileName": "Jellyfish1.mp4",
"XMP:Description": "Jellyfish Video",
"XMP:Title": "Jellyfish",
"XMP:Title": "Jellyfish1",
"XMP:TagsList": "Travel",
"XMP:Subject": "Travel",
"QuickTime:GPSCoordinates": "34.053345 -118.242349",
@@ -516,6 +518,12 @@ UUID_NO_LIKES = [
]
def modify_file(filename):
""" appends data to a file to modify it """
with open(filename, "ab") as fd:
fd.write(b"foo")
@pytest.fixture(autouse=True)
def reset_globals():
""" reset globals in __main__ that tests may have changed """
@@ -3787,6 +3795,184 @@ def test_export_touch_files_exiftool_update():
assert "skipped: 18" in result.output
def test_export_ignore_signature():
""" test export with --ignore-signature """
import os
from osxphotos.__main__ import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
# first, export some files
result = runner.invoke(export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V"])
assert result.exit_code == 0
# modify a couple of files
for filename in CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES:
modify_file(f"./{filename}")
# export with --update and --ignore-signature
# which should ignore the two modified files
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--update",
"--ignore-signature",
],
)
assert result.exit_code == 0
assert "exported: 0, updated: 0" in result.output
# export with --update and not --ignore-signature
# which should updated the two modified files
result = runner.invoke(
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--update"]
)
assert result.exit_code == 0
assert "updated: 2" in result.output
# run --update again, should be 0 files exported
result = runner.invoke(
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--update"]
)
assert result.exit_code == 0
assert "exported: 0, updated: 0" in result.output
def test_export_ignore_signature_sidecar():
""" test export with --ignore-signature and --sidecar """
"""
Test the following use cases:
If the metadata (in Photos) that went into the sidecar did not change, the sidecar will not be updated
If the metadata (in Photos) that went into the sidecar did change, a new sidecar is written but a new image file is not
If a sidecar does not exist for the photo, a sidecar will be written whether or not the photo file was written
"""
import osxphotos
import os
from osxphotos.__main__ import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
# first, export some files
result = runner.invoke(
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--sidecar", "XMP"]
)
assert result.exit_code == 0
# export with --update and --ignore-signature
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--update",
"--sidecar",
"XMP",
"--ignore-signature",
],
)
assert result.exit_code == 0
assert "exported: 0, updated: 0" in result.output
assert "Writing XMP sidecar" not in result.output
# modify a couple of files
for filename in CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES:
modify_file(f"./{filename}")
# export with --update and --ignore-signature
# which should ignore the two modified files
# sidecar files should not be re-written
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--update",
"--sidecar",
"XMP",
"--ignore-signature",
],
)
assert result.exit_code == 0
assert "exported: 0" in result.output
assert "Writing XMP sidecar" not in result.output
# change the sidecar data in export DB
# should result in a new sidecar being exported but not the image itself
exportdb = osxphotos.export_db.ExportDB("./.osxphotos_export.db")
for filename in CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES:
exportdb.set_sidecar_for_file(f"{filename}.xmp", "FOO", (0, 1, 2))
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--update",
"--ignore-signature",
"--sidecar",
"XMP",
],
)
assert result.exit_code == 0
assert "exported: 0, updated: 0" in result.output
assert result.output.count("Writing XMP sidecar") == len(
CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES
)
# run --update again, should be 0 files exported
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--update",
"--ignore-signature",
"--sidecar",
"XMP",
],
)
assert result.exit_code == 0
assert "exported: 0, updated: 0" in result.output
assert "Writing XMP sidecar" not in result.output
# remove XMP files and run again to verify the files get written
for filename in CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES:
os.unlink(f"./{filename}.xmp")
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--update",
"--ignore-signature",
"--sidecar",
"XMP",
],
)
assert result.exit_code == 0
assert "exported: 0, updated: 0" in result.output
assert result.output.count("Writing XMP sidecar") == len(
CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES
)
def test_labels():
"""Test osxphotos labels """
import json