Blackified files

This commit is contained in:
Rhet Turnbull
2022-01-22 09:25:08 -08:00
parent edcc7ea34f
commit 3bafdf7bfd
27 changed files with 327 additions and 300 deletions

View File

@@ -42,7 +42,10 @@ _PHOTOS_5_VERSION = "5000" # I've seen both 5001 and 6000. 6000 is most common
# Ranges for model version by Photos version # Ranges for model version by Photos version
_PHOTOS_5_MODEL_VERSION = [13000, 13999] _PHOTOS_5_MODEL_VERSION = [13000, 13999]
_PHOTOS_6_MODEL_VERSION = [14000, 14999] _PHOTOS_6_MODEL_VERSION = [14000, 14999]
_PHOTOS_7_MODEL_VERSION = [15000, 15999] # Monterey developer preview is 15134, 12.1 is 15331 _PHOTOS_7_MODEL_VERSION = [
15000,
15999,
] # Monterey developer preview is 15134, 12.1 is 15331
# some table names differ between Photos 5 and Photos 6 # some table names differ between Photos 5 and Photos 6
_DB_TABLE_NAMES = { _DB_TABLE_NAMES = {
@@ -260,7 +263,7 @@ EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db" OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
# bit flags for burst images ("burstPickType") # bit flags for burst images ("burstPickType")
BURST_PICK_TYPE_NONE = 0b0 # 0: sometimes used for single images with a burst UUID BURST_PICK_TYPE_NONE = 0b0 # 0: sometimes used for single images with a burst UUID
BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected
BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made
BURST_SELECTED = 0b1000 # 8: burst image is selected BURST_SELECTED = 0b1000 # 8: burst image is selected

View File

@@ -75,37 +75,37 @@ class AdjustmentsInfo:
@property @property
def plist(self): def plist(self):
"""The actual adjustments plist content as a dict """ """The actual adjustments plist content as a dict"""
return self._plist return self._plist
@property @property
def data(self): def data(self):
"""The raw adjustments data as a binary blob """ """The raw adjustments data as a binary blob"""
return self._data return self._data
@property @property
def editor(self): def editor(self):
"""The editor bundle ID for app/plug-in which made the adjustments """ """The editor bundle ID for app/plug-in which made the adjustments"""
return self._editor_bundle_id return self._editor_bundle_id
@property @property
def format_id(self): def format_id(self):
"""The value of the adjustmentFormatIdentifier field in the plist """ """The value of the adjustmentFormatIdentifier field in the plist"""
return self._format_identifier return self._format_identifier
@property @property
def base_version(self): def base_version(self):
"""Value of adjustmentBaseVersion field """ """Value of adjustmentBaseVersion field"""
return self._base_version return self._base_version
@property @property
def format_version(self): def format_version(self):
"""The value of the adjustmentFormatVersion in the plist """ """The value of the adjustmentFormatVersion in the plist"""
return self._format_version return self._format_version
@property @property
def timestamp(self): def timestamp(self):
"""The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp """ """The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp"""
return self._timestamp return self._timestamp
@property @property

View File

@@ -24,12 +24,14 @@ from ._constants import (
from .datetime_utils import get_local_tz from .datetime_utils import get_local_tz
from .query_builder import get_query from .query_builder import get_query
__all__ = ["sort_list_by_keys", __all__ = [
"AlbumInfoBaseClass", "sort_list_by_keys",
"AlbumInfo", "AlbumInfoBaseClass",
"ImportInfo", "AlbumInfo",
"ProjectInfo", "ImportInfo",
"FolderInfo"] "ProjectInfo",
"FolderInfo",
]
def sort_list_by_keys(values, sort_keys): def sort_list_by_keys(values, sort_keys):

View File

@@ -71,49 +71,51 @@ from .sqlgrep import sqlgrep
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import expand_and_validate_filepath, load_function, normalize_fs_path from .utils import expand_and_validate_filepath, load_function, normalize_fs_path
__all__ = ["verbose_", __all__ = [
"get_photos_db", "verbose_",
"DateTimeISO8601", "get_photos_db",
"BitMathSize", "DateTimeISO8601",
"TimeISO8601", "BitMathSize",
"FunctionCall", "TimeISO8601",
"CLI_Obj", "FunctionCall",
"deleted_options", "CLI_Obj",
"QUERY_OPTIONS", "deleted_options",
"cli", "QUERY_OPTIONS",
"export", "cli",
"help", "export",
"query", "help",
"print_photo_info", "query",
"export_photo", "print_photo_info",
"export_photo_to_directory", "export_photo",
"get_filenames_from_template", "export_photo_to_directory",
"get_dirnames_from_template", "get_filenames_from_template",
"find_files_in_branch", "get_dirnames_from_template",
"load_uuid_from_file", "find_files_in_branch",
"write_export_report", "load_uuid_from_file",
"cleanup_files", "write_export_report",
"write_finder_tags", "cleanup_files",
"write_extended_attributes", "write_finder_tags",
"run_post_command", "write_extended_attributes",
"install", "run_post_command",
"uninstall", "install",
"keywords", "uninstall",
"albums", "keywords",
"persons", "albums",
"labels", "persons",
"info", "labels",
"places", "info",
"dump", "places",
"list_libraries", "dump",
"uuid", "list_libraries",
"about", "uuid",
"tutorial", "about",
"repl", "tutorial",
"grep", "repl",
"debug_dump", "grep",
"snap", "debug_dump",
"diff"] "snap",
"diff",
]
# global variable to control verbose output # global variable to control verbose output
# set via --verbose/-V # set via --verbose/-V

View File

@@ -22,14 +22,16 @@ from .phototemplate import (
get_template_help, get_template_help,
) )
__all__ = ["ExportCommand", __all__ = [
"template_help", "ExportCommand",
"tutorial_help", "template_help",
"rich_text", "tutorial_help",
"strip_md_header_and_links", "rich_text",
"strip_md_links", "strip_md_header_and_links",
"strip_html_comments", "strip_md_links",
"get_tutorial_text"] "strip_html_comments",
"get_tutorial_text",
]
# TODO: The following help text could probably be done as mako template # TODO: The following help text could probably be done as mako template

View File

@@ -1,14 +1,16 @@
""" ConfigOptions class to load/save config settings for osxphotos CLI """ """ ConfigOptions class to load/save config settings for osxphotos CLI """
import toml import toml
__all__ = ["ConfigOptionsException", __all__ = [
"ConfigOptionsInvalidError", "ConfigOptionsException",
"ConfigOptionsLoadError", "ConfigOptionsInvalidError",
"ConfigOptions"] "ConfigOptionsLoadError",
"ConfigOptions",
]
class ConfigOptionsException(Exception): class ConfigOptionsException(Exception):
""" Invalid combination of options. """ """Invalid combination of options."""
def __init__(self, message): def __init__(self, message):
self.message = message self.message = message
@@ -24,10 +26,10 @@ class ConfigOptionsLoadError(ConfigOptionsException):
class ConfigOptions: class ConfigOptions:
""" data class to store and load options for osxphotos commands """ """data class to store and load options for osxphotos commands"""
def __init__(self, name, attrs, ignore=None): def __init__(self, name, attrs, ignore=None):
""" init ConfigOptions class """init ConfigOptions class
Args: Args:
name: name for these options, will be used for section heading in TOML file when saving/loading from file name: name for these options, will be used for section heading in TOML file when saving/loading from file
@@ -58,21 +60,21 @@ class ConfigOptions:
raise KeyError(f"Missing argument: {attr}") raise KeyError(f"Missing argument: {attr}")
def validate(self, exclusive=None, inclusive=None, dependent=None, cli=False): def validate(self, exclusive=None, inclusive=None, dependent=None, cli=False):
""" validate combinations of otions """validate combinations of otions
Args: Args:
exclusive: list of tuples in form [("option_1", "option_2")...] which are exclusive; exclusive: list of tuples in form [("option_1", "option_2")...] which are exclusive;
ie. either option_1 can be set or option_2 but not both; ie. either option_1 can be set or option_2 but not both;
inclusive: list of tuples in form [("option_1", "option_2")...] which are inclusive; inclusive: list of tuples in form [("option_1", "option_2")...] which are inclusive;
ie. if either option_1 or option_2 is set, the other must be set ie. if either option_1 or option_2 is set, the other must be set
dependent: list of tuples in form [("option_1", ("option_2", "option_3"))...] dependent: list of tuples in form [("option_1", ("option_2", "option_3"))...]
where if option_1 is set, then at least one of the options in the second tuple must also be set where if option_1 is set, then at least one of the options in the second tuple must also be set
cli: bool, set to True if called to validate CLI options; cli: bool, set to True if called to validate CLI options;
will prepend '--' to option names in InvalidOptions.message and change _ to - in option names will prepend '--' to option names in InvalidOptions.message and change _ to - in option names
Returns: Returns:
True if all options valid True if all options valid
Raises: Raises:
InvalidOption if any combination of options is invalid InvalidOption if any combination of options is invalid
InvalidOption.message will be descriptive message of invalid options InvalidOption.message will be descriptive message of invalid options
@@ -126,7 +128,7 @@ class ConfigOptions:
return True return True
def write_to_file(self, filename): def write_to_file(self, filename):
""" Write self to TOML file """Write self to TOML file
Args: Args:
filename: full path to TOML file to write; filename will be overwritten if it exists filename: full path to TOML file to write; filename will be overwritten if it exists
@@ -146,7 +148,7 @@ class ConfigOptions:
toml.dump({self._name: data}, fd) toml.dump({self._name: data}, fd)
def load_from_file(self, filename, override=False): def load_from_file(self, filename, override=False):
""" Load options from a TOML file. """Load options from a TOML file.
Args: Args:
filename: full path to TOML file filename: full path to TOML file

View File

@@ -6,67 +6,67 @@ __all__ = ["DateTimeFormatter"]
class DateTimeFormatter: class DateTimeFormatter:
""" provides property access to formatted datetime.datetime strftime values """ """provides property access to formatted datetime.datetime strftime values"""
def __init__(self, dt: datetime.datetime): def __init__(self, dt: datetime.datetime):
self.dt = dt self.dt = dt
@property @property
def date(self): def date(self):
""" ISO date in form 2020-03-22 """ """ISO date in form 2020-03-22"""
return self.dt.date().isoformat() return self.dt.date().isoformat()
@property @property
def year(self): def year(self):
""" 4 digit year """ """4 digit year"""
return f"{self.dt.year}" return f"{self.dt.year}"
@property @property
def yy(self): def yy(self):
""" 2 digit year """ """2 digit year"""
return f"{self.dt.strftime('%y')}" return f"{self.dt.strftime('%y')}"
@property @property
def mm(self): def mm(self):
""" 2 digit month """ """2 digit month"""
return f"{self.dt.strftime('%m')}" return f"{self.dt.strftime('%m')}"
@property @property
def month(self): def month(self):
""" Month as locale's full name """ """Month as locale's full name"""
return f"{self.dt.strftime('%B')}" return f"{self.dt.strftime('%B')}"
@property @property
def mon(self): def mon(self):
""" Month as locale's abbreviated name """ """Month as locale's abbreviated name"""
return f"{self.dt.strftime('%b')}" return f"{self.dt.strftime('%b')}"
@property @property
def dd(self): def dd(self):
""" 2-digit day of the month """ """2-digit day of the month"""
return f"{self.dt.strftime('%d')}" return f"{self.dt.strftime('%d')}"
@property @property
def dow(self): def dow(self):
""" Day of week as locale's name """ """Day of week as locale's name"""
return f"{self.dt.strftime('%A')}" return f"{self.dt.strftime('%A')}"
@property @property
def doy(self): def doy(self):
""" Julian day of year starting from 001 """ """Julian day of year starting from 001"""
return f"{self.dt.strftime('%j')}" return f"{self.dt.strftime('%j')}"
@property @property
def hour(self): def hour(self):
""" 2-digit hour """ """2-digit hour"""
return f"{self.dt.strftime('%H')}" return f"{self.dt.strftime('%H')}"
@property @property
def min(self): def min(self):
""" 2-digit minute """ """2-digit minute"""
return f"{self.dt.strftime('%M')}" return f"{self.dt.strftime('%M')}"
@property @property
def sec(self): def sec(self):
""" 2-digit second """ """2-digit second"""
return f"{self.dt.strftime('%S')}" return f"{self.dt.strftime('%S')}"

View File

@@ -2,21 +2,23 @@
import datetime import datetime
__all__ = ["get_local_tz", __all__ = [
"datetime_has_tz", "get_local_tz",
"datetime_tz_to_utc", "datetime_has_tz",
"datetime_remove_tz", "datetime_tz_to_utc",
"datetime_naive_to_utc", "datetime_remove_tz",
"datetime_naive_to_local", "datetime_naive_to_utc",
"datetime_utc_to_local"] "datetime_naive_to_local",
"datetime_utc_to_local",
]
def get_local_tz(dt): def get_local_tz(dt):
""" Return local timezone as datetime.timezone tzinfo for dt """Return local timezone as datetime.timezone tzinfo for dt
Args: Args:
dt: datetime.datetime dt: datetime.datetime
Returns: Returns:
local timezone for dt as datetime.timezone local timezone for dt as datetime.timezone
@@ -30,14 +32,14 @@ def get_local_tz(dt):
def datetime_has_tz(dt): def datetime_has_tz(dt):
""" Return True if datetime dt has tzinfo else False """Return True if datetime dt has tzinfo else False
Args: Args:
dt: datetime.datetime dt: datetime.datetime
Returns: Returns:
True if dt is timezone aware, else False True if dt is timezone aware, else False
Raises: Raises:
TypeError if dt is not a datetime.datetime object TypeError if dt is not a datetime.datetime object
""" """
@@ -49,15 +51,15 @@ def datetime_has_tz(dt):
def datetime_tz_to_utc(dt): def datetime_tz_to_utc(dt):
""" Convert datetime.datetime object with timezone to UTC timezone """Convert datetime.datetime object with timezone to UTC timezone
Args: Args:
dt: datetime.datetime object dt: datetime.datetime object
Returns: Returns:
datetime.datetime in UTC timezone datetime.datetime in UTC timezone
Raises: Raises:
TypeError if dt is not datetime.datetime object TypeError if dt is not datetime.datetime object
ValueError if dt does not have timeone information ValueError if dt does not have timeone information
""" """
@@ -72,14 +74,14 @@ def datetime_tz_to_utc(dt):
def datetime_remove_tz(dt): def datetime_remove_tz(dt):
""" Remove timezone from a datetime.datetime object """Remove timezone from a datetime.datetime object
Args: Args:
dt: datetime.datetime object with tzinfo dt: datetime.datetime object with tzinfo
Returns: Returns:
dt without any timezone info (naive datetime object) dt without any timezone info (naive datetime object)
Raises: Raises:
TypeError if dt is not a datetime.datetime object TypeError if dt is not a datetime.datetime object
""" """
@@ -91,15 +93,15 @@ def datetime_remove_tz(dt):
def datetime_naive_to_utc(dt): def datetime_naive_to_utc(dt):
""" Convert naive (timezone unaware) datetime.datetime """Convert naive (timezone unaware) datetime.datetime
to aware timezone in UTC timezone to aware timezone in UTC timezone
Args: Args:
dt: datetime.datetime without timezone dt: datetime.datetime without timezone
Returns: Returns:
datetime.datetime with UTC timezone datetime.datetime with UTC timezone
Raises: Raises:
TypeError if dt is not a datetime.datetime object TypeError if dt is not a datetime.datetime object
ValueError if dt is not a naive/timezone unaware object ValueError if dt is not a naive/timezone unaware object
@@ -119,15 +121,15 @@ def datetime_naive_to_utc(dt):
def datetime_naive_to_local(dt): def datetime_naive_to_local(dt):
""" Convert naive (timezone unaware) datetime.datetime """Convert naive (timezone unaware) datetime.datetime
to aware timezone in local timezone to aware timezone in local timezone
Args: Args:
dt: datetime.datetime without timezone dt: datetime.datetime without timezone
Returns: Returns:
datetime.datetime with local timezone datetime.datetime with local timezone
Raises: Raises:
TypeError if dt is not a datetime.datetime object TypeError if dt is not a datetime.datetime object
ValueError if dt is not a naive/timezone unaware object ValueError if dt is not a naive/timezone unaware object
@@ -147,7 +149,7 @@ def datetime_naive_to_local(dt):
def datetime_utc_to_local(dt): def datetime_utc_to_local(dt):
""" Convert datetime.datetime object in UTC timezone to local timezone """Convert datetime.datetime object in UTC timezone to local timezone
Args: Args:
dt: datetime.datetime object dt: datetime.datetime object

View File

@@ -17,12 +17,14 @@ import subprocess
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from functools import lru_cache # pylint: disable=syntax-error from functools import lru_cache # pylint: disable=syntax-error
__all__ = ["escape_str", __all__ = [
"unescape_str", "escape_str",
"terminate_exiftool", "unescape_str",
"get_exiftool_path", "terminate_exiftool",
"ExifTool", "get_exiftool_path",
"ExifToolCaching"] "ExifTool",
"ExifToolCaching",
]
# exiftool -stay_open commands outputs this EOF marker after command is run # exiftool -stay_open commands outputs this EOF marker after command is run
EXIFTOOL_STAYOPEN_EOF = "{ready}" EXIFTOOL_STAYOPEN_EOF = "{ready}"

View File

@@ -15,7 +15,7 @@ __all__ = ["FileUtilABC", "FileUtilMacOS", "FileUtil", "FileUtilNoOp"]
class FileUtilABC(ABC): class FileUtilABC(ABC):
""" Abstract base class for FileUtil """ """Abstract base class for FileUtil"""
@classmethod @classmethod
@abstractmethod @abstractmethod
@@ -69,14 +69,14 @@ class FileUtilABC(ABC):
class FileUtilMacOS(FileUtilABC): class FileUtilMacOS(FileUtilABC):
""" Various file utilities """ """Various file utilities"""
@classmethod @classmethod
def hardlink(cls, src, dest): def hardlink(cls, src, dest):
""" Hardlinks a file from src path to dest path """Hardlinks a file from src path to dest path
src: source path as string src: source path as string
dest: destination path as string dest: destination path as string
Raises exception if linking fails or either path is None """ Raises exception if linking fails or either path is None"""
if src is None or dest is None: if src is None or dest is None:
raise ValueError("src and dest must not be None", src, dest) raise ValueError("src and dest must not be None", src, dest)
@@ -92,17 +92,17 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def copy(cls, src, dest): def copy(cls, src, dest):
""" Copies a file from src path to dest path """Copies a file from src path to dest path
Args: Args:
src: source path as string; must be a valid file path src: source path as string; must be a valid file path
dest: destination path as string dest: destination path as string
dest may be either directory or file; in either case, src file must not exist in dest dest may be either directory or file; in either case, src file must not exist in dest
Note: src and dest may be either a string or a pathlib.Path object Note: src and dest may be either a string or a pathlib.Path object
Returns: Returns:
True if copy succeeded True if copy succeeded
Raises: Raises:
OSError if copy fails OSError if copy fails
TypeError if either path is None TypeError if either path is None
@@ -126,7 +126,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def unlink(cls, filepath): def unlink(cls, filepath):
""" unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink """ """unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink"""
if isinstance(filepath, pathlib.Path): if isinstance(filepath, pathlib.Path):
filepath.unlink() filepath.unlink()
else: else:
@@ -134,7 +134,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def rmdir(cls, dirpath): def rmdir(cls, dirpath):
""" remove directory filepath; dirpath must be empty """ """remove directory filepath; dirpath must be empty"""
if isinstance(dirpath, pathlib.Path): if isinstance(dirpath, pathlib.Path):
dirpath.rmdir() dirpath.rmdir()
else: else:
@@ -142,7 +142,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def utime(cls, path, times): def utime(cls, path, times):
""" Set the access and modified time of path. """ """Set the access and modified time of path."""
os.utime(path, times) os.utime(path, times)
@classmethod @classmethod
@@ -154,7 +154,7 @@ class FileUtilMacOS(FileUtilABC):
mtime1 -- optional, pass alternate file modification timestamp for f1; will be converted to int mtime1 -- optional, pass alternate file modification timestamp for f1; will be converted to int
Return value: Return value:
True if the file signatures as returned by stat are the same, False otherwise. True if the file signatures as returned by stat are the same, False otherwise.
Does not do a byte-by-byte comparison. Does not do a byte-by-byte comparison.
""" """
@@ -188,20 +188,20 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def file_sig(cls, f1): def file_sig(cls, f1):
""" return os.stat signature for file f1 """ """return os.stat signature for file f1"""
return cls._sig(os.stat(f1)) return cls._sig(os.stat(f1))
@classmethod @classmethod
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0): def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
""" converts image file src_file to jpeg format as dest_file """converts image file src_file to jpeg format as dest_file
Args: Args:
src_file: image file to convert src_file: image file to convert
dest_file: destination path to write converted file to dest_file: destination path to write converted file to
compression quality: JPEG compression quality in range 0.0 <= compression_quality <= 1.0; default 1.0 (best quality) compression quality: JPEG compression quality in range 0.0 <= compression_quality <= 1.0; default 1.0 (best quality)
Returns: Returns:
True if success, otherwise False True if success, otherwise False
""" """
converter = ImageConverter() converter = ImageConverter()
return converter.write_jpeg( return converter.write_jpeg(
@@ -210,40 +210,40 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def rename(cls, src, dest): def rename(cls, src, dest):
""" Copy src to dest """Copy src to dest
Args: Args:
src: path to source file src: path to source file
dest: path to destination file dest: path to destination file
Returns: Returns:
Name of renamed file (dest) Name of renamed file (dest)
""" """
os.rename(str(src), str(dest)) os.rename(str(src), str(dest))
return dest return dest
@staticmethod @staticmethod
def _sig(st): def _sig(st):
""" return tuple of (mode, size, mtime) of file based on os.stat """return tuple of (mode, size, mtime) of file based on os.stat
Args: Args:
st: os.stat signature st: os.stat signature
""" """
# use int(st.st_mtime) because ditto does not copy fractional portion of mtime # use int(st.st_mtime) because ditto does not copy fractional portion of mtime
return (stat.S_IFMT(st.st_mode), st.st_size, int(st.st_mtime)) return (stat.S_IFMT(st.st_mode), st.st_size, int(st.st_mtime))
class FileUtil(FileUtilMacOS): class FileUtil(FileUtilMacOS):
""" Various file utilities """ """Various file utilities"""
pass pass
class FileUtilNoOp(FileUtil): class FileUtilNoOp(FileUtil):
""" No-Op implementation of FileUtil for testing / dry-run mode """No-Op implementation of FileUtil for testing / dry-run mode
all methods with exception of cmp, cmp_file_sig and file_cmp are no-op all methods with exception of cmp, cmp_file_sig and file_cmp are no-op
cmp and cmp_file_sig functions as FileUtil methods do cmp and cmp_file_sig functions as FileUtil methods do
file_cmp returns mock data file_cmp returns mock data
""" """
@staticmethod @staticmethod

View File

@@ -1,4 +1,3 @@
__all__ = ["MomentInfo"] __all__ = ["MomentInfo"]
"""MomentInfo class with details about photo moments.""" """MomentInfo class with details about photo moments."""

View File

@@ -4,11 +4,13 @@ import pathvalidate
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
__all__ = ["sanitize_filepath", __all__ = [
"is_valid_filepath", "sanitize_filepath",
"sanitize_filename", "is_valid_filepath",
"sanitize_dirname", "sanitize_filename",
"sanitize_pathpart"] "sanitize_dirname",
"sanitize_pathpart",
]
def sanitize_filepath(filepath): def sanitize_filepath(filepath):

View File

@@ -53,7 +53,7 @@ class PersonInfo:
@property @property
def photos(self): def photos(self):
""" Returns list of PhotoInfo objects associated with this person """ """Returns list of PhotoInfo objects associated with this person"""
return self._db.photos_by_uuid(self._db._dbfaces_pk[self._pk]) return self._db.photos_by_uuid(self._db._dbfaces_pk[self._pk])
@property @property
@@ -73,7 +73,7 @@ class PersonInfo:
return [] return []
def asdict(self): def asdict(self):
""" Returns dictionary representation of class instance """ """Returns dictionary representation of class instance"""
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
return { return {
"uuid": self.uuid, "uuid": self.uuid,
@@ -85,7 +85,7 @@ class PersonInfo:
} }
def json(self): def json(self):
""" Returns JSON representation of class instance """ """Returns JSON representation of class instance"""
return json.dumps(self.asdict()) return json.dumps(self.asdict())
def __str__(self): def __str__(self):
@@ -203,7 +203,7 @@ class FaceInfo:
@property @property
def person_info(self): def person_info(self):
""" PersonInfo instance for person associated with this face """ """PersonInfo instance for person associated with this face"""
try: try:
return self._person return self._person
except AttributeError: except AttributeError:
@@ -212,7 +212,7 @@ class FaceInfo:
@property @property
def photo(self): def photo(self):
""" PhotoInfo instance associated with this face """ """PhotoInfo instance associated with this face"""
try: try:
return self._photo return self._photo
except AttributeError: except AttributeError:
@@ -294,7 +294,7 @@ class FaceInfo:
return [(x0, y0), (x1, y1)] return [(x0, y0), (x1, y1)]
def roll_pitch_yaw(self): def roll_pitch_yaw(self):
""" Roll, pitch, yaw of face in radians as tuple """ """Roll, pitch, yaw of face in radians as tuple"""
info = self._info info = self._info
roll = 0 if info["roll"] is None else info["roll"] roll = 0 if info["roll"] is None else info["roll"]
pitch = 0 if info["pitch"] is None else info["pitch"] pitch = 0 if info["pitch"] is None else info["pitch"]
@@ -304,19 +304,19 @@ class FaceInfo:
@property @property
def roll(self): def roll(self):
""" Return roll angle in radians of the face region """ """Return roll angle in radians of the face region"""
roll, _, _ = self.roll_pitch_yaw() roll, _, _ = self.roll_pitch_yaw()
return roll return roll
@property @property
def pitch(self): def pitch(self):
""" Return pitch angle in radians of the face region """ """Return pitch angle in radians of the face region"""
_, pitch, _ = self.roll_pitch_yaw() _, pitch, _ = self.roll_pitch_yaw()
return pitch return pitch
@property @property
def yaw(self): def yaw(self):
""" Return yaw angle in radians of the face region """ """Return yaw angle in radians of the face region"""
_, _, yaw = self.roll_pitch_yaw() _, _, yaw = self.roll_pitch_yaw()
return yaw return yaw
@@ -404,7 +404,7 @@ class FaceInfo:
return (int(xr), int(yr)) return (int(xr), int(yr))
def asdict(self): def asdict(self):
""" Returns dict representation of class instance """ """Returns dict representation of class instance"""
roll, pitch, yaw = self.roll_pitch_yaw() roll, pitch, yaw = self.roll_pitch_yaw()
return { return {
"_pk": self._pk, "_pk": self._pk,
@@ -453,7 +453,7 @@ class FaceInfo:
} }
def json(self): def json(self):
""" Return JSON representation of FaceInfo instance """ """Return JSON representation of FaceInfo instance"""
return json.dumps(self.asdict()) return json.dumps(self.asdict())
def __str__(self): def __str__(self):

View File

@@ -48,12 +48,14 @@ from .phototemplate import RenderOptions
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import increment_filename, increment_filename_with_count, lineno from .utils import increment_filename, increment_filename_with_count, lineno
__all__ = ["ExportError", __all__ = [
"ExportOptions", "ExportError",
"ExportResults", "ExportOptions",
"PhotoExporter", "ExportResults",
"hexdigest", "PhotoExporter",
"rename_jpeg_files"] "hexdigest",
"rename_jpeg_files",
]
if TYPE_CHECKING: if TYPE_CHECKING:
from .photoinfo import PhotoInfo from .photoinfo import PhotoInfo

View File

@@ -36,25 +36,27 @@ from .fileutil import FileUtil
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import _get_os_version, increment_filename from .utils import _get_os_version, increment_filename
__all__ = ["NSURL_to_path", __all__ = [
"path_to_NSURL", "NSURL_to_path",
"check_photokit_authorization", "path_to_NSURL",
"request_photokit_authorization", "check_photokit_authorization",
"PhotoKitError", "request_photokit_authorization",
"PhotoKitFetchFailed", "PhotoKitError",
"PhotoKitAuthError", "PhotoKitFetchFailed",
"PhotoKitExportError", "PhotoKitAuthError",
"PhotoKitMediaTypeError", "PhotoKitExportError",
"ImageData", "PhotoKitMediaTypeError",
"AVAssetData", "ImageData",
"PHAssetResourceData", "AVAssetData",
"PhotoKitNotificationDelegate", "PHAssetResourceData",
"PhotoAsset", "PhotoKitNotificationDelegate",
"SlowMoVideoExporter", "PhotoAsset",
"VideoAsset", "SlowMoVideoExporter",
"LivePhotoRequest", "VideoAsset",
"LivePhotoAsset", "LivePhotoRequest",
"PhotoLibrary"] "LivePhotoAsset",
"PhotoLibrary",
]
# NOTE: This requires user have granted access to the terminal (e.g. Terminal.app or iTerm) # NOTE: This requires user have granted access to the terminal (e.g. Terminal.app or iTerm)
# to access Photos. This should happen automatically the first time it's called. I've # to access Photos. This should happen automatically the first time it's called. I've

View File

@@ -10,9 +10,9 @@ from ..utils import _open_sql_file, normalize_unicode
def _process_comments(self): def _process_comments(self):
""" load the comments and likes data from the database """load the comments and likes data from the database
this is a PhotosDB method that should be imported in this is a PhotosDB method that should be imported in
the PhotosDB class definition in photosdb.py the PhotosDB class definition in photosdb.py
""" """
self._db_hashed_person_id = {} self._db_hashed_person_id = {}
self._db_comments_uuid = {} self._db_comments_uuid = {}
@@ -24,7 +24,7 @@ def _process_comments(self):
@dataclass @dataclass
class CommentInfo: class CommentInfo:
""" Class for shared photo comments """ """Class for shared photo comments"""
datetime: datetime.datetime datetime: datetime.datetime
user: str user: str
@@ -37,7 +37,7 @@ class CommentInfo:
@dataclass @dataclass
class LikeInfo: class LikeInfo:
""" Class for shared photo likes """ """Class for shared photo likes"""
datetime: datetime.datetime datetime: datetime.datetime
user: str user: str
@@ -50,16 +50,16 @@ class LikeInfo:
# The following methods do not get imported into PhotosDB # The following methods do not get imported into PhotosDB
# but will get called by _process_comments # but will get called by _process_comments
def _process_comments_4(photosdb): def _process_comments_4(photosdb):
""" process comments and likes info for Photos <= 4 """process comments and likes info for Photos <= 4
photosdb: PhotosDB instance """ photosdb: PhotosDB instance"""
raise NotImplementedError( raise NotImplementedError(
f"Not implemented for database version {photosdb._db_version}." f"Not implemented for database version {photosdb._db_version}."
) )
def _process_comments_5(photosdb): def _process_comments_5(photosdb):
""" process comments and likes info for Photos >= 5 """process comments and likes info for Photos >= 5
photosdb: PhotosDB instance """ photosdb: PhotosDB instance"""
db = photosdb._tmp_db db = photosdb._tmp_db

View File

@@ -7,10 +7,11 @@ from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION
from ..utils import _db_is_locked, _debug, _open_sql_file from ..utils import _db_is_locked, _debug, _open_sql_file
from .photosdb_utils import get_db_version from .photosdb_utils import get_db_version
def _process_exifinfo(self): def _process_exifinfo(self):
""" load the exif data from the database """load the exif data from the database
this is a PhotosDB method that should be imported in this is a PhotosDB method that should be imported in
the PhotosDB class definition in photosdb.py the PhotosDB class definition in photosdb.py
""" """
if self._db_version <= _PHOTOS_4_VERSION: if self._db_version <= _PHOTOS_4_VERSION:
_process_exifinfo_4(self) _process_exifinfo_4(self)
@@ -23,20 +24,20 @@ def _process_exifinfo(self):
def _process_exifinfo_4(photosdb): def _process_exifinfo_4(photosdb):
""" process exif info for Photos <= 4 """process exif info for Photos <= 4
photosdb: PhotosDB instance """ photosdb: PhotosDB instance"""
photosdb._db_exifinfo_uuid = {} photosdb._db_exifinfo_uuid = {}
raise NotImplementedError(f"search info not implemented for this database version") raise NotImplementedError(f"search info not implemented for this database version")
def _process_exifinfo_5(photosdb): def _process_exifinfo_5(photosdb):
""" process exif info for Photos >= 5 """process exif info for Photos >= 5
photosdb: PhotosDB instance """ photosdb: PhotosDB instance"""
db = photosdb._tmp_db db = photosdb._tmp_db
asset_table = _DB_TABLE_NAMES[photosdb._photos_ver]["ASSET"] asset_table = _DB_TABLE_NAMES[photosdb._photos_ver]["ASSET"]
(conn, cursor) = _open_sql_file(db) (conn, cursor) = _open_sql_file(db)
result = conn.execute( result = conn.execute(

View File

@@ -22,8 +22,7 @@ from .photosdb_utils import get_db_version
def _process_faceinfo(self): def _process_faceinfo(self):
""" Process face information """Process face information"""
"""
self._db_faceinfo_pk = {} self._db_faceinfo_pk = {}
self._db_faceinfo_uuid = {} self._db_faceinfo_uuid = {}
@@ -36,7 +35,7 @@ def _process_faceinfo(self):
def _process_faceinfo_4(photosdb): def _process_faceinfo_4(photosdb):
""" Process face information for Photos 4 databases """Process face information for Photos 4 databases
Args: Args:
photosdb: an OSXPhotosDB instance photosdb: an OSXPhotosDB instance
@@ -172,7 +171,7 @@ def _process_faceinfo_4(photosdb):
def _process_faceinfo_5(photosdb): def _process_faceinfo_5(photosdb):
""" Process face information for Photos 5 databases """Process face information for Photos 5 databases
Args: Args:
photosdb: an OSXPhotosDB instance photosdb: an OSXPhotosDB instance

View File

@@ -22,8 +22,8 @@ from .photosdb_utils import get_db_version
def _process_scoreinfo(self): def _process_scoreinfo(self):
""" Process computed photo scores """Process computed photo scores
Note: Only works on Photos version == 5.0 Note: Only works on Photos version == 5.0
""" """
# _db_scoreinfo_uuid is dict in form {uuid: {score values}} # _db_scoreinfo_uuid is dict in form {uuid: {score values}}
@@ -38,7 +38,7 @@ def _process_scoreinfo(self):
def _process_scoreinfo_5(photosdb): def _process_scoreinfo_5(photosdb):
""" Process computed photo scores for Photos 5 databases """Process computed photo scores for Photos 5 databases
Args: Args:
photosdb: an OSXPhotosDB instance photosdb: an OSXPhotosDB instance
@@ -147,4 +147,4 @@ def _process_scoreinfo_5(photosdb):
scores["well_timed_shot"] = row[27] scores["well_timed_shot"] = row[27]
photosdb._db_scoreinfo_uuid[uuid] = scores photosdb._db_scoreinfo_uuid[uuid] = scores
conn.close() conn.close()

View File

@@ -35,10 +35,10 @@ from ..utils import _db_is_locked, _debug, _open_sql_file, normalize_unicode
def _process_searchinfo(self): def _process_searchinfo(self):
""" load machine learning/search term label info from a Photos library """load machine learning/search term label info from a Photos library
db_connection: a connection to the SQLite database file containing the db_connection: a connection to the SQLite database file containing the
search terms. In Photos 5, this is called psi.sqlite search terms. In Photos 5, this is called psi.sqlite
Note: Only works on Photos version == 5.0 """ Note: Only works on Photos version == 5.0"""
# _db_searchinfo_uuid is dict in form {uuid : [list of associated search info records] # _db_searchinfo_uuid is dict in form {uuid : [list of associated search info records]
self._db_searchinfo_uuid = _db_searchinfo_uuid = {} self._db_searchinfo_uuid = _db_searchinfo_uuid = {}
@@ -155,7 +155,7 @@ def _process_searchinfo(self):
@property @property
def labels(self): def labels(self):
""" return list of all search info labels found in the library """ """return list of all search info labels found in the library"""
if self._db_version <= _PHOTOS_4_VERSION: if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(f"SearchInfo not implemented for this library version") logging.warning(f"SearchInfo not implemented for this library version")
return [] return []
@@ -165,7 +165,7 @@ def labels(self):
@property @property
def labels_normalized(self): def labels_normalized(self):
""" return list of all normalized search info labels found in the library """ """return list of all normalized search info labels found in the library"""
if self._db_version <= _PHOTOS_4_VERSION: if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(f"SearchInfo not implemented for this library version") logging.warning(f"SearchInfo not implemented for this library version")
return [] return []
@@ -175,7 +175,7 @@ def labels_normalized(self):
@property @property
def labels_as_dict(self): def labels_as_dict(self):
""" return labels as dict of label: count in reverse sorted order (descending) """ """return labels as dict of label: count in reverse sorted order (descending)"""
if self._db_version <= _PHOTOS_4_VERSION: if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(f"SearchInfo not implemented for this library version") logging.warning(f"SearchInfo not implemented for this library version")
return dict() return dict()
@@ -187,7 +187,7 @@ def labels_as_dict(self):
@property @property
def labels_normalized_as_dict(self): def labels_normalized_as_dict(self):
""" return normalized labels as dict of label: count in reverse sorted order (descending) """ """return normalized labels as dict of label: count in reverse sorted order (descending)"""
if self._db_version <= _PHOTOS_4_VERSION: if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(f"SearchInfo not implemented for this library version") logging.warning(f"SearchInfo not implemented for this library version")
return dict() return dict()
@@ -201,8 +201,8 @@ def labels_normalized_as_dict(self):
@lru_cache(maxsize=128) @lru_cache(maxsize=128)
def ints_to_uuid(uuid_0, uuid_1): def ints_to_uuid(uuid_0, uuid_1):
""" convert two signed ints into a UUID strings """convert two signed ints into a UUID strings
uuid_0, uuid_1: the two int components of an RFC 4122 UUID """ 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) # assumes uuid imported as uuidlib (to avoid namespace conflict with other uses of uuid)

View File

@@ -16,11 +16,13 @@ from .._constants import (
) )
from ..utils import _open_sql_file from ..utils import _open_sql_file
__all__ = ["get_db_version", __all__ = [
"get_model_version", "get_db_version",
"get_db_model_version", "get_model_version",
"UnknownLibraryVersion", "get_db_model_version",
"get_photos_library_version"] "UnknownLibraryVersion",
"get_photos_library_version",
]
def get_db_version(db_file): def get_db_version(db_file):

View File

@@ -22,12 +22,14 @@ from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .text_detection import detect_text from .text_detection import detect_text
from .utils import expand_and_validate_filepath, load_function from .utils import expand_and_validate_filepath, load_function
__all__ = ["RenderOptions", __all__ = [
"PhotoTemplateParser", "RenderOptions",
"PhotoTemplate", "PhotoTemplateParser",
"parse_default_kv", "PhotoTemplate",
"get_template_help", "parse_default_kv",
"format_str_value"] "get_template_help",
"format_str_value",
]
# TODO: a lot of values are passed from function to function like path_sep--make these all class properties # TODO: a lot of values are passed from function to function like path_sep--make these all class properties

View File

@@ -14,13 +14,15 @@ from bpylist import archiver
from ._constants import UNICODE_FORMAT from ._constants import UNICODE_FORMAT
from .utils import normalize_unicode from .utils import normalize_unicode
__all__ = ["PLRevGeoLocationInfo", __all__ = [
"PLRevGeoMapItem", "PLRevGeoLocationInfo",
"PLRevGeoMapItemAdditionalPlaceInfo", "PLRevGeoMapItem",
"CNPostalAddress", "PLRevGeoMapItemAdditionalPlaceInfo",
"PlaceInfo", "CNPostalAddress",
"PlaceInfo4", "PlaceInfo",
"PlaceInfo5"] "PlaceInfo4",
"PlaceInfo5",
]
# postal address information, returned by PlaceInfo.address # postal address information, returned by PlaceInfo.address
PostalAddress = namedtuple( PostalAddress = namedtuple(
@@ -73,7 +75,7 @@ PlaceNames = namedtuple(
# in ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA # in ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA
# These classes are used by bpylist.archiver to unarchive the serialized objects # These classes are used by bpylist.archiver to unarchive the serialized objects
class PLRevGeoLocationInfo: class PLRevGeoLocationInfo:
""" The top level reverse geolocation object """ """The top level reverse geolocation object"""
def __init__( def __init__(
self, self,
@@ -155,7 +157,7 @@ class PLRevGeoLocationInfo:
class PLRevGeoMapItem: class PLRevGeoMapItem:
""" Stores the list of place names, organized by area """ """Stores the list of place names, organized by area"""
def __init__(self, sortedPlaceInfos, finalPlaceInfos): def __init__(self, sortedPlaceInfos, finalPlaceInfos):
self.sortedPlaceInfos = sortedPlaceInfos self.sortedPlaceInfos = sortedPlaceInfos
@@ -190,7 +192,7 @@ class PLRevGeoMapItem:
class PLRevGeoMapItemAdditionalPlaceInfo: class PLRevGeoMapItemAdditionalPlaceInfo:
""" Additional info about individual places """ """Additional info about individual places"""
def __init__(self, area, name, placeType, dominantOrderType): def __init__(self, area, name, placeType, dominantOrderType):
self.area = area self.area = area
@@ -229,7 +231,7 @@ class PLRevGeoMapItemAdditionalPlaceInfo:
class CNPostalAddress: class CNPostalAddress:
""" postal address for the reverse geolocation info """ """postal address for the reverse geolocation info"""
def __init__( def __init__(
self, self,
@@ -362,17 +364,17 @@ class PlaceInfo(ABC):
class PlaceInfo4(PlaceInfo): class PlaceInfo4(PlaceInfo):
""" Reverse geolocation place info for a photo (Photos <= 4) """ """Reverse geolocation place info for a photo (Photos <= 4)"""
def __init__(self, place_names, country_code): def __init__(self, place_names, country_code):
""" place_names: list of place name tuples in ascending order by area """place_names: list of place name tuples in ascending order by area
tuple fields are: modelID, place name, place type, area, e.g. tuple fields are: modelID, place name, place type, area, e.g.
[(5, "St James's Park", 45, 0), [(5, "St James's Park", 45, 0),
(4, 'Westminster', 16, 22097376), (4, 'Westminster', 16, 22097376),
(3, 'London', 4, 1596146816), (3, 'London', 4, 1596146816),
(2, 'England', 2, 180406091776), (2, 'England', 2, 180406091776),
(1, 'United Kingdom', 1, 414681432064)] (1, 'United Kingdom', 1, 414681432064)]
country_code: two letter country code for the country country_code: two letter country code for the country
""" """
self._place_names = place_names self._place_names = place_names
self._country_code = country_code self._country_code = country_code
@@ -412,7 +414,7 @@ class PlaceInfo4(PlaceInfo):
) )
def _process_place_info(self): def _process_place_info(self):
""" Process place_names to set self._name and self._names """ """Process place_names to set self._name and self._names"""
places = self._place_names places = self._place_names
# build a dictionary where key is placetype # build a dictionary where key is placetype
@@ -508,38 +510,38 @@ class PlaceInfo4(PlaceInfo):
class PlaceInfo5(PlaceInfo): class PlaceInfo5(PlaceInfo):
""" Reverse geolocation place info for a photo (Photos >= 5) """ """Reverse geolocation place info for a photo (Photos >= 5)"""
def __init__(self, revgeoloc_bplist): def __init__(self, revgeoloc_bplist):
""" revgeoloc_bplist: a binary plist blob containing """revgeoloc_bplist: a binary plist blob containing
a serialized PLRevGeoLocationInfo object """ a serialized PLRevGeoLocationInfo object"""
self._bplist = revgeoloc_bplist self._bplist = revgeoloc_bplist
self._plrevgeoloc = archiver.unarchive(revgeoloc_bplist) self._plrevgeoloc = archiver.unarchive(revgeoloc_bplist)
self._process_place_info() self._process_place_info()
@property @property
def address_str(self): def address_str(self):
""" returns the postal address as a string """ """returns the postal address as a string"""
return self._plrevgeoloc.addressString return self._plrevgeoloc.addressString
@property @property
def country_code(self): def country_code(self):
""" returns the country code """ """returns the country code"""
return self._plrevgeoloc.countryCode return self._plrevgeoloc.countryCode
@property @property
def ishome(self): def ishome(self):
""" returns True if place is user's home address """ """returns True if place is user's home address"""
return self._plrevgeoloc.isHome return self._plrevgeoloc.isHome
@property @property
def name(self): def name(self):
""" returns local place name """ """returns local place name"""
return self._name return self._name
@property @property
def names(self): def names(self):
""" returns PlaceNames tuple with detailed reverse geolocation place names """ """returns PlaceNames tuple with detailed reverse geolocation place names"""
return self._names return self._names
@property @property
@@ -564,7 +566,7 @@ class PlaceInfo5(PlaceInfo):
return postal_address return postal_address
def _process_place_info(self): def _process_place_info(self):
""" Process sortedPlaceInfos to set self._name and self._names """ """Process sortedPlaceInfos to set self._name and self._names"""
places = self._plrevgeoloc.mapItem.sortedPlaceInfos places = self._plrevgeoloc.mapItem.sortedPlaceInfos
# build a dictionary where key is placetype # build a dictionary where key is placetype

View File

@@ -1,4 +1,3 @@
__all__ = ["PyReplQuitter", "embed_repl"] __all__ = ["PyReplQuitter", "embed_repl"]
""" Custom Python REPL based on ptpython that allows quitting with custom keywords instead of `quit()` """ """ Custom Python REPL based on ptpython that allows quitting with custom keywords instead of `quit()` """

View File

@@ -9,7 +9,7 @@ __all__ = ["ScoreInfo"]
@dataclass(frozen=True) @dataclass(frozen=True)
class ScoreInfo: class ScoreInfo:
""" Computed photo score info associated with a photo from the Photos library """ """Computed photo score info associated with a photo from the Photos library"""
overall: float overall: float
curation: float curation: float
@@ -38,4 +38,3 @@ class ScoreInfo:
well_chosen_subject: float well_chosen_subject: float
well_framed_subject: float well_framed_subject: float
well_timed_shot: float well_timed_shot: float

View File

@@ -1,4 +1,3 @@
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"] __all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
""" get UTI for a given file extension and the preferred extension for a given UTI """ """ get UTI for a given file extension and the preferred extension for a given UTI """

View File

@@ -24,19 +24,21 @@ from Foundation import NSString
from ._constants import UNICODE_FORMAT from ._constants import UNICODE_FORMAT
__all__ = ["noop", __all__ = [
"lineno", "noop",
"dd_to_dms_str", "lineno",
"get_system_library_path", "dd_to_dms_str",
"get_last_library_path", "get_system_library_path",
"list_photo_libraries", "get_last_library_path",
"normalize_fs_path", "list_photo_libraries",
"findfiles", "normalize_fs_path",
"normalize_unicode", "findfiles",
"increment_filename_with_count", "normalize_unicode",
"increment_filename", "increment_filename_with_count",
"expand_and_validate_filepath", "increment_filename",
"load_function"] "expand_and_validate_filepath",
"load_function",
]
_DEBUG = False _DEBUG = False
@@ -379,7 +381,9 @@ def normalize_unicode(value):
return None return None
def increment_filename_with_count(filepath: Union[str,pathlib.Path], count: int = 0) -> str: def increment_filename_with_count(
filepath: Union[str, pathlib.Path], count: int = 0
) -> str:
"""Return filename (1).ext, etc if filename.ext exists """Return filename (1).ext, etc if filename.ext exists
If file exists in filename's parent folder with same stem as filename, If file exists in filename's parent folder with same stem as filename,