Compare commits

..

6 Commits

Author SHA1 Message Date
Rhet Turnbull
d1a71bba1e Added beta macos runners
https://github.blog/changelog/2023-04-24-github-actions-faster-macos-runners-are-now-available-in-open-public-beta/
2023-04-25 21:03:21 -04:00
Rhet Turnbull
0c85298c03 Updated API_README.md to add tables() 2023-04-16 09:19:10 -07:00
Rhet Turnbull
b4b58d3b00 Updated API_README.md to add tables() 2023-04-16 09:17:56 -07:00
Rhet Turnbull
ed543aa2d0 Feature phototables (#1059)
* Added tables() method to PhotoInfo to get access to underlying tables

* This time with the phototables code...
2023-04-16 09:02:59 -07:00
allcontributors[bot]
1b0c91db97 add pekingduck as a contributor for bug (#1058)
* update README.md [skip ci]

* update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-04-12 20:41:51 -07:00
Rhet Turnbull
dd3914328b Updated CHANGELOG.md [skip ci] 2023-04-10 20:59:09 -07:00
11 changed files with 338 additions and 8 deletions

View File

@@ -530,7 +530,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/2597142?v=4",
"profile": "https://github.com/pekingduck",
"contributions": [
"ideas"
"ideas",
"bug"
]
},
{

View File

@@ -18,7 +18,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
os: [macos-latest]
os: [macos-latest-xl]
python-version: ['3.9', '3.10', '3.11']
steps:

View File

@@ -23,6 +23,7 @@ In addition to a command line interface, OSXPhotos provides a access to a Python
* [CommentInfo](#commentinfo)
* [LikeInfo](#likeinfo)
* [AdjustmentsInfo](#adjustmentsinfo)
* [PhotoTables](#phototables)
* [Raw Photos](#raw-photos)
* [Template System](#template-system)
* [ExifTool](#exiftoolExifTool)
@@ -1395,6 +1396,12 @@ Returns a unique fingerprint for the original photo file. This is a hash of the
Returns a unique digest of the photo's properties and metadata; useful for detecting changes in any property/metadata of the photo.
#### `tables()`
Returns a PhotoTables object which provides access to the underlying SQLite database tables for the photo.
See [PhotoTables](#phototables) for more details. This is useful for debugging or developing new features but
is not intended for general use.
#### `json()`
Returns a JSON representation of all photo info.
@@ -2157,6 +2164,37 @@ Returns a JSON representation of the FaceInfo instance.
* `adj_version_info`: version info for the application which made the adjustments to the photo decoded from the adjustments data.
* `asdict()`: dict representation of the AdjustmentsInfo object; contains all properties with exception of `plist`.
### PhotoTables
[PhotoInfo.tables](#tables) returns a PhotoTables object that contains information about the tables in the Photos database that contain information about the photo.
The following properties are available:
* `ZASSET`
* `ZADDITIONALASSETATTRIBUTES`
* `ZDETECTEDFACE`
* `ZPERSON`
Each of these properties returns a `Table` object that provides access to the row(s) in the table that correspond to the photo.
The Table object has dynamically created properties that correspond to the associated column in the table and return a tuple of values for that column.
```pycon
>>> photo.tables().ZADDITIONALASSETATTRIBUTES.ZTITLE
("St. James's Park",)
```
The Table object also provides a `rows()` method which returns a list a of tuples for the matching rows in the table
and a `rows_dict()` method which returns a list of dicts for the matching rows in the table.
```pycon
>>> photo.tables().ZASSET.rows()
[(6, 3, 35, 0, 0, 0, 0, 0, 0, None, None, None, None, None, 0, 0, 1, 0, 0, 0, 0, -100, 0, 1, 0, 1356, 0, 0, 0, 0, 0, 0, 0, 1, 6192599813128215, 1, 2814835671629878, 1, 0, 3, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 2047, 7, None, 8, None, None, None, None, None, None, None, None, 3, 6, 6, 6, None, 6, 4, None, None, 8, 4, None, 2, None, 3, None, 3, None, None, 585926209.859624, 596906868.198932, 689981763.374756, None, None, None, 0.5, 561129492.501, 0.0, 596906868.198932, None, 0.03816793893129771, None, 51.50357167, -0.1318055, 689982854.802854, 0.6494140625, 0.0, 561129492.501, None, None, None, None, None, None, None, 'D', 'DC99FBDD-7A52-4100-A5BB-344131646C30.jpeg', None, 'sRGB IEC61966-2.1', 'public.jpeg', 'DC99FBDD-7A52-4100-A5BB-344131646C30', b'Ki\t@\x01\x00\x00\x00\td\tH\x01\x00\x00\x00\x93\\\tL\x01\x00\x00\x00\x1aK\x0c\x03\x0c\xa8q\x92\x00\x12C\x0c\x03\x0c"\r\x90\x00\x00<\x0c\x03\x08"\x19\x80\x00', b'\xca\xebV\tu\xc0I@/j\xf7\xab\x00\xdf\xc0\xbf\xcd\xcc\xcc\xcc\xcc\xcc\x04@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')]
>>> photo.tables().ZASSET.rows_dict()
[{'Z_PK': 6, 'Z_ENT': 3, 'Z_OPT': 35, 'ZACTIVELIBRARYSCOPEPARTICIPATIONSTATE': 0, 'ZAVALANCHEPICKTYPE': 0, 'ZBUNDLESCOPE': 0, 'ZCAMERAPROCESSINGADJUSTMENTSTATE': 0, 'ZCLOUDDELETESTATE': 0, 'ZCLOUDDOWNLOADREQUESTS': 0, 'ZCLOUDHASCOMMENTSBYME': None, 'ZCLOUDHASCOMMENTSCONVERSATION': None, 'ZCLOUDHASUNSEENCOMMENTS': None, 'ZCLOUDISDELETABLE': None, 'ZCLOUDISMYASSET': None, 'ZCLOUDLOCALSTATE': 0, 'ZCLOUDPLACEHOLDERKIND': 0, 'ZCOMPLETE': 1, 'ZDEFERREDPROCESSINGNEEDED': 0, 'ZDEPTHTYPE': 0, 'ZDERIVEDCAMERACAPTUREDEVICE': 0, 'ZDUPLICATEASSETVISIBILITYSTATE': 0, 'ZFACEAREAPOINTS': -100, 'ZFAVORITE': 0, 'ZHASADJUSTMENTS': 1, 'ZHDRTYPE': 0, 'ZHEIGHT': 1356, 'ZHIDDEN': 0, 'ZHIGHFRAMERATESTATE': 0, 'ZISMAGICCARPET': 0, 'ZKIND': 0, 'ZKINDSUBTYPE': 0, 'ZLIBRARYSCOPESHARESTATE': 0, 'ZMONOSKITYPE': 0, 'ZORIENTATION': 1, 'ZPACKEDACCEPTABLECROPRECT': 6192599813128215, 'ZPACKEDBADGEATTRIBUTES': 1, 'ZPACKEDPREFERREDCROPRECT': 2814835671629878, 'ZPLAYBACKSTYLE': 1, 'ZPLAYBACKVARIATION': 0, 'ZSAVEDASSETTYPE': 3, 'ZSEARCHINDEXREBUILDSTATE': 0, 'ZSYNDICATIONSTATE': 0, 'ZTHUMBNAILINDEX': 5, 'ZTRASHEDSTATE': 0, 'ZVIDEOCPDURATIONVALUE': 0, 'ZVIDEOCPVISIBILITYSTATE': 0, 'ZVIDEODEFERREDPROCESSINGNEEDED': 0, 'ZVIDEOKEYFRAMETIMESCALE': 0, 'ZVIDEOKEYFRAMEVALUE': 0, 'ZVISIBILITYSTATE': 0, 'ZWIDTH': 2047, 'ZADDITIONALATTRIBUTES': 7, 'ZCLOUDFEEDASSETSENTRY': None, 'ZCOMPUTEDATTRIBUTES': 8, 'ZCONVERSATION': None, 'ZDAYGROUPHIGHLIGHTBEINGASSETS': None, 'ZDAYGROUPHIGHLIGHTBEINGEXTENDEDASSETS': None, 'ZDAYGROUPHIGHLIGHTBEINGKEYASSETPRIVATE': None, 'ZDAYGROUPHIGHLIGHTBEINGKEYASSETSHARED': None, 'ZDAYGROUPHIGHLIGHTBEINGSUMMARYASSETS': None, 'ZDUPLICATEMETADATAMATCHINGALBUM': None, 'ZDUPLICATEPERCEPTUALMATCHINGALBUM': None, 'ZEXTENDEDATTRIBUTES': 3, 'ZHIGHLIGHTBEINGASSETS': 6, 'ZHIGHLIGHTBEINGEXTENDEDASSETS': 6, 'ZHIGHLIGHTBEINGKEYASSETPRIVATE': 6, 'ZHIGHLIGHTBEINGKEYASSETSHARED': None, 'ZHIGHLIGHTBEINGSUMMARYASSETS': 6, 'ZIMPORTSESSION': 4, 'ZLIBRARYSCOPE': None, 'ZMASTER': None, 'ZMEDIAANALYSISATTRIBUTES': 8, 'ZMOMENT': 4, 'ZMOMENTSHARE': None, 'ZMONTHHIGHLIGHTBEINGKEYASSETPRIVATE': 2, 'ZMONTHHIGHLIGHTBEINGKEYASSETSHARED': None, 'ZPHOTOANALYSISATTRIBUTES': 3, 'ZTRASHEDBYPARTICIPANT': None, 'ZYEARHIGHLIGHTBEINGKEYASSETPRIVATE': 3, 'ZYEARHIGHLIGHTBEINGKEYASSETSHARED': None, 'Z_FOK_CLOUDFEEDASSETSENTRY': None, 'ZADDEDDATE': 585926209.859624, 'ZADJUSTMENTTIMESTAMP': 596906868.198932, 'ZANALYSISSTATEMODIFICATIONDATE': 689981763.374756, 'ZCLOUDBATCHPUBLISHDATE': None, 'ZCLOUDLASTVIEWEDCOMMENTDATE': None, 'ZCLOUDSERVERPUBLISHDATE': None, 'ZCURATIONSCORE': 0.5, 'ZDATECREATED': 561129492.501, 'ZDURATION': 0.0, 'ZFACEADJUSTMENTVERSION': 596906868.198932, 'ZHDRGAIN': None, 'ZHIGHLIGHTVISIBILITYSCORE': 0.03816793893129771, 'ZLASTSHAREDDATE': None, 'ZLATITUDE': 51.50357167, 'ZLONGITUDE': -0.1318055, 'ZMODIFICATIONDATE': 689982854.802854, 'ZOVERALLAESTHETICSCORE': 0.6494140625, 'ZPROMOTIONSCORE': 0.0, 'ZSORTTOKEN': 561129492.501, 'ZTRASHEDDATE': None, 'ZAVALANCHEUUID': None, 'ZCLOUDASSETGUID': None, 'ZCLOUDBATCHID': None, 'ZCLOUDCOLLECTIONGUID': None, 'ZCLOUDOWNERHASHEDPERSONID': None, 'ZDELETEREASON': None, 'ZDIRECTORY': 'D', 'ZFILENAME': 'DC99FBDD-7A52-4100-A5BB-344131646C30.jpeg', 'ZMEDIAGROUPUUID': None, 'ZORIGINALCOLORSPACE': 'sRGB IEC61966-2.1', 'ZUNIFORMTYPEIDENTIFIER': 'public.jpeg', 'ZUUID': 'DC99FBDD-7A52-4100-A5BB-344131646C30', 'ZIMAGEREQUESTHINTS': b'Ki\t@\x01\x00\x00\x00\td\tH\x01\x00\x00\x00\x93\\\tL\x01\x00\x00\x00\x1aK\x0c\x03\x0c\xa8q\x92\x00\x12C\x0c\x03\x0c"\r\x90\x00\x00<\x0c\x03\x08"\x19\x80\x00', 'ZLOCATIONDATA': b'\xca\xebV\tu\xc0I@/j\xf7\xab\x00\xdf\xc0\xbf\xcd\xcc\xcc\xcc\xcc\xcc\x04@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}]
```
### Raw Photos
Handling raw photos in `osxphotos` requires a bit of extra work. Raw photos in Photos can be imported in two different ways: 1) a single raw photo with no associated JPEG image is imported 2) a raw+JPEG pair is imported -- two separate images with same file stem (e.g. `IMG_0001.CR2` and `IMG_001.JPG`) are imported.

View File

@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
## [v0.59.3](https://github.com/RhetTbull/osxphotos/compare/v0.59.2...v0.59.3)
Bug fixes for memory leak, crash during export
### 10 April 2023
#### Fixed
- Fixed memory leak in export (#1047)
- Fixed crash during export (#1046)
- Fixed large crash log size (#1048)
#### Changed
- Added better help for no selection with --selected (#1036)
- Changed PhotoInfo.asdict() and PhotoInfo.json() to allow deep or shallow option (#1038)
- Updated development docs (#1043)
#### Contributors
- @RhetTbull for code changes
- @wernerzj for finding bug with memory leak
- @rajscode for finding export crash
- @oPromessa for development docs fix
## [v0.59.2](https://github.com/RhetTbull/osxphotos/compare/v0.59.1...v0.59.2)
Bug Fix for Export

View File

@@ -2701,7 +2701,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aa599"><img src="https://avatars.githubusercontent.com/u/37746269?v=4?s=75" width="75px;" alt="aa599"/><br /><sub><b>aa599</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aaa599" title="Bug reports">🐛</a> <a href="#ideas-aa599" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swduncan"><img src="https://avatars.githubusercontent.com/u/2053195?v=4?s=75" width="75px;" alt="Steve Duncan"/><br /><sub><b>Steve Duncan</b></sub></a><br /><a href="#ideas-swduncan" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.projany.com"><img src="https://avatars.githubusercontent.com/u/15144745?v=4?s=75" width="75px;" alt="Ian Moir"/><br /><sub><b>Ian Moir</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aianmmoir" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pekingduck"><img src="https://avatars.githubusercontent.com/u/2597142?v=4?s=75" width="75px;" alt="Peking Duck"/><br /><sub><b>Peking Duck</b></sub></a><br /><a href="#ideas-pekingduck" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pekingduck"><img src="https://avatars.githubusercontent.com/u/2597142?v=4?s=75" width="75px;" alt="Peking Duck"/><br /><sub><b>Peking Duck</b></sub></a><br /><a href="#ideas-pekingduck" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Apekingduck" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.patreon.com/cclauss"><img src="https://avatars.githubusercontent.com/u/3709715?v=4?s=75" width="75px;" alt="Christian Clauss"/><br /><sub><b>Christian Clauss</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=cclauss" title="Code">💻</a></td>
</tr>
<tr>

View File

@@ -19,6 +19,7 @@ from .photoinfo import PhotoInfo
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
from .photosdb import PhotosDB
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
from .phototables import PhotoTables
from .phototemplate import PhotoTemplate
from .placeinfo import PlaceInfo
from .queryoptions import QueryOptions
@@ -53,6 +54,7 @@ __all__ = [
"PersonInfo",
"PhotoExporter",
"PhotoInfo",
"PhotoTables",
"PhotoTemplate",
"PhotosAlbum",
"PhotosAlbumPhotoScript",

View File

@@ -1,14 +1,17 @@
"""repl command for osxphotos CLI"""
from __future__ import annotations
import os
import os.path
import pathlib
import re
import sys
import time
from typing import List
import click
import photoscript
from applescript import ScriptError
from rich import pretty, print
import osxphotos
@@ -191,10 +194,16 @@ def _get_selected(photosdb):
"""get list of PhotoInfo objects for photos selected in Photos"""
def get_selected():
selected = photoscript.PhotosLibrary().selection
if not selected:
return []
return photosdb.photos(uuid=[p.uuid for p in selected])
try:
selected = photoscript.PhotosLibrary().selection
except ScriptError as e:
# some photos (e.g. shared items) can't be selected and raise ScriptError:
# applescript.ScriptError: Photos got an error: Cant get media item id "34C26DFA-0CEA-4DB7-8FDA-B87789B3209D/L0/001". (-1728) app='Photos' range=16820-16873
# In this case, we can parse the UUID from the error (though this only works for a single selected item)
if match := re.match(r".*Cant get media item id \"(.*)\".*", str(e)):
uuid = match[1].split("/")[0]
return photosdb.photos(uuid=[uuid])
return photosdb.photos(uuid=[p.uuid for p in selected]) if selected else []
return get_selected

View File

@@ -59,6 +59,7 @@ from .exiftool import ExifToolCaching, get_exiftool_path
from .momentinfo import MomentInfo
from .personinfo import FaceInfo, PersonInfo
from .photoexporter import ExportOptions, PhotoExporter
from .phototables import PhotoTables
from .phototemplate import PhotoTemplate, RenderOptions
from .placeinfo import PlaceInfo4, PlaceInfo5
from .query_builder import get_query
@@ -1893,6 +1894,10 @@ class PhotoInfo:
dict_data[k] = sorted(v, key=lambda v: v if v is not None else "")
return json.dumps(dict_data, sort_keys=True, default=default, indent=indent)
def tables(self) -> PhotoTables:
"""Return PhotoTables object to provide access database tables associated with this photo (Photos 5+)"""
return None if self._db._photos_ver < 5 else PhotoTables(self)
def _json_hexdigest(self):
"""JSON for use by hexdigest()"""

230
osxphotos/phototables.py Normal file
View File

@@ -0,0 +1,230 @@
"""Provide direct access to the database tables associated with a photo."""
from __future__ import annotations
import sqlite3
from typing import Any
import osxphotos
from ._constants import _DB_TABLE_NAMES
def get_table_columns(conn: sqlite3.Connection, table_name: str) -> list[str]:
cursor = conn.cursor()
cursor.execute(f"PRAGMA table_info({table_name})")
return [col[1] for col in cursor.fetchall()]
class PhotoTables:
def __init__(self, photo: osxphotos.PhotoInfo):
"""Create a PhotoTables object.
Args:
db: PhotosDB object
uuid: The UUID of the photo.
"""
self.db = photo._db
self.photo = photo
self.uuid = photo.uuid
self.version = self.db._photos_ver
@property
def ZASSET(self) -> Table:
"""Return the ZASSET table."""
return AssetTable(self.db, self.version, self.uuid)
@property
def ZADDITIONALASSETATTRIBUTES(self) -> Table:
"""Return the ZADDITIONALASSETATTRIBUTES table."""
return AdditionalAttributesTable(self.db, self.version, self.uuid)
@property
def ZDETECTEDFACE(self) -> Table:
"""Return the ZDETECTEDFACE table."""
return DetectedFaceTable(self.db, self.version, self.uuid)
@property
def ZPERSON(self) -> Table:
"""Return the ZPERSON table."""
return PersonTable(self.db, self.version, self.uuid)
class Table:
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
"""Create a Table object.
Args:
db: PhotosDB object
table_name: The name of the table.
"""
self.db = db
self.conn, _ = self.db.get_db_connection()
self.version = version
self.uuid = uuid
self.asset_table = _DB_TABLE_NAMES[self.version]["ASSET"]
self.columns = [] # must be set in subclass
self.table_name = "" # must be set in subclass
def rows(self) -> list[tuple[Any]]:
"""Return rows for this photo from the table."""
# this should be implemented in the subclass
raise NotImplementedError
def rows_dict(self) -> list[dict[str, Any]]:
"""Return rows for this photo from the table as a list of dicts."""
rows = self.rows()
return [dict(zip(self.columns, row)) for row in rows] if rows else []
def _get_column(self, column: str):
"""Get column value for this photo from the table."""
# this should be implemented in the subclass
raise NotImplementedError
def __getattr__(self, name):
"""Get column value for this photo from the table."""
if name not in self.__dict__ and name in self.columns:
return self._get_column(name)
else:
raise AttributeError(f"Table {self.table_name} has no column {name}")
class AssetTable(Table):
"""ZASSET table."""
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
"""Create a Table object."""
super().__init__(db, version, uuid)
self.columns = get_table_columns(self.conn, self.asset_table)
self.table_name = self.asset_table
def rows(self) -> list[Any]:
"""Return row2 for this photo from the ZASSET table."""
conn, cursor = self.db.get_db_connection()
cursor.execute(
f"SELECT * FROM {self.asset_table} WHERE ZUUID = ?", (self.uuid,)
)
return result if (result := cursor.fetchall()) else []
def _get_column(self, column: str) -> tuple[Any]:
"""Get column value for this photo from the ZASSET table."""
conn, cursor = self.db.get_db_connection()
cursor.execute(
f"SELECT {column} FROM {self.asset_table} WHERE ZUUID = ?",
(self.uuid,),
)
return (
tuple(result[0] for result in results)
if (results := cursor.fetchall())
else ()
)
class AdditionalAttributesTable(Table):
"""ZADDITIONALASSETATTRIBUTES table."""
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
"""Create a Table object."""
super().__init__(db, version, uuid)
self.columns = get_table_columns(self.conn, "ZADDITIONALASSETATTRIBUTES")
self.table_name = "ZADDITIONALASSETATTRIBUTES"
def rows(self) -> list[tuple[Any]]:
"""Return rows for this photo from the ZADDITIONALASSETATTRIBUTES table."""
conn, cursor = self.db.get_db_connection()
sql = f""" SELECT ZADDITIONALASSETATTRIBUTES.*
FROM ZADDITIONALASSETATTRIBUTES
JOIN {self.asset_table} ON {self.asset_table}.Z_PK = ZADDITIONALASSETATTRIBUTES.ZASSET
WHERE {self.asset_table}.ZUUID = ?;
"""
cursor.execute(sql, (self.uuid,))
return result if (result := cursor.fetchall()) else []
def _get_column(self, column: str) -> tuple[Any]:
"""Get column value for this photo from the ZADDITIONALASSETATTRIBUTES table."""
conn, cursor = self.db.get_db_connection()
sql = f""" SELECT ZADDITIONALASSETATTRIBUTES.{column}
FROM ZADDITIONALASSETATTRIBUTES
JOIN {self.asset_table} ON {self.asset_table}.Z_PK = ZADDITIONALASSETATTRIBUTES.ZASSET
WHERE {self.asset_table}.ZUUID = ?;
"""
cursor.execute(sql, (self.uuid,))
return (
tuple(result[0] for result in results)
if (results := cursor.fetchall())
else ()
)
class DetectedFaceTable(Table):
"""ZDETECTEDFACE table."""
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
"""Create a Table object."""
super().__init__(db, version, uuid)
self.columns = get_table_columns(self.conn, "ZDETECTEDFACE")
self.table_name = "ZDETECTEDFACE"
def rows(self) -> list[tuple[Any]]:
"""Return rows for this photo from the ZDETECTEDFACE table."""
conn, cursor = self.db.get_db_connection()
sql = f""" SELECT ZDETECTEDFACE.*
FROM ZDETECTEDFACE
JOIN {self.asset_table} ON {self.asset_table}.Z_PK = ZDETECTEDFACE.ZASSET
WHERE {self.asset_table}.ZUUID = ?;
"""
cursor.execute(sql, (self.uuid,))
return result if (result := cursor.fetchall()) else []
def _get_column(self, column: str) -> tuple[Any]:
"""Get column value for this photo from the ZDETECTEDFACE table."""
conn, cursor = self.db.get_db_connection()
sql = f""" SELECT ZDETECTEDFACE.{column}
FROM ZDETECTEDFACE
JOIN {self.asset_table} ON {self.asset_table}.Z_PK = ZDETECTEDFACE.ZASSET
WHERE {self.asset_table}.ZUUID = ?;
"""
cursor.execute(sql, (self.uuid,))
return (
tuple(result[0] for result in results)
if (results := cursor.fetchall())
else ()
)
class PersonTable(Table):
"""ZPERSON table."""
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
"""Create a Table object."""
super().__init__(db, version, uuid)
self.columns = get_table_columns(self.conn, "ZPERSON")
self.table_name = "ZPERSON"
def rows(self) -> list[tuple[Any]]:
"""Return rows for this photo from the ZPERSON table."""
conn, cursor = self.db.get_db_connection()
sql = f""" SELECT ZPERSON.*
FROM ZPERSON
JOIN ZDETECTEDFACE ON ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK
JOIN ZASSET ON ZASSET.Z_PK = ZDETECTEDFACE.ZASSET
WHERE {self.asset_table}.ZUUID = ?;
"""
cursor.execute(sql, (self.uuid,))
return result if (result := cursor.fetchall()) else []
def _get_column(self, column: str) -> tuple[Any]:
"""Get column value for this photo from the ZPERSON table."""
conn, cursor = self.db.get_db_connection()
sql = f""" SELECT ZPERSON.{column}
FROM ZPERSON
JOIN ZDETECTEDFACE ON ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK
JOIN ZASSET ON ZASSET.Z_PK = ZDETECTEDFACE.ZASSET
WHERE {self.asset_table}.ZUUID = ?;
"""
cursor.execute(sql, (self.uuid,))
return (
tuple(result[0] for result in results)
if (results := cursor.fetchall())
else ()
)

View File

@@ -5,6 +5,7 @@ from collections import namedtuple
import pytest
import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
@@ -531,9 +532,10 @@ def test_photosdb_repr():
def test_photosinfo_repr():
import osxphotos
import datetime # needed for eval to work
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
photo = photos[0]
@@ -689,3 +691,10 @@ def test_fingerprint(photosdb):
for uuid, fingerprint in UUID_FINGERPRINT.items():
photo = photosdb.get_photo(uuid)
assert photo.fingerprint == fingerprint
def test_tables(photosdb: osxphotos.PhotosDB):
"""Test PhotoInfo.tables"""
photo = photosdb.get_photo(UUID_DICT["favorite"])
tables = photo.tables()
assert tables is None

View File

@@ -1304,3 +1304,14 @@ def test_photosdb_photos_by_uuid(photosdb: osxphotos.PhotosDB):
assert len(photos) == len(UUID_DICT)
for photo in photos:
assert photo.uuid in UUID_DICT.values()
def test_tables(photosdb: osxphotos.PhotosDB):
"""Test PhotoInfo.tables"""
photo = photosdb.get_photo(UUID_DICT["in_album"])
tables = photo.tables()
assert isinstance(tables, osxphotos.PhotoTables)
assert tables.ZASSET.ZUUID[0] == photo.uuid
assert tables.ZADDITIONALASSETATTRIBUTES.ZTITLE[0] == photo.title
assert len(tables.ZASSET.rows()) == 1
assert tables.ZASSET.rows_dict()[0]["ZUUID"] == photo.uuid