Refactored photosdb and photoinfo to add SearchInfo and labels

This commit is contained in:
Rhet Turnbull
2020-05-10 19:55:09 -07:00
parent 397db0d72f
commit 98b3f63a92
13 changed files with 707 additions and 21 deletions

View File

@@ -701,6 +701,29 @@ Returns a dictionary of shared albums (e.g. shared via iCloud photo sharing) fou
**Note**: *Photos 5 / MacOS 10.15 only*. On earlier versions of Photos, prints warning and returns empty dictionary.
#### `labels`
Returns image categorization labels associated with photos in the library as list of str.
**Note**: Only valid on Photos 5; on earlier versions, returns empty list. In Photos 5, Photos runs machine learning image categorization against photos in the library and automatically assigns labels to photos such as "People", "Dog", "Water", etc. A photo may have zero or more labels associated with it. See also [labels_normalized](#labels_normalized).
#### `labels_normalized`
Returns image categorization labels associated with photos in the library as list of str. Labels are normalized (e.g. converted to lower case). Use of normalized strings makes it easier to search if you don't how Apple capitalizes a label.
**Note**: Only valid on Photos 5; on earlier versions, returns empty list. In Photos 5, Photos runs machine learning image categorization against photos in the library and automatically assigns labels to photos such as "People", "Dog", "Water", etc. A photo may have zero or more labels associated with it. See also [labels](#labels).
#### `labels_as_dict`
Returns dictionary image categorization labels associated with photos in the library where key is label and value is number of photos in the library with the label.
**Note**: Only valid on Photos 5; on earlier versions, logs warning and returns empty dict. In Photos 5, Photos runs machine learning image categorization against photos in the library and automatically assigns labels to photos such as "People", "Dog", "Water", etc. A photo may have zero or more labels associated with it. See also [labels_normalized_as_dict](#labels_normalized_as_dict).
#### `labels_normalized_as_dict`
Returns dictionary of image categorization labels associated with photos in the library where key is normalized label and value is number of photos in the library with that label. Labels are normalized (e.g. converted to lower case). Use of normalized strings makes it easier to search if you don't how Apple capitalizes a label.
**Note**: Only valid on Photos 5; on earlier versions, logs warning and returns empty dict. In Photos 5, Photos runs machine learning image categorization against photos in the library and automatically assigns labels to photos such as "People", "Dog", "Water", etc. A photo may have zero or more labels associated with it. See also [labels_as_dict](#labels_as_dict).
#### `library_path`
```python
# assumes photosdb is a PhotosDB object (see above)
@@ -980,6 +1003,27 @@ Returns True if photo is a panorama, otherwise False.
**Note**: The result of `PhotoInfo.panorama` will differ from the "Panoramas" Media Types smart album in that it will also identify panorama photos from older phones that Photos does not recognize as panoramas.
#### `labels`
Returns image categorization labels associated with the photo as list of str.
**Note**: Only valid on Photos 5; on earlier versions, returns empty list. In Photos 5, Photos runs machine learning image categorization against photos in the library and automatically assigns labels to photos such as "People", "Dog", "Water", etc. A photo may have zero or more labels associated with it. See also [labels_normalized](#labels_normalized).
#### `labels_normalized`
Returns image categorization labels associated with the photo as list of str. Labels are normalized (e.g. converted to lower case). Use of normalized strings makes it easier to search if you don't how Apple capitalizes a label. For example:
```python
import osxphotos
photosdb = osxphotos.PhotosDB()
for photo in photosdb.photos():
if "statue" in photo.labels_normalized:
print(f"I found a statue! {photo.original_filename}")
```
**Note**: Only valid on Photos 5; on earlier versions, returns empty list. In Photos 5, Photos runs machine learning image categorization against photos in the library and automatically assigns labels to photos such as "People", "Dog", "Water", etc. A photo may have zero or more labels associated with it. See also [labels](#labels).
#### `json()`
Returns a JSON representation of all photo info

View File

@@ -60,3 +60,6 @@ _MAX_IPTC_KEYWORD_LEN = 64
# Sentinel value for detecting if a template in keyword_template doesn't match
# If anyone has a keyword matching this, then too bad...
_OSXPHOTOS_NONE_SENTINEL = "OSXPhotosXYZZY42_Sentinel$"
# SearchInfo categories for Photos 5, corresponds to categories in database/search/psi.sqlite
SEARCH_CATEGORY_LABEL = 2024

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.28.14"
__version__ = "0.28.15"

View File

@@ -0,0 +1,8 @@
"""
PhotoInfo class
Represents a single photo in the Photos library and provides access to the photo's attributes
PhotosDB.photos() returns a list of PhotoInfo objects
"""
from .photoinfo import PhotoInfo

View File

@@ -19,7 +19,7 @@ from pprint import pformat
import yaml
from mako.template import Template
from ._constants import (
from .._constants import (
_MAX_IPTC_KEYWORD_LEN,
_MOVIE_TYPE,
_OSXPHOTOS_NONE_SENTINEL,
@@ -30,16 +30,16 @@ from ._constants import (
_UNKNOWN_PERSON,
_XMP_TEMPLATE_NAME,
)
from .albuminfo import AlbumInfo
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifTool
from .placeinfo import PlaceInfo4, PlaceInfo5
from .template import (
from ..albuminfo import AlbumInfo
from ..datetime_formatter import DateTimeFormatter
from ..exiftool import ExifTool
from ..placeinfo import PlaceInfo4, PlaceInfo5
from ..template import (
MULTI_VALUE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
)
from .utils import (
from ..utils import (
_hardlink_file,
_copy_file,
_export_photo_uuid_applescript,
@@ -49,8 +49,11 @@ from .utils import (
get_preferred_uti_extension,
)
# Mixins
from .photoinfo_mixin_searchinfo import PhotoInfoMixinSearchInfo, SearchInfo
class PhotoInfo:
class PhotoInfo(PhotoInfoMixinSearchInfo):
"""
Info about a specific photo, contains all the details about the photo
including keywords, persons, albums, uuid, path, etc.
@@ -808,7 +811,7 @@ class PhotoInfo:
if export_as_hardlink:
_hardlink_file(src, dest)
else:
_copy_file(src, dest, norsrc=no_xattr)
_copy_file(src, dest, norsrc=no_xattr)
exported_files.append(str(dest))
# copy live photo associated .mov if requested

View File

@@ -0,0 +1,96 @@
""" SearchInfo class exposing labels and other search info for Photos 5 databases
and
PhotoInfoMixinSearchInfo mixin class for PhotoInfo """
from .._constants import _PHOTOS_4_VERSION, SEARCH_CATEGORY_LABEL
class PhotoInfoMixinSearchInfo:
""" Mixin class for PhotoInfo exposing SearchInfo data such as labels
Adds the following properties to PhotoInfo (valid only for Photos 5):
search_info: returns a SearchInfo object
labels: returns list of labels
labels_normalized: returns list of normalized labels
"""
@property
def search_info(self):
""" returns SearchInfo object for photo
only valid on Photos 5, on older libraries, returns None
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return None
# memoize SearchInfo object
try:
return self._search_info
except AttributeError:
self._search_info = SearchInfo(self)
return self._search_info
@property
def labels(self):
""" returns list of labels applied to photo by Photos image categorization
only valid on Photos 5, on older libraries returns empty list
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return []
return self.search_info.labels
@property
def labels_normalized(self):
""" returns normalized list of labels applied to photo by Photos image categorization
only valid on Photos 5, on older libraries returns empty list
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return []
return self.search_info.labels_normalized
class SearchInfo:
""" Info about search terms such as machine learning labels that Photos knows about a photo """
def __init__(self, photo):
""" photo: PhotoInfo object """
if photo._db._db_version <= _PHOTOS_4_VERSION:
raise NotImplementedError(
f"search info not implemented for this database version"
)
self._photo = photo
self.uuid = photo.uuid
try:
# get search info for this UUID
# there might not be any search info data (e.g. if Photo was missing or photoanalysisd not run yet)
self._db_searchinfo = photo._db._db_searchinfo_uuid[self.uuid]
except KeyError:
self._db_searchinfo = None
@property
def labels(self):
""" return list of labels associated with Photo """
if self._db_searchinfo:
labels = [
rec["content_string"]
for rec in self._db_searchinfo
if rec["category"] == SEARCH_CATEGORY_LABEL
]
else:
labels = []
return labels
@property
def labels_normalized(self):
""" return list of normalized labels associated with Photo """
if self._db_searchinfo:
labels = [
rec["normalized_string"]
for rec in self._db_searchinfo
if rec["category"] == SEARCH_CATEGORY_LABEL
]
else:
labels = []
return labels

View File

@@ -0,0 +1,6 @@
"""
PhotosDB class
Processes a Photos.app library database to extract information about photos
"""
from .photosdb import PhotosDB

View File

@@ -15,7 +15,7 @@ from datetime import datetime
from pprint import pformat
from shutil import copyfile
from ._constants import (
from .._constants import (
_MOVIE_TYPE,
_PHOTO_TYPE,
_PHOTOS_3_VERSION,
@@ -32,10 +32,10 @@ from ._constants import (
_TESTED_OS_VERSIONS,
_UNKNOWN_PERSON,
)
from ._version import __version__
from .albuminfo import AlbumInfo, FolderInfo
from .photoinfo import PhotoInfo
from .utils import (
from .._version import __version__
from ..albuminfo import AlbumInfo, FolderInfo
from ..photoinfo import PhotoInfo
from ..utils import (
_check_file_exists,
_db_is_locked,
_debug,
@@ -44,6 +44,9 @@ from .utils import (
get_last_library_path,
)
# mixins
from .photosdb_mixin_searchinfo import PhotosDBMixinSearchInfo
# TODO: Add test for imageTimeZoneOffsetSeconds = None
# TODO: Fix command line so multiple --keyword, etc. are AND (instead of OR as they are in .photos())
# Or fix the help text to match behavior
@@ -52,7 +55,7 @@ from .utils import (
# TODO: fix "if X not in y" dictionary checks to use try/except EAFP style
class PhotosDB:
class PhotosDB(PhotosDBMixinSearchInfo):
""" Processes a Photos.app library database to extract information about photos """
def __init__(self, *dbfile_, dbfile=None):
@@ -1842,6 +1845,9 @@ class PhotosDB:
# close connection and remove temporary files
conn.close()
# process search info
self._process_searchinfo()
# done processing, dump debug data if requested
if _debug():
logging.debug("Faces (_dbfaces_uuid):")

View File

@@ -0,0 +1,189 @@
""" Mixin class for PhotosDB to add Photos 5 search info such as machine learning labels """
import logging
import pathlib
import uuid as uuidlib
from pprint import pformat
from .._constants import _PHOTOS_4_VERSION, SEARCH_CATEGORY_LABEL
from ..utils import _db_is_locked, _debug, _open_sql_file
class PhotosDBMixinSearchInfo:
""" Mixin class to extend PhotosDB to process search info terms
This mixin adds the following method to PhotosDB:
_process_searchinfo: process search terms from psi.sqlite
The following properties are added to PhotosDB
labels: list of all labels in the library
labels_normalized: list of all labels normalized in the library
labels_as_dict: dict of {label: count of photos} in reverse sorted order (most photos first)
labels_normalized_as_dict: dict of {normalized label: count of photos} in reverse sorted order (most photos first)
The following data structures are added to PhotosDB
self._db_searchinfo_categories
self._db_searchinfo_uuid
self._db_searchinfo_labels
self._db_searchinfo_labels_normalized
These methods only work on Photos 5 databases. Will print warning on earlier library versions.
"""
def _process_searchinfo(self):
""" load machine learning/search term label info from a Photos library
db_connection: a connection to the SQLite database file containing the
search terms. In Photos 5, this is called psi.sqlite
Note: Only works on Photos version == 5.0 """
if self._db_version <= _PHOTOS_4_VERSION:
raise NotImplementedError(
f"search info not implemented for this database version"
)
search_db_path = pathlib.Path(self._dbfile).parent / "search" / "psi.sqlite"
if not search_db_path.exists():
raise FileNotFoundError(f"could not find search db: {search_db_path}")
if _db_is_locked(search_db_path):
search_db = self._copy_db_file(search_db_path)
else:
search_db = search_db_path
(conn, c) = _open_sql_file(search_db)
result = conn.execute(
"""
select
ga.rowid,
assets.uuid_0,
assets.uuid_1,
groups.rowid as groupid,
groups.category,
groups.owning_groupid,
groups.content_string,
groups.normalized_string,
groups.lookup_identifier
from
ga
join groups on groups.rowid = ga.groupid
join assets on ga.assetid = assets.rowid
order by
ga.rowid
"""
)
# _db_searchinfo_uuid is dict in form {uuid : [list of associated search info records]
_db_searchinfo_uuid = {}
# _db_searchinfo_categories is dict in form {search info category id: list normalized strings for the category
# right now, this is mostly for debugging to easily see which search terms are in the library
_db_searchinfo_categories = {}
# _db_searchinfo_labels is dict in form {normalized label: [list of photo uuids]}
# this serves as a reverse index from label to photos containing the label
# _db_searchinfo_labels_normalized is the same but with normalized (lower case) version of the label
_db_searchinfo_labels = {}
_db_searchinfo_labels_normalized = {}
cols = [c[0] for c in result.description]
for row in result.fetchall():
record = dict(zip(cols, row))
uuid = ints_to_uuid(record["uuid_0"], record["uuid_1"])
# strings have null character appended, so strip it
for key in record:
if isinstance(record[key], str):
record[key] = record[key].replace("\x00", "")
try:
_db_searchinfo_uuid[uuid].append(record)
except KeyError:
_db_searchinfo_uuid[uuid] = [record]
category = record["category"]
try:
_db_searchinfo_categories[record["category"]].append(
record["normalized_string"]
)
except KeyError:
_db_searchinfo_categories[record["category"]] = [
record["normalized_string"]
]
if record["category"] == SEARCH_CATEGORY_LABEL:
label = record["content_string"]
label_norm = record["normalized_string"]
try:
_db_searchinfo_labels[label].append(uuid)
_db_searchinfo_labels_normalized[label_norm].append(uuid)
except KeyError:
_db_searchinfo_labels[label] = [uuid]
_db_searchinfo_labels_normalized[label_norm] = [uuid]
self._db_searchinfo_categories = _db_searchinfo_categories
self._db_searchinfo_uuid = _db_searchinfo_uuid
self._db_searchinfo_labels = _db_searchinfo_labels
self._db_searchinfo_labels_normalized = _db_searchinfo_labels_normalized
if _debug():
logging.debug(
"_db_searchinfo_categories: \n"
+ pformat(self._db_searchinfo_categories)
)
logging.debug("_db_searchinfo_uuid: \n" + pformat(self._db_searchinfo_uuid))
logging.debug(
"_db_searchinfo_labels: \n" + pformat(self._db_searchinfo_labels)
)
logging.debug(
"_db_searchinfo_labels_normalized: \n"
+ pformat(self._db_searchinfo_labels_normalized)
)
@property
def labels(self):
""" return list of all search info labels found in the library """
if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(f"SearchInfo not implemented for this library version")
return []
return list(self._db_searchinfo_labels.keys())
@property
def labels_normalized(self):
""" return list of all normalized search info labels found in the library """
if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(f"SearchInfo not implemented for this library version")
return []
return list(self._db_searchinfo_labels_normalized.keys())
@property
def labels_as_dict(self):
""" return labels as dict of label: count in reverse sorted order (descending) """
if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(f"SearchInfo not implemented for this library version")
return dict()
labels = {k: len(v) for k, v in self._db_searchinfo_labels.items()}
labels = dict(sorted(labels.items(), key=lambda kv: kv[1], reverse=True))
return labels
@property
def labels_normalized_as_dict(self):
""" return normalized labels as dict of label: count in reverse sorted order (descending) """
if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(f"SearchInfo not implemented for this library version")
return dict()
labels = {k: len(v) for k, v in self._db_searchinfo_labels_normalized.items()}
labels = dict(sorted(labels.items(), key=lambda kv: kv[1], reverse=True))
return labels
def ints_to_uuid(uuid_0, uuid_1):
""" convert two signed ints into a UUID strings
uuid_0, uuid_1: the two int components of an RFC 4122 UUID """
# assumes uuid imported as uuidlib (to avoid namespace conflict with other uses of uuid)
bytes_ = uuid_0.to_bytes(8, "little", signed=True) + uuid_1.to_bytes(
8, "little", signed=True
)
return str(uuidlib.UUID(bytes=bytes_)).upper()

View File

@@ -109,14 +109,15 @@ def test_init4():
def test_init5(mocker):
# test failed get_last_library_path
import osxphotos
def bad_library():
return None
# get_last_library actually in utils but need to patch it in photosdb because it's imported into photosdb
# as: from .utils import get_last_library
mocker.patch("osxphotos.photosdb.get_last_library_path", new=bad_library)
# because of the layout of photosdb/ need to patch it this way...don't really understand why, but it works
mocker.patch("osxphotos.photosdb.photosdb.get_last_library_path", new=bad_library)
with pytest.raises(Exception):
assert osxphotos.PhotosDB()

View File

@@ -115,8 +115,8 @@ def test_init5(mocker):
return None
# get_last_library actually in utils but need to patch it in photosdb because it's imported into photosdb
# as: from .utils import get_last_library
mocker.patch("osxphotos.photosdb.get_last_library_path", new=bad_library)
# because of the layout of photosdb/ need to patch it this way...don't really understand why, but it works
mocker.patch("osxphotos.photosdb.photosdb.get_last_library_path", new=bad_library)
with pytest.raises(Exception):
assert osxphotos.PhotosDB()

View File

@@ -0,0 +1,97 @@
""" test PhotoInfo.search_info """
# On 10.14.6, SearchInfo is not valid and returns None
import pytest
from osxphotos._constants import _UNKNOWN_PERSON
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
LABELS_DICT = {
# 8SOE9s0XQVGsuq4ONohTng Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
"8SOE9s0XQVGsuq4ONohTng": [],
# HrK3ZQdlQ7qpDA0FgOYXLA Pumpkins3.jpg None Kids in pumpkin field [] False
"HrK3ZQdlQ7qpDA0FgOYXLA": [],
# YZFCPY24TUySvpu7owiqxA Tulips.jpg Tulips tied together at a flower shop Wedding tulips [] False
"YZFCPY24TUySvpu7owiqxA": [],
# 15uNd7%8RguTEgNPKHfTWw Pumkins2.jpg I found one! Girl holding pumpkin [] False
"15uNd7%8RguTEgNPKHfTWw": [],
# 3Jn73XpSQQCluzRBMWRsMA St James Park.jpg St. James's Park None [] False
"3Jn73XpSQQCluzRBMWRsMA": [],
# 6bxcNnzRQKGnK4uPrCJ9UQ wedding.jpg None Bride Wedding day [] False
"6bxcNnzRQKGnK4uPrCJ9UQ": [],
# od0fmC7NQx+ayVr+%i06XA Pumpkins4.jpg Pumpkin heads None [] True
"od0fmC7NQx+ayVr+%i06XA": [],
}
LABELS_NORMALIZED_DICT = {
# 8SOE9s0XQVGsuq4ONohTng Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
"8SOE9s0XQVGsuq4ONohTng": [],
# HrK3ZQdlQ7qpDA0FgOYXLA Pumpkins3.jpg None Kids in pumpkin field [] False
"HrK3ZQdlQ7qpDA0FgOYXLA": [],
# YZFCPY24TUySvpu7owiqxA Tulips.jpg Tulips tied together at a flower shop Wedding tulips [] False
"YZFCPY24TUySvpu7owiqxA": [],
# 15uNd7%8RguTEgNPKHfTWw Pumkins2.jpg I found one! Girl holding pumpkin [] False
"15uNd7%8RguTEgNPKHfTWw": [],
# 3Jn73XpSQQCluzRBMWRsMA St James Park.jpg St. James's Park None [] False
"3Jn73XpSQQCluzRBMWRsMA": [],
# 6bxcNnzRQKGnK4uPrCJ9UQ wedding.jpg None Bride Wedding day [] False
"6bxcNnzRQKGnK4uPrCJ9UQ": [],
# od0fmC7NQx+ayVr+%i06XA Pumpkins4.jpg Pumpkin heads None [] True
"od0fmC7NQx+ayVr+%i06XA": [],
}
SEARCH_INFO_DICT = {
# 8SOE9s0XQVGsuq4ONohTng Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
"8SOE9s0XQVGsuq4ONohTng": [],
# HrK3ZQdlQ7qpDA0FgOYXLA Pumpkins3.jpg None Kids in pumpkin field [] False
"HrK3ZQdlQ7qpDA0FgOYXLA": [],
# YZFCPY24TUySvpu7owiqxA Tulips.jpg Tulips tied together at a flower shop Wedding tulips [] False
"YZFCPY24TUySvpu7owiqxA": [],
# 15uNd7%8RguTEgNPKHfTWw Pumkins2.jpg I found one! Girl holding pumpkin [] False
"15uNd7%8RguTEgNPKHfTWw": [],
# 3Jn73XpSQQCluzRBMWRsMA St James Park.jpg St. James's Park None [] False
"3Jn73XpSQQCluzRBMWRsMA": [],
# 6bxcNnzRQKGnK4uPrCJ9UQ wedding.jpg None Bride Wedding day [] False
"6bxcNnzRQKGnK4uPrCJ9UQ": [],
# od0fmC7NQx+ayVr+%i06XA Pumpkins4.jpg Pumpkin heads None [] True
"od0fmC7NQx+ayVr+%i06XA": [],
}
@pytest.fixture
def photosdb():
# return PhotosDB object for the tests
import osxphotos
return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
def test_search_info(photosdb):
for uuid in SEARCH_INFO_DICT:
photo = photosdb.photos(uuid=[uuid])[0]
assert photo.search_info is None
def test_labels_normalized(photosdb):
for uuid in LABELS_NORMALIZED_DICT:
photo = photosdb.photos(uuid=[uuid])[0]
assert sorted(photo.labels_normalized) == sorted(LABELS_NORMALIZED_DICT[uuid])
def test_labels(photosdb):
for uuid in LABELS_DICT:
photo = photosdb.photos(uuid=[uuid])[0]
assert sorted(photo.labels) == sorted(LABELS_DICT[uuid])
def test_photosdb_labels(photosdb):
assert photosdb.labels == []
assert photosdb.labels_normalized == []
def test_photosdb_labels_as_dict(photosdb):
assert photosdb.labels_as_dict == dict()
assert photosdb.labels_normalized_as_dict == dict()

View File

@@ -0,0 +1,233 @@
""" test PhotoInfo.search_info """
import pytest
from osxphotos._constants import _UNKNOWN_PERSON
PHOTOS_DB = "./tests/Test-10.15.4.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.15.4.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "/Test-10.15.4.photoslibrary"
LABELS_DICT = {
# A92D9C26-3A50-4197-9388-CB5F7DB9FA91 IMG_1994.JPG None RAW + JPEG, JPEG Original [] False
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91": [],
# F12384F6-CD17-4151-ACBA-AE0E3688539E Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [],
# 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
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068": [],
# A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C Pumpkins4.jpg Pumpkin heads None [] True
"A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C": [],
# 3DD2C897-F19E-4CA6-8C22-B027D5A71907 IMG_4547.jpg Elder Park Elder Park, Adelaide, Australia ['Statue', 'Art'] False
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": ["Statue", "Art"],
# 8E1D7BC9-9321-44F9-8CFB-4083F6B9232A IMG_2000.JPG None RAW + JPEG, Not copied to library [] False
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": [],
# 4D521201-92AC-43E5-8F7C-59BC41C37A96 IMG_1997.JPG None RAW + JPEG, RAW original [] False
"4D521201-92AC-43E5-8F7C-59BC41C37A96": [],
# 6191423D-8DB8-4D4C-92BE-9BBBA308AAC4 Tulips.jpg Tulips tied together at a flower shop Wedding tulips ['Flower', 'Vase', 'Bouquet', 'Container', 'Art', 'Flower Arrangement', 'Plant'] False
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4": [
"Flower",
"Vase",
"Bouquet",
"Container",
"Art",
"Flower Arrangement",
"Plant",
],
# 1EB2B765-0765-43BA-A90C-0D0580E6172C Pumpkins3.jpg None Kids in pumpkin field [] False
"1EB2B765-0765-43BA-A90C-0D0580E6172C": [],
# 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",
"Plant",
"Waterways",
"River",
"Sky",
"Cloudy",
"Land",
"Water Body",
"Water",
"Outdoor",
],
# E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51 wedding.jpg None Bride Wedding day [] False
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": [],
}
LABELS_NORMALIZED_DICT = {
# 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",
"plant",
"waterways",
"river",
"sky",
"cloudy",
"land",
"water body",
"water",
"outdoor",
],
# 4D521201-92AC-43E5-8F7C-59BC41C37A96 IMG_1997.JPG None RAW + JPEG, RAW original [] False
"4D521201-92AC-43E5-8F7C-59BC41C37A96": [],
# A92D9C26-3A50-4197-9388-CB5F7DB9FA91 IMG_1994.JPG None RAW + JPEG, JPEG Original [] False
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91": [],
# E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51 wedding.jpg None Bride Wedding day [] False
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": [],
# D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068 DSC03584.dng None RAW only [] False
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068": [],
# 8E1D7BC9-9321-44F9-8CFB-4083F6B9232A IMG_2000.JPG None RAW + JPEG, Not copied to library [] False
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": [],
# 3DD2C897-F19E-4CA6-8C22-B027D5A71907 IMG_4547.jpg Elder Park Elder Park, Adelaide, Australia ['statue', 'art'] False
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": ["statue", "art"],
# 6191423D-8DB8-4D4C-92BE-9BBBA308AAC4 Tulips.jpg Tulips tied together at a flower shop Wedding tulips ['flower', 'vase', 'bouquet', 'container', 'art', 'flower arrangement', 'plant'] False
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4": [
"flower",
"vase",
"bouquet",
"container",
"art",
"flower arrangement",
"plant",
],
# D79B8D77-BFFC-460B-9312-034F2877D35B Pumkins2.jpg I found one! Girl holding pumpkin [] False
"D79B8D77-BFFC-460B-9312-034F2877D35B": [],
# F12384F6-CD17-4151-ACBA-AE0E3688539E Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [],
# A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C Pumpkins4.jpg Pumpkin heads None [] True
"A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C": [],
}
SEARCH_INFO_DICT = {
# valid search_info
"DC99FBDD-7A52-4100-A5BB-344131646C30": True,
# missing, so no search_info
"A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C": False,
}
LABELS = [
"Tree",
"Plant",
"Waterways",
"River",
"Sky",
"Cloudy",
"Land",
"Water Body",
"Water",
"Outdoor",
"Statue",
"Art",
"Flower",
"Vase",
"Bouquet",
"Container",
"Flower Arrangement",
]
LABELS_NORMALIZED = [
"tree",
"plant",
"waterways",
"river",
"sky",
"cloudy",
"land",
"water body",
"water",
"outdoor",
"statue",
"art",
"flower",
"vase",
"bouquet",
"container",
"flower arrangement",
]
LABELS_AS_DICT = {
"Plant": 2,
"Art": 2,
"Tree": 1,
"Waterways": 1,
"River": 1,
"Sky": 1,
"Cloudy": 1,
"Land": 1,
"Water Body": 1,
"Water": 1,
"Outdoor": 1,
"Statue": 1,
"Flower": 1,
"Vase": 1,
"Bouquet": 1,
"Container": 1,
"Flower Arrangement": 1,
}
LABELS_NORMALIZED_AS_DICT = {
"plant": 2,
"art": 2,
"tree": 1,
"waterways": 1,
"river": 1,
"sky": 1,
"cloudy": 1,
"land": 1,
"water body": 1,
"water": 1,
"outdoor": 1,
"statue": 1,
"flower": 1,
"vase": 1,
"bouquet": 1,
"container": 1,
"flower arrangement": 1,
}
@pytest.fixture
def photosdb():
# return a PhotosDB object for use by tests
import osxphotos
return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
def test_search_info(photosdb):
for uuid in SEARCH_INFO_DICT:
photo = photosdb.photos(uuid=[uuid])[0]
if SEARCH_INFO_DICT[uuid]:
assert photo.search_info
else:
# still have a search info object but should have no data
assert photo.search_info._db_searchinfo is None
def test_labels_normalized(photosdb):
for uuid in LABELS_NORMALIZED_DICT:
photo = photosdb.photos(uuid=[uuid])[0]
assert sorted(photo.search_info.labels_normalized) == sorted(
LABELS_NORMALIZED_DICT[uuid]
)
assert sorted(photo.labels_normalized) == sorted(LABELS_NORMALIZED_DICT[uuid])
def test_labels(photosdb):
for uuid in LABELS_DICT:
photo = photosdb.photos(uuid=[uuid])[0]
assert sorted(photo.search_info.labels) == sorted(LABELS_DICT[uuid])
assert sorted(photo.labels) == sorted(LABELS_DICT[uuid])
def test_photosdb_labels(photosdb):
assert sorted(photosdb.labels) == sorted(LABELS)
assert sorted(photosdb.labels_normalized) == sorted(LABELS_NORMALIZED)
def test_photosdb_labels_as_dict(photosdb):
assert photosdb.labels_as_dict == LABELS_AS_DICT
assert photosdb.labels_normalized_as_dict == LABELS_NORMALIZED_AS_DICT