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.
9 from .lazy import LazyUUIDTaskSet, LazyUUIDTask
11 DATE_FORMAT = '%Y%m%dT%H%M%SZ'
12 local_zone = tzlocal.get_localzone()
15 class SerializingObject(object):
17 Common ancestor for TaskResource & TaskWarriorFilter, since they both
18 need to serialize arguments.
20 Serializing method should hold the following contract:
21 - any empty value (meaning removal of the attribute)
22 is deserialized into a empty string
23 - None denotes a empty value for any attribute
25 Deserializing method should hold the following contract:
26 - None denotes a empty value for any attribute (however,
27 this is here as a safeguard, TaskWarrior currently does
28 not export empty-valued attributes) if the attribute
29 is not iterable (e.g. list or set), in which case
30 a empty iterable should be used.
32 Normalizing methods should hold the following contract:
33 - They are used to validate and normalize the user input.
34 Any attribute value that comes from the user (during Task
35 initialization, assignign values to Task attributes, or
36 filtering by user-provided values of attributes) is first
37 validated and normalized using the normalize_{key} method.
38 - If validation or normalization fails, normalizer is expected
42 def __init__(self, backend):
43 self.backend = backend
45 def _deserialize(self, key, value):
46 hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
47 lambda x: x if x != '' else None)
48 return hydrate_func(value)
50 def _serialize(self, key, value):
51 dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
52 lambda x: x if x is not None else '')
53 return dehydrate_func(value)
55 def _normalize(self, key, value):
57 Use normalize_<key> methods to normalize user input. Any user
58 input will be normalized at the moment it is used as filter,
59 or entered as a value of Task attribute.
62 # None value should not be converted by normalizer
66 normalize_func = getattr(self, 'normalize_{0}'.format(key),
69 return normalize_func(value)
71 def timestamp_serializer(self, date):
75 # Any serialized timestamp should be localized, we need to
76 # convert to UTC before converting to string (DATE_FORMAT uses UTC)
77 date = date.astimezone(pytz.utc)
79 return date.strftime(DATE_FORMAT)
81 def timestamp_deserializer(self, date_str):
85 # Return timestamp localized in the local zone
86 naive_timestamp = datetime.datetime.strptime(date_str, DATE_FORMAT)
87 localized_timestamp = pytz.utc.localize(naive_timestamp)
88 return localized_timestamp.astimezone(local_zone)
90 def serialize_entry(self, value):
91 return self.timestamp_serializer(value)
93 def deserialize_entry(self, value):
94 return self.timestamp_deserializer(value)
96 def normalize_entry(self, value):
97 return self.datetime_normalizer(value)
99 def serialize_modified(self, value):
100 return self.timestamp_serializer(value)
102 def deserialize_modified(self, value):
103 return self.timestamp_deserializer(value)
105 def normalize_modified(self, value):
106 return self.datetime_normalizer(value)
108 def serialize_start(self, value):
109 return self.timestamp_serializer(value)
111 def deserialize_start(self, value):
112 return self.timestamp_deserializer(value)
114 def normalize_start(self, value):
115 return self.datetime_normalizer(value)
117 def serialize_end(self, value):
118 return self.timestamp_serializer(value)
120 def deserialize_end(self, value):
121 return self.timestamp_deserializer(value)
123 def normalize_end(self, value):
124 return self.datetime_normalizer(value)
126 def serialize_due(self, value):
127 return self.timestamp_serializer(value)
129 def deserialize_due(self, value):
130 return self.timestamp_deserializer(value)
132 def normalize_due(self, value):
133 return self.datetime_normalizer(value)
135 def serialize_scheduled(self, value):
136 return self.timestamp_serializer(value)
138 def deserialize_scheduled(self, value):
139 return self.timestamp_deserializer(value)
141 def normalize_scheduled(self, value):
142 return self.datetime_normalizer(value)
144 def serialize_until(self, value):
145 return self.timestamp_serializer(value)
147 def deserialize_until(self, value):
148 return self.timestamp_deserializer(value)
150 def normalize_until(self, value):
151 return self.datetime_normalizer(value)
153 def serialize_wait(self, value):
154 return self.timestamp_serializer(value)
156 def deserialize_wait(self, value):
157 return self.timestamp_deserializer(value)
159 def normalize_wait(self, value):
160 return self.datetime_normalizer(value)
162 def serialize_annotations(self, value):
163 value = value if value is not None else []
165 # This may seem weird, but it's correct, we want to export
166 # a list of dicts as serialized value
167 serialized_annotations = [json.loads(annotation.export_data())
168 for annotation in value]
169 return serialized_annotations if serialized_annotations else ''
171 def deserialize_annotations(self, data):
172 task_module = importlib.import_module('tasklib.task')
173 TaskAnnotation = getattr(task_module, 'TaskAnnotation')
174 return [TaskAnnotation(self, d) for d in data] if data else []
176 def serialize_tags(self, tags):
177 return ','.join(tags) if tags else ''
179 def deserialize_tags(self, tags):
180 if isinstance(tags, six.string_types):
181 return set(tags.split(',')) if tags else set()
182 return set(tags or [])
184 def serialize_parent(self, parent):
185 return parent['uuid'] if parent else ''
187 def deserialize_parent(self, uuid):
188 return LazyUUIDTask(self.backend, uuid) if uuid else None
190 def serialize_depends(self, value):
191 # Return the list of uuids
192 value = value if value is not None else set()
194 if isinstance(value, LazyUUIDTaskSet):
195 return ','.join(value._uuids)
197 return ','.join(task['uuid'] for task in value)
199 def deserialize_depends(self, raw_uuids):
200 raw_uuids = raw_uuids or [] # Convert None to empty list
205 # TW 2.4.4 encodes list of dependencies as a single string
206 if type(raw_uuids) is not list:
207 uuids = raw_uuids.split(',')
208 # TW 2.4.5 and later exports them as a list, no conversion needed
212 return LazyUUIDTaskSet(self.backend, uuids)
214 def datetime_normalizer(self, value):
216 Normalizes date/datetime value (considered to come from user input)
217 to localized datetime value. Following conversions happen:
219 naive date -> localized datetime with the same date, and time=midnight
220 naive datetime -> localized datetime with the same value
221 localized datetime -> localized datetime (no conversion)
225 isinstance(value, datetime.date)
226 and not isinstance(value, datetime.datetime)
228 # Convert to local midnight
229 value_full = datetime.datetime.combine(value, datetime.time.min)
230 localized = local_zone.localize(value_full)
231 elif isinstance(value, datetime.datetime):
232 if value.tzinfo is None:
233 # Convert to localized datetime object
234 localized = local_zone.localize(value)
236 # If the value is already localized, there is no need to change
237 # time zone at this point. Also None is a valid value too.
239 elif isinstance(value, six.string_types):
240 localized = self.backend.convert_datetime_string(value)
242 raise ValueError("Provided value could not be converted to "
243 "datetime, its type is not supported: {}"
244 .format(type(value)))
248 def normalize_uuid(self, value):
250 if not isinstance(value, six.string_types) or value == '':
251 raise ValueError("UUID must be a valid non-empty string, "
252 "not: {}".format(value))