-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
1,000 additions
and
792 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
from collections import namedtuple | ||
import datetime | ||
|
||
from .constants import * | ||
from .classes import _AttributeList, _AttributeTagsSet | ||
|
||
# Information about metadata attributes that can be set | ||
# Each attribute type needs an Attribute namedtuple entry in ATTRIBUTES dict | ||
# To add new entries, create an Attribute namedtuple and create an entry in | ||
# ATTRIBUTES dict where key is short name for the attribute | ||
# Fields in the Attribute namedtuple are: | ||
# name: short name of the attribute -- will also be used as attribute/property | ||
# in the OSXMetaData class | ||
# constant: the name of the constant for the attribute | ||
# (e.g. com.apple.metadata:kMDItemCreator) | ||
# See https://developer.apple.com/documentation/coreservices/file_metadata/mditem?language=objc | ||
# for reference on common metadata attributes | ||
# type_: expected type for the attribute, e.g. if Apple says it's a CFString, it'll be python str | ||
# CFNumber = python float, etc. | ||
# (called type_ so pylint doesn't complain about misplaced type identifier) | ||
# list: (boolean) True if attribute is a list (e.g. a CFArray) | ||
# as_list: (boolean) True if attribute is a single value but stored in a list | ||
# Note: the only attribute I've seen this on is com.apple.metadata:kMDItemDownloadedDate | ||
# class: the attribute class to use, e.g. _AttributeList or str | ||
# Note: also add short name to __slots__ in OSXMetaData so pylint doesn't protest | ||
|
||
|
||
Attribute = namedtuple( | ||
"Attribute", ["name", "constant", "type_", "list", "as_list", "class_"] | ||
) | ||
|
||
ATTRIBUTES = { | ||
"authors": Attribute("authors", kMDItemAuthors, str, True, False, _AttributeList), | ||
"creator": Attribute("creator", kMDItemCreator, str, False, False, str), | ||
"description": Attribute("description", kMDItemDescription, str, False, False, str), | ||
"downloadeddate": Attribute( | ||
"downloadeddate", | ||
kMDItemDownloadedDate, | ||
datetime.datetime, | ||
# False, | ||
True, | ||
# True, | ||
False, | ||
_AttributeList, | ||
), | ||
"findercomment": Attribute( | ||
"findercomment", kMDItemFinderComment, str, False, False, str | ||
), | ||
"headline": Attribute("headline", kMDItemHeadline, str, False, False, str), | ||
"keywords": Attribute( | ||
"keywords", kMDItemKeywords, str, True, False, _AttributeList | ||
), | ||
"tags": Attribute("tags", _kMDItemUserTags, str, True, False, _AttributeTagsSet), | ||
"wherefroms": Attribute( | ||
"wherefroms", kMDItemWhereFroms, str, True, False, _AttributeList | ||
), | ||
"test": Attribute( | ||
"test", | ||
"com.osxmetadata.test:DontTryThisAtHomeKids", | ||
datetime.datetime, | ||
True, | ||
False, | ||
_AttributeList, | ||
), | ||
"test_float": Attribute( | ||
"test_float", | ||
"com.osxmetadata.test:DontTryThisAtHomeKids", | ||
float, | ||
False, | ||
False, | ||
float, | ||
), | ||
"test_str": Attribute( | ||
"test_str", "com.osxmetadata.test:String", str, False, False, str | ||
), | ||
} | ||
|
||
# used for formatting output of --list | ||
_SHORT_NAME_WIDTH = max([len(x) for x in ATTRIBUTES.keys()]) + 5 | ||
_LONG_NAME_WIDTH = max([len(x.constant) for x in ATTRIBUTES.values()]) + 10 | ||
_CONSTANT_WIDTH = 21 + 5 # currently is kMDItemDownloadedDate | ||
|
||
# also add entries for attributes by constant and short constant | ||
# do this after computing widths above | ||
_temp_attributes = {} | ||
for attribute in ATTRIBUTES.values(): | ||
if attribute.constant not in ATTRIBUTES: | ||
_temp_attributes[attribute.constant] = attribute | ||
constant_name = attribute.constant.split(":", 2)[1] | ||
_temp_attributes[constant_name] = attribute | ||
else: | ||
raise ValueError(f"Duplicate attribute in ATTRIBUTES: {attribute}") | ||
if _temp_attributes: | ||
ATTRIBUTES.update(_temp_attributes) | ||
|
||
# list of all attributes for help text | ||
ATTRIBUTES_LIST = [ | ||
f"{'Short Name':{_SHORT_NAME_WIDTH}} {'Constant':{_CONSTANT_WIDTH}} Long Name" | ||
] | ||
ATTRIBUTES_LIST.extend( | ||
[ | ||
f"{a.name:{_SHORT_NAME_WIDTH}} " | ||
f"{a.constant.split(':',2)[1]:{_CONSTANT_WIDTH}} " | ||
f"{a.constant}" | ||
for a in [ | ||
ATTRIBUTES[a] | ||
for a in set([attribute.name for attribute in ATTRIBUTES.values()]) | ||
] | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
""" Classes for metadata attribute types """ | ||
|
||
import collections.abc | ||
import datetime | ||
import plistlib | ||
from plistlib import FMT_BINARY # pylint: disable=E0611 | ||
|
||
from .constants import _COLORNAMES, _VALID_COLORIDS | ||
|
||
|
||
class _AttributeList(collections.abc.MutableSequence): | ||
""" represents a multi-valued OSXMetaData attribute list """ | ||
|
||
def __init__(self, attribute, xattr_): | ||
self._attribute = attribute | ||
self._attrs = xattr_ | ||
self._constant = attribute.constant | ||
|
||
self.data = [] | ||
self._values = [] | ||
self._load_data() | ||
|
||
def set_value(self, value): | ||
self.data = value | ||
self._write_data() | ||
|
||
def _load_data(self): | ||
self._values = [] | ||
try: | ||
# self._tagvalues = self._attrs[self._constant] | ||
# load the binary plist value | ||
self._values = plistlib.loads(self._attrs[self._constant]) | ||
if self._values: | ||
try: | ||
self.data = list(self._values) | ||
except TypeError: | ||
self.data = set([self._values]) | ||
else: | ||
self.data = [] | ||
# for x in self._tagvalues: | ||
# (tag, color) = self._tag_split(x) | ||
# self._tags[tag] = color | ||
# # self._tags = [self._tag_strip_color(x) for x in self._tagvalues] | ||
except KeyError: | ||
self.data = [] | ||
|
||
def __delitem__(self, index): | ||
self.data.__delitem__(index) | ||
self._write_data() | ||
|
||
def __getitem__(self, index): | ||
self._load_data() | ||
return self.data.__getitem__(index) | ||
|
||
def __len__(self): | ||
return self.data.__len__() | ||
|
||
def __setitem__(self, index, value): | ||
self.data.__setitem__(index, value) | ||
self._write_data() | ||
self._load_data() | ||
|
||
def insert(self, index, value): | ||
self.data.insert(index, value) | ||
self._write_data() | ||
|
||
def _write_data(self): | ||
# Overwrites the existing attribute values with the iterable of values provided. | ||
plist = plistlib.dumps(self.data, fmt=FMT_BINARY) | ||
self._attrs.set(self._constant, plist) | ||
|
||
def __repr__(self): | ||
# return f"{type(self).__name__}({super().__repr__()})" | ||
return repr(self.data) | ||
|
||
def __eq__(self, other): | ||
return self.data == other | ||
|
||
|
||
class _AttributeSet: | ||
""" represents a multi-valued OSXMetaData attribute set """ | ||
|
||
def __init__(self, attribute, xattr_): | ||
self._attribute = attribute | ||
self._attrs = xattr_ | ||
self._constant = attribute.constant | ||
|
||
self.data = set() | ||
|
||
# # used for __iter__ | ||
# self._list = None | ||
# self._count = None | ||
# self._counter = None | ||
|
||
# initialize | ||
self._load_data() | ||
|
||
def set_value(self, values): | ||
""" set value to values """ | ||
self.data = set(map(self._normalize, values)) | ||
self._write_data() | ||
|
||
def add(self, value): | ||
""" add a value""" | ||
# if not isinstance(tag, list): | ||
# raise TypeError("Tags must be strings") | ||
self._load_data() | ||
values = set(map(self._normalize, self.data)) | ||
self.data.add(self._normalize(value)) | ||
self._write_data() | ||
|
||
def update(self, *values): | ||
""" update data adding any new values in *values """ | ||
if not all(isinstance(value, str) for value in values): | ||
raise TypeError("Value must be string") | ||
self._load_data() | ||
old_values = set(map(self._normalize, self.data)) | ||
new_values = old_values.union(set(map(self._normalize, values))) | ||
self.data = new_values | ||
self._write_data() | ||
|
||
def clear(self): | ||
""" clear attribute (removes all values) """ | ||
try: | ||
self._attrs.remove(self._constant) | ||
except (IOError, OSError): | ||
pass | ||
|
||
def remove(self, value): | ||
""" remove a value, raise exception if value does not exist in data set """ | ||
self._load_data() | ||
if not isinstance(value, str): | ||
raise TypeError("Value must be string") | ||
values = set(map(self._normalize, self.data)) | ||
values.remove(self._normalize(value)) | ||
self.data = values | ||
self._write_data() | ||
|
||
def discard(self, value): | ||
""" remove a value, does not raise exception if value does not exist """ | ||
self._load_data() | ||
if not isinstance(value, str): | ||
raise TypeError("Value must be string") | ||
values = set(map(self._normalize, self.data)) | ||
values.discard(self._normalize(value)) | ||
self.data = values | ||
self._write_data() | ||
|
||
def _load_data(self): | ||
self._values = [] | ||
try: | ||
# self._tagvalues = self._attrs[self._constant] | ||
# load the binary plist value | ||
self._values = plistlib.loads(self._attrs[self._constant]) | ||
if self._values: | ||
try: | ||
self.data = set(self._values) | ||
except TypeError: | ||
self.data = set([self._values]) | ||
else: | ||
self.data = set() | ||
# for x in self._tagvalues: | ||
# (tag, color) = self._tag_split(x) | ||
# self._tags[tag] = color | ||
# # self._tags = [self._tag_strip_color(x) for x in self._tagvalues] | ||
except KeyError: | ||
self.data = set() | ||
|
||
def _write_data(self): | ||
# TODO: change this to write_data and to read from self.data | ||
# Overwrites the existing tags with the iterable of tags provided. | ||
plist = plistlib.dumps(list(map(self._normalize, self.data)), fmt=FMT_BINARY) | ||
self._attrs.set(self._constant, plist) | ||
|
||
def _normalize(self, value): | ||
""" processes a value to normalize/transform the value if needed | ||
override in sublcass if desired (e.g. used _TagsSet) """ | ||
return value | ||
|
||
def __iter__(self): | ||
self._load_data() | ||
for value in self.data: | ||
yield value | ||
|
||
def __len__(self): | ||
self._load_data() | ||
return len(self.data) | ||
|
||
def __repr__(self): | ||
self._load_data() | ||
return repr(self.data) | ||
|
||
def __str__(self): | ||
self._load_data() | ||
if self._attribute.type_ == datetime.datetime: | ||
values = [d.isoformat() for d in self.data] | ||
else: | ||
values = self.data | ||
return str(list(values)) | ||
|
||
def __iadd__(self, value): | ||
for v in value: | ||
self.add(v) | ||
return self | ||
|
||
|
||
class _AttributeTagsSet(_AttributeSet): | ||
""" represents a _kMDItemUserTag attribute set """ | ||
|
||
def _tag_split(self, tag): | ||
# Extracts the color information from a Finder tag. | ||
|
||
parts = tag.rsplit("\n", 1) | ||
if len(parts) == 1: | ||
return parts[0], 0 | ||
elif ( | ||
len(parts[1]) != 1 or parts[1] not in _VALID_COLORIDS | ||
): # Not a color number | ||
return tag, 0 | ||
else: | ||
return parts[0], int(parts[1]) | ||
|
||
def _normalize(self, tag): | ||
""" | ||
Ensures a color is set if not none. | ||
:param tag: a possibly non-normal tag. | ||
:return: A colorized tag. | ||
""" | ||
tag, color = self._tag_split(tag) | ||
if tag.title() in _COLORNAMES: | ||
# ignore the color passed and set proper color name | ||
return self._tag_colored(tag.title(), _COLORNAMES[tag.title()]) | ||
else: | ||
return self._tag_colored(tag, color) | ||
|
||
def _tag_nocolor(self, tag): | ||
""" | ||
Removes the color information from a Finder tag. | ||
""" | ||
return tag.rsplit("\n", 1)[0] | ||
|
||
def _tag_colored(self, tag, color): | ||
""" | ||
Sets the color of a tag. | ||
Parameters: | ||
tag(str): a tag name | ||
color(int): an integer from 1 through 7 | ||
Return: | ||
(str) the tag with encoded color. | ||
""" | ||
return "{}\n{}".format(self._tag_nocolor(tag), color) | ||
|
||
def _load_data(self): | ||
self._tags = {} | ||
try: | ||
self._tagvalues = self._attrs[self._constant] | ||
# load the binary plist value | ||
self._tagvalues = plistlib.loads(self._tagvalues) | ||
for x in self._tagvalues: | ||
(tag, color) = self._tag_split(x) | ||
self._tags[tag] = color | ||
# self._tags = [self._tag_strip_color(x) for x in self._tagvalues] | ||
except KeyError: | ||
self._tags = None | ||
if self._tags: | ||
self.data = set(self._tags.keys()) | ||
else: | ||
self.data = set() |
Oops, something went wrong.