@@ -3,3 +3,4 @@ include README.rst
|
|||||||
include osxphotos/templates/*
|
include osxphotos/templates/*
|
||||||
include osxphotos/phototemplate.tx
|
include osxphotos/phototemplate.tx
|
||||||
include osxphotos/phototemplate.md
|
include osxphotos/phototemplate.md
|
||||||
|
include osxphotos/queries/*
|
||||||
16
README.md
16
README.md
@@ -1702,7 +1702,7 @@ Substitution Description
|
|||||||
{lf} A line feed: '\n', alias for {newline}
|
{lf} A line feed: '\n', alias for {newline}
|
||||||
{cr} A carriage return: '\r'
|
{cr} A carriage return: '\r'
|
||||||
{crlf} a carriage return + line feed: '\r\n'
|
{crlf} a carriage return + line feed: '\r\n'
|
||||||
{osxphotos_version} The osxphotos version, e.g. '0.42.84'
|
{osxphotos_version} The osxphotos version, e.g. '0.42.85'
|
||||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||||
|
|
||||||
The following substitutions may result in multiple values. Thus if specified for
|
The following substitutions may result in multiple values. Thus if specified for
|
||||||
@@ -2516,7 +2516,12 @@ Returns a [PlaceInfo](#PlaceInfo) object with reverse geolocation data or None i
|
|||||||
#### `shared`
|
#### `shared`
|
||||||
Returns True if photo is in a shared album, otherwise False.
|
Returns True if photo is in a shared album, otherwise False.
|
||||||
|
|
||||||
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None instead of True/False.
|
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||||
|
|
||||||
|
#### `owner`
|
||||||
|
Returns full name of the photo owner (person who shared the photo) for shared photos or None if photo is not shared. Also returns None if you are the person who shared the photo.
|
||||||
|
|
||||||
|
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||||
|
|
||||||
#### `comments`
|
#### `comments`
|
||||||
Returns list of [CommentInfo](#commentinfo) objects for comments on shared photos or empty list if no comments.
|
Returns list of [CommentInfo](#commentinfo) objects for comments on shared photos or empty list if no comments.
|
||||||
@@ -2890,6 +2895,11 @@ Photos Library
|
|||||||
#### `parent`
|
#### `parent`
|
||||||
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
||||||
|
|
||||||
|
#### `owner`
|
||||||
|
Returns full name of the album owner (person who shared the album) for shared albums or None if album is not shared.
|
||||||
|
|
||||||
|
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||||
|
|
||||||
### ImportInfo
|
### ImportInfo
|
||||||
PhotosDB.import_info returns a list of ImportInfo objects. Each ImportInfo object represents an import session in the library. PhotoInfo.import_info returns a single ImportInfo object representing the import session for the photo (or `None` if no associated import session).
|
PhotosDB.import_info returns a list of ImportInfo objects. Each ImportInfo object represents an import session in the library. PhotoInfo.import_info returns a single ImportInfo object representing the import session for the photo (or `None` if no associated import session).
|
||||||
|
|
||||||
@@ -3561,7 +3571,7 @@ The following template field substitutions are availabe for use the templating s
|
|||||||
|{lf}|A line feed: '\n', alias for {newline}|
|
|{lf}|A line feed: '\n', alias for {newline}|
|
||||||
|{cr}|A carriage return: '\r'|
|
|{cr}|A carriage return: '\r'|
|
||||||
|{crlf}|a carriage return + line feed: '\r\n'|
|
|{crlf}|a carriage return + line feed: '\r\n'|
|
||||||
|{osxphotos_version}|The osxphotos version, e.g. '0.42.84'|
|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.85'|
|
||||||
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
||||||
|{album}|Album(s) photo is contained in|
|
|{album}|Album(s) photo is contained in|
|
||||||
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.84"
|
__version__ = "0.42.85"
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from ._constants import (
|
|||||||
AlbumSortOrder,
|
AlbumSortOrder,
|
||||||
)
|
)
|
||||||
from .datetime_utils import get_local_tz
|
from .datetime_utils import get_local_tz
|
||||||
|
from .query_builder import get_query
|
||||||
|
|
||||||
|
|
||||||
def sort_list_by_keys(values, sort_keys):
|
def sort_list_by_keys(values, sort_keys):
|
||||||
@@ -131,6 +132,22 @@ class AlbumInfoBaseClass:
|
|||||||
def photos(self):
|
def photos(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self):
|
||||||
|
"""Return name of photo owner for shared album (Photos 5+ only), or None if not shared"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._owner
|
||||||
|
except AttributeError:
|
||||||
|
query = get_query(
|
||||||
|
"cloud_album_owner", photos_ver=self._db._photos_ver, uuid=self.uuid
|
||||||
|
)
|
||||||
|
result = self._db.execute(query).fetchone()
|
||||||
|
self._owner = result[0] if result else None
|
||||||
|
return self._owner
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
"""return number of photos contained in album"""
|
"""return number of photos contained in album"""
|
||||||
return len(self.photos)
|
return len(self.photos)
|
||||||
|
|||||||
@@ -4103,7 +4103,11 @@ def repl(ctx, cli_obj, db):
|
|||||||
get_photo = photosdb.get_photo
|
get_photo = photosdb.get_photo
|
||||||
show = _show_photo
|
show = _show_photo
|
||||||
get_selected = _get_selected(photosdb)
|
get_selected = _get_selected(photosdb)
|
||||||
|
try:
|
||||||
selected = get_selected()
|
selected = get_selected()
|
||||||
|
except Exception:
|
||||||
|
# get_selected sometimes fails
|
||||||
|
selected = []
|
||||||
|
|
||||||
def inspect(obj):
|
def inspect(obj):
|
||||||
"""inspect object"""
|
"""inspect object"""
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from ..albuminfo import AlbumInfo, ImportInfo
|
|||||||
from ..personinfo import FaceInfo, PersonInfo
|
from ..personinfo import FaceInfo, PersonInfo
|
||||||
from ..phototemplate import PhotoTemplate, RenderOptions
|
from ..phototemplate import PhotoTemplate, RenderOptions
|
||||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||||
|
from ..query_builder import get_query
|
||||||
from ..text_detection import detect_text
|
from ..text_detection import detect_text
|
||||||
from ..uti import get_preferred_uti_extension, get_uti_for_extension
|
from ..uti import get_preferred_uti_extension, get_uti_for_extension
|
||||||
from ..utils import _debug, _get_resource_loc, findfiles
|
from ..utils import _debug, _get_resource_loc, findfiles
|
||||||
@@ -1098,6 +1099,22 @@ class PhotoInfo:
|
|||||||
logging.warning(f"Did not find signature for {self.uuid} in _db_signatures")
|
logging.warning(f"Did not find signature for {self.uuid} in _db_signatures")
|
||||||
return duplicates
|
return duplicates
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self):
|
||||||
|
"""Return name of photo owner for shared photos (Photos 5+ only), or None if not shared"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._owner
|
||||||
|
except AttributeError:
|
||||||
|
query = get_query(
|
||||||
|
"shared_owner", photos_ver=self._db._photos_ver, uuid=self.uuid
|
||||||
|
)
|
||||||
|
result = self._db.execute(query).fetchone()
|
||||||
|
self._owner = result[0] if result else None
|
||||||
|
return self._owner
|
||||||
|
|
||||||
def render_template(
|
def render_template(
|
||||||
self, template_str: str, options: Optional[RenderOptions] = None
|
self, template_str: str, options: Optional[RenderOptions] = None
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -330,6 +330,8 @@ class PhotosDB:
|
|||||||
else:
|
else:
|
||||||
self._process_database5()
|
self._process_database5()
|
||||||
|
|
||||||
|
self._db_connection, _ = self.get_db_connection()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keywords_as_dict(self):
|
def keywords_as_dict(self):
|
||||||
"""return keywords as dict of keyword, count in reverse sorted order (descending)"""
|
"""return keywords as dict of keyword, count in reverse sorted order (descending)"""
|
||||||
@@ -1104,7 +1106,9 @@ class PhotosDB:
|
|||||||
# get info on special types
|
# get info on special types
|
||||||
self._dbphotos[uuid]["specialType"] = row[25]
|
self._dbphotos[uuid]["specialType"] = row[25]
|
||||||
self._dbphotos[uuid]["masterModelID"] = row[26]
|
self._dbphotos[uuid]["masterModelID"] = row[26]
|
||||||
self._dbphotos[uuid]["pk"] = row[26] # same as masterModelID, to match Photos 5
|
self._dbphotos[uuid]["pk"] = row[
|
||||||
|
26
|
||||||
|
] # same as masterModelID, to match Photos 5
|
||||||
self._dbphotos[uuid]["panorama"] = True if row[25] == 1 else False
|
self._dbphotos[uuid]["panorama"] = True if row[25] == 1 else False
|
||||||
self._dbphotos[uuid]["slow_mo"] = True if row[25] == 2 else False
|
self._dbphotos[uuid]["slow_mo"] = True if row[25] == 2 else False
|
||||||
self._dbphotos[uuid]["time_lapse"] = True if row[25] == 3 else False
|
self._dbphotos[uuid]["time_lapse"] = True if row[25] == 3 else False
|
||||||
@@ -3354,6 +3358,10 @@ class PhotosDB:
|
|||||||
|
|
||||||
return photos
|
return photos
|
||||||
|
|
||||||
|
def execute(self, sql):
|
||||||
|
"""Execute sql statement and return cursor"""
|
||||||
|
return self._db_connection.cursor().execute(sql)
|
||||||
|
|
||||||
def _duplicate_signature(self, uuid):
|
def _duplicate_signature(self, uuid):
|
||||||
"""Compute a signature for finding possible duplicates"""
|
"""Compute a signature for finding possible duplicates"""
|
||||||
return (
|
return (
|
||||||
@@ -3381,6 +3389,10 @@ class PhotosDB:
|
|||||||
"""
|
"""
|
||||||
return len(self._dbphotos)
|
return len(self._dbphotos)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if getattr(self, "_db_connection", None):
|
||||||
|
self._db_connection.close()
|
||||||
|
|
||||||
|
|
||||||
def _get_photos_by_attribute(photos, attribute, values, ignore_case):
|
def _get_photos_by_attribute(photos, attribute, values, ignore_case):
|
||||||
"""Search for photos based on values being in PhotoInfo.attribute
|
"""Search for photos based on values being in PhotoInfo.attribute
|
||||||
|
|||||||
5
osxphotos/queries/README.md
Normal file
5
osxphotos/queries/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Query templates
|
||||||
|
|
||||||
|
This folder contains sql query templates for getting various photo properties
|
||||||
|
|
||||||
|
The query templates must be rendered with mako (see query_builder.py)
|
||||||
4
osxphotos/queries/cloud_album_owner.sql.mako
Normal file
4
osxphotos/queries/cloud_album_owner.sql.mako
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- Get owner name for shared iCloud album
|
||||||
|
SELECT ZGENERICALBUM.ZCLOUDOWNERFULLNAME AS OWNER_FULLNAME
|
||||||
|
FROM ZGENERICALBUM
|
||||||
|
WHERE ZGENERICALBUM.ZUUID = '${uuid}'
|
||||||
25
osxphotos/queries/shared_owner.sql.mako
Normal file
25
osxphotos/queries/shared_owner.sql.mako
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- Get the owner name of person who owns a photo in a shared album
|
||||||
|
WITH case1 AS
|
||||||
|
(
|
||||||
|
-- Case where someone has invited you to a shared album
|
||||||
|
-- Need to get the owner of the shared album
|
||||||
|
SELECT ZGENERICALBUM.ZCLOUDOWNERFULLNAME as OWNER_FULLNAME
|
||||||
|
FROM ZGENERICALBUM
|
||||||
|
JOIN ${asset_table} ON ${asset_table}.ZCLOUDOWNERHASHEDPERSONID = ZGENERICALBUM.ZCLOUDOWNERHASHEDPERSONID
|
||||||
|
WHERE ${asset_table}.ZUUID = "${uuid}"
|
||||||
|
),
|
||||||
|
case2 AS
|
||||||
|
(
|
||||||
|
-- Case where you have invited someone to a shared album
|
||||||
|
-- Need to get the data for person who was invited to the album
|
||||||
|
SELECT
|
||||||
|
ZCLOUDSHAREDALBUMINVITATIONRECORD.ZINVITEEFULLNAME AS OWNER_FULLNAME
|
||||||
|
FROM ZCLOUDSHAREDALBUMINVITATIONRECORD
|
||||||
|
JOIN ${asset_table} ON ${asset_table}.ZCLOUDOWNERHASHEDPERSONID = ZCLOUDSHAREDALBUMINVITATIONRECORD.ZINVITEEHASHEDPERSONID
|
||||||
|
WHERE ${asset_table}.ZUUID = "${uuid}"
|
||||||
|
ORDER BY ZCLOUDSHAREDALBUMINVITATIONRECORD.Z_PK
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
SELECT * FROM case1
|
||||||
|
UNION
|
||||||
|
SELECT * FROM case2 WHERE NOT EXISTS (SELECT * FROM case1)
|
||||||
6
osxphotos/queries/title.sql.mako
Normal file
6
osxphotos/queries/title.sql.mako
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- Get title of a photo with given UUID
|
||||||
|
SELECT
|
||||||
|
ZADDITIONALASSETATTRIBUTES.ZTITLE
|
||||||
|
FROM ZADDITIONALASSETATTRIBUTES
|
||||||
|
JOIN ${asset_table} ON ${asset_table}.Z_PK = ZADDITIONALASSETATTRIBUTES.ZASSET
|
||||||
|
WHERE ${asset_table}.ZUUID = "${uuid}"
|
||||||
36
osxphotos/query_builder.py
Normal file
36
osxphotos/query_builder.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""Build sql queries from template to retrieve info from the database"""
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import pathlib
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from mako.template import Template
|
||||||
|
|
||||||
|
from ._constants import _DB_TABLE_NAMES
|
||||||
|
|
||||||
|
QUERY_DIR = os.path.join(os.path.dirname(__file__), "queries")
|
||||||
|
|
||||||
|
|
||||||
|
def get_query(query_name, photos_ver, **kwargs):
|
||||||
|
"""Return sqlite query string for an attribute and a given database version"""
|
||||||
|
|
||||||
|
# there can be a single query for multiple database versions or separate queries for each version
|
||||||
|
# try generic version first (most common case), if that fails, look for version specific query
|
||||||
|
query_string = _get_query_string(query_name, photos_ver)
|
||||||
|
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
||||||
|
query_template = Template(query_string)
|
||||||
|
return query_template.render(asset_table=asset_table, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def _get_query_string(query_name, photos_ver):
|
||||||
|
"""Return sqlite query string for an attribute and a given database version"""
|
||||||
|
query_file = pathlib.Path(QUERY_DIR) / f"{query_name}.sql.mako"
|
||||||
|
if not query_file.is_file():
|
||||||
|
query_file = pathlib.Path(QUERY_DIR) / f"{query_name}_{photos_ver}.sql.mako"
|
||||||
|
if not query_file.is_file():
|
||||||
|
raise FileNotFoundError(f"Query file '{query_file}' not found")
|
||||||
|
|
||||||
|
with open(query_file, "r") as f:
|
||||||
|
query_string = f.read()
|
||||||
|
return query_string
|
||||||
@@ -960,7 +960,7 @@ def test_photosdb_repr():
|
|||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
photosdb2 = eval(repr(photosdb))
|
photosdb2 = eval(repr(photosdb))
|
||||||
|
|
||||||
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name"]
|
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name", "_db_connection"]
|
||||||
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
|
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
|
||||||
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
|
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1066,7 +1066,7 @@ def test_photosdb_repr():
|
|||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
photosdb2 = eval(repr(photosdb))
|
photosdb2 = eval(repr(photosdb))
|
||||||
|
|
||||||
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name"]
|
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name", "_db_connection"]
|
||||||
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
|
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
|
||||||
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
|
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
|
||||||
}
|
}
|
||||||
|
|||||||
43
tests/test_cloud_owner_catalina.py
Normal file
43
tests/test_cloud_owner_catalina.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Test cloud photos and album owner
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
PHOTOS_DB_CLOUD = "./tests/Test-Cloud-10.15.6.photoslibrary/"
|
||||||
|
PHOTOS_DB_NOT_CLOUD = "./tests/Test-10.15.6.photoslibrary/"
|
||||||
|
|
||||||
|
UUID_DICT = {
|
||||||
|
"not_cloudasset": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
|
||||||
|
"owner": "7572C53E-1D6A-410C-A2B1-18CCA3B5AD9F",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def photosdb_cloud():
|
||||||
|
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_CLOUD)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def photosdb_nocloud():
|
||||||
|
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_NOT_CLOUD)
|
||||||
|
|
||||||
|
|
||||||
|
def test_album_owner_cloud(photosdb_cloud):
|
||||||
|
album = [a for a in photosdb_cloud.album_info_shared if a.title == "osxphotos"][0]
|
||||||
|
assert album.owner == "Rhet Turnbull"
|
||||||
|
|
||||||
|
|
||||||
|
def test_album_owner_not_cloud(photosdb_nocloud):
|
||||||
|
album = [a for a in photosdb_nocloud.album_info if a.title == "Test Album"][0]
|
||||||
|
assert album.owner is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_photo_owner_cloud(photosdb_cloud):
|
||||||
|
photo = photosdb_cloud.get_photo(UUID_DICT["owner"])
|
||||||
|
assert photo.owner == "Rhet Turnbull"
|
||||||
|
|
||||||
|
|
||||||
|
def test_photo_owner_nocloud(photosdb_nocloud):
|
||||||
|
photo = photosdb_nocloud.get_photo(UUID_DICT["not_cloudasset"])
|
||||||
|
assert photo.owner is None
|
||||||
@@ -517,7 +517,7 @@ def test_photosdb_repr():
|
|||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
photosdb2 = eval(repr(photosdb))
|
photosdb2 = eval(repr(photosdb))
|
||||||
|
|
||||||
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name"]
|
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name", "_db_connection"]
|
||||||
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
|
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
|
||||||
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
|
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1033,7 +1033,7 @@ def test_photosdb_repr():
|
|||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
photosdb2 = eval(repr(photosdb))
|
photosdb2 = eval(repr(photosdb))
|
||||||
|
|
||||||
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name"]
|
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name", "_db_connection"]
|
||||||
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
|
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
|
||||||
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
|
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user