Implemented PersonInfo, closes #181

This commit is contained in:
Rhet Turnbull 2020-07-17 22:06:37 -07:00
parent 091e7b8f2e
commit 3f19276c5c
66 changed files with 558 additions and 63 deletions

View File

@ -18,6 +18,7 @@
+ [FolderInfo](#folderinfo)
+ [PlaceInfo](#placeinfo)
+ [ScoreInfo](#scoreinfo)
+ [PersonInfo](#personinfo)
+ [Template Substitutions](#template-substitutions)
+ [Utility Functions](#utility-functions)
* [Examples](#examples)
@ -790,7 +791,15 @@ Returns a list names of top level folder names in the database.
persons = photosdb.persons
```
Returns a list of the persons (faces) found in the Photos library
Returns a list of the person names (faces) found in the Photos library. **Note**: It is of course possible to have more than one person with the same name, e.g. "Maria Smith", in the database. `persons` assumes these are the same person and will list only one person named "Maria Smith". If you need more information about persons in the database, see [person_info](#dbpersoninfo).
#### <a name="dbpersoninfo">`person_info`</a>
```python
# assumes photosdb is a PhotosDB object (see above)
person_info = photosdb.person_info
```
Returns a list of [PersonInfo](#personinfo) objects representing persons who appear in photos in the database.
#### `keywords_as_dict`
```python
@ -806,7 +815,8 @@ Returns a dictionary of keywords found in the Photos library where key is the ke
persons_dict = photosdb.persons_as_dict
```
Returns a dictionary of persons (faces) found in the Photos library where key is the person name and value is the count of how many times that person appears in the library (ie. how many photos are tagged with the person). Resulting dictionary is in reverse sorted order (e.g. person who appears in the most photos is listed first).
Returns a dictionary of persons (faces) found in the Photos library where key is the person name and value is the count of how many times that person appears in the library (ie. how many photos are tagged with the person). Resulting dictionary is in reverse sorted order (e.g. person who appears in the most photos is listed first). **Note**: It is of course possible to have more than one person with the same name, e.g. "Maria Smith", in the database. `persons_as_dict` assumes these are the same person and will list only one person named "Maria Smith". If you need more information about persons in the database, see [person_info](#dbpersoninfo).
#### `albums_as_dict`
```python
@ -892,8 +902,7 @@ for row in results:
conn.close()
```
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=True, from_date=None, to_date=None, intrash=False)`
#### <A name="photos">`photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=True, from_date=None, to_date=None, intrash=False)`</a>
```python
# assumes photosdb is a PhotosDB object (see above)
@ -929,6 +938,8 @@ photos = photosdb.photos(
- ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None
- ```intrash```: if True, finds only photos in the "Recently Deleted" or trash folder, if False does not find any photos in the trash; default is False
See also [get_photo()](#getphoto) which is much faster for retrieving a single photo.
If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g.
Finds all photos with (keyword = "wedding" or "birthday") and (persons = "Juan Rodriguez")
@ -1003,6 +1014,9 @@ For example, in my library, Photos says I have 19,386 photos and 474 movies. Ho
>>>
```
#### <a name="getphoto">`get_photo(uuid)`</A>
Returns a single PhotoInfo instance for photo with UUID matching `uuid` or None if no photo is found matching `uuid`. If you know the UUID of a photo, `get_photo()` is much faster than `photos`. See also [photos()](#photos).
### PhotoInfo
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
@ -1040,6 +1054,9 @@ Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the ph
#### `persons`
Returns a list of the names of the persons in the photo
#### <a name="photopersoninfo">`person_info`</a>
Returns a list of [PersonInfo](#personinfo) objects representing persons in the photo.
#### `path`
Returns the absolute path to the photo on disk as a string. **Note**: this returns the path to the *original* unedited file (see [hasadjustments](#hasadjustments)). If the file is missing on disk, path=`None` (see [ismissing](#ismissing)).
@ -1353,7 +1370,7 @@ Returns the universally unique identifier (uuid) of the album. This is how Phot
#### `title`
Returns the title or name of the album.
#### `photos`
#### <a name="albumphotos">`photos`</a>
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the album sorted in the same order as in Photos. (e.g. if photos were manually sorted in the Photos albums, photos returned by `photos` will be in same order as they appear in the Photos album)
#### `folder_list`
@ -1528,6 +1545,31 @@ Example: find your "best" photo of food
>>> best_food_photo = sorted([p for p in photos if "food" in p.labels_normalized], key=lambda p: p.score.overall, reverse=True)[0]
```
### PersonInfo
[PhotosDB.person_info](#dbpersoninfo) and [PhotoInfo.person_info](#photopersoninfo) return a list of PersonInfo objects represents persons in the database and in a photo, respectively. The PersonInfo class has the following properties and methods.
#### `name`
Returns the full name of the person represented in the photo. For example, "Maria Smith".
#### `display_name`
Returns the display name of the person represented in the photo. For example, "Maria".
#### `uuid`
Returns the UUID of the person as stored in the Photos library database.
#### `keyphoto`
Returns a PhotoInfo instance for the photo designated as the key photo for the person. This is the Photos uses to display the person's face thumbnail in Photos' "People" view.
#### `facecount`
Returns a count of how many times this person appears in images in the database.
#### <a name="personphotos">photos`</a>
Returns a list of PhotoInfo objects representing all photos the person appears in.
#### `json()`
Returns a json string representation of the PersonInfo instance.
### Template Substitutions
The following substitutions are availabe for use with `PhotoInfo.render_template()`

View File

@ -58,5 +58,6 @@ if __name__ == "__main__":
print("getting photos")
tic = time.perf_counter()
photos = photosdb.photos(images=True, movies=True)
photos.extend(photosdb.photos(images=True, movies=True, intrash=True))
toc = time.perf_counter()
print(f"found {len(photos)} photos in {toc-tic} seconds")

View File

@ -1,3 +1,3 @@
""" version info """
__version__ = "0.30.10"
__version__ = "0.30.11"

View File

@ -55,7 +55,9 @@ class AlbumInfo:
# so need to build photo list one a time
# sort uuids by sort order
sorted_uuid = sorted(zip(sort_order, uuid))
self._photos = [self._db.photos(uuid=[uuid])[0] for _, uuid in sorted_uuid]
self._photos = [
self._db.photos(uuid=[uuid])[0] for _, uuid in sorted_uuid
]
else:
self._photos = []
return self._photos

77
osxphotos/personinfo.py Normal file
View File

@ -0,0 +1,77 @@
""" PhotoInfo methods to expose info about person in the Photos library """
import json
import logging
class PersonInfo:
""" Info about a person in the Photos library
"""
def __init__(self, db=None, pk=None):
""" Creates a new PersonInfo instance
Arguments:
db: instance of PhotosDB object
pk: primary key value of person to initialize PersonInfo with
Returns:
PersonInfo instance
"""
self._db = db
self._pk = pk
person = self._db._dbpersons_pk[pk]
self.uuid = person["uuid"]
self.name = person["fullname"]
self.display_name = person["displayname"]
self.keyface = person["keyface"]
self.facecount = person["facecount"]
@property
def keyphoto(self):
try:
return self._keyphoto
except AttributeError:
person = self._db._dbpersons_pk[self._pk]
if person["photo_uuid"]:
try:
key_photo = self._db.get_photo(person["photo_uuid"])
except IndexError:
key_photo = None
else:
key_photo = None
self._keyphoto = key_photo
return self._keyphoto
@property
def photos(self):
""" Returns list of PhotoInfo objects associated with this person """
return self._db.photos_by_uuid(self._db._dbfaces_pk[self._pk])
def json(self):
""" Returns JSON representation of class instance """
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
person = {
"uuid": self.uuid,
"name": self.name,
"displayname": self.display_name,
"keyface": self.keyface,
"facecount": self.facecount,
"keyphoto": keyphoto,
}
return json.dumps(person)
def __str__(self):
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return all(
getattr(self, field) == getattr(other, field) for field in ["_db", "_pk"]
)
def __ne__(self, other):
return not self.__eq__(other)

View File

@ -5,6 +5,7 @@ import os
from ..exiftool import ExifTool, get_exiftool_path
@property
def exiftool(self):
""" Returns an ExifTool object for the photo
@ -26,8 +27,9 @@ def exiftool(self):
except FileNotFoundError:
# get_exiftool_path raises FileNotFoundError if exiftool not found
exiftool = None
logging.warning(f"exiftool not in path; download and install from https://exiftool.org/")
logging.warning(
f"exiftool not in path; download and install from https://exiftool.org/"
)
self._exiftool = exiftool
return self._exiftool

View File

@ -29,6 +29,7 @@ from .._constants import (
_PHOTOS_5_SHARED_PHOTO_PATH,
)
from ..albuminfo import AlbumInfo
from ..personinfo import PersonInfo
from ..phototemplate import PhotoTemplate
from ..placeinfo import PlaceInfo4, PlaceInfo5
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
@ -339,7 +340,18 @@ class PhotoInfo:
@property
def persons(self):
""" list of persons in picture """
return [self._db._dbpersons_pk[k]["fullname"] for k in self._info["persons"]]
return [self._db._dbpersons_pk[pk]["fullname"] for pk in self._info["persons"]]
@property
def person_info(self):
""" list of PersonInfo objects for person in picture """
try:
return self._personinfo
except AttributeError:
self._personinfo = [
PersonInfo(db=self._db, pk=pk) for pk in self._info["persons"]
]
return self._personinfo
@property
def albums(self):

View File

@ -34,6 +34,7 @@ from .._constants import (
)
from .._version import __version__
from ..albuminfo import AlbumInfo, FolderInfo
from ..personinfo import PersonInfo
from ..photoinfo import PhotoInfo
from ..utils import (
_check_file_exists,
@ -44,7 +45,6 @@ from ..utils import (
get_last_library_path,
)
# TODO: Add test for imageTimeZoneOffsetSeconds = None
# TODO: Add test for __str__
# TODO: Add special albums and magic albums
@ -87,6 +87,7 @@ class PhotosDB:
# set up the data structures used to store all the Photo database info
# TODO: I don't think these keywords flags are actually used
# if True, will treat persons as keywords when exporting metadata
self.use_persons_as_keywords = False
@ -372,6 +373,17 @@ class PhotosDB:
persons = {self._dbpersons_pk[k]["fullname"] for k in self._dbfaces_pk}
return list(persons)
@property
def person_info(self):
""" return list of PersonInfo objects for each person in the photos database """
try:
return self._person_info
except AttributeError:
self._person_info = [
PersonInfo(db=self, pk=pk) for pk in self._dbpersons_pk
]
return self._person_info
@property
def folder_info(self):
""" return list FolderInfo objects representing top-level folders in the photos database """
@ -561,7 +573,8 @@ class PhotosDB:
RKPerson.uuid,
RKPerson.name,
RKPerson.faceCount,
RKPerson.displayName
RKPerson.displayName,
RKPerson.representativeFaceId
FROM RKPerson
"""
)
@ -571,6 +584,7 @@ class PhotosDB:
# 2 RKPerson.name,
# 3 RKPerson.faceCount,
# 4 RKPerson.displayName
# 5 RKPerson.representativeFaceId
for person in c:
pk = person[0]
@ -580,14 +594,43 @@ class PhotosDB:
"uuid": person[1],
"fullname": fullname,
"facecount": person[3],
"keyface": None,
"keyface": person[5],
"displayname": person[4],
"photo_uuid": None,
"keyface_uuid": None,
}
try:
self._dbpersons_fullname[fullname].append(pk)
except KeyError:
self._dbpersons_fullname[fullname] = [pk]
# get info on key face
c.execute(
""" SELECT
RKPerson.modelID,
RKPerson.representativeFaceId,
RKVersion.uuid,
RKFace.uuid
FROM RKPerson, RKFace, RKVersion
WHERE
RKFace.modelId = RKPerson.representativeFaceId AND
RKVersion.modelId = RKFace.ImageModelId
"""
)
# 0 RKPerson.modelID,
# 1 RKPerson.representativeFaceId
# 2 RKVersion.uuid,
# 3 RKFace.uuid
for person in c:
pk = person[0]
try:
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
except KeyError:
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
# get information on detected faces
c.execute(
""" SELECT
@ -632,8 +675,8 @@ class PhotosDB:
RKVersion.uuid,
RKCustomSortOrder.orderNumber
FROM RKVersion
JOIN RKCustomSortOrder on RKCustomSortOrder.objectUuid = RKVersion.uuid
JOIN RKAlbum on RKAlbum.uuid = RKCustomSortOrder.containerUuid
JOIN RKCustomSortOrder on RKCustomSortOrder.objectUuid = RKVersion.uuid
JOIN RKAlbum on RKAlbum.uuid = RKCustomSortOrder.containerUuid
"""
)
@ -1417,7 +1460,7 @@ class PhotosDB:
# 2 ZPERSON.ZFULLNAME,
# 3 ZPERSON.ZFACECOUNT,
# 4 ZPERSON.ZKEYFACE,
# 5 ZPERSON.ZDISPLAYNAME,
# 5 ZPERSON.ZDISPLAYNAME
for person in c:
pk = person[0]
@ -1429,12 +1472,41 @@ class PhotosDB:
"facecount": person[3],
"keyface": person[4],
"displayname": person[5],
"photo_uuid": None,
"keyface_uuid": None,
}
try:
self._dbpersons_fullname[fullname].append(pk)
except KeyError:
self._dbpersons_fullname[fullname] = [pk]
# get info on keyface -- some photos have null keyface so can't do a single query
# (at least not with my SQL skills)
c.execute(
""" SELECT
ZPERSON.Z_PK,
ZPERSON.ZKEYFACE,
ZGENERICASSET.ZUUID,
ZDETECTEDFACE.ZUUID
FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET
WHERE ZDETECTEDFACE.Z_PK = ZPERSON.ZKEYFACE AND
ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK
"""
)
# 0 ZPERSON.Z_PK,
# 1 ZPERSON.ZKEYFACE,
# 2 ZGENERICASSET.ZUUID,
# 3 ZDETECTEDFACE.ZUUID
for person in c:
pk = person[0]
try:
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
except KeyError:
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
# get information on detected faces
c.execute(
""" SELECT
@ -1442,7 +1514,7 @@ class PhotosDB:
ZGENERICASSET.ZUUID
FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET
WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND
ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK;
ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK
"""
)
@ -1474,7 +1546,7 @@ class PhotosDB:
""" SELECT
ZGENERICALBUM.ZUUID,
ZGENERICASSET.ZUUID,
Z_26ASSETS.Z_FOK_34ASSETS
Z_26ASSETS.Z_FOK_34ASSETS
FROM ZGENERICASSET
JOIN Z_26ASSETS ON Z_26ASSETS.Z_34ASSETS = ZGENERICASSET.Z_PK
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS
@ -1483,7 +1555,7 @@ class PhotosDB:
# 0 ZGENERICALBUM.ZUUID,
# 1 ZGENERICASSET.ZUUID,
# 2 Z_26ASSETS.Z_FOK_34ASSETS
# 2 Z_26ASSETS.Z_FOK_34ASSETS
for album in c:
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
@ -1633,15 +1705,15 @@ class PhotosDB:
ZGENERICASSET.ZCLOUDBATCHPUBLISHDATE,
ZGENERICASSET.ZKIND,
ZGENERICASSET.ZUNIFORMTYPEIDENTIFIER,
ZGENERICASSET.ZAVALANCHEUUID,
ZGENERICASSET.ZAVALANCHEPICKTYPE,
ZGENERICASSET.ZAVALANCHEUUID,
ZGENERICASSET.ZAVALANCHEPICKTYPE,
ZGENERICASSET.ZKINDSUBTYPE,
ZGENERICASSET.ZCUSTOMRENDEREDVALUE,
ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE,
ZGENERICASSET.ZCLOUDASSETGUID,
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
ZGENERICASSET.ZMOMENT,
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
ZGENERICASSET.ZTRASHEDSTATE,
ZGENERICASSET.ZHEIGHT,
ZGENERICASSET.ZWIDTH,
@ -2437,7 +2509,11 @@ class PhotosDB:
for person in persons:
if person in self._dbpersons_fullname:
for pk in self._dbpersons_fullname[person]:
person_set.update(self._dbfaces_pk[pk])
try:
person_set.update(self._dbfaces_pk[pk])
except KeyError:
# some persons have zero photos so they won't be in _dbfaces_pk
pass
else:
logging.debug(f"Could not find person '{person}' in database")
photos_sets.append(person_set)
@ -2476,6 +2552,42 @@ class PhotosDB:
return photoinfo
def get_photo(self, uuid):
""" Returns a single photo matching uuid
Arguments:
uuid: the UUID of photo to get
Returns:
PhotoInfo instance for photo with UUID matching uuid or None if no match
"""
try:
return PhotoInfo(db=self, uuid=uuid, info=self._dbphotos[uuid])
except KeyError:
return None
# TODO: add to docs and test
def photos_by_uuid(self, uuids):
""" Returns a list of photos with UUID in uuids.
Does not generate error if invalid or missing UUID passed.
This is faster than using PhotosDB.photos if you have list of UUIDs.
Returns photos regardless of intrash state.
Arguments:
uuid: list of UUIDs of photos to get
Returns:
list of PhotoInfo instance for photo with UUID matching uuid or [] if no match
"""
photos = []
for uuid in uuids:
try:
photos.append(PhotoInfo(db=self, uuid=uuid, info=self._dbphotos[uuid]))
except KeyError:
# ignore missing/invlaid UUID
pass
return photos
def __repr__(self):
return f"osxphotos.{self.__class__.__name__}(dbfile='{self.db_path}')"

View File

@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-04-25T23:54:43Z</date>
<date>2020-07-16T04:41:20Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-07-06T16:39:04Z</date>
<date>2020-07-16T04:41:20Z</date>
</dict>
</plist>

View File

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

View File

@ -11,6 +11,6 @@
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
<integer>1</integer>
<key>PLLastRevGeoVerFileFetchDateKey</key>
<date>2020-07-06T16:39:09Z</date>
<date>2020-07-16T04:41:16Z</date>
</dict>
</plist>

View File

@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastHistoryRowId</key>
<integer>664</integer>
<integer>707</integer>
<key>LibraryBuildTag</key>
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
<key>LibrarySchemaVersion</key>

View File

@ -9,7 +9,7 @@
<key>HistoricalMarker</key>
<dict>
<key>LastHistoryRowId</key>
<integer>664</integer>
<integer>707</integer>
<key>LibraryBuildTag</key>
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
<key>LibrarySchemaVersion</key>
@ -24,7 +24,7 @@
<key>SnapshotCompletedDate</key>
<date>2019-07-27T13:16:43Z</date>
<key>SnapshotLastValidated</key>
<date>2020-07-06T16:39:02Z</date>
<date>2020-07-16T04:41:16Z</date>
<key>SnapshotTables</key>
<dict/>
</dict>

View File

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

View File

@ -25,7 +25,7 @@ ALBUMS = [
"Pumpkin Farm",
"Test Album",
"AlbumInFolder",
"Raw"
"Raw",
] # Note: there are 2 albums named "Test Album" for testing duplicate album names
KEYWORDS_DICT = {
"Kids": 4,
@ -782,7 +782,7 @@ def test_from_to_date():
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
assert len(photos) ==6
assert len(photos) == 6
photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28))
assert len(photos) == 6

View File

@ -81,12 +81,13 @@ UUID_PUMPKIN_FARM = [
]
ALBUM_SORT_ORDER = [
"1EB2B765-0765-43BA-A90C-0D0580E6172C",
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
"D79B8D77-BFFC-460B-9312-034F2877D35B",
"1EB2B765-0765-43BA-A90C-0D0580E6172C",
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
"D79B8D77-BFFC-460B-9312-034F2877D35B",
]
ALBUM_KEY_PHOTO = "D79B8D77-BFFC-460B-9312-034F2877D35B"
def test_init1():
# test named argument
import osxphotos
@ -223,6 +224,7 @@ def test_albums_as_dict():
assert albums["Pumpkin Farm"] == 3
assert albums == ALBUM_DICT
def test_album_sort_order():
import osxphotos
@ -233,6 +235,7 @@ def test_album_sort_order():
uuids = [p.uuid for p in photos]
assert uuids == ALBUM_SORT_ORDER
def test_album_empty_album():
import osxphotos
@ -241,6 +244,7 @@ def test_album_empty_album():
photos = album.photos
assert photos == []
def test_attributes():
import datetime
import osxphotos
@ -583,6 +587,7 @@ def test_album_folder_name():
photos = photosdb.photos(albums=["Pumpkin Farm"])
assert sorted(p.uuid for p in photos) == sorted(UUID_PUMPKIN_FARM)
def test_multi_person():
import osxphotos
@ -591,6 +596,7 @@ def test_multi_person():
assert len(photos) == 3
def test_get_db_path():
import osxphotos
@ -1068,4 +1074,3 @@ def test_date_modified_invalid():
assert len(photos) == 1
p = photos[0]
assert p.date_modified is None

View File

@ -231,12 +231,18 @@ CLI_EXIFTOOL = {
LABELS_JSON = {
"labels": {
"Plant": 5,
"Plant": 7,
"Outdoor": 4,
"Sky": 3,
"Tree": 2,
"Sky": 2,
"Outdoor": 2,
"Art": 2,
"Foliage": 2,
"People": 2,
"Agriculture": 2,
"Farm": 2,
"Food": 2,
"Vegetable": 2,
"Pumpkin": 2,
"Waterways": 1,
"River": 1,
"Cloudy": 1,
@ -254,6 +260,10 @@ LABELS_JSON = {
"Vase": 1,
"Container": 1,
"Camera": 1,
"Child": 1,
"Clothing": 1,
"Jeans": 1,
"Straw Hay": 1,
}
}
@ -971,7 +981,7 @@ def test_query_label_4():
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 6
assert len(json_got) == 8
def test_query_deleted_deleted_only():

View File

@ -825,4 +825,3 @@ def test_xmp_sidecar_gps():
sorted(xmp_expected_lines), sorted(xmp_got_lines)
):
assert line_expected == line_got

View File

@ -57,4 +57,3 @@ def test_cloudasset_3():
photos = photosdb.photos(uuid=[UUID_DICT["not_cloudasset"]])
assert not photos[0].iscloudasset

View File

@ -57,4 +57,3 @@ def test_cloudasset_3():
photos = photosdb.photos(uuid=[UUID_DICT["not_cloudasset"]])
assert not photos[0].iscloudasset

View File

@ -26,4 +26,3 @@ def test_not_modified():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["not_modified"]])
assert photos[0].date_modified is None

View File

@ -27,4 +27,3 @@ def test_modified():
# photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# photos = photosdb.photos(uuid=[UUID_DICT["not_modified"]])
# assert photos[0].date_modified is None

View File

@ -554,4 +554,3 @@ def test_date_modified_invalid():
assert len(photos) == 1
p = photos[0]
assert p.date_modified is None

150
tests/test_personinfo.py Normal file
View File

@ -0,0 +1,150 @@
""" Test PersonInfo class """
import pytest
PHOTOS_DB_5 = "tests/Test-10.15.5.photoslibrary"
PHOTOS_DB_4 = "tests/Test-10.14.6.photoslibrary"
UUID_DICT = {
"katie_5": "0FFCE0A2-BE93-4661-A783-957BE54072E4",
"katie_4": "D%zgor6TRmGng5V75UBy5A",
}
PHOTO_DICT = {
"katie_5": [
"1EB2B765-0765-43BA-A90C-0D0580E6172C",
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
"D79B8D77-BFFC-460B-9312-034F2877D35B",
],
"katie_4": [
"8SOE9s0XQVGsuq4ONohTng",
"HrK3ZQdlQ7qpDA0FgOYXLA",
"15uNd7%8RguTEgNPKHfTWw",
],
}
KEY_DICT = {
"katie_5": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
"katie_4": "8SOE9s0XQVGsuq4ONohTng",
}
STR_DICT = {
"katie_5": "PersonInfo(name=Katie, display_name=Katie, uuid=0FFCE0A2-BE93-4661-A783-957BE54072E4, facecount=3)",
"katie_4": "PersonInfo(name=Katie, display_name=Katie, uuid=D%zgor6TRmGng5V75UBy5A, facecount=3)",
}
JSON_DICT = {
"katie_5": {
"uuid": "0FFCE0A2-BE93-4661-A783-957BE54072E4",
"name": "Katie",
"displayname": "Katie",
"keyface": 2,
"facecount": 3,
"keyphoto": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
},
"katie_4": {
"uuid": "D%zgor6TRmGng5V75UBy5A",
"name": "Katie",
"displayname": "Katie",
"keyface": 7,
"facecount": 3,
"keyphoto": "8SOE9s0XQVGsuq4ONohTng",
},
}
@pytest.fixture
def photosdb5():
import osxphotos
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_5)
@pytest.fixture
def photosdb4():
import osxphotos
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_4)
def test_person_info_photosdb_v5(photosdb5):
""" Test PersonInfo object """
import json
test_key = "katie_5"
katie = [p for p in photosdb5.person_info if p.uuid == UUID_DICT[test_key]][0]
assert katie.facecount == 3
assert katie.name == "Katie"
assert katie.display_name == "Katie"
photos = katie.photos
assert len(photos) == 3
uuid = [p.uuid for p in photos]
assert sorted(uuid) == sorted(PHOTO_DICT[test_key])
assert str(katie) == STR_DICT[test_key]
assert json.loads(katie.json()) == JSON_DICT[test_key]
def test_person_info_photosinfo_v5(photosdb5):
""" Test PersonInfo object """
import json
test_key = "katie_5"
photo = photosdb5.photos(uuid=[KEY_DICT[test_key]])[0]
assert "Katie" in photo.persons
person_info = photo.person_info
assert len(person_info) == 2
katie = [p for p in person_info if p.name == "Katie"][0]
assert katie.facecount == 3
assert katie.name == "Katie"
assert katie.display_name == "Katie"
photos = katie.photos
assert len(photos) == 3
uuid = [p.uuid for p in photos]
assert sorted(uuid) == sorted(PHOTO_DICT[test_key])
assert katie.keyphoto.uuid == KEY_DICT[test_key]
assert str(katie) == STR_DICT[test_key]
assert json.loads(katie.json()) == JSON_DICT[test_key]
def test_person_info_photosdb_v4(photosdb4):
""" Test PersonInfo object """
import json
test_key = "katie_4"
katie = [p for p in photosdb4.person_info if p.uuid == UUID_DICT[test_key]][0]
assert katie.facecount == 3
assert katie.name == "Katie"
assert katie.display_name == "Katie"
photos = katie.photos
assert len(photos) == 3
uuid = [p.uuid for p in photos]
assert sorted(uuid) == sorted(PHOTO_DICT[test_key])
assert katie.keyphoto.uuid == KEY_DICT[test_key]
assert json.loads(katie.json()) == JSON_DICT[test_key]
def test_person_info_photosinfo_v4(photosdb4):
""" Test PersonInfo object """
import json
test_key = "katie_4"
photo = photosdb4.photos(uuid=[KEY_DICT[test_key]])[0]
assert "Katie" in photo.persons
person_info = photo.person_info
assert len(person_info) == 2
katie = [p for p in person_info if p.name == "Katie"][0]
assert katie.facecount == 3
assert katie.name == "Katie"
assert katie.display_name == "Katie"
photos = katie.photos
assert len(photos) == 3
uuid = [p.uuid for p in photos]
assert sorted(uuid) == sorted(PHOTO_DICT[test_key])
assert katie.keyphoto.uuid == KEY_DICT[test_key]
assert json.loads(katie.json()) == JSON_DICT[test_key]

View File

@ -87,6 +87,7 @@ def test_place_str():
"field16=[], street_address=[], body_of_water=[])', country_code='GB')"
)
def test_place_as_dict():
# test PlaceInfo.as_dict()
import osxphotos

View File

@ -20,7 +20,17 @@ LABELS_DICT = {
"Tree",
],
# F12384F6-CD17-4151-ACBA-AE0E3688539E Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [],
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [
"Vegetable",
"Pumpkin",
"Farm",
"Food",
"Outdoor",
"Agriculture",
"People",
"Plant",
"Straw Hay",
],
# D79B8D77-BFFC-460B-9312-034F2877D35B Pumkins2.jpg I found one! Girl holding pumpkin [] False
"D79B8D77-BFFC-460B-9312-034F2877D35B": [],
# D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068 DSC03584.dng None RAW only [] False
@ -49,7 +59,20 @@ LABELS_DICT = {
"Plant",
],
# 1EB2B765-0765-43BA-A90C-0D0580E6172C Pumpkins3.jpg None Kids in pumpkin field [] False
"1EB2B765-0765-43BA-A90C-0D0580E6172C": [],
"1EB2B765-0765-43BA-A90C-0D0580E6172C": [
"Child",
"Sky",
"Plant",
"People",
"Clothing",
"Jeans",
"Outdoor",
"Agriculture",
"Farm",
"Food",
"Vegetable",
"Pumpkin",
],
# DC99FBDD-7A52-4100-A5BB-344131646C30 St James Park.jpg St. James's Park None ['Tree', 'Plant', 'Waterways', 'River', 'Sky', 'Cloudy', 'Land', 'Water Body', 'Water', 'Outdoor'] False
"DC99FBDD-7A52-4100-A5BB-344131646C30": [
"Tree",
@ -117,8 +140,33 @@ LABELS_NORMALIZED_DICT = {
],
# D79B8D77-BFFC-460B-9312-034F2877D35B Pumkins2.jpg I found one! Girl holding pumpkin [] False
"D79B8D77-BFFC-460B-9312-034F2877D35B": [],
# 1EB2B765-0765-43BA-A90C-0D0580E6172C Pumpkins3.jpg None Kids in pumpkin field [] False
"1EB2B765-0765-43BA-A90C-0D0580E6172C": [
"child",
"sky",
"plant",
"people",
"clothing",
"jeans",
"outdoor",
"agriculture",
"farm",
"food",
"vegetable",
"pumpkin",
],
# F12384F6-CD17-4151-ACBA-AE0E3688539E Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [],
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [
"vegetable",
"pumpkin",
"farm",
"food",
"outdoor",
"agriculture",
"people",
"plant",
"straw hay",
],
# A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C Pumpkins4.jpg Pumpkin heads None [] True
"A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C": [],
}
@ -155,6 +203,16 @@ LABELS = [
"Vase",
"Container",
"Camera",
"Child",
"People",
"Clothing",
"Jeans",
"Agriculture",
"Farm",
"Food",
"Vegetable",
"Pumpkin",
"Straw Hay",
]
LABELS_NORMALIZED = [
@ -181,15 +239,31 @@ LABELS_NORMALIZED = [
"vase",
"container",
"camera",
"child",
"people",
"clothing",
"jeans",
"agriculture",
"farm",
"food",
"vegetable",
"pumpkin",
"straw hay",
]
LABELS_AS_DICT = {
"Plant": 5,
"Plant": 7,
"Outdoor": 4,
"Sky": 3,
"Tree": 2,
"Sky": 2,
"Outdoor": 2,
"Art": 2,
"Foliage": 2,
"People": 2,
"Agriculture": 2,
"Farm": 2,
"Food": 2,
"Vegetable": 2,
"Pumpkin": 2,
"Waterways": 1,
"River": 1,
"Cloudy": 1,
@ -207,15 +281,25 @@ LABELS_AS_DICT = {
"Vase": 1,
"Container": 1,
"Camera": 1,
"Child": 1,
"Clothing": 1,
"Jeans": 1,
"Straw Hay": 1,
}
LABELS_NORMALIZED_AS_DICT = {
"plant": 5,
"plant": 7,
"outdoor": 4,
"sky": 3,
"tree": 2,
"sky": 2,
"outdoor": 2,
"art": 2,
"foliage": 2,
"people": 2,
"agriculture": 2,
"farm": 2,
"food": 2,
"vegetable": 2,
"pumpkin": 2,
"waterways": 1,
"river": 1,
"cloudy": 1,
@ -233,6 +317,10 @@ LABELS_NORMALIZED_AS_DICT = {
"vase": 1,
"container": 1,
"camera": 1,
"child": 1,
"clothing": 1,
"jeans": 1,
"straw hay": 1,
}

View File

@ -502,4 +502,3 @@ def test_subst_expand_inplace_3():
template, expand_inplace=True, inplace_sep="; "
)
assert sorted(rendered) == sorted(expected)