Refactored photosdb and photoinfo to add SearchInfo and labels
This commit is contained in:
44
README.md
44
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.28.14"
|
||||
__version__ = "0.28.15"
|
||||
|
||||
8
osxphotos/photoinfo/__init__.py
Normal file
8
osxphotos/photoinfo/__init__.py
Normal 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
|
||||
@@ -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
|
||||
96
osxphotos/photoinfo/photoinfo_mixin_searchinfo.py
Normal file
96
osxphotos/photoinfo/photoinfo_mixin_searchinfo.py
Normal 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
|
||||
6
osxphotos/photosdb/__init__.py
Normal file
6
osxphotos/photosdb/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
PhotosDB class
|
||||
Processes a Photos.app library database to extract information about photos
|
||||
"""
|
||||
|
||||
from .photosdb import PhotosDB
|
||||
@@ -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):")
|
||||
189
osxphotos/photosdb/photosdb_mixin_searchinfo.py
Normal file
189
osxphotos/photosdb/photosdb_mixin_searchinfo.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
97
tests/test_search_info_10_14_6.py
Normal file
97
tests/test_search_info_10_14_6.py
Normal 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()
|
||||
233
tests/test_search_info_10_15_4.py
Normal file
233
tests/test_search_info_10_15_4.py
Normal 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
|
||||
Reference in New Issue
Block a user