All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
   1 class SerializingObject(object):
 
   3     Common ancestor for TaskResource & TaskWarriorFilter, since they both
 
   4     need to serialize arguments.
 
   6     Serializing method should hold the following contract:
 
   7       - any empty value (meaning removal of the attribute)
 
   8         is deserialized into a empty string
 
   9       - None denotes a empty value for any attribute
 
  11     Deserializing method should hold the following contract:
 
  12       - None denotes a empty value for any attribute (however,
 
  13         this is here as a safeguard, TaskWarrior currently does
 
  14         not export empty-valued attributes) if the attribute
 
  15         is not iterable (e.g. list or set), in which case
 
  16         a empty iterable should be used.
 
  18     Normalizing methods should hold the following contract:
 
  19       - They are used to validate and normalize the user input.
 
  20         Any attribute value that comes from the user (during Task
 
  21         initialization, assignign values to Task attributes, or
 
  22         filtering by user-provided values of attributes) is first
 
  23         validated and normalized using the normalize_{key} method.
 
  24       - If validation or normalization fails, normalizer is expected
 
  28     def __init__(self, warrior):
 
  29         self.warrior = warrior
 
  31     def _deserialize(self, key, value):
 
  32         hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
 
  33                                lambda x: x if x != '' else None)
 
  34         return hydrate_func(value)
 
  36     def _serialize(self, key, value):
 
  37         dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
 
  38                                  lambda x: x if x is not None else '')
 
  39         return dehydrate_func(value)
 
  41     def _normalize(self, key, value):
 
  43         Use normalize_<key> methods to normalize user input. Any user
 
  44         input will be normalized at the moment it is used as filter,
 
  45         or entered as a value of Task attribute.
 
  48         # None value should not be converted by normalizer
 
  52         normalize_func = getattr(self, 'normalize_{0}'.format(key),
 
  55         return normalize_func(value)
 
  57     def timestamp_serializer(self, date):
 
  61         # Any serialized timestamp should be localized, we need to
 
  62         # convert to UTC before converting to string (DATE_FORMAT uses UTC)
 
  63         date = date.astimezone(pytz.utc)
 
  65         return date.strftime(DATE_FORMAT)
 
  67     def timestamp_deserializer(self, date_str):
 
  71         # Return timestamp localized in the local zone
 
  72         naive_timestamp = datetime.datetime.strptime(date_str, DATE_FORMAT)
 
  73         localized_timestamp = pytz.utc.localize(naive_timestamp)
 
  74         return localized_timestamp.astimezone(local_zone)
 
  76     def serialize_entry(self, value):
 
  77         return self.timestamp_serializer(value)
 
  79     def deserialize_entry(self, value):
 
  80         return self.timestamp_deserializer(value)
 
  82     def normalize_entry(self, value):
 
  83         return self.datetime_normalizer(value)
 
  85     def serialize_modified(self, value):
 
  86         return self.timestamp_serializer(value)
 
  88     def deserialize_modified(self, value):
 
  89         return self.timestamp_deserializer(value)
 
  91     def normalize_modified(self, value):
 
  92         return self.datetime_normalizer(value)
 
  94     def serialize_start(self, value):
 
  95         return self.timestamp_serializer(value)
 
  97     def deserialize_start(self, value):
 
  98         return self.timestamp_deserializer(value)
 
 100     def normalize_start(self, value):
 
 101         return self.datetime_normalizer(value)
 
 103     def serialize_end(self, value):
 
 104         return self.timestamp_serializer(value)
 
 106     def deserialize_end(self, value):
 
 107         return self.timestamp_deserializer(value)
 
 109     def normalize_end(self, value):
 
 110         return self.datetime_normalizer(value)
 
 112     def serialize_due(self, value):
 
 113         return self.timestamp_serializer(value)
 
 115     def deserialize_due(self, value):
 
 116         return self.timestamp_deserializer(value)
 
 118     def normalize_due(self, value):
 
 119         return self.datetime_normalizer(value)
 
 121     def serialize_scheduled(self, value):
 
 122         return self.timestamp_serializer(value)
 
 124     def deserialize_scheduled(self, value):
 
 125         return self.timestamp_deserializer(value)
 
 127     def normalize_scheduled(self, value):
 
 128         return self.datetime_normalizer(value)
 
 130     def serialize_until(self, value):
 
 131         return self.timestamp_serializer(value)
 
 133     def deserialize_until(self, value):
 
 134         return self.timestamp_deserializer(value)
 
 136     def normalize_until(self, value):
 
 137         return self.datetime_normalizer(value)
 
 139     def serialize_wait(self, value):
 
 140         return self.timestamp_serializer(value)
 
 142     def deserialize_wait(self, value):
 
 143         return self.timestamp_deserializer(value)
 
 145     def normalize_wait(self, value):
 
 146         return self.datetime_normalizer(value)
 
 148     def serialize_annotations(self, value):
 
 149         value = value if value is not None else []
 
 151         # This may seem weird, but it's correct, we want to export
 
 152         # a list of dicts as serialized value
 
 153         serialized_annotations = [json.loads(annotation.export_data())
 
 154                                   for annotation in value]
 
 155         return serialized_annotations if serialized_annotations else ''
 
 157     def deserialize_annotations(self, data):
 
 158         return [TaskAnnotation(self, d) for d in data] if data else []
 
 160     def serialize_tags(self, tags):
 
 161         return ','.join(tags) if tags else ''
 
 163     def deserialize_tags(self, tags):
 
 164         if isinstance(tags, six.string_types):
 
 165             return tags.split(',') if tags else []
 
 168     def serialize_depends(self, value):
 
 169         # Return the list of uuids
 
 170         value = value if value is not None else set()
 
 171         return ','.join(task['uuid'] for task in value)
 
 173     def deserialize_depends(self, raw_uuids):
 
 174         raw_uuids = raw_uuids or []  # Convert None to empty list
 
 176         # TW 2.4.4 encodes list of dependencies as a single string
 
 177         if type(raw_uuids) is not list:
 
 178             uuids = raw_uuids.split(',')
 
 179         # TW 2.4.5 and later exports them as a list, no conversion needed
 
 183         return set(self.warrior.tasks.get(uuid=uuid) for uuid in uuids if uuid)
 
 185     def datetime_normalizer(self, value):
 
 187         Normalizes date/datetime value (considered to come from user input)
 
 188         to localized datetime value. Following conversions happen:
 
 190         naive date -> localized datetime with the same date, and time=midnight
 
 191         naive datetime -> localized datetime with the same value
 
 192         localized datetime -> localized datetime (no conversion)
 
 195         if (isinstance(value, datetime.date)
 
 196             and not isinstance(value, datetime.datetime)):
 
 197             # Convert to local midnight
 
 198             value_full = datetime.datetime.combine(value, datetime.time.min)
 
 199             localized = local_zone.localize(value_full)
 
 200         elif isinstance(value, datetime.datetime):
 
 201             if value.tzinfo is None:
 
 202                 # Convert to localized datetime object
 
 203                 localized = local_zone.localize(value)
 
 205                 # If the value is already localized, there is no need to change
 
 206                 # time zone at this point. Also None is a valid value too.
 
 208         elif (isinstance(value, six.string_types)
 
 209                 and self.warrior.version >= VERSION_2_4_0):
 
 210             # For strings, use 'task calc' to evaluate the string to datetime
 
 211             # available since TW 2.4.0
 
 213             result = self.warrior.execute_command(['calc'] + args)
 
 214             naive = datetime.datetime.strptime(result[0], DATE_FORMAT_CALC)
 
 215             localized = local_zone.localize(naive)
 
 217             raise ValueError("Provided value could not be converted to "
 
 218                              "datetime, its type is not supported: {}"
 
 219                              .format(type(value)))
 
 223     def normalize_uuid(self, value):
 
 225         if not isinstance(value, six.string_types) or value == '':
 
 226             raise ValueError("UUID must be a valid non-empty string, "
 
 227                              "not: {}".format(value))