Added tests

This commit is contained in:
Rhet Turnbull 2019-07-26 19:51:14 -04:00
parent 1c1ebed491
commit 3023f568b7
90 changed files with 448 additions and 38 deletions

View File

@ -4,14 +4,16 @@ from pathlib import Path
from plistlib import load as plistload
from datetime import datetime
import tempfile
import objc
import CoreFoundation
from Foundation import *
import urllib.parse
import sys
from shutil import copyfile
import pprint
import sqlite3
import objc
import CoreFoundation
from Foundation import *
from . import _applescript
from loguru import logger
@ -72,7 +74,7 @@ class PhotosDB:
dbfile = os.path.join(library_path, "database/photos.db")
logger.debug(dbfile)
logger.debug("filename = %s" % dbfile)
logger.debug(f"filename = {dbfile}")
# TODO: replace os.path with pathlib
# TODO: clean this up -- we'll already know library_path
@ -80,12 +82,12 @@ class PhotosDB:
(library_path, tmp) = os.path.split(library_path)
masters_path = os.path.join(library_path, "Masters")
self._masters_path = masters_path
logger.debug("library = %s, masters = %s" % (library_path, masters_path))
logger.debug(f"library = {library_path}, masters = {masters_path}")
if not _check_file_exists(dbfile):
sys.exit("_dbfile %s does not exist" % (dbfile))
sys.exit(f"_dbfile {dbfile} does not exist")
logger.info("database filename = %s" % dbfile)
logger.info(f"database filename = {dbfile}")
self._dbfile = dbfile
self._setup_applescript()
@ -234,12 +236,12 @@ class PhotosDB:
def _open_sql_file(self, file):
fname = file
logger.debug("Trying to open database %s" % (fname))
logger.debug(f"Trying to open database {fname}")
try:
conn = sqlite3.connect("%s" % (fname))
conn = sqlite3.connect(f"{fname}")
c = conn.cursor()
except sqlite3.Error as e:
print("An error occurred: %s %s" % (e.args[0], fname))
print(f"An error occurred: {e.args[0]} {fname}")
sys.exit(3)
logger.debug("SQLite database is open")
return (conn, c)
@ -277,7 +279,7 @@ class PhotosDB:
)
for person in c:
if person[0] == None:
logger.debug("skipping person = None %s" % person[1])
logger.debug(f"skipping person = None {person[1]}")
continue
if not person[1] in self._dbfaces_uuid:
self._dbfaces_uuid[person[1]] = []
@ -314,7 +316,7 @@ class PhotosDB:
self._dbalbums_album[album[0]] = []
self._dbalbums_uuid[album[1]].append(album[0])
self._dbalbums_album[album[0]].append(album[1])
logger.debug("%s %s" % (album[1], album[0]))
logger.debug(f"{album[1]} {album[0]}")
# set_pbar_status(i)
i = i + 1
logger.debug("Finished walking through albums")
@ -344,7 +346,7 @@ class PhotosDB:
self._dbkeywords_keyword[keyword[0]] = []
self._dbkeywords_uuid[keyword[1]].append(keyword[0])
self._dbkeywords_keyword[keyword[0]].append(keyword[1])
logger.debug("%s %s" % (keyword[1], keyword[0]))
logger.debug(f"{keyword[1]} {keyword[0]}")
# set_pbar_status(i)
i = i + 1
logger.debug("Finished walking through keywords")
@ -357,7 +359,7 @@ class PhotosDB:
i = 0
for vol in c:
self._dbvolumes[vol[0]] = vol[1]
logger.debug("%s %s" % (vol[0], vol[1]))
logger.debug(f"{vol[0]} {vol[1]}")
# set_pbar_status(i)
i = i + 1
logger.debug("Finished walking through volumes")
@ -385,7 +387,7 @@ class PhotosDB:
i = i + 1
uuid = row[0]
if _debug:
print("i = %d, uuid = '%s, master = '%s" % (i, uuid, row[2]))
print(f"i = {i:d}, uuid = '{uuid}, master = '{row[2]}")
self._dbphotos[uuid] = {}
self._dbphotos[uuid]["modelID"] = row[1]
self._dbphotos[uuid]["masterUuid"] = row[2]
@ -490,58 +492,52 @@ class PhotosDB:
If called with no args, returns the entire database of photos
If called with args, returns photos matching the args (e.g. keywords, persons, etc.)
If more than one arg, returns photos matching all the criteria (e.g. keywords AND persons)
TODO: Still need to fix AND
"""
def photos(self, keywords=[], uuid=[], persons=[], albums=[]):
#TODO: combine photos and photos_sets, I think only one needed
# photos = [] # list of photos (PhotoInfo objects) that will be returned
photos_sets = [] # list of sets to perform intersection of
photos_sets = [] # list of photo sets to perform intersection of
if not keywords and not uuid and not persons and not albums:
# return all the photos
# append keys of all photos as a single set to photos_sets
logger.debug("return all photos")
photos_sets.append(set(self._dbphotos.keys()))
else:
if albums:
for album in albums:
logger.info("album=%s" % album)
logger.info(f"album={album}")
if album in self._dbalbums_album:
logger.info("processing album %s:" % album)
logger.info(f"processing album {album}:")
photos_sets.append(set(self._dbalbums_album[album]))
else:
logger.debug("Could not find album '%s' in database" % (album))
logger.debug(f"Could not find album '{album}' in database")
if uuid:
for u in uuid:
logger.info("uuid=%s" % u)
logger.info(f"uuid={u}")
if u in self._dbphotos:
logger.info("processing uuid %s:" % u)
logger.info(f"processing uuid {u}:")
photos_sets.append(set([u]))
else:
logger.debug("Could not find uuid '%s' in database" % (u))
logger.debug(f"Could not find uuid '{u}' in database")
if keywords:
for keyword in keywords:
logger.info("keyword=%s" % keyword)
logger.info(f"keyword={keyword}")
if keyword in self._dbkeywords_keyword:
logger.info("processing keyword %s:" % keyword)
logger.info(f"processing keyword {keyword}:")
photos_sets.append(set(self._dbkeywords_keyword[keyword]))
logger.debug(f"photos_sets {photos_sets}")
else:
logger.debug(
"Could not find keyword '%s' in database" % (keyword)
)
logger.debug(f"Could not find keyword '{keyword}' in database")
if persons:
for person in persons:
logger.info("person=%s" % person)
logger.info(f"person={person}")
if person in self._dbfaces_person:
logger.info("processing person %s:" % person)
logger.info(f"processing person {person}:")
photos_sets.append(set(self._dbfaces_person[person]))
else:
logger.debug(
"Could not find person '%s' in database" % (person)
)
logger.debug(f"Could not find person '{person}' in database")
photoinfo = []
if photos_sets: # found some photos
@ -622,6 +618,9 @@ class PhotoInfo:
def uuid(self):
return self.__uuid
def ismissing(self):
return True if self.__info["isMissing"] == 1 else False
# compare two PhotoInfo objects for equality
def __eq__(self, other):
if isinstance(other, self.__class__):

View File

@ -27,7 +27,7 @@
# SOFTWARE.
# from distutils.core import setup
from setuptools import setup
from setuptools import setup, find_packages
# read the contents of README file
from os import path
@ -47,7 +47,7 @@ setup(
url="https://github.com/RhetTbull/",
project_urls={"GitHub": "https://github.com/RhetTbull/osxphotos"},
download_url="https://github.com/RhetTbull/osxphotos",
packages=["osxphotos"],
packages=find_packages(exclude=["tests","examples"]),
license="License :: OSI Approved :: MIT License",
classifiers=[
"Development Status :: 4 - Beta",
@ -55,7 +55,7 @@ setup(
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=["pyobjc","loguru"],

21
tests/README.md Normal file
View File

@ -0,0 +1,21 @@
# Tests for osxphotos #
## Running Tests ##
Tests require pytest:
`pip install pytest`
To run the tests, do the following from the main source folder:
`python -m pytest tests/`
Running the tests this way allows the library to be tested without installing it.
## Attribution ##
These tests utilize a test Photos library. The test library is populated with photos from [flickr](https://www.flickr.com). All images used are licensed under Creative Commons 2.0 Attribution [license](https://creativecommons.org/licenses/by/2.0/).
Images used from:
- [Jeff Hitchcock](https://www.flickr.com/photos/arbron/48353451872/)
- [Carlos Montesdeoca](https://www.flickr.com/photos/carlosmontesdeocastudio)
- [Rydale Clothing](https://www.flickr.com/photos/rydaleclothing)
- [Marco Verch](https://www.flickr.com/photos/30478819@N08/48228222317/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>DatabaseMinorVersion</key>
<integer>1</integer>
<key>DatabaseVersion</key>
<integer>112</integer>
<key>LastOpenMode</key>
<integer>2</integer>
<key>LibrarySchemaVersion</key>
<integer>3301</integer>
<key>MetaSchemaVersion</key>
<integer>2</integer>
<key>createDate</key>
<date>2019-07-26T20:15:17Z</date>
</dict>
</plist>

Binary file not shown.

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Photos</key>
<dict>
<key>CollapsedSidebarSectionIdentifiers</key>
<array/>
<key>ExpandedSidebarItemIdentifiers</key>
<array>
<string>TopLevelAlbums</string>
<string>TopLevelSlideshows</string>
</array>
<key>IPXWorkspaceControllerZoomLevelsKey</key>
<dict>
<key>kZoomLevelIdentifierAlbums</key>
<integer>7</integer>
<key>kZoomLevelIdentifierVersions</key>
<integer>7</integer>
</dict>
<key>lastAddToDestination</key>
<dict>
<key>key</key>
<integer>1</integer>
<key>lastKnownDisplayName</key>
<string>September 28, 2018</string>
<key>type</key>
<string>album</string>
<key>uuid</key>
<string>+Ep8CrNRRhea9eVA618FMg</string>
</dict>
<key>lastKnownItemCounts</key>
<dict>
<key>other</key>
<integer>0</integer>
<key>photos</key>
<integer>7</integer>
<key>videos</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2019-07-26T20:15:18Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2019-07-26T20:15:18Z</date>
</dict>
</plist>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PVClustererBringUpState</key>
<integer>50</integer>
</dict>
</plist>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IncrementalPersonProcessingStage</key>
<integer>0</integer>
<key>PersonBuilderLastMinimumFaceGroupSizeForCreatingMergeCandidates</key>
<integer>15</integer>
<key>PersonBuilderMergeCandidatesEnabled</key>
<true/>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PLLanguageAndLocaleKey</key>
<string>en-US:en_US</string>
<key>PLLastGeoProviderIdKey</key>
<string>7618</string>
<key>PLLastLocationInfoFormatVer</key>
<integer>12</integer>
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
<integer>1</integer>
<key>PLLastRevGeoVerFileFetchDateKey</key>
<date>2019-07-26T20:15:18Z</date>
</dict>
</plist>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LastHistoryRowId</key>
<integer>545</integer>
<key>LibraryBuildTag</key>
<string>BEA5F0E8-BA6B-4462-8F73-3E53BBE4C943</string>
<key>LibrarySchemaVersion</key>
<integer>3301</integer>
</dict>
</plist>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FileVersion</key>
<integer>11</integer>
<key>Source</key>
<dict>
<key>35230</key>
<dict>
<key>CountryMinVersions</key>
<dict>
<key>OTHER</key>
<integer>1</integer>
</dict>
<key>CurrentVersion</key>
<integer>1</integer>
<key>NoResultErrorIsSuccess</key>
<true/>
</dict>
<key>57879</key>
<dict>
<key>CountryMinVersions</key>
<dict>
<key>OTHER</key>
<integer>1</integer>
</dict>
<key>CurrentVersion</key>
<integer>1</integer>
<key>NoResultErrorIsSuccess</key>
<true/>
</dict>
<key>7618</key>
<dict>
<key>AddCountyIfNeeded</key>
<true/>
<key>CountryMinVersions</key>
<dict>
<key>OTHER</key>
<integer>10</integer>
</dict>
<key>CurrentVersion</key>
<integer>10</integer>
</dict>
</dict>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>DatabaseMinorVersion</key>
<integer>1</integer>
<key>DatabaseVersion</key>
<integer>112</integer>
<key>HistoricalMarker</key>
<dict>
<key>LastHistoryRowId</key>
<integer>551</integer>
<key>LibraryBuildTag</key>
<string>BEA5F0E8-BA6B-4462-8F73-3E53BBE4C943</string>
<key>LibrarySchemaVersion</key>
<integer>3301</integer>
</dict>
<key>LibrarySchemaVersion</key>
<integer>3301</integer>
<key>MetaSchemaVersion</key>
<integer>2</integer>
<key>SnapshotComplete</key>
<true/>
<key>SnapshotCompletedDate</key>
<date>2019-07-26T20:15:17Z</date>
<key>SnapshotLastValidated</key>
<date>2019-07-26T20:15:17Z</date>
<key>SnapshotTables</key>
<dict/>
</dict>
</plist>

188
tests/test_1.py Normal file
View File

@ -0,0 +1,188 @@
import pytest
# TODO: put some of this code into a pre-function
PHOTOS_DB = "./tests/Test-10.13.6.photoslibrary/database/photos.db"
KEYWORDS = [
"Kids",
"wedding",
"flowers",
"England",
"London",
"London 2018",
"St. James's Park",
"UK",
"United Kingdom",
]
PERSONS = ["Katie", "Suzy", "Maria"]
ALBUMS = ["Pumpkin Farm"]
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}
def test_init():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert isinstance(photosdb, osxphotos.PhotosDB)
def test_persons():
import osxphotos
import collections
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert "Katie" in photosdb.persons()
assert collections.Counter(PERSONS) == collections.Counter(photosdb.persons())
def test_keywords():
import osxphotos
import collections
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert "wedding" in photosdb.keywords()
assert collections.Counter(KEYWORDS) == collections.Counter(photosdb.keywords())
def test_albums():
import osxphotos
import collections
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert "Pumpkin Farm" in photosdb.albums()
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums())
def test_keywords_dict():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
keywords = photosdb.keywords_as_dict()
assert keywords["wedding"] == 2
assert keywords == KEYWORDS_DICT
def test_persons_as_dict():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
persons = photosdb.persons_as_dict()
assert persons["Maria"] == 1
assert persons == PERSONS_DICT
def test_albums_as_dict():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
albums = photosdb.albums_as_dict()
assert albums["Pumpkin Farm"] == 3
assert albums == ALBUM_DICT
def test_attributes():
import datetime
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["RWmFYiDjSyKjeK8Pfna0Eg"])
assert len(photos) == 1
p = photos[0]
assert p.keywords() == ["Kids"]
assert p.filename() == "Pumkins2.jpg"
assert p.date() == datetime.datetime(2018, 9, 28, 16, 7, 7)
assert p.description() == "Girl holding pumpkin"
assert p.name() == "I found one!"
assert p.albums() == ["Pumpkin Farm"]
assert p.persons() == ["Katie"]
assert (
p.path()
== "./tests/Test-10.13.6.photoslibrary/Masters/2019/07/26/20190726-203227/Pumkins2.jpg"
)
assert p.ismissing() == False
def test_missing():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["6iAZJP7ZQ5iXxapoJb3ytA"])
assert len(photos) == 1
p = photos[0]
assert p.path() == None
assert p.ismissing() == True
def test_count():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos()
assert len(photos) == 7
def test_keyword_2():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(keywords=["wedding"])
assert len(photos) == 2
def test_keyword_not_in_album():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# find all photos with keyword "Kids" not in the album "Pumpkin Farm"
photos1 = photosdb.photos(albums=["Pumpkin Farm"])
photos2 = photosdb.photos(keywords=["Kids"])
photos3 = [p for p in photos2 if p not in photos1]
assert len(photos3) == 1
assert photos3[0].uuid() == "6iAZJP7ZQ5iXxapoJb3ytA"
# def main():
# photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# print(photosdb.keywords())
# print(photosdb.persons())
# print(photosdb.albums())
# print(photosdb.keywords_as_dict())
# print(photosdb.persons_as_dict())
# print(photosdb.albums_as_dict())
# # # find all photos with Keyword = Foo and containing John Smith
# # photos = photosdb.photos(keywords=["Foo"],persons=["John Smith"])
# #
# # # find all photos that include Alice Smith but do not contain the keyword Bar
# # photos = [p for p in photosdb.photos(persons=["Alice Smith"])
# # if p not in photosdb.photos(keywords=["Bar"]) ]
# photos = photosdb.photos()
# for p in photos:
# print(
# p.uuid(),
# p.filename(),
# p.date(),
# p.description(),
# p.name(),
# p.keywords(),
# p.albums(),
# p.persons(),
# p.path(),
# )
# if __name__ == "__main__":
# main()