Source code for osxphotos.queryoptions

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

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

import bitmath

__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 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: search for burst photos 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 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 on or before this date uti: list of UTIs to search for uuid: list of uuids to search for year: search for photos taken in a given year """ 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 def asdict(self): return asdict(self)
def query_options_from_kwargs(**kwargs) -> QueryOptions: """Validate query options and create a QueryOptions instance""" # 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"), ] # 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"--{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 uuid = None if uuid_from_file := kwargs.get("uuid_from_file"): uuid_list = list(kwargs.get("uuid", [])) # Click option is a tuple uuid_list.extend(load_uuid_from_file(uuid_from_file)) uuid = tuple(uuid_list) 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"] = uuid return QueryOptions(**query_dict) def load_uuid_from_file(filename): """Load UUIDs from file. Does not validate UUIDs. 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 """ if not pathlib.Path(filename).is_file(): raise FileNotFoundError(f"Could not find file {filename}") uuid = [] with open(filename, "r") as uuid_file: for line in uuid_file: line = line.strip() if len(line) and line[0] != "#": uuid.append(line) return uuid