diff --git a/examples/query_function.py b/examples/query_function.py index 61aa76a4..075c2be0 100644 --- a/examples/query_function.py +++ b/examples/query_function.py @@ -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 \ No newline at end of file + return best_selfies diff --git a/osxphotos/_version.py b/osxphotos/_version.py index b4a9a0c2..789e9078 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.42.44" +__version__ = "0.42.45" diff --git a/osxphotos/cli.py b/osxphotos/cli.py index 2b0859f2..d915cd0c 100644 --- a/osxphotos/cli.py +++ b/osxphotos/cli.py @@ -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, ) diff --git a/osxphotos/photosdb/photosdb.py b/osxphotos/photosdb/photosdb.py index 28c94bff..f634a3d0 100644 --- a/osxphotos/photosdb/photosdb.py +++ b/osxphotos/photosdb/photosdb.py @@ -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): diff --git a/osxphotos/queryoptions.py b/osxphotos/queryoptions.py index 3171d7ac..c5e85641 100644 --- a/osxphotos/queryoptions.py +++ b/osxphotos/queryoptions.py @@ -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) diff --git a/tests/test_cli.py b/tests/test_cli.py index c787e42f..0698d6d8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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