Feature add query command (#970)

* Added query_command and example

* Refactored QUERY_OPTIONS, added query_command, refactored verbose, #930, #931

* Added query options to debug-dump, #966

* Refactored query, #602

* Added precedence test for --load-config

* Refactored handling of query options

* Refactored export_photo

* Removed extraneous print

* Updated API_README

* Updated examples
This commit is contained in:
Rhet Turnbull
2023-02-05 14:48:42 -08:00
committed by GitHub
parent 0f1866e39d
commit 007f0e0960
42 changed files with 2322 additions and 1334 deletions

View File

@@ -3616,7 +3616,7 @@ def test_export_sidecar_invalid():
],
)
assert result.exit_code != 0
assert "Cannot use --sidecar json with --sidecar exiftool" in result.output
assert "cannot use --sidecar json with --sidecar exiftool" in result.output
def test_export_live():
@@ -4242,7 +4242,9 @@ def test_places():
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(places, [os.path.join(cwd, PLACES_PHOTOS_DB), "--json"])
result = runner.invoke(
places, ["--db", os.path.join(cwd, PLACES_PHOTOS_DB), "--json"]
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert json_got == json.loads(CLI_PLACES_JSON)
@@ -4257,7 +4259,13 @@ def test_place_13():
with runner.isolated_filesystem():
result = runner.invoke(
query,
[os.path.join(cwd, PLACES_PHOTOS_DB_13), "--json", "--place", "Adelaide"],
[
"--db",
os.path.join(cwd, PLACES_PHOTOS_DB_13),
"--json",
"--place",
"Adelaide",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
@@ -4274,7 +4282,8 @@ def test_no_place_13():
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
query, [os.path.join(cwd, PLACES_PHOTOS_DB_13), "--json", "--no-place"]
query,
["--db", os.path.join(cwd, PLACES_PHOTOS_DB_13), "--json", "--no-place"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
@@ -4292,7 +4301,13 @@ def test_place_15_1():
with runner.isolated_filesystem():
result = runner.invoke(
query,
[os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--place", "Washington"],
[
"--db",
os.path.join(cwd, PLACES_PHOTOS_DB),
"--json",
"--place",
"Washington",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
@@ -4310,7 +4325,13 @@ def test_place_15_2():
with runner.isolated_filesystem():
result = runner.invoke(
query,
[os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--place", "United States"],
[
"--db",
os.path.join(cwd, PLACES_PHOTOS_DB),
"--json",
"--place",
"United States",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
@@ -4329,7 +4350,7 @@ def test_no_place_15():
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
query, [os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--no-place"]
query, ["--db", os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--no-place"]
)
assert result.exit_code == 0
json_got = json.loads(result.output)
@@ -4346,7 +4367,14 @@ def test_no_folder_1_15():
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
query, [os.path.join(cwd, PHOTOS_DB_15_7), "--json", "--folder", "Folder1"]
query,
[
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--json",
"--folder",
"Folder1",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
@@ -4381,6 +4409,7 @@ def test_no_folder_2_15():
result = runner.invoke(
query,
[
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--json",
"--folder",
@@ -4408,7 +4437,14 @@ def test_no_folder_1_14():
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
query, [os.path.join(cwd, PHOTOS_DB_14_6), "--json", "--folder", "Folder1"]
query,
[
"--db",
os.path.join(cwd, PHOTOS_DB_14_6),
"--json",
"--folder",
"Folder1",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
@@ -5727,19 +5763,6 @@ def test_keywords():
assert json_got == KEYWORDS_JSON
# TODO: this fails with result.exit_code == 1 but I think this has to
# do with how pytest is invoking the command
# def test_albums_str():
# """Test osxphotos albums string output """
# runner = CliRunner()
# cwd = os.getcwd()
# result = runner.invoke(albums, ["--db", os.path.join(cwd, PHOTOS_DB_15_7), ])
# assert result.exit_code == 0
# assert result.output == ALBUMS_STR
def test_albums_json():
"""Test osxphotos albums json output"""
@@ -6648,6 +6671,57 @@ def test_config_only():
assert "config.toml" in files
def test_config_command_line_precedence():
"""Test that command line options take precedence over config file"""
runner = CliRunner()
cwd = os.getcwd()
with runner.isolated_filesystem():
# create a config file
with open("config.toml", "w") as fd:
fd.write("[export]\n")
fd.write(
"uuid = ["
+ ", ".join(f'"{u}"' for u in UUID_EXPECTED_FROM_FILE)
+ "]\n"
)
result = runner.invoke(
export,
[
"--db",
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--load-config",
"config.toml",
],
)
assert result.exit_code == 0
for uuid in UUID_EXPECTED_FROM_FILE:
assert uuid in result.output
# now run with a command line option that should override the config file
result = runner.invoke(
export,
[
"--db",
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--uuid",
UUID_NOT_FROM_FILE,
"--load-config",
"config.toml",
],
)
assert result.exit_code == 0
assert UUID_NOT_FROM_FILE in result.output
for uuid in UUID_EXPECTED_FROM_FILE:
assert uuid not in result.output
def test_export_exportdb():
"""test --exportdb"""
@@ -6657,7 +6731,14 @@ def test_export_exportdb():
with runner.isolated_filesystem():
result = runner.invoke(
export,
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V", "--exportdb", "export.db"],
[
"--db",
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--exportdb",
"export.db",
],
)
assert result.exit_code == 0
assert re.search(r"Created export database.*export\.db", result.output)
@@ -7921,6 +8002,7 @@ def test_query_function():
result = runner.invoke(
query,
[
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--query-function",
f"{tmpdir}/query1.py::query",
@@ -7942,6 +8024,7 @@ def test_query_added_after():
results = runner.invoke(
query,
[
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--json",
"--added-after",
@@ -7962,6 +8045,7 @@ def test_query_added_before():
results = runner.invoke(
query,
[
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--json",
"--added-before",

View File

@@ -0,0 +1,96 @@
""" Test osxphotos cli commands to verify they run without error.
These tests simply run the commands to verify no errors are thrown.
They do not verify the output of the commands. More complex tests are
in test_cli.py and test_cli__xxx.py for specific commands.
Complex commands such as export are not tested here.
"""
from __future__ import annotations
import os
from typing import Any, Callable
import pytest
from click.testing import CliRunner
TEST_DB = "tests/Test-13.0.0.photoslibrary"
TEST_DB = os.path.join(os.getcwd(), TEST_DB)
TEST_RUN_SCRIPT = "examples/cli_example_1.py"
@pytest.fixture(scope="module")
def runner() -> CliRunner:
return CliRunner()
from osxphotos.cli import (
about,
albums,
debug_dump,
docs_command,
dump,
grep,
help,
info,
keywords,
labels,
list_libraries,
orphans,
persons,
places,
theme,
tutorial,
uuid,
version,
)
def test_about(runner: CliRunner):
with runner.isolated_filesystem():
result = runner.invoke(about)
assert result.exit_code == 0
@pytest.mark.parametrize(
"command",
[
albums,
docs_command,
dump,
help,
info,
keywords,
labels,
list_libraries,
orphans,
persons,
places,
tutorial,
uuid,
version,
],
)
def test_cli_comands(runner: CliRunner, command: Callable[..., Any]):
with runner.isolated_filesystem():
result = runner.invoke(albums, ["--db", TEST_DB])
assert result.exit_code == 0
def test_grep(runner: CliRunner):
with runner.isolated_filesystem():
result = runner.invoke(grep, ["--db", TEST_DB, "test"])
assert result.exit_code == 0
def test_debug_dump(runner: CliRunner):
with runner.isolated_filesystem():
result = runner.invoke(debug_dump, ["--db", TEST_DB, "--dump", "persons"])
assert result.exit_code == 0
def test_theme(runner: CliRunner):
with runner.isolated_filesystem():
result = runner.invoke(theme, ["--list"])
assert result.exit_code == 0

View File

@@ -8,7 +8,7 @@ from typing import Any
import pytest
import osxphotos.sqlitekvstore
from osxphotos.sqlitekvstore import SQLiteKVStore
def pickle_and_zip(data: Any) -> bytes:
@@ -41,7 +41,7 @@ def unzip_and_unpickle(data: bytes) -> Any:
def test_basic_get_set(tmpdir):
"""Test basic functionality"""
dbpath = tmpdir / "kvtest.db"
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
kvstore = SQLiteKVStore(dbpath)
kvstore.set("foo", "bar")
assert kvstore.get("foo") == "bar"
assert kvstore.get("FOOBAR") is None
@@ -61,7 +61,7 @@ def test_basic_get_set(tmpdir):
def test_basic_get_set_wal(tmpdir):
"""Test basic functionality with WAL mode"""
dbpath = tmpdir / "kvtest.db"
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath, wal=True)
kvstore = SQLiteKVStore(dbpath, wal=True)
kvstore.set("foo", "bar")
assert kvstore.get("foo") == "bar"
assert kvstore.get("FOOBAR") is None
@@ -84,14 +84,14 @@ def test_set_many(tmpdir):
"""Test set_many()"""
dbpath = tmpdir / "kvtest.db"
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
kvstore = SQLiteKVStore(dbpath)
kvstore.set_many([("foo", "bar"), ("baz", "qux")])
assert kvstore.get("foo") == "bar"
assert kvstore.get("baz") == "qux"
kvstore.close()
# make sure values got committed
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
kvstore = SQLiteKVStore(dbpath)
assert kvstore.get("foo") == "bar"
assert kvstore.get("baz") == "qux"
kvstore.close()
@@ -101,14 +101,14 @@ def test_set_many_dict(tmpdir):
"""Test set_many() with dict of values"""
dbpath = tmpdir / "kvtest.db"
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
kvstore = SQLiteKVStore(dbpath)
kvstore.set_many({"foo": "bar", "baz": "qux"})
assert kvstore.get("foo") == "bar"
assert kvstore.get("baz") == "qux"
kvstore.close()
# make sure values got committed
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
kvstore = SQLiteKVStore(dbpath)
assert kvstore.get("foo") == "bar"
assert kvstore.get("baz") == "qux"
kvstore.close()
@@ -118,7 +118,7 @@ def test_basic_context_handler(tmpdir):
"""Test basic functionality with context handler"""
dbpath = tmpdir / "kvtest.db"
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
with SQLiteKVStore(dbpath) as kvstore:
kvstore.set("foo", "bar")
assert kvstore.get("foo") == "bar"
assert kvstore.get("FOOBAR") is None
@@ -134,7 +134,7 @@ def test_basic_context_handler(tmpdir):
def test_about(tmpdir):
"""Test about property"""
dbpath = tmpdir / "kvtest.db"
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
with SQLiteKVStore(dbpath) as kvstore:
kvstore.about = "My description"
assert kvstore.about == "My description"
kvstore.about = "My new description"
@@ -144,17 +144,17 @@ def test_about(tmpdir):
def test_existing_db(tmpdir):
"""Test that opening an existing database works as expected"""
dbpath = tmpdir / "kvtest.db"
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
with SQLiteKVStore(dbpath) as kvstore:
kvstore.set("foo", "bar")
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
with SQLiteKVStore(dbpath) as kvstore:
assert kvstore.get("foo") == "bar"
def test_dict_interface(tmpdir):
""" "Test dict interface"""
dbpath = tmpdir / "kvtest.db"
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
with SQLiteKVStore(dbpath) as kvstore:
kvstore["foo"] = "bar"
assert kvstore["foo"] == "bar"
assert len(kvstore) == 1
@@ -186,9 +186,7 @@ def test_dict_interface(tmpdir):
def test_serialize_deserialize(tmpdir):
"""Test serialize/deserialize"""
dbpath = tmpdir / "kvtest.db"
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(
dbpath, serialize=json.dumps, deserialize=json.loads
)
kvstore = SQLiteKVStore(dbpath, serialize=json.dumps, deserialize=json.loads)
kvstore.set("foo", {"bar": "baz"})
assert kvstore.get("foo") == {"bar": "baz"}
assert kvstore.get("FOOBAR") is None
@@ -197,7 +195,7 @@ def test_serialize_deserialize(tmpdir):
def test_serialize_deserialize_binary_data(tmpdir):
"""Test serialize/deserialize with binary data"""
dbpath = tmpdir / "kvtest.db"
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(
kvstore = SQLiteKVStore(
dbpath, serialize=pickle_and_zip, deserialize=unzip_and_unpickle
)
kvstore.set("foo", {"bar": "baz"})
@@ -209,16 +207,16 @@ def test_serialize_deserialize_bad_callable(tmpdir):
"""Test serialize/deserialize with bad values"""
dbpath = tmpdir / "kvtest.db"
with pytest.raises(TypeError):
osxphotos.sqlitekvstore.SQLiteKVStore(dbpath, serialize=1, deserialize=None)
SQLiteKVStore(dbpath, serialize=1, deserialize=None)
with pytest.raises(TypeError):
osxphotos.sqlitekvstore.SQLiteKVStore(dbpath, serialize=None, deserialize=1)
SQLiteKVStore(dbpath, serialize=None, deserialize=1)
def test_iter(tmpdir):
"""Test generator behavior"""
dbpath = tmpdir / "kvtest.db"
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
kvstore = SQLiteKVStore(dbpath)
kvstore.set("foo", "bar")
kvstore.set("baz", "qux")
kvstore.set("quux", "corge")
@@ -230,7 +228,7 @@ def test_iter(tmpdir):
def test_keys_values_items(tmpdir):
"""Test keys, values, items"""
dbpath = tmpdir / "kvtest.db"
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
kvstore = SQLiteKVStore(dbpath)
kvstore.set("foo", "bar")
kvstore.set("baz", "qux")
kvstore.set("quux", "corge")
@@ -243,3 +241,26 @@ def test_keys_values_items(tmpdir):
("grault", "garply"),
("quux", "corge"),
]
def test_path(tmpdir):
"""Test path property"""
dbpath = tmpdir / "kvtest.db"
kvstore = SQLiteKVStore(dbpath)
assert kvstore.path == dbpath
def test_wipe(tmpdir):
"""Test wipe"""
dbpath = tmpdir / "kvtest.db"
kvstore = SQLiteKVStore(dbpath)
kvstore.set("foo", "bar")
kvstore.set("baz", "qux")
kvstore.set("quux", "corge")
kvstore.set("grault", "garply")
assert len(kvstore) == 4
kvstore.wipe()
assert len(kvstore) == 0
assert "foo"
kvstore.set("foo", "bar")
assert kvstore.get("foo") == "bar"