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 """
from typing import List
from osxphotos import PhotoInfo
@@ -13,9 +12,20 @@ def best_selfies(photos: List[PhotoInfo]) -> List[PhotoInfo]:
# get list of selfies sorted by 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
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__ = "0.42.44"
__version__ = "0.42.45"

View File

@@ -543,6 +543,18 @@ def QUERY_OPTIONS(f):
"CRITERIA must be a valid python expression. "
"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]:
f = o(f)
@@ -1141,6 +1153,7 @@ def export(
max_size,
regex,
query_eval,
query_function,
duplicate,
post_command,
post_function,
@@ -1300,6 +1313,7 @@ def export(
max_size = cfg.max_size
regex = cfg.regex
query_eval = cfg.query_eval
query_function = cfg.query_function
duplicate = cfg.duplicate
post_command = cfg.post_command
post_function = cfg.post_function
@@ -1611,6 +1625,7 @@ def export(
max_size=max_size,
regex=regex,
query_eval=query_eval,
function=query_function,
duplicate=duplicate,
)
@@ -2013,6 +2028,7 @@ def query(
max_size,
regex,
query_eval,
query_function,
add_to_album,
):
"""Query the Photos database using 1 or more search options;
@@ -2041,6 +2057,7 @@ def query(
label,
is_reference,
query_eval,
query_function,
min_size,
max_size,
regex,
@@ -2172,6 +2189,7 @@ def query(
min_size=min_size,
max_size=max_size,
query_eval=query_eval,
function=query_function,
regex=regex,
duplicate=duplicate,
)

View File

@@ -3259,6 +3259,10 @@ class PhotosDB:
elif options.no_location:
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
def _duplicate_signature(self, uuid):

View File

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

View File

@@ -6530,6 +6530,39 @@ def test_query_regex_4():
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():
"""Test {export_dir} template"""
import json
@@ -6830,3 +6863,39 @@ def test_export_directory_template_function():
)
assert result.exit_code == 0
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