Implemented --query-function, #430

This commit is contained in:
Rhet Turnbull
2021-06-20 17:26:07 -07:00
parent be363b9727
commit 07da8031c6
6 changed files with 109 additions and 6 deletions

View File

@@ -1,6 +1,5 @@
""" example function for osxphotos --query-function """ """ example function for osxphotos --query-function """
from typing import List from typing import List
from osxphotos import PhotoInfo from osxphotos import PhotoInfo
@@ -13,9 +12,20 @@ def best_selfies(photos: List[PhotoInfo]) -> List[PhotoInfo]:
# get list of selfies sorted by date # get list of selfies sorted by date
photos = sorted([p for p in photos if p.selfie], key=lambda p: p.date) photos = sorted([p for p in photos if p.selfie], key=lambda p: p.date)
if not photos:
return []
start_year = photos[0].date.year start_year = photos[0].date.year
stop_year = photos[-1].date.year stop_year = photos[-1].date.year
print(start_year, stop_year) best_selfies = []
for year in range(start_year, stop_year + 1):
# find best selfie each year as determined by overall aesthetic score
selfies = sorted(
[p for p in photos if p.date.year == year],
key=lambda p: p.score.overall,
reverse=True,
)
if selfies:
best_selfies.append(selfies[0])
return photos return best_selfies

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.42.44" __version__ = "0.42.45"

View File

@@ -543,6 +543,18 @@ def QUERY_OPTIONS(f):
"CRITERIA must be a valid python expression. " "CRITERIA must be a valid python expression. "
"See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.", "See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
), ),
o(
"--query-function",
metavar="filename.py::function",
multiple=True,
type=FunctionCall(),
help="Run function to filter photos. Use this in format: --query-function filename.py::function where filename.py is a python "
+ "file you've created and function is the name of the function in the python file you want to call. "
+ "Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. "
+ "You may use more than one function by repeating the --query-function option with a different value. "
+ "Your query function will be called after all other query options have been evaluated. "
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.",
),
] ]
for o in options[::-1]: for o in options[::-1]:
f = o(f) f = o(f)
@@ -1141,6 +1153,7 @@ def export(
max_size, max_size,
regex, regex,
query_eval, query_eval,
query_function,
duplicate, duplicate,
post_command, post_command,
post_function, post_function,
@@ -1300,6 +1313,7 @@ def export(
max_size = cfg.max_size max_size = cfg.max_size
regex = cfg.regex regex = cfg.regex
query_eval = cfg.query_eval query_eval = cfg.query_eval
query_function = cfg.query_function
duplicate = cfg.duplicate duplicate = cfg.duplicate
post_command = cfg.post_command post_command = cfg.post_command
post_function = cfg.post_function post_function = cfg.post_function
@@ -1611,6 +1625,7 @@ def export(
max_size=max_size, max_size=max_size,
regex=regex, regex=regex,
query_eval=query_eval, query_eval=query_eval,
function=query_function,
duplicate=duplicate, duplicate=duplicate,
) )
@@ -2013,6 +2028,7 @@ def query(
max_size, max_size,
regex, regex,
query_eval, query_eval,
query_function,
add_to_album, add_to_album,
): ):
"""Query the Photos database using 1 or more search options; """Query the Photos database using 1 or more search options;
@@ -2041,6 +2057,7 @@ def query(
label, label,
is_reference, is_reference,
query_eval, query_eval,
query_function,
min_size, min_size,
max_size, max_size,
regex, regex,
@@ -2172,6 +2189,7 @@ def query(
min_size=min_size, min_size=min_size,
max_size=max_size, max_size=max_size,
query_eval=query_eval, query_eval=query_eval,
function=query_function,
regex=regex, regex=regex,
duplicate=duplicate, duplicate=duplicate,
) )

View File

@@ -3259,6 +3259,10 @@ class PhotosDB:
elif options.no_location: elif options.no_location:
photos = [p for p in photos if p.location == (None, None)] photos = [p for p in photos if p.location == (None, None)]
if options.function:
for function in options.function:
photos = function[0](photos)
return photos return photos
def _duplicate_signature(self, uuid): def _duplicate_signature(self, uuid):

View File

@@ -1,8 +1,9 @@
""" QueryOptions class for PhotosDB.query """ """ QueryOptions class for PhotosDB.query """
from dataclasses import dataclass, asdict
from typing import Optional, Iterable, Tuple
import datetime import datetime
from dataclasses import asdict, dataclass
from typing import Iterable, List, Optional, Tuple
import bitmath import bitmath
@@ -81,6 +82,7 @@ class QueryOptions:
duplicate: Optional[bool] = None duplicate: Optional[bool] = None
location: Optional[bool] = None location: Optional[bool] = None
no_location: Optional[bool] = None no_location: Optional[bool] = None
function: Optional[List[Tuple[callable, str]]] = None
def asdict(self): def asdict(self):
return asdict(self) return asdict(self)

View File

@@ -6530,6 +6530,39 @@ def test_query_regex_4():
assert len(json_got) == 2 assert len(json_got) == 2
def test_query_function():
"""test query --query-function"""
import json
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
with open("query1.py", "w") as f:
f.writelines(
[
"def query(photos):\n",
" return [p for p in photos if 'DSC03584' in p.original_filename]",
]
)
tmpdir = os.getcwd()
result = runner.invoke(
query,
[
os.path.join(cwd, PHOTOS_DB_15_7),
"--query-function",
f"{tmpdir}/query1.py::query",
"--json",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
assert json_got[0]["original_filename"] == "DSC03584.dng"
def test_export_export_dir_template(): def test_export_export_dir_template():
"""Test {export_dir} template""" """Test {export_dir} template"""
import json import json
@@ -6830,3 +6863,39 @@ def test_export_directory_template_function():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert pathlib.Path(f"foo/bar/{CLI_EXPORT_UUID_FILENAME}").is_file() assert pathlib.Path(f"foo/bar/{CLI_EXPORT_UUID_FILENAME}").is_file()
def test_export_query_function():
"""Test --query-function"""
import os.path
from osxphotos.cli import cli
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
with open("query2.py", "w") as f:
f.writelines(
[
"def query(photos):\n",
" return [p for p in photos if p.title and 'Tulips' in p.title]\n",
]
)
tempdir = os.getcwd()
result = runner.invoke(
cli,
[
"export",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"--query-function",
f"{tempdir}/query2.py::query",
"-V",
"--skip-edited",
],
)
assert result.exit_code == 0
assert "exported: 1" in result.output