Source code for osxphotos.queryoptions

""" QueryOptions class for PhotosDB.query """

import dataclasses
import datetime
import io
import pathlib
import re
import sys
from dataclasses import asdict, dataclass
from typing import Iterable, List, Optional, Tuple

import bitmath

from ._constants import UUID_PATTERN

__all__ = ["QueryOptions", "query_options_from_kwargs", "IncompatibleQueryOptions"]


class IncompatibleQueryOptions(Exception):
    """Incompatible query options"""

    pass


[docs] @dataclass class QueryOptions: """QueryOptions class for PhotosDB.query Attributes: added_after: search for photos added on or after a given date added_before: search for photos added before a given date added_in_last: search for photos added in last X datetime.timedelta album: list of album names to search for burst_photos: include all associated burst photos for photos in query results burst: search for burst photos cloudasset: search for photos that are managed by iCloud deleted_only: search only for deleted photos deleted: also include deleted photos description: list of descriptions to search for duplicate: search for duplicate photos edited: search for edited photos exif: search for photos with EXIF tags that matches the given data external_edit: search for photos edited in external apps favorite: search for favorite photos folder: list of folder names to search for from_date: search for photos taken on or after this date from_time: search for photos taken on or after this time of day function: list of query functions to evaluate has_comment: search for photos with comments has_likes: search for shared photos with likes has_raw: search for photos with associated raw files hdr: search for HDR photos hidden: search for hidden photos ignore_case: ignore case when searching in_album: search for photos in an album incloud: search for cloud assets that are synched to iCloud is_reference: search for photos stored by reference (that is, they are not managed by Photos) keyword: list of keywords to search for label: list of labels to search for live: search for live photos location: search for photos with a location max_size: maximum size of photos to search for min_size: minimum size of photos to search for missing_bursts: for burst photos, also include burst photos that are missing missing: search for missing photos movies: search for movies name: list of names to search for no_comment: search for photos with no comments no_description: search for photos with no description no_likes: search for shared photos with no likes no_location: search for photos with no location no_keyword: search for photos with no keywords no_place: search for photos with no place no_title: search for photos with no title not_burst: search for non-burst photos not_cloudasset: search for photos that are not managed by iCloud not_edited: search for photos that have not been edited not_favorite: search for non-favorite photos not_hdr: search for non-HDR photos not_hidden: search for non-hidden photos not_in_album: search for photos not in an album not_incloud: search for cloud asset photos that are not yet synched to iCloud not_live: search for non-live photos not_missing: search for non-missing photos not_panorama: search for non-panorama photos not_portrait: search for non-portrait photos not_reference: search for photos not stored by reference (that is, they are managed by Photos) not_screenshot: search for non-screenshot photos not_selfie: search for non-selfie photos not_shared: search for non-shared photos not_slow_mo: search for non-slow-mo photos not_time_lapse: search for non-time-lapse photos panorama: search for panorama photos person: list of person names to search for photos: search for photos place: list of place names to search for portrait: search for portrait photos query_eval: list of query expressions to evaluate regex: list of regular expressions to search for screenshot: search for screenshot photos selected: search for selected photos selfie: search for selfie photos shared: search for shared photos slow_mo: search for slow-mo photos time_lapse: search for time-lapse photos title: list of titles to search for to_date: search for photos taken before this date to_time: search for photos taken before this time of day uti: list of UTIs to search for uuid: list of uuids to search for year: search for photos taken in a given year syndicated: search for photos that have been shared via syndication ("Shared with You" album via Messages, etc.) not_syndicated: search for photos that have not been shared via syndication ("Shared with You" album via Messages, etc.) saved_to_library: search for syndicated photos that have been saved to the Photos library not_saved_to_library: search for syndicated photos that have not been saved to the Photos library shared_moment: search for photos that have been shared via a shared moment not_shared_moment: search for photos that have not been shared via a shared moment shared_library: search for photos that are part of a shared iCloud library not_shared_library: search for photos that are not part of a shared iCloud library """ added_after: Optional[datetime.datetime] = None added_before: Optional[datetime.datetime] = None added_in_last: Optional[datetime.timedelta] = None album: Optional[Iterable[str]] = None burst_photos: Optional[bool] = None burst: Optional[bool] = None cloudasset: Optional[bool] = None deleted_only: Optional[bool] = None deleted: Optional[bool] = None description: Optional[Iterable[str]] = None duplicate: Optional[bool] = None edited: Optional[bool] = None exif: Optional[Iterable[Tuple[str, str]]] = None external_edit: Optional[bool] = None favorite: Optional[bool] = None folder: Optional[Iterable[str]] = None from_date: Optional[datetime.datetime] = None from_time: Optional[datetime.time] = None function: Optional[List[Tuple[callable, str]]] = None has_comment: Optional[bool] = None has_likes: Optional[bool] = None has_raw: Optional[bool] = None hdr: Optional[bool] = None hidden: Optional[bool] = None ignore_case: Optional[bool] = None in_album: Optional[bool] = None incloud: Optional[bool] = None is_reference: Optional[bool] = None keyword: Optional[Iterable[str]] = None label: Optional[Iterable[str]] = None live: Optional[bool] = None location: Optional[bool] = None max_size: Optional[bitmath.Byte] = None min_size: Optional[bitmath.Byte] = None missing_bursts: Optional[bool] = None missing: Optional[bool] = None movies: Optional[bool] = True name: Optional[Iterable[str]] = None no_comment: Optional[bool] = None no_description: Optional[bool] = None no_likes: Optional[bool] = None no_location: Optional[bool] = None no_keyword: Optional[bool] = None no_place: Optional[bool] = None no_title: Optional[bool] = None not_burst: Optional[bool] = None not_cloudasset: Optional[bool] = None not_edited: Optional[bool] = None not_favorite: Optional[bool] = None not_hdr: Optional[bool] = None not_hidden: Optional[bool] = None not_in_album: Optional[bool] = None not_incloud: Optional[bool] = None not_live: Optional[bool] = None not_missing: Optional[bool] = None not_panorama: Optional[bool] = None not_portrait: Optional[bool] = None not_reference: Optional[bool] = None not_screenshot: Optional[bool] = None not_selfie: Optional[bool] = None not_shared: Optional[bool] = None not_slow_mo: Optional[bool] = None not_time_lapse: Optional[bool] = None panorama: Optional[bool] = None person: Optional[Iterable[str]] = None photos: Optional[bool] = True place: Optional[Iterable[str]] = None portrait: Optional[bool] = None query_eval: Optional[Iterable[str]] = None regex: Optional[Iterable[Tuple[str, str]]] = None screenshot: Optional[bool] = None selected: Optional[bool] = None selfie: Optional[bool] = None shared: Optional[bool] = None slow_mo: Optional[bool] = None time_lapse: Optional[bool] = None title: Optional[Iterable[str]] = None to_date: Optional[datetime.datetime] = None to_time: Optional[datetime.time] = None uti: Optional[Iterable[str]] = None uuid: Optional[Iterable[str]] = None year: Optional[Iterable[int]] = None syndicated: Optional[bool] = None not_syndicated: Optional[bool] = None saved_to_library: Optional[bool] = None not_saved_to_library: Optional[bool] = None shared_moment: Optional[bool] = None not_shared_moment: Optional[bool] = None shared_library: Optional[bool] = None not_shared_library: Optional[bool] = None def asdict(self): return asdict(self)
def query_options_from_kwargs(**kwargs) -> QueryOptions: """Validate query options and create a QueryOptions instance. Note: this will block on stdin if uuid_from_file is set to "-" so it is best to call function before creating the PhotosDB instance so that the validation of query options can happen before the database is loaded. """ # sanity check input args nonexclusive = [ "added_after", "added_before", "added_in_last", "album", "duplicate", "exif", "external_edit", "folder", "from_date", "from_time", "has_raw", "keyword", "label", "max_size", "min_size", "name", "person", "query_eval", "query_function", "regex", "selected", "to_date", "to_time", "uti", "uuid", "uuid_from_file", "year", ] exclusive = [ ("burst", "not_burst"), ("cloudasset", "not_cloudasset"), ("edited", "not_edited"), ("favorite", "not_favorite"), ("has_comment", "no_comment"), ("has_likes", "no_likes"), ("hdr", "not_hdr"), ("hidden", "not_hidden"), ("in_album", "not_in_album"), ("incloud", "not_incloud"), ("is_reference", "not_reference"), ("keyword", "no_keyword"), ("live", "not_live"), ("location", "no_location"), ("missing", "not_missing"), ("only_photos", "only_movies"), ("panorama", "not_panorama"), ("portrait", "not_portrait"), ("screenshot", "not_screenshot"), ("selfie", "not_selfie"), ("shared", "not_shared"), ("slow_mo", "not_slow_mo"), ("time_lapse", "not_time_lapse"), ("deleted", "not_deleted"), ("deleted", "deleted_only"), ("deleted_only", "not_deleted"), ("syndicated", "not_syndicated"), ("saved_to_library", "not_saved_to_library"), ("shared_moment", "not_shared_moment"), ("shared_library", "not_shared_library"), ] # TODO: add option to validate requiring at least one query arg for arg, not_arg in exclusive: if kwargs.get(arg) and kwargs.get(not_arg): arg = arg.replace("_", "-") not_arg = not_arg.replace("_", "-") raise IncompatibleQueryOptions( f"Incompatible query options: --{arg} and --{not_arg} are mutually exclusive" ) # some options like title can be specified multiple times # check if any of them are specified along with their no_ counterpart exclusive_multi_options = ["title", "description", "place", "keyword"] for option in exclusive_multi_options: if kwargs.get(option) and kwargs.get("no_{option}"): raise IncompatibleQueryOptions( f"--{option} and --no-{option} are mutually exclusive" ) include_photos = True include_movies = True # default searches for everything if kwargs.get("only_movies"): include_photos = False if kwargs.get("only_photos"): include_movies = False # load UUIDs if necessary and append to any uuids passed with --uuid uuids = list(kwargs.get("uuid", [])) # Click option is a tuple if uuid_from_file := kwargs.get("uuid_from_file"): uuids.extend(load_uuid_from_file(uuid_from_file)) uuids = tuple(uuids) query_fields = [field.name for field in dataclasses.fields(QueryOptions)] query_dict = {field: kwargs.get(field) for field in query_fields} query_dict["photos"] = include_photos query_dict["movies"] = include_movies query_dict["uuid"] = uuids query_dict["function"] = kwargs.get("query_function") return QueryOptions(**query_dict) def load_uuid_from_file(filename: str) -> list[str]: """ Load UUIDs from file. Does not validate UUIDs but does validate that the UUIDs are in the correct format. Format is 1 UUID per line, any line beginning with # is ignored. Whitespace is stripped. Arguments: filename: file name of the file containing UUIDs Returns: list of UUIDs or empty list of no UUIDs in file Raises: FileNotFoundError if file does not exist ValueError if UUID is not in correct format """ if filename == "-": return _load_uuid_from_stream(sys.stdin) if not pathlib.Path(filename).is_file(): raise FileNotFoundError(f"Could not find file {filename}") with open(filename, "r") as f: return _load_uuid_from_stream(f) def _load_uuid_from_stream(stream: io.IOBase) -> list[str]: """ Load UUIDs from stream. Does not validate UUIDs but does validate that the UUIDs are in the correct format. Format is 1 UUID per line, any line beginning with # is ignored. Whitespace is stripped. Arguments: filename: file name of the file containing UUIDs Returns: list of UUIDs or empty list of no UUIDs in file Raises: ValueError if UUID is not in correct format """ uuid = [] for line in stream: line = line.strip() if len(line) and line[0] != "#": if not re.match(f"^{UUID_PATTERN}$", line): raise ValueError(f"Invalid UUID: {line}") line = line.upper() uuid.append(line) return uuid