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.
8 DATE_FORMAT = '%Y%m%dT%H%M%SZ'
9 local_zone = tzlocal.get_localzone()
11 class SerializingObject(object):
13 Common ancestor for TaskResource & TaskWarriorFilter, since they both
14 need to serialize arguments.
16 Serializing method should hold the following contract:
17 - any empty value (meaning removal of the attribute)
18 is deserialized into a empty string
19 - None denotes a empty value for any attribute
21 Deserializing method should hold the following contract:
22 - None denotes a empty value for any attribute (however,
23 this is here as a safeguard, TaskWarrior currently does
24 not export empty-valued attributes) if the attribute
25 is not iterable (e.g. list or set), in which case
26 a empty iterable should be used.
28 Normalizing methods should hold the following contract:
29 - They are used to validate and normalize the user input.
30 Any attribute value that comes from the user (during Task
31 initialization, assignign values to Task attributes, or
32 filtering by user-provided values of attributes) is first
33 validated and normalized using the normalize_{key} method.
34 - If validation or normalization fails, normalizer is expected
38 def __init__(self, backend):
39 self.backend = backend
41 def _deserialize(self, key, value):
42 hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
43 lambda x: x if x != '' else None)
44 return hydrate_func(value)
46 def _serialize(self, key, value):
47 dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
48 lambda x: x if x is not None else '')
49 return dehydrate_func(value)
51 def _normalize(self, key, value):
53 Use normalize_<key> methods to normalize user input. Any user
54 input will be normalized at the moment it is used as filter,
55 or entered as a value of Task attribute.
58 # None value should not be converted by normalizer
62 normalize_func = getattr(self, 'normalize_{0}'.format(key),
65 return normalize_func(value)
67 def timestamp_serializer(self, date):
71 # Any serialized timestamp should be localized, we need to
72 # convert to UTC before converting to string (DATE_FORMAT uses UTC)
73 date = date.astimezone(pytz.utc)
75 return date.strftime(DATE_FORMAT)
77 def timestamp_deserializer(self, date_str):
81 # Return timestamp localized in the local zone
82 naive_timestamp = datetime.datetime.strptime(date_str, DATE_FORMAT)
83 localized_timestamp = pytz.utc.localize(naive_timestamp)
84 return localized_timestamp.astimezone(local_zone)
86 def serialize_entry(self, value):
87 return self.timestamp_serializer(value)
89 def deserialize_entry(self, value):
90 return self.timestamp_deserializer(value)
92 def normalize_entry(self, value):
93 return self.datetime_normalizer(value)
95 def serialize_modified(self, value):
96 return self.timestamp_serializer(value)
98 def deserialize_modified(self, value):
99 return self.timestamp_deserializer(value)
101 def normalize_modified(self, value):
102 return self.datetime_normalizer(value)
104 def serialize_start(self, value):
105 return self.timestamp_serializer(value)
107 def deserialize_start(self, value):
108 return self.timestamp_deserializer(value)
110 def normalize_start(self, value):
111 return self.datetime_normalizer(value)
113 def serialize_end(self, value):
114 return self.timestamp_serializer(value)
116 def deserialize_end(self, value):
117 return self.timestamp_deserializer(value)
119 def normalize_end(self, value):
120 return self.datetime_normalizer(value)
122 def serialize_due(self, value):
123 return self.timestamp_serializer(value)
125 def deserialize_due(self, value):
126 return self.timestamp_deserializer(value)
128 def normalize_due(self, value):
129 return self.datetime_normalizer(value)
131 def serialize_scheduled(self, value):
132 return self.timestamp_serializer(value)
134 def deserialize_scheduled(self, value):
135 return self.timestamp_deserializer(value)
137 def normalize_scheduled(self, value):
138 return self.datetime_normalizer(value)
140 def serialize_until(self, value):
141 return self.timestamp_serializer(value)
143 def deserialize_until(self, value):
144 return self.timestamp_deserializer(value)
146 def normalize_until(self, value):
147 return self.datetime_normalizer(value)
149 def serialize_wait(self, value):
150 return self.timestamp_serializer(value)
152 def deserialize_wait(self, value):
153 return self.timestamp_deserializer(value)
155 def normalize_wait(self, value):
156 return self.datetime_normalizer(value)
158 def serialize_annotations(self, value):
159 value = value if value is not None else []
161 # This may seem weird, but it's correct, we want to export
162 # a list of dicts as serialized value
163 serialized_annotations = [json.loads(annotation.export_data())
164 for annotation in value]
165 return serialized_annotations if serialized_annotations else ''
167 def deserialize_annotations(self, data):
168 task_module = importlib.import_module('tasklib.task')
169 TaskAnnotation = getattr(task_module, 'TaskAnnotation')
170 return [TaskAnnotation(self, d) for d in data] if data else []
172 def serialize_tags(self, tags):
173 return ','.join(tags) if tags else ''
175 def deserialize_tags(self, tags):
176 if isinstance(tags, six.string_types):
177 return tags.split(',') if tags else []
180 def serialize_depends(self, value):
181 # Return the list of uuids
182 value = value if value is not None else set()
183 return ','.join(task['uuid'] for task in value)
185 def deserialize_depends(self, raw_uuids):
186 raw_uuids = raw_uuids or [] # Convert None to empty list
188 # TW 2.4.4 encodes list of dependencies as a single string
189 if type(raw_uuids) is not list:
190 uuids = raw_uuids.split(',')
191 # TW 2.4.5 and later exports them as a list, no conversion needed
195 return set(self.backend.tasks.get(uuid=uuid) for uuid in uuids if uuid)
197 def datetime_normalizer(self, value):
199 Normalizes date/datetime value (considered to come from user input)
200 to localized datetime value. Following conversions happen:
202 naive date -> localized datetime with the same date, and time=midnight
203 naive datetime -> localized datetime with the same value
204 localized datetime -> localized datetime (no conversion)
207 if (isinstance(value, datetime.date)
208 and not isinstance(value, datetime.datetime)):
209 # Convert to local midnight
210 value_full = datetime.datetime.combine(value, datetime.time.min)
211 localized = local_zone.localize(value_full)
212 elif isinstance(value, datetime.datetime):
213 if value.tzinfo is None:
214 # Convert to localized datetime object
215 localized = local_zone.localize(value)
217 # If the value is already localized, there is no need to change
218 # time zone at this point. Also None is a valid value too.
220 elif isinstance(value, six.string_types):
221 localized = self.backend.convert_datetime_string(value)
223 raise ValueError("Provided value could not be converted to "
224 "datetime, its type is not supported: {}"
225 .format(type(value)))
229 def normalize_uuid(self, value):
231 if not isinstance(value, six.string_types) or value == '':
232 raise ValueError("UUID must be a valid non-empty string, "
233 "not: {}".format(value))