Implemented --query-function, #430
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.44"
|
__version__ = "0.42.45"
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user