Restructured entire code base to make it easier to maintain. Closes #16

This commit is contained in:
Rhet Turnbull
2019-12-21 08:06:25 -08:00
parent cd51782ef2
commit b794e226e3
14 changed files with 1814 additions and 1777 deletions

File diff suppressed because it is too large Load Diff

17
osxphotos/_constants.py Normal file
View File

@@ -0,0 +1,17 @@
# which Photos library database versions have been tested
# Photos 2.0 (10.12.6) == 2622
# Photos 3.0 (10.13.6) == 3301
# Photos 4.0 (10.14.5) == 4016
# Photos 4.0 (10.14.6) == 4025
# Photos 5.0 (10.15.0) == 6000
# TODO: Should this also use compatibleBackToVersion from LiGlobals?
_TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
# versions later than this have a different database structure
_PHOTOS_5_VERSION = "6000"
# which major version operating systems have been tested
_TESTED_OS_VERSIONS = ["12", "13", "14", "15"]
# Photos 5 has persons who are empty string if unidentified face
_UNKNOWN_PERSON = "_UNKNOWN_"

View File

@@ -1,4 +1,3 @@
""" version info """
__version__ = "0.15.01"
__version__ = "0.16.01"

View File

@@ -135,13 +135,13 @@ def dump(cli_obj):
@click.pass_obj
def list_libraries(cli_obj):
""" Print list of Photos libraries found on the system. """
photo_libs = osxphotos.list_photo_libraries()
photo_libs = osxphotos.utils.list_photo_libraries()
sys_lib = None
_, major, _ = osxphotos._get_os_version()
_, major, _ = osxphotos.utils._get_os_version()
if int(major) >= 15:
sys_lib = osxphotos.get_system_library_path()
last_lib = osxphotos.get_last_library_path()
sys_lib = osxphotos.utils.get_system_library_path()
last_lib = osxphotos.utils.get_last_library_path()
last_lib_flag = sys_lib_flag = False
@@ -162,6 +162,7 @@ def list_libraries(cli_obj):
if last_lib_flag:
click.echo("(#)\tLast opened Photos Library")
@cli.command()
@click.option("--keyword", default=None, multiple=True, help="Search for keyword(s).")
@click.option("--person", default=None, multiple=True, help="Search for person(s).")

490
osxphotos/photoinfo.py Normal file
View File

@@ -0,0 +1,490 @@
import json
import logging
import os.path
import pathlib
import re
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
import yaml
from ._constants import _PHOTOS_5_VERSION
from .utils import _get_resource_loc, dd_to_dms_str
class PhotoInfo:
"""
Info about a specific photo, contains all the details about the photo
including keywords, persons, albums, uuid, path, etc.
"""
def __init__(self, db=None, uuid=None, info=None):
self._uuid = uuid
self._info = info
self._db = db
def filename(self):
""" filename of the picture """
return self._info["filename"]
def original_filename(self):
""" original filename of the picture """
""" Photos 5 mangles filenames upon import """
return self._info["originalFilename"]
def date(self):
""" image creation date as timezone aware datetime object """
imagedate = self._info["imageDate"]
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
delta = timedelta(seconds=seconds)
tz = timezone(delta)
imagedate_utc = imagedate.astimezone(tz=tz)
return imagedate_utc
def tzoffset(self):
""" timezone offset from UTC in seconds """
return self._info["imageTimeZoneOffsetSeconds"]
def path(self):
""" absolute path on disk of the original picture """
photopath = ""
if self._db._db_version < _PHOTOS_5_VERSION:
vol = self._info["volume"]
if vol is not None:
photopath = os.path.join("/Volumes", vol, self._info["imagePath"])
else:
photopath = os.path.join(
self._db._masters_path, self._info["imagePath"]
)
if self._info["isMissing"] == 1:
photopath = None # path would be meaningless until downloaded
# TODO: Is there a way to use applescript or PhotoKit to force the download in this
else:
if self._info["masterFingerprint"]:
# if masterFingerprint is not null, path appears to be valid
if self._info["directory"].startswith("/"):
photopath = os.path.join(
self._info["directory"], self._info["filename"]
)
else:
photopath = os.path.join(
self._db._masters_path,
self._info["directory"],
self._info["filename"],
)
else:
photopath = None
logging.debug(f"WARNING: masterFingerprint null {pformat(self._info)}")
# TODO: fix the logic for isMissing
if self._info["isMissing"] == 1:
photopath = None # path would be meaningless until downloaded
logging.debug(photopath)
return photopath
def path_edited(self):
""" absolute path on disk of the edited picture """
""" None if photo has not been edited """
photopath = ""
if self._db._db_version < _PHOTOS_5_VERSION:
if self._info["hasAdjustments"]:
edit_id = self._info["edit_resource_id"]
if edit_id is not None:
library = self._db._library_path
folder_id, file_id = _get_resource_loc(edit_id)
# todo: is this always true or do we need to search file file_id under folder_id
photopath = os.path.join(
library,
"resources",
"media",
"version",
folder_id,
"00",
f"fullsizeoutput_{file_id}.jpeg",
)
if not os.path.isfile(photopath):
logging.warning(
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
logging.warning(
f"{self.uuid} hasAdjustments but edit_model_id is None"
)
else:
photopath = None
# if self._info["isMissing"] == 1:
# photopath = None # path would be meaningless until downloaded
else:
# in Photos 5.0 / Catalina / MacOS 10.15:
# edited photos appear to always be converted to .jpeg and stored in
# library_name/resources/renders/X/UUID_1_201_a.jpeg
# where X = first letter of UUID
# and UUID = UUID of image
# this seems to be true even for photos not copied to Photos library and
# where original format was not jpg/jpeg
# if more than one edit, previous edit is stored as UUID_p.jpeg
if self._info["hasAdjustments"]:
library = self._db._library_path
directory = self._uuid[0] # first char of uuid
photopath = os.path.join(
library,
"resources",
"renders",
directory,
f"{self._uuid}_1_201_a.jpeg",
)
if not os.path.isfile(photopath):
logging.warning(
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
photopath = None
# TODO: might be possible for original/master to be missing but edit to still be there
# if self._info["isMissing"] == 1:
# photopath = None # path would be meaningless until downloaded
logging.debug(photopath)
return photopath
def description(self):
""" long / extended description of picture """
return self._info["extendedDescription"]
def persons(self):
""" list of persons in picture """
return self._info["persons"]
def albums(self):
""" list of albums picture is contained in """
albums = []
for album in self._info["albums"]:
albums.append(self._db._dbalbum_details[album]["title"])
return albums
def keywords(self):
""" list of keywords for picture """
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):
""" UUID of picture """
return self._uuid
def ismissing(self):
""" returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
NOTE: the photos.db database uses an asynchrounous write-ahead log so changes in Photos
do not immediately get written to disk. In particular, I've noticed that downloading
an image from the cloud does not force the database to be updated until something else
e.g. an edit, keyword, etc. occurs forcing a database synch
The exact process / timing is a mystery to be but be aware that if some photos were recently
downloaded from cloud to local storate their status in the database might still show
isMissing = 1
"""
return True if self._info["isMissing"] == 1 else False
def hasadjustments(self):
""" True if picture has adjustments / edits """
return True if self._info["hasAdjustments"] == 1 else False
def external_edit(self):
""" Returns True if picture was edited outside of Photos using external editor """
return (
True
if self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
else False
)
def favorite(self):
""" True if picture is marked as favorite """
return True if self._info["favorite"] == 1 else False
def hidden(self):
""" True if picture is hidden """
return True if self._info["hidden"] == 1 else False
def location(self):
""" returns (latitude, longitude) as float in degrees or None """
return (self._latitude(), self._longitude())
def export(
self,
dest,
*filename,
edited=False,
overwrite=False,
increment=True,
sidecar=False,
):
""" 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 """
""" if sidecar=True, will also write a json sidecar with EXIF data in format readable by exiftool """
""" sidecar filename will be dest/filename.ext.json where ext is suffix of the image file (e.g. jpeg or jpg) """
""" returns the full path to the exported file """
# TODO: add this docs:
# ( for jpeg in *.jpeg; do exiftool -v -json=$jpeg.json $jpeg; done )
# check arguments and get destination path and filename (if provided)
if filename and len(filename) > 2:
raise TypeError(
"Too many positional arguments. Should be at most two: destination, filename."
)
else:
# verify destination is a valid path
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 filename and len(filename) == 1:
# second arg is filename of picture
filename = filename[0]
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
if sidecar:
logging.debug("writing exiftool_json_sidecar")
sidecar_filename = f"{dest}.json"
json_sidecar_str = self._exiftool_json_sidecar()
try:
self._write_sidecar_car(sidecar_filename, json_sidecar_str)
except Exception as e:
logging.critical(f"Error writing json sidecar to {sidecar_filename}")
raise e
return str(dest)
def _exiftool_json_sidecar(self):
""" return json string of EXIF details in exiftool sidecar format """
exif = {}
exif["FileName"] = self.filename()
if self.description():
exif["ImageDescription"] = self.description()
exif["Description"] = self.description()
if self.title():
exif["Title"] = self.title()
if self.keywords():
exif["TagsList"] = exif["Keywords"] = self.keywords()
if self.persons():
exif["PersonInImage"] = self.persons()
# if self.favorite():
# exif["Rating"] = 5
(lat, lon) = self.location()
if lat is not None and lon is not None:
lat_str, lon_str = dd_to_dms_str(lat, lon)
exif["GPSLatitude"] = lat_str
exif["GPSLongitude"] = lon_str
exif["GPSPosition"] = f"{lat_str}, {lon_str}"
lat_ref = "North" if lat >= 0 else "South"
lon_ref = "East" if lon >= 0 else "West"
exif["GPSLatitudeRef"] = lat_ref
exif["GPSLongitudeRef"] = lon_ref
# process date/time and timezone offset
date = self.date()
# exiftool expects format to "2015:01:18 12:00:00"
datetimeoriginal = date.strftime("%Y:%m:%d %H:%M:%S")
offsettime = date.strftime("%z")
# find timezone offset in format "-04:00"
offset = re.findall(r"([+-]?)([\d]{2})([\d]{2})", offsettime)
offset = offset[0] # findall returns list of tuples
offsettime = f"{offset[0]}{offset[1]}:{offset[2]}"
exif["DateTimeOriginal"] = datetimeoriginal
exif["OffsetTimeOriginal"] = offsettime
json_str = json.dumps([exif])
return json_str
def _write_sidecar_car(self, filename, json_str):
if not filename and not json_str:
raise (
ValueError(
f"filename {filename} and json_str {json_str} must not be None"
)
)
# TODO: catch exception?
f = open(filename, "w")
f.write(json_str)
f.close()
def _longitude(self):
""" Returns longitude, in degrees """
return self._info["longitude"]
def _latitude(self):
""" Returns latitude, in degrees """
return self._info["latitude"]
def __repr__(self):
# TODO: update to use __class__ and __name__
return f"osxphotos.PhotoInfo(db={self._db}, uuid='{self._uuid}', info={self._info})"
def __str__(self):
info = {
"uuid": self.uuid(),
"filename": self.filename(),
"original_filename": self.original_filename(),
"date": str(self.date()),
"description": self.description(),
"name": self.name(),
"keywords": self.keywords(),
"albums": self.albums(),
"persons": self.persons(),
"path": self.path(),
"ismissing": self.ismissing(),
"hasadjustments": self.hasadjustments(),
"external_edit": self.external_edit(),
"favorite": self.favorite(),
"hidden": self.hidden(),
"latitude": self._latitude(),
"longitude": self._longitude(),
"path_edited": self.path_edited(),
}
return yaml.dump(info, sort_keys=False)
def to_json(self):
""" return JSON representation """
# TODO: Add additional details here
pic = {
"uuid": self.uuid(),
"filename": self.filename(),
"original_filename": self.original_filename(),
"date": str(self.date()),
"description": self.description(),
"name": self.name(),
"keywords": self.keywords(),
"albums": self.albums(),
"persons": self.persons(),
"path": self.path(),
"ismissing": self.ismissing(),
"hasadjustments": self.hasadjustments(),
"external_edit": self.external_edit(),
"favorite": self.favorite(),
"hidden": self.hidden(),
"latitude": self._latitude(),
"longitude": self._longitude(),
"path_edited": self.path_edited(),
}
return json.dumps(pic)
# compare two PhotoInfo objects for equality
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
else:
return False
def __ne__(self, other):
return not self.__eq__(other)

1068
osxphotos/photosdb.py Normal file

File diff suppressed because it is too large Load Diff

207
osxphotos/utils.py Normal file
View File

@@ -0,0 +1,207 @@
import glob
import logging
import os.path
import platform
import subprocess
import urllib.parse
from pathlib import Path
from plistlib import load as plistload
import CoreFoundation
import objc
from Foundation import *
def _get_os_version():
# returns tuple containing OS version
# e.g. 10.13.6 = (10, 13, 6)
version = platform.mac_ver()[0].split(".")
if len(version) == 2:
(ver, major) = version
minor = "0"
elif len(version) == 3:
(ver, major, minor) = version
else:
raise (
ValueError(
f"Could not parse version string: {platform.mac_ver()} {version}"
)
)
return (ver, major, minor)
def _check_file_exists(filename):
""" returns true if file exists and is not a directory
otherwise returns false """
filename = os.path.abspath(filename)
return os.path.exists(filename) and not os.path.isdir(filename)
def _get_resource_loc(model_id):
""" returns folder_id and file_id needed to find location of edited photo """
""" and live photos for version <= Photos 4.0 """
# determine folder where Photos stores edited version
# edited images are stored in:
# Photos Library.photoslibrary/resources/media/version/XX/00/fullsizeoutput_Y.jpeg
# where XX and Y are computed based on RKModelResources.modelId
# file_id (Y in above example) is hex representation of model_id without leading 0x
file_id = hex_id = hex(model_id)[2:]
# folder_id (XX) in above example if first two chars of model_id converted to hex
# and left padded with zeros if < 4 digits
folder_id = hex_id.zfill(4)[0:2]
return folder_id, file_id
def _dd_to_dms(dd):
""" convert lat or lon in decimal degrees (dd) to degrees, minutes, seconds """
""" return tuple of int(deg), int(min), float(sec) """
dd = float(dd)
negative = dd < 0
dd = abs(dd)
min_, sec_ = divmod(dd * 3600, 60)
deg_, min_ = divmod(min_, 60)
if negative:
if deg_ > 0:
deg_ = deg_ * -1
elif min_ > 0:
min_ = min_ * -1
else:
sec_ = sec_ * -1
return int(deg_), int(min_), sec_
def dd_to_dms_str(lat, lon):
""" convert latitude, longitude in degrees to degrees, minutes, seconds as string """
""" lat: latitude in degrees """
""" lon: longitude in degrees """
""" returns: string tuple in format ("51 deg 30' 12.86\" N", "0 deg 7' 54.50\" W") """
""" this is the same format used by exiftool's json format """
# TODO: add this to readme
lat_deg, lat_min, lat_sec = _dd_to_dms(lat)
lon_deg, lon_min, lon_sec = _dd_to_dms(lon)
lat_hemisphere = "N"
if any([lat_deg < 0, lat_min < 0, lat_sec < 0]):
lat_hemisphere = "S"
lon_hemisphere = "E"
if any([lon_deg < 0, lon_min < 0, lon_sec < 0]):
lon_hemisphere = "W"
lat_str = (
f"{abs(lat_deg)} deg {abs(lat_min)}' {abs(lat_sec):.2f}\" {lat_hemisphere}"
)
lon_str = (
f"{abs(lon_deg)} deg {abs(lon_min)}' {abs(lon_sec):.2f}\" {lon_hemisphere}"
)
return lat_str, lon_str
def get_system_library_path():
""" return the path to the system Photos library as string """
""" only works on MacOS 10.15+ """
""" on earlier versions, will raise exception """
_, major, _ = _get_os_version()
if int(major) < 15:
raise Exception(
"get_system_library_path not implemented for MacOS < 10.15", major
)
plist_file = Path(
str(Path.home())
+ "/Library/Containers/com.apple.photolibraryd/Data/Library/Preferences/com.apple.photolibraryd.plist"
)
if plist_file.is_file():
with open(plist_file, "rb") as fp:
pl = plistload(fp)
else:
logging.warning(f"could not find plist file: {str(plist_file)}")
return None
photospath = pl["SystemLibraryPath"]
if photospath is not None:
return photospath
else:
logging.warning("Could not get path to Photos database")
return None
def get_last_library_path():
""" return the path to the last opened Photos library """
# TODO: Need a module level method for this and another PhotosDB method to get current library path
plist_file = Path(
str(Path.home())
+ "/Library/Containers/com.apple.Photos/Data/Library/Preferences/com.apple.Photos.plist"
)
if plist_file.is_file():
with open(plist_file, "rb") as fp:
pl = plistload(fp)
else:
logging.warning(f"could not find plist file: {str(plist_file)}")
return None
# get the IPXDefaultLibraryURLBookmark from com.apple.Photos.plist
# this is a serialized CFData object
photosurlref = pl["IPXDefaultLibraryURLBookmark"]
if photosurlref is not None:
# use CFURLCreateByResolvingBookmarkData to de-serialize bookmark data into a CFURLRef
photosurl = CoreFoundation.CFURLCreateByResolvingBookmarkData(
kCFAllocatorDefault, photosurlref, 0, None, None, None, None
)
# the CFURLRef we got is a sruct that python treats as an array
# I'd like to pass this to CFURLGetFileSystemRepresentation to get the path but
# CFURLGetFileSystemRepresentation barfs when it gets an array from python instead of expected struct
# first element is the path string in form:
# file:///Users/username/Pictures/Photos%20Library.photoslibrary/
photosurlstr = photosurl[0].absoluteString() if photosurl[0] else None
# now coerce the file URI back into an OS path
# surely there must be a better way
if photosurlstr is not None:
photospath = os.path.normpath(
urllib.parse.unquote(urllib.parse.urlparse(photosurlstr).path)
)
else:
logging.warning(
"Could not extract photos URL String from IPXDefaultLibraryURLBookmark"
)
return None
return photospath
else:
logging.warning("Could not get path to Photos database")
return None
def list_photo_libraries():
""" returns list of Photos libraries found on the system """
""" on MacOS < 10.15, this may omit some libraries """
# On 10.15, mdfind appears to find all libraries
# On older MacOS versions, mdfind appears to ignore some libraries
# glob to find libraries in ~/Pictures then mdfind to find all the others
# TODO: make this more robust
lib_list = glob.glob(f"{str(Path.home())}/Pictures/*.photoslibrary")
# On older OS, may not get all libraries so make sure we get the last one
last_lib = get_last_library_path()
if last_lib:
lib_list.append(last_lib)
output = subprocess.check_output(
["/usr/bin/mdfind", "-onlyin", "/", "-name", ".photoslibrary"]
).splitlines()
for lib in output:
lib_list.append(lib.decode("utf-8"))
lib_list = list(set(lib_list))
lib_list.sort()
return lib_list

View File

@@ -50,8 +50,8 @@ def test_db_version():
def test_os_version():
import osxphotos
(_, major, _) = osxphotos._get_os_version()
assert major in osxphotos._TESTED_OS_VERSIONS
(_, major, _) = osxphotos.utils._get_os_version()
assert major in osxphotos._constants._TESTED_OS_VERSIONS
def test_persons():
@@ -170,4 +170,3 @@ def test_keyword_not_in_album():
photos3 = [p for p in photos2 if p not in photos1]
assert len(photos3) == 1
assert photos3[0].uuid() == "Pj99JmYjQkeezdY2OFuSaw"

View File

@@ -1,6 +1,6 @@
import pytest
from osxphotos import _UNKNOWN_PERSON
from osxphotos._constants import _UNKNOWN_PERSON
# TODO: put some of this code into a pre-function
@@ -109,7 +109,8 @@ def test_init5():
def bad_library():
return None
osxphotos.get_last_library_path = bad_library
# force get_last_library to return a bad path for testing
osxphotos.photosdb.get_last_library_path = bad_library
with pytest.raises(Exception):
assert osxphotos.PhotosDB()
@@ -126,8 +127,8 @@ def test_db_version():
def test_os_version():
import osxphotos
(_, major, _) = osxphotos._get_os_version()
assert major in osxphotos._TESTED_OS_VERSIONS
(_, major, _) = osxphotos.utils._get_os_version()
assert major in osxphotos._constants._TESTED_OS_VERSIONS
def test_persons():
@@ -735,4 +736,3 @@ def test_export_13():
with pytest.raises(Exception) as e:
assert photos[0].export(dest)
assert e.type == type(FileNotFoundError())

View File

@@ -1,6 +1,7 @@
import pytest
from osxphotos import _UNKNOWN_PERSON
from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.utils import dd_to_dms_str
# TODO: put some of this code into a pre-function
@@ -396,7 +397,7 @@ def test_export_13():
def test_dd_to_dms_str_1():
import osxphotos
lat_str, lon_str = osxphotos.dd_to_dms_str(
lat_str, lon_str = dd_to_dms_str(
34.559331096, 69.206499174
) # Kabul, 34°33'33.59" N 69°12'23.40" E
@@ -407,7 +408,7 @@ def test_dd_to_dms_str_1():
def test_dd_to_dms_str_2():
import osxphotos
lat_str, lon_str = osxphotos.dd_to_dms_str(
lat_str, lon_str = dd_to_dms_str(
-34.601997592, -58.375665164
) # Buenos Aires, 34°36'7.19" S 58°22'32.39" W
@@ -418,7 +419,7 @@ def test_dd_to_dms_str_2():
def test_dd_to_dms_str_3():
import osxphotos
lat_str, lon_str = osxphotos.dd_to_dms_str(
lat_str, lon_str = dd_to_dms_str(
-1.2666656, 36.7999968
) # Nairobi, 1°15'60.00" S 36°47'59.99" E
@@ -429,7 +430,7 @@ def test_dd_to_dms_str_3():
def test_dd_to_dms_str_4():
import osxphotos
lat_str, lon_str = osxphotos.dd_to_dms_str(
lat_str, lon_str = dd_to_dms_str(
38.889248, -77.050636
) # DC: 38° 53' 21.2928" N, 77° 3' 2.2896" W
@@ -469,4 +470,3 @@ def test_exiftool_json_sidecar():
assert item[0][1] == item[1][1]
# assert sorted(json_got[0].items()) == sorted(json_expected[0].items())

View File

@@ -1,6 +1,6 @@
import pytest
from osxphotos import _UNKNOWN_PERSON
from osxphotos._constants import _UNKNOWN_PERSON
# TODO: put some of this code into a pre-function
@@ -44,6 +44,7 @@ UUID_DICT = {
"location": "3Jn73XpSQQCluzRBMWRsMA",
}
def test_export_1():
# test basic export
# get an unedited image and export it using default filename
@@ -409,5 +410,3 @@ def test_exiftool_json_sidecar():
assert sorted(item[0][1]) == sorted(item[1][1])
else:
assert item[0][1] == item[1][1]
# assert sorted(json_got[0].items()) == sorted(json_expected[0].items())

View File

@@ -49,8 +49,8 @@ def test_db_version():
def test_os_version():
import osxphotos
(_, major, _) = osxphotos._get_os_version()
assert major in osxphotos._TESTED_OS_VERSIONS
(_, major, _) = osxphotos.utils._get_os_version()
assert major in osxphotos._constants._TESTED_OS_VERSIONS
def test_persons():
@@ -169,4 +169,3 @@ def test_keyword_not_in_album():
photos3 = [p for p in photos2 if p not in photos1]
assert len(photos3) == 1
assert photos3[0].uuid() == "6iAZJP7ZQ5iXxapoJb3ytA"

View File

@@ -49,8 +49,8 @@ def test_db_version():
def test_os_version():
import osxphotos
(_, major, _) = osxphotos._get_os_version()
assert major in osxphotos._TESTED_OS_VERSIONS
(_, major, _) = osxphotos.utils._get_os_version()
assert major in osxphotos._constants._TESTED_OS_VERSIONS
def test_persons():
@@ -169,4 +169,3 @@ def test_keyword_not_in_album():
photos3 = [p for p in photos2 if p not in photos1]
assert len(photos3) == 1
assert photos3[0].uuid() == "od0fmC7NQx+ayVr+%i06XA"

View File

@@ -45,15 +45,15 @@ def test_db_version():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert photosdb.get_db_version() in osxphotos._TESTED_DB_VERSIONS
assert photosdb.get_db_version() in osxphotos._constants._TESTED_DB_VERSIONS
assert photosdb.get_db_version() == "4025"
def test_os_version():
import osxphotos
(_, major, _) = osxphotos._get_os_version()
assert major in osxphotos._TESTED_OS_VERSIONS
(_, major, _) = osxphotos.utils._get_os_version()
assert major in osxphotos._constants._TESTED_OS_VERSIONS
def test_persons():
@@ -324,4 +324,3 @@ def test_get_library_path():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
lib_path = photosdb.get_library_path()
assert lib_path.endswith(PHOTOS_LIBRARY_PATH)