Added PhotoInfo.export(); closes #10

This commit is contained in:
Rhet Turnbull 2019-12-14 10:29:06 -08:00
parent d5a5bd41b3
commit 800daf3658
46 changed files with 1354 additions and 33 deletions

View File

@ -5,15 +5,14 @@
## What is osxphotos?
OSXPhotos provides the ability to interact with and query Apple's Photos.app library database on MacOS. Using this module you can query the Photos database for information about the photos stored in a Photos library on your Mac--for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
OSXPhotos provides the ability to interact with and query Apple's Photos.app library database on MacOS. Using this module you can query the Photos database for information about the photos stored in a Photos library on your Mac--for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
**NOTE**: OSXPhotos currently only supports image files -- e.g. it does not handle movies.
## Supported operating systems
Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0 and MacOS 10.14.5, 10.14.6 / Photos 4.0. Requires python >= 3.6
Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0, MacOS 10.14.5, 10.14.6 / Photos 4.0, MacOS 10.15.1 / Photos 5.0. Requires python >= 3.6
**NOTE**: Alpha support for Mac OS 10.15.0 / Photos 5.0. Photos 5.0 uses a new database format which required rewrite of much of the code for this module. If you find bugs, please open an [issue](https://github.com/RhetTbull/osxphotos/issues/).
This module will read Photos databases for any supported version on any supported OS version. E.g. you can read a database created with Photos 4.0 on MacOS 10.14 on a machine running MacOS 10.12
@ -409,11 +408,28 @@ Returns latitude and longitude as a tuple of floats (latitude, longitude). If l
#### `to_json()`
Returns a JSON representation of all photo info
Examples:
#### `export(self, *args, edited=False, overwrite=False, increment=True)`
Export photo from the Photos library to another destination on disk.
- First argument of *args must be valid destination path (or exception raised).
- Second argument of *args (optional): name of picture; if not provided, will use current filename
- edited: boolean; if True (default=False), will export the edited version of the photo (or raise exception if no edited version)
- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist
- increment: boolean; if True (default=True), will increment file name until a non-existant name is found
If overwrite=False and increment=False, export will fail if destination file already exists
Returns the full path to the exported file
**Implementation Note**: Because the usual python file copy methods don't preserve all the metadata available on MacOS, export uses /usr/bin/ditto to do the copy for export. ditto preserves most metadata such as extended attributes, permissions, ACLs, etc.
### Examples
```python
# assumes photosdb is a PhotosDB object (see above)
import osxphotos
photosdb = osxphotos.PhotosDB()
photos=photosdb.photos()
for p in photos:
print(
p.uuid(),
@ -431,10 +447,42 @@ for p in photos:
)
```
```python
""" Export all photos to ~/Desktop/export
If file has been edited, export the edited version,
otherwise, export the original version """
import os.path
import osxphotos
photosdb = osxphotos.PhotosDB()
photos = photosdb.photos()
export_path = os.path.expanduser("~/Desktop/export")
for p in photos:
if not p.ismissing():
if p.hasadjustments():
exported = p.export(export_path, edited=True)
else:
exported = p.export(export_path)
print(f"Exported {p.filename()} to {exported}")
else:
print(f"Skipping missing photo: {p.filename()}")
```
## History
This project started as a command line utility, `photosmeta`, available at [photosmeta](https://github.com/RhetTbull/photosmeta) This module converts the photosmeta Photos library query functionality into a module.
## Contributing
Contributing is easy! If you find bugs or want to suggest additional features/changes, please open an [issue](https://github.com/RhetTbull/osxphotos/issues/).
I'll gladly consider pull requests for bug fixes or feature implementations.
If you have an interesting example that shows usage of this module, submit an issue or pull request and I'll include it or link to it.
## Implementation Notes
@ -450,7 +498,7 @@ Apple does provide a framework ([PhotoKit](https://developer.apple.com/documenta
- [Click](https://pypi.org/project/click/)
## Acknowledgements
This project was inspired by photo-export by Patrick Fältström see: (https://github.com/patrikhson/photo-export) Copyright (c) 2015 Patrik Fältström paf@frobbit.se
This project was originally inspired by photo-export by Patrick Fältström see: (https://github.com/patrikhson/photo-export) Copyright (c) 2015 Patrik Fältström paf@frobbit.se
To interact with the Photos app, I use [py-applescript]( https://github.com/rdhyee/py-applescript) by "Raymond Yee / rdhyee". Rather than import this module, I included the entire module
(which is published as public domain code) in a private module to prevent ambiguity with

22
examples/export.py Normal file
View File

@ -0,0 +1,22 @@
""" Export all photos to ~/Desktop/export
If file has been edited, export the edited version,
otherwise, export the original version """
import os.path
import osxphotos
photosdb = osxphotos.PhotosDB()
photos = photosdb.photos()
export_path = os.path.expanduser("~/Desktop/export")
for p in photos:
if not p.ismissing():
if p.hasadjustments():
exported = p.export(export_path, edited=True)
else:
exported = p.export(export_path)
print(f"Exported {p.filename()} to {exported}")
else:
print(f"Skipping missing photo: {p.filename()}")

View File

@ -2,6 +2,7 @@ import glob
import json
import logging
import os.path
import pathlib
import platform
import sqlite3
import subprocess
@ -1414,7 +1415,13 @@ class PhotoInfo:
return self._info["keywords"]
def name(self):
""" (deprecated) name / title of picture """
# TODO: add warning on deprecation
return self._info["name"]
def title(self):
""" name / title of picture """
# TODO: Update documentation and tests to use title
return self._info["name"]
def uuid(self):
@ -1457,6 +1464,126 @@ class PhotoInfo:
""" returns (latitude, longitude) as float in degrees or None """
return (self._latitude(), self._longitude())
def export(self, *args, edited=False, overwrite=False, increment=True):
""" export photo """
""" first argument must be valid destination path (or exception raised) """
""" second argument (optional): name of picture; if not provided, will use current filename """
""" if edited=True (default=False), will export the edited version of the photo (or raise exception if no edited version) """
""" if overwrite=True (default=False), will overwrite files if they alreay exist """
""" if increment=True (default=True), will increment file name until a non-existant name is found """
""" if overwrite=False and increment=False, export will fail if destination file already exists """
""" returns the full path to the exported file """
# TODO: find better way to do *args
# maybe dest, *filename?
# check arguments and get destination path and filename (if provided)
dest = None # destination path
filename = None # photo filename
if not args:
# need at least one arg (destination)
raise TypeError("Must pass destination as first argument")
else:
if len(args) > 2:
raise TypeError(
"Too many positional arguments. Should be at most two: destination, filename."
)
else:
# verify destination is a valid path
dest = args[0]
if dest is None:
raise ValueError("Destination must not be None")
elif not os.path.isdir(dest):
raise FileNotFoundError("Invalid path passed to export")
if len(args) == 2:
# second arg is filename of picture
filename = args[1]
else:
# no filename provided so use the default
# if edited file requested, use filename but add _edited
# need to use file extension from edited file as Photos saves a jpeg once edited
if edited:
# verify we have a valid path_edited and use that to get filename
if not self.path_edited():
raise FileNotFoundError(
f"edited=True but path_edited is none; hasadjustments: {self.hasadjustments()}"
)
edited_name = Path(self.path_edited()).name
edited_suffix = Path(edited_name).suffix
filename = (
Path(self.filename()).stem + "_edited" + edited_suffix
)
else:
filename = self.filename()
# get path to source file and verify it's not None and is valid file
# TODO: how to handle ismissing or not hasadjustments and edited=True cases?
if edited:
if not self.hasadjustments():
logging.warning(
"Attempting to export edited photo but hasadjustments=False"
)
if self.path_edited() is not None:
src = self.path_edited()
else:
raise FileNotFoundError(
f"edited=True but path_edited is none; hasadjustments: {self.hasadjustments()}"
)
else:
if self.ismissing():
logging.warning(
f"Attempting to export photo with ismissing=True: path = {self.path()}"
)
if self.path() is None:
logging.warning(
f"Attempting to export photo but path is None: ismissing = {self.ismissing()}"
)
raise FileNotFoundError("Cannot export photo if path is None")
else:
src = self.path()
if not os.path.isfile(src):
raise FileNotFoundError(f"{src} does not appear to exist")
dest = pathlib.Path(dest)
filename = pathlib.Path(filename)
dest = dest / filename
# check to see if file exists and if so, add (1), (2), etc until we find one that works
if increment and not overwrite:
count = 1
dest_new = dest
while dest_new.exists():
dest_new = dest.parent / f"{dest.stem} ({count}){dest.suffix}"
count += 1
dest = dest_new
logging.debug(
f"exporting {src} to {dest}, overwrite={overwrite}, incremetn={increment}, dest exists: {dest.exists()}"
)
# if overwrite==False and #increment==False, export should fail if file exists
if dest.exists() and not overwrite and not increment:
raise FileExistsError(
f"destination exists ({dest}); overwrite={overwrite}, increment={increment}"
)
# if error on copy, subprocess will raise CalledProcessError
try:
subprocess.run(
["/usr/bin/ditto", src, dest], check=True, stderr=subprocess.PIPE
)
except subprocess.CalledProcessError as e:
logging.critical(
f"ditto returned error: {e.returncode} {e.stderr.decode(sys.getfilesystemencoding()).rstrip()}"
)
raise e
return str(dest)
def _longitude(self):
""" Returns longitude, in degrees """
return self._info["longitude"]

View File

@ -1,4 +1,4 @@
""" version info """
__version__ = "0.14.21"
__version__ = "0.15.0"

View File

@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>4178</integer>
<integer>423</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>2019-12-08T05:40:32Z</date>
<date>2019-12-14T18:19:30Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2019-12-08T05:40:32Z</date>
<date>2019-12-14T18:19:29Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2019-12-08T05:40:32Z</date>
<date>2019-12-14T18:19:30Z</date>
<key>BackgroundJobSearch</key>
<date>2019-12-08T05:40:32Z</date>
<date>2019-12-14T18:19:30Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2019-12-08T05:40:31Z</date>
<date>2019-12-14T18:19:28Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2019-12-08T05:40:32Z</date>
<date>0000-12-30T00:00:00Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2019-12-08T05:40:44Z</date>
<date>2019-12-14T18:19:28Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2019-12-07T19:48:13Z</date>
<date>2019-12-14T18:19:28Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2019-12-08T05:40:33Z</date>
<date>2019-12-10T06:45:58Z</date>
<key>SiriPortraitDonation</key>
<date>2019-12-08T05:40:32Z</date>
<date>0000-12-30T00:00:00Z</date>
</dict>
</plist>

View File

@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>FaceIDModelLastGenerationKey</key>
<date>2019-12-08T05:40:33Z</date>
<date>2019-12-10T06:45:58Z</date>
<key>LastContactClassificationKey</key>
<date>2019-12-08T05:40:34Z</date>
<date>2019-12-10T06:46:00Z</date>
</dict>
</plist>

View File

@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>IncrementalPersonProcessingStage</key>
<integer>0</integer>
<integer>6</integer>
<key>PersonBuilderLastMinimumFaceGroupSizeForCreatingMergeCandidates</key>
<integer>15</integer>
<key>PersonBuilderMergeCandidatesEnabled</key>

View File

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2019-12-08T18:06:37Z</date>
<key>coalescePayloadVersion</key>
<integer>1</integer>
<key>currentPayloadVersion</key>
<integer>1</integer>
<key>snapshotDate</key>

View File

@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2019-10-27T15:36:05Z</date>
<date>2019-12-08T18:06:37Z</date>
<key>coalescePayloadVersion</key>
<integer>1</integer>
<key>currentPayloadVersion</key>

View File

@ -42,6 +42,21 @@ ALBUM_DICT = {
"Test Album": 2,
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
UUID_DICT = {
"missing": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C",
"favorite": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"not_favorite": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C",
"hidden": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C",
"not_hidden": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"has_adjustments": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"no_adjustments": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"location": "DC99FBDD-7A52-4100-A5BB-344131646C30",
"no_location": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"external_edit": "DC99FBDD-7A52-4100-A5BB-344131646C30",
"no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
}
def test_init():
# test named argument
@ -197,7 +212,7 @@ def test_missing():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"])
photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
assert len(photos) == 1
p = photos[0]
assert p.path() == None
@ -208,7 +223,7 @@ def test_favorite():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"])
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
assert len(photos) == 1
p = photos[0]
assert p.favorite() == True
@ -218,7 +233,7 @@ def test_not_favorite():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"])
photos = photosdb.photos(uuid=[UUID_DICT["not_favorite"]])
assert len(photos) == 1
p = photos[0]
assert p.favorite() == False
@ -228,7 +243,7 @@ def test_hidden():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"])
photos = photosdb.photos(uuid=[UUID_DICT["hidden"]])
assert len(photos) == 1
p = photos[0]
assert p.hidden() == True
@ -238,7 +253,7 @@ def test_not_hidden():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"])
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
assert len(photos) == 1
p = photos[0]
assert p.hidden() == False
@ -249,7 +264,7 @@ def test_location_1():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["DC99FBDD-7A52-4100-A5BB-344131646C30"])
photos = photosdb.photos(uuid=[UUID_DICT["location"]])
assert len(photos) == 1
p = photos[0]
lat, lon = p.location()
@ -262,7 +277,7 @@ def test_location_2():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"])
photos = photosdb.photos(uuid=[UUID_DICT["no_location"]])
assert len(photos) == 1
p = photos[0]
lat, lon = p.location()
@ -275,7 +290,7 @@ def test_hasadjustments1():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"])
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
assert len(photos) == 1
p = photos[0]
assert p.hasadjustments() == True
@ -286,7 +301,7 @@ def test_hasadjustments2():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"])
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
assert len(photos) == 1
p = photos[0]
assert p.hasadjustments() == False
@ -297,7 +312,7 @@ def test_external_edit1():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["DC99FBDD-7A52-4100-A5BB-344131646C30"])
photos = photosdb.photos(uuid=[UUID_DICT["external_edit"]])
assert len(photos) == 1
p = photos[0]
@ -309,7 +324,7 @@ def test_external_edit2():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"])
photos = photosdb.photos(uuid=[UUID_DICT["no_external_edit"]])
assert len(photos) == 1
p = photos[0]
@ -386,3 +401,338 @@ def test_get_library_path():
lib_path = photosdb.get_library_path()
assert lib_path.endswith(PHOTOS_LIBRARY_PATH)
def test_export_1():
# test basic export
# get an unedited image and export it using default filename
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
assert got_dest == expected_dest
assert os.path.isfile(got_dest)
# remove the temporary file
os.remove(got_dest)
def test_export_2():
# test export with user provided filename
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-2-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename)
assert got_dest == expected_dest
assert os.path.isfile(got_dest)
# remove the temporary file
os.remove(got_dest)
def test_export_3():
# test file already exists and test increment=True (default)
import os
import os.path
import pathlib
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest = os.path.join(dest, filename)
expected_dest_2 = os.path.join(dest, filename2)
got_dest = photos[0].export(dest)
got_dest_2 = photos[0].export(dest)
assert got_dest_2 == expected_dest_2
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
os.remove(got_dest_2)
def test_export_4():
# test user supplied file already exists and test increment=True (default)
import os
import os.path
import pathlib
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-2-test-{timestamp}.jpg"
filename2 = f"osxphotos-export-2-test-{timestamp} (1).jpg"
expected_dest = os.path.join(dest, filename)
expected_dest_2 = os.path.join(dest, filename2)
got_dest = photos[0].export(dest, filename)
got_dest_2 = photos[0].export(dest, filename)
assert got_dest_2 == expected_dest_2
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
os.remove(got_dest_2)
def test_export_5():
# test file already exists and test increment=True (default)
# and overwrite = True
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
got_dest_2 = photos[0].export(dest, overwrite=True)
assert got_dest_2 == got_dest
assert got_dest_2 == expected_dest
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
def test_export_6():
# test user supplied file already exists and test increment=True (default)
# and overwrite = True
import os
import os.path
import pathlib
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename)
got_dest_2 = photos[0].export(dest, filename, overwrite=True)
assert got_dest_2 == got_dest
assert got_dest_2 == expected_dest
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
def test_export_7():
# test file already exists and test increment=False (not default), overwrite=False (default)
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
with pytest.raises(Exception) as e:
# try to export again with increment = False
assert photos[0].export(dest, increment=False)
assert e.type == type(FileExistsError())
# remove the temporary file
os.remove(got_dest)
def test_export_8():
# try to export missing file
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest)
assert e.type == type(FileNotFoundError())
def test_export_9():
# try to export edited file that's not edited
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest, edited=True)
assert e.type == type(FileNotFoundError())
def test_export_10():
# try to export edited file that's not edited and name provided
# should raise exception
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest, filename, edited=True)
assert e.type == type(FileNotFoundError())
def test_export_11():
# export edited file with name provided
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename, edited=True)
assert got_dest == expected_dest
# remove the temporary file
os.remove(got_dest)
def test_export_12():
# export edited file with default name
import os
import os.path
import pathlib
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
edited_name = pathlib.Path(photos[0].path_edited()).name
edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename()).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)
assert got_dest == expected_dest
# remove the temporary file
os.remove(got_dest)
def test_export_13():
# export to invalid destination
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
# create a folder that doesn't exist
i = 0
while os.path.isdir(dest):
dest = os.path.join(dest, str(i))
i += 1
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest)
assert e.type == type(FileNotFoundError())

View File

@ -0,0 +1,392 @@
import pytest
from osxphotos import _UNKNOWN_PERSON
# TODO: put some of this code into a pre-function
PHOTOS_DB = "./tests/Test-10.15.1.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.15.1.photoslibrary/database/Photos.sqlite"
PHOTOS_LIBRARY_PATH = "/Test-10.15.1.photoslibrary"
KEYWORDS = [
"Kids",
"wedding",
"flowers",
"England",
"London",
"London 2018",
"St. James's Park",
"UK",
"United Kingdom",
]
# Photos 5 includes blank person for detected face
PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
ALBUMS = [
"Pumpkin Farm",
"Test Album",
] # Note: there are 2 albums named "Test Album" for testing duplicate album names
KEYWORDS_DICT = {
"Kids": 4,
"wedding": 2,
"flowers": 1,
"England": 1,
"London": 1,
"London 2018": 1,
"St. James's Park": 1,
"UK": 1,
"United Kingdom": 1,
}
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
ALBUM_DICT = {
"Pumpkin Farm": 3,
"Test Album": 2,
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
UUID_DICT = {
"missing": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C",
"favorite": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"not_favorite": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C",
"hidden": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C",
"not_hidden": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"has_adjustments": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"no_adjustments": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"location": "DC99FBDD-7A52-4100-A5BB-344131646C30",
"no_location": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"external_edit": "DC99FBDD-7A52-4100-A5BB-344131646C30",
"no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
}
def test_export_1():
# test basic export
# get an unedited image and export it using default filename
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
assert got_dest == expected_dest
assert os.path.isfile(got_dest)
# remove the temporary file
os.remove(got_dest)
def test_export_2():
# test export with user provided filename
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-2-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename)
assert got_dest == expected_dest
assert os.path.isfile(got_dest)
# remove the temporary file
os.remove(got_dest)
def test_export_3():
# test file already exists and test increment=True (default)
import os
import os.path
import pathlib
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest = os.path.join(dest, filename)
expected_dest_2 = os.path.join(dest, filename2)
got_dest = photos[0].export(dest)
got_dest_2 = photos[0].export(dest)
assert got_dest_2 == expected_dest_2
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
os.remove(got_dest_2)
def test_export_4():
# test user supplied file already exists and test increment=True (default)
import os
import os.path
import pathlib
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-2-test-{timestamp}.jpg"
filename2 = f"osxphotos-export-2-test-{timestamp} (1).jpg"
expected_dest = os.path.join(dest, filename)
expected_dest_2 = os.path.join(dest, filename2)
got_dest = photos[0].export(dest, filename)
got_dest_2 = photos[0].export(dest, filename)
assert got_dest_2 == expected_dest_2
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
os.remove(got_dest_2)
def test_export_5():
# test file already exists and test increment=True (default)
# and overwrite = True
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
got_dest_2 = photos[0].export(dest, overwrite=True)
assert got_dest_2 == got_dest
assert got_dest_2 == expected_dest
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
def test_export_6():
# test user supplied file already exists and test increment=True (default)
# and overwrite = True
import os
import os.path
import pathlib
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename)
got_dest_2 = photos[0].export(dest, filename, overwrite=True)
assert got_dest_2 == got_dest
assert got_dest_2 == expected_dest
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
def test_export_7():
# test file already exists and test increment=False (not default), overwrite=False (default)
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
with pytest.raises(Exception) as e:
# try to export again with increment = False
assert photos[0].export(dest, increment=False)
assert e.type == type(FileExistsError())
# remove the temporary file
os.remove(got_dest)
def test_export_8():
# try to export missing file
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest)
assert e.type == type(FileNotFoundError())
def test_export_9():
# try to export edited file that's not edited
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest, edited=True)
assert e.type == type(FileNotFoundError())
def test_export_10():
# try to export edited file that's not edited and name provided
# should raise exception
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest, filename, edited=True)
assert e.type == type(FileNotFoundError())
def test_export_11():
# export edited file with name provided
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename, edited=True)
assert got_dest == expected_dest
# remove the temporary file
os.remove(got_dest)
def test_export_12():
# export edited file with default name
import os
import os.path
import pathlib
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
edited_name = pathlib.Path(photos[0].path_edited()).name
edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename()).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)
assert got_dest == expected_dest
# remove the temporary file
os.remove(got_dest)
def test_export_13():
# export to invalid destination
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
# create a folder that doesn't exist
i = 0
while os.path.isdir(dest):
dest = os.path.join(dest, str(i))
i += 1
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest)
assert e.type == type(FileNotFoundError())

View File

@ -0,0 +1,378 @@
import pytest
from osxphotos import _UNKNOWN_PERSON
# TODO: put some of this code into a pre-function
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "/Test-10.14.6.photoslibrary"
KEYWORDS = [
"Kids",
"wedding",
"flowers",
"England",
"London",
"London 2018",
"St. James's Park",
"UK",
"United Kingdom",
]
PERSONS = ["Katie", "Suzy", "Maria"]
ALBUMS = ["Pumpkin Farm", "Test Album", "Test Album (1)"]
KEYWORDS_DICT = {
"Kids": 4,
"wedding": 2,
"flowers": 1,
"England": 1,
"London": 1,
"London 2018": 1,
"St. James's Park": 1,
"UK": 1,
"United Kingdom": 1,
}
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1}
ALBUM_DICT = {"Pumpkin Farm": 3, "Test Album": 1, "Test Album (1)": 1}
UUID_DICT = {
"missing": "od0fmC7NQx+ayVr+%i06XA",
"has_adjustments": "6bxcNnzRQKGnK4uPrCJ9UQ",
"no_adjustments": "15uNd7%8RguTEgNPKHfTWw",
"export": "15uNd7%8RguTEgNPKHfTWw",
}
def test_export_1():
# test basic export
# get an unedited image and export it using default filename
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
assert got_dest == expected_dest
assert os.path.isfile(got_dest)
# remove the temporary file
os.remove(got_dest)
def test_export_2():
# test export with user provided filename
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-2-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename)
assert got_dest == expected_dest
assert os.path.isfile(got_dest)
# remove the temporary file
os.remove(got_dest)
def test_export_3():
# test file already exists and test increment=True (default)
import os
import os.path
import pathlib
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest = os.path.join(dest, filename)
expected_dest_2 = os.path.join(dest, filename2)
got_dest = photos[0].export(dest)
got_dest_2 = photos[0].export(dest)
assert got_dest_2 == expected_dest_2
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
os.remove(got_dest_2)
def test_export_4():
# test user supplied file already exists and test increment=True (default)
import os
import os.path
import pathlib
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-2-test-{timestamp}.jpg"
filename2 = f"osxphotos-export-2-test-{timestamp} (1).jpg"
expected_dest = os.path.join(dest, filename)
expected_dest_2 = os.path.join(dest, filename2)
got_dest = photos[0].export(dest, filename)
got_dest_2 = photos[0].export(dest, filename)
assert got_dest_2 == expected_dest_2
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
os.remove(got_dest_2)
def test_export_5():
# test file already exists and test increment=True (default)
# and overwrite = True
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
got_dest_2 = photos[0].export(dest, overwrite=True)
assert got_dest_2 == got_dest
assert got_dest_2 == expected_dest
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
def test_export_6():
# test user supplied file already exists and test increment=True (default)
# and overwrite = True
import os
import os.path
import pathlib
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename)
got_dest_2 = photos[0].export(dest, filename, overwrite=True)
assert got_dest_2 == got_dest
assert got_dest_2 == expected_dest
assert os.path.isfile(got_dest_2)
# remove the temporary file
os.remove(got_dest)
def test_export_7():
# test file already exists and test increment=False (not default), overwrite=False (default)
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)
with pytest.raises(Exception) as e:
# try to export again with increment = False
assert photos[0].export(dest, increment=False)
assert e.type == type(FileExistsError())
# remove the temporary file
os.remove(got_dest)
def test_export_8():
# try to export missing file
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest)
assert e.type == type(FileNotFoundError())
def test_export_9():
# try to export edited file that's not edited
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest, edited=True)
assert e.type == type(FileNotFoundError())
def test_export_10():
# try to export edited file that's not edited and name provided
# should raise exception
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest, filename, edited=True)
assert e.type == type(FileNotFoundError())
def test_export_11():
# export edited file with name provided
import os
import os.path
import tempfile
import time
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
timestamp = time.time()
filename = f"osxphotos-export-test-{timestamp}.jpg"
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, filename, edited=True)
assert got_dest == expected_dest
# remove the temporary file
os.remove(got_dest)
def test_export_12():
# export edited file with default name
import os
import os.path
import pathlib
import tempfile
import osxphotos
dest = tempfile.gettempdir()
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
edited_name = pathlib.Path(photos[0].path_edited()).name
edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename()).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)
assert got_dest == expected_dest
# remove the temporary file
os.remove(got_dest)
def test_export_13():
# export to invalid destination
# should raise exception
import os
import os.path
import tempfile
import osxphotos
dest = tempfile.gettempdir()
# create a folder that doesn't exist
i = 0
while os.path.isdir(dest):
dest = os.path.join(dest, str(i))
i += 1
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename()
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest)
assert e.type == type(FileNotFoundError())