From d4f9906d61259769af56385bb5940dfcd49b089d Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sat, 29 Feb 2020 15:18:27 -0800 Subject: [PATCH] Added --json to CLI --- README.md | 12 +++-- osxmetadata/__init__.py | 14 +++--- osxmetadata/__main__.py | 103 +++++++++++++++++++++++----------------- osxmetadata/_version.py | 2 +- tests/test_cli.py | 56 ++++++++++++++++++++++ 5 files changed, 132 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 5e0d203..cf4a78f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Options: -v, --version Show the version and exit. -w, --walk Walk directory tree, processing each file in the tree + -j, --json Print output in JSON format, for use with --list + and --get. --set ATTRIBUTE VALUE Set ATTRIBUTE to VALUE --list List all metadata attributes for FILE --clear ATTRIBUTE Remove attribute from FILE @@ -65,7 +67,7 @@ set keywords to ['foo', 'bar'] Short Name Description authors kMDItemAuthors, com.apple.metadata:kMDItemAuthors; The - author, or authors, of the contents of the file. An array of + author, or authors, of the contents of the file. A list of strings. comment kMDItemComment, com.apple.metadata:kMDItemComment; A comment related to the file. This differs from the Finder comment, @@ -94,15 +96,15 @@ keywords kMDItemKeywords, com.apple.metadata:kMDItemKeywords; Keywords associated with this file. For example, “Birthday”, “Important”, etc. This differs from Finder tags (_kMDItemUserTags) which are keywords/tags shown in the - Finder and searchable in Spotlight using "tag:tag_name"An - array of strings. + Finder and searchable in Spotlight using "tag:tag_name"A + list of strings. tags _kMDItemUserTags, com.apple.metadata:_kMDItemUserTags; Finder tags; searchable in Spotlight using "tag:tag_name". If you want tags/keywords visible in the Finder, use this - instead of kMDItemKeywords. + instead of kMDItemKeywords. A list of strings. wherefroms kMDItemWhereFroms, com.apple.metadata:kMDItemWhereFroms; Describes where the file was obtained from (e.g. URL - downloaded from). An array of strings. + downloaded from). A list of strings. ``` diff --git a/osxmetadata/__init__.py b/osxmetadata/__init__.py index 0453efe..ef7500c 100644 --- a/osxmetadata/__init__.py +++ b/osxmetadata/__init__.py @@ -17,7 +17,7 @@ import xattr from .attributes import ATTRIBUTES, Attribute -from .classes import _AttributeList, _AttributeTagsList +from .classes import _AttributeList, _AttributeTagsList from .constants import ( # _DOWNLOAD_DATE,; _FINDER_COMMENT,; _TAGS,; _WHERE_FROM, _COLORIDS, _COLORNAMES, @@ -46,9 +46,11 @@ clear_finder_comment, validate_attribute_value, ) +from ._version import __version__ __all__ = [ "OSXMetaData", + "__version__", "ATTRIBUTES", "kMDItemAuthors", "kMDItemComment", @@ -127,7 +129,8 @@ def get_attribute(self, attribute_name): # user tags need special processing to normalize names if attribute.name == "tags": - return self.tags + self.tags._load_data() + return self.tags.data try: plist = plistlib.loads(self._attrs[attribute.constant]) @@ -167,7 +170,7 @@ def set_attribute(self, attribute_name, value): # verify type is correct value = validate_attribute_value(attribute, value) - + # if attribute.list and (type(value) == list or type(value) == set): # for val in value: # if attribute.type_ != type(val): @@ -219,7 +222,7 @@ def append_attribute(self, attribute_name, value, update=False): # start with existing values new_value = self.get_attribute(attribute.name) - value = validate_attribute_value(attribute,value) + value = validate_attribute_value(attribute, value) if attribute.list: if new_value is not None: @@ -240,8 +243,7 @@ def append_attribute(self, attribute_name, value, update=False): if new_value is not None: new_value += value else: - new_value = value - + new_value = value # # verify type is correct # if attribute.list and (type(value) == list or type(value) == set): diff --git a/osxmetadata/__main__.py b/osxmetadata/__main__.py index ce23c84..1a8284a 100644 --- a/osxmetadata/__main__.py +++ b/osxmetadata/__main__.py @@ -14,6 +14,7 @@ from ._version import __version__ from .attributes import _LONG_NAME_WIDTH, _SHORT_NAME_WIDTH, ATTRIBUTES +from .classes import _AttributeList, _AttributeTagsList from .constants import _TAGS_NAMES from .utils import validate_attribute_value @@ -106,9 +107,8 @@ def get_help(self, ctx): "-j", "json_", is_flag=True, - help="Print output in JSON format, for use with --list.", + help="Print output in JSON format, for use with --list and --get.", default=False, - hidden=True, # not yet implemented ) DEBUG_OPTION = click.option( "--debug", required=False, is_flag=True, default=False, hidden=True @@ -218,7 +218,14 @@ def cli( if invalid_attr: click.echo("") # add a new line before rest of help text click.echo(ctx.get_help()) - ctx.exit() + ctx.exit(2) + + # check that json_ only used with get or list_ + if json_ and not any([get, list_]): + click.echo("--json can only be used with --get or --list", err=True) + click.echo("") # add a new line before rest of help text + click.echo(ctx.get_help()) + ctx.exit(2) for f in files: if walk and os.path.isdir(f): @@ -319,59 +326,69 @@ def process_file(fpath, json_, set_, append, update, remove, clear, get, list_): if get: logging.debug(f"get: {get}") + if json_: + data = {} + data["_version"] = __version__ + data["_filepath"] = str(fpath) + data["_filename"] = fpath.name for attr in get: attribute = ATTRIBUTES[attr] logging.debug(f"getting {attr}") - value = md.get_attribute(attribute.name) - click.echo( - f"{attribute.name:{_SHORT_NAME_WIDTH}}{attribute.constant:{_LONG_NAME_WIDTH}} = {value}" - ) + if json_: + if attribute.type_ == datetime.datetime: + # need to convert datetime.datetime to string to serialize + value = md.get_attribute(attribute.name) + if type(value) == list: + value = [v.isoformat() for v in value] + else: + value = value.isoformat() + data[attribute.constant] = value + else: + # get raw value + data[attribute.constant] = md.get_attribute(attribute.name) + else: + value = md.get_attribute_str(attribute.name) + click.echo( + f"{attribute.name:{_SHORT_NAME_WIDTH}}{attribute.constant:{_LONG_NAME_WIDTH}} = {value}" + ) + if json_: + json_str = json.dumps(data) + click.echo(json_str) if list_: attribute_list = md.list_metadata() + if json_: + data = {} + data["_version"] = __version__ + data["_filepath"] = str(fpath) + data["_filename"] = fpath.name for attr in attribute_list: try: attribute = ATTRIBUTES[attr] - value = md.get_attribute_str(attribute.name) - click.echo( - f"{attribute.name:{_SHORT_NAME_WIDTH}}{attribute.constant:{_LONG_NAME_WIDTH}} = {value}" - ) + if json_: + if attribute.type_ == datetime.datetime: + # need to convert datetime.datetime to string to serialize + value = md.get_attribute(attribute.name) + if type(value) == list: + value = [v.isoformat() for v in value] + else: + value = value.isoformat() + data[attribute.constant] = value + else: + # get raw value + data[attribute.constant] = md.get_attribute(attribute.name) + else: + value = md.get_attribute_str(attribute.name) + click.echo( + f"{attribute.name:{_SHORT_NAME_WIDTH}}{attribute.constant:{_LONG_NAME_WIDTH}} = {value}" + ) except KeyError: click.echo( f"{'UNKNOWN':{_SHORT_NAME_WIDTH}}{attr:{_LONG_NAME_WIDTH}} = THIS ATTRIBUTE NOT HANDLED" ) - - -# def write_json_data(fp, data): -# json.dump(data, fp) -# fp.write("\n") - - -# def write_text_data(fp, data): -# file = data["file"] - -# fc = data["fc"] -# fc = fc if fc is not None else "" - -# dldate = data["dldate"] -# dldate = dldate if dldate is not None else "" - -# desc = data["description"] -# desc = desc if desc is not None else "" - -# where_from = data["where_from"] -# where_from = where_from if where_from is not None else "" - -# tags = data["tags"] -# tags = tags if len(tags) != 0 else "" - -# print(f"file: {file}", file=fp) -# print(f"description: {desc}", file=fp) -# print(f"tags: {tags}", file=fp) -# print(f"Finder comment: {fc}", file=fp) -# print(f"Download date: {dldate}", file=fp) -# print(f"Where from: {where_from}", file=fp) -# print("\n", file=fp) + if json_: + json_str = json.dumps(data) + click.echo(json_str) # def restore_from_json(json_file, quiet=False): diff --git a/osxmetadata/_version.py b/osxmetadata/_version.py index a125e30..b40ddac 100644 --- a/osxmetadata/_version.py +++ b/osxmetadata/_version.py @@ -1 +1 @@ -__version__ = "0.98.4" +__version__ = "0.98.5" diff --git a/tests/test_cli.py b/tests/test_cli.py index b14044a..2e6eed0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -206,6 +206,62 @@ def test_datetime_list_attributes(temp_file, attribute): assert meta.get_attribute(attribute) == [dt] +def test_get_json(temp_file): + import json + import pathlib + from osxmetadata import OSXMetaData, ATTRIBUTES, __version__ + from osxmetadata.__main__ import cli + + runner = CliRunner() + result = runner.invoke( + cli, ["--set", "tags", "foo", "--set", "tags", "bar", temp_file] + ) + result = runner.invoke(cli, ["--get", "tags", "--json", temp_file]) + assert result.exit_code == 0 + json_ = json.loads(result.stdout) + assert json_["com.apple.metadata:_kMDItemUserTags"] == ["foo", "bar"] + assert json_["_version"] == __version__ + assert json_["_filename"] == pathlib.Path(temp_file).name + + +def test_list_json(temp_file): + import json + import pathlib + from osxmetadata import OSXMetaData, ATTRIBUTES, __version__ + from osxmetadata.__main__ import cli + + runner = CliRunner() + result = runner.invoke( + cli, ["--set", "tags", "foo", "--set", "tags", "bar", temp_file] + ) + result = runner.invoke(cli, ["--list", "--json", temp_file]) + assert result.exit_code == 0 + json_ = json.loads(result.stdout) + assert json_["com.apple.metadata:_kMDItemUserTags"] == ["foo", "bar"] + assert json_["_version"] == __version__ + assert json_["_filename"] == pathlib.Path(temp_file).name + + +def test_cli_error_json(temp_file): + from osxmetadata import OSXMetaData, ATTRIBUTES + from osxmetadata.__main__ import cli + + runner = CliRunner() + result = runner.invoke(cli, ["--set", "tags", "foo", "--json", temp_file]) + assert result.exit_code == 2 + assert "--json can only be used with --get or --list" in result.stdout + + +def test_cli_error_bad_attribute(temp_file): + from osxmetadata import OSXMetaData, ATTRIBUTES + from osxmetadata.__main__ import cli + + runner = CliRunner() + result = runner.invoke(cli, ["--set", "foo", "bar", temp_file]) + assert result.exit_code == 2 + assert "Invalid attribute foo" in result.stdout + + def test_cli_error(temp_file): from osxmetadata.__main__ import cli