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, backend):
29 self.backend = backend
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.backend.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 localized = self.backend.convert_datetime_string(value)
211 raise ValueError("Provided value could not be converted to "
212 "datetime, its type is not supported: {}"
213 .format(type(value)))
217 def normalize_uuid(self, value):
219 if not isinstance(value, six.string_types) or value == '':
220 raise ValueError("UUID must be a valid non-empty string, "
221 "not: {}".format(value))