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.
7 DATE_FORMAT = '%Y%m%dT%H%M%SZ'
8 local_zone = tzlocal.get_localzone()
10 class SerializingObject(object):
12 Common ancestor for TaskResource & TaskWarriorFilter, since they both
13 need to serialize arguments.
15 Serializing method should hold the following contract:
16 - any empty value (meaning removal of the attribute)
17 is deserialized into a empty string
18 - None denotes a empty value for any attribute
20 Deserializing method should hold the following contract:
21 - None denotes a empty value for any attribute (however,
22 this is here as a safeguard, TaskWarrior currently does
23 not export empty-valued attributes) if the attribute
24 is not iterable (e.g. list or set), in which case
25 a empty iterable should be used.
27 Normalizing methods should hold the following contract:
28 - They are used to validate and normalize the user input.
29 Any attribute value that comes from the user (during Task
30 initialization, assignign values to Task attributes, or
31 filtering by user-provided values of attributes) is first
32 validated and normalized using the normalize_{key} method.
33 - If validation or normalization fails, normalizer is expected
37 def __init__(self, backend):
38 self.backend = backend
40 def _deserialize(self, key, value):
41 hydrate_func = getattr(self, 'deserialize_{0}'.format(key),
42 lambda x: x if x != '' else None)
43 return hydrate_func(value)
45 def _serialize(self, key, value):
46 dehydrate_func = getattr(self, 'serialize_{0}'.format(key),
47 lambda x: x if x is not None else '')
48 return dehydrate_func(value)
50 def _normalize(self, key, value):
52 Use normalize_<key> methods to normalize user input. Any user
53 input will be normalized at the moment it is used as filter,
54 or entered as a value of Task attribute.
57 # None value should not be converted by normalizer
61 normalize_func = getattr(self, 'normalize_{0}'.format(key),
64 return normalize_func(value)
66 def timestamp_serializer(self, date):
70 # Any serialized timestamp should be localized, we need to
71 # convert to UTC before converting to string (DATE_FORMAT uses UTC)
72 date = date.astimezone(pytz.utc)
74 return date.strftime(DATE_FORMAT)
76 def timestamp_deserializer(self, date_str):
80 # Return timestamp localized in the local zone
81 naive_timestamp = datetime.datetime.strptime(date_str, DATE_FORMAT)
82 localized_timestamp = pytz.utc.localize(naive_timestamp)
83 return localized_timestamp.astimezone(local_zone)
85 def serialize_entry(self, value):
86 return self.timestamp_serializer(value)
88 def deserialize_entry(self, value):
89 return self.timestamp_deserializer(value)
91 def normalize_entry(self, value):
92 return self.datetime_normalizer(value)
94 def serialize_modified(self, value):
95 return self.timestamp_serializer(value)
97 def deserialize_modified(self, value):
98 return self.timestamp_deserializer(value)
100 def normalize_modified(self, value):
101 return self.datetime_normalizer(value)
103 def serialize_start(self, value):
104 return self.timestamp_serializer(value)
106 def deserialize_start(self, value):
107 return self.timestamp_deserializer(value)
109 def normalize_start(self, value):
110 return self.datetime_normalizer(value)
112 def serialize_end(self, value):
113 return self.timestamp_serializer(value)
115 def deserialize_end(self, value):
116 return self.timestamp_deserializer(value)
118 def normalize_end(self, value):
119 return self.datetime_normalizer(value)
121 def serialize_due(self, value):
122 return self.timestamp_serializer(value)
124 def deserialize_due(self, value):
125 return self.timestamp_deserializer(value)
127 def normalize_due(self, value):
128 return self.datetime_normalizer(value)
130 def serialize_scheduled(self, value):
131 return self.timestamp_serializer(value)
133 def deserialize_scheduled(self, value):
134 return self.timestamp_deserializer(value)
136 def normalize_scheduled(self, value):
137 return self.datetime_normalizer(value)
139 def serialize_until(self, value):
140 return self.timestamp_serializer(value)
142 def deserialize_until(self, value):
143 return self.timestamp_deserializer(value)
145 def normalize_until(self, value):
146 return self.datetime_normalizer(value)
148 def serialize_wait(self, value):
149 return self.timestamp_serializer(value)
151 def deserialize_wait(self, value):
152 return self.timestamp_deserializer(value)
154 def normalize_wait(self, value):
155 return self.datetime_normalizer(value)
157 def serialize_annotations(self, value):
158 value = value if value is not None else []
160 # This may seem weird, but it's correct, we want to export
161 # a list of dicts as serialized value
162 serialized_annotations = [json.loads(annotation.export_data())
163 for annotation in value]
164 return serialized_annotations if serialized_annotations else ''
166 def deserialize_annotations(self, data):
167 return [TaskAnnotation(self, d) for d in data] if data else []
169 def serialize_tags(self, tags):
170 return ','.join(tags) if tags else ''
172 def deserialize_tags(self, tags):
173 if isinstance(tags, six.string_types):
174 return tags.split(',') if tags else []
177 def serialize_depends(self, value):
178 # Return the list of uuids
179 value = value if value is not None else set()
180 return ','.join(task['uuid'] for task in value)
182 def deserialize_depends(self, raw_uuids):
183 raw_uuids = raw_uuids or [] # Convert None to empty list
185 # TW 2.4.4 encodes list of dependencies as a single string
186 if type(raw_uuids) is not list:
187 uuids = raw_uuids.split(',')
188 # TW 2.4.5 and later exports them as a list, no conversion needed
192 return set(self.backend.tasks.get(uuid=uuid) for uuid in uuids if uuid)
194 def datetime_normalizer(self, value):
196 Normalizes date/datetime value (considered to come from user input)
197 to localized datetime value. Following conversions happen:
199 naive date -> localized datetime with the same date, and time=midnight
200 naive datetime -> localized datetime with the same value
201 localized datetime -> localized datetime (no conversion)
204 if (isinstance(value, datetime.date)
205 and not isinstance(value, datetime.datetime)):
206 # Convert to local midnight
207 value_full = datetime.datetime.combine(value, datetime.time.min)
208 localized = local_zone.localize(value_full)
209 elif isinstance(value, datetime.datetime):
210 if value.tzinfo is None:
211 # Convert to localized datetime object
212 localized = local_zone.localize(value)
214 # If the value is already localized, there is no need to change
215 # time zone at this point. Also None is a valid value too.
217 elif isinstance(value, six.string_types):
218 localized = self.backend.convert_datetime_string(value)
220 raise ValueError("Provided value could not be converted to "
221 "datetime, its type is not supported: {}"
222 .format(type(value)))
226 def normalize_uuid(self, value):
228 if not isinstance(value, six.string_types) or value == '':
229 raise ValueError("UUID must be a valid non-empty string, "
230 "not: {}".format(value))