]> git.madduck.net Git - etc/taskwarrior.git/blob - tasklib/serializing.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

tests: Do not use string-based dates for compatibility reasons
[etc/taskwarrior.git] / tasklib / serializing.py
1 import datetime
2 import importlib
3 import json
4 import pytz
5 import six
6 import tzlocal
7
8
9 from .lazy import LazyUUIDTaskSet, LazyUUIDTask
10
11 DATE_FORMAT = '%Y%m%dT%H%M%SZ'
12 local_zone = tzlocal.get_localzone()
13
14
15 class SerializingObject(object):
16     """
17     Common ancestor for TaskResource & TaskWarriorFilter, since they both
18     need to serialize arguments.
19
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
24
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.
31
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
39         to raise ValueError.
40     """
41
42     def __init__(self, backend):
43         self.backend = backend
44
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)
49
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)
54
55     def _normalize(self, key, value):
56         """
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.
60         """
61
62         # None value should not be converted by normalizer
63         if value is None:
64             return None
65
66         normalize_func = getattr(self, 'normalize_{0}'.format(key),
67                                  lambda x: x)
68
69         return normalize_func(value)
70
71     def timestamp_serializer(self, date):
72         if not date:
73             return ''
74
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)
78
79         return date.strftime(DATE_FORMAT)
80
81     def timestamp_deserializer(self, date_str):
82         if not date_str:
83             return None
84
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)
89
90     def serialize_entry(self, value):
91         return self.timestamp_serializer(value)
92
93     def deserialize_entry(self, value):
94         return self.timestamp_deserializer(value)
95
96     def normalize_entry(self, value):
97         return self.datetime_normalizer(value)
98
99     def serialize_modified(self, value):
100         return self.timestamp_serializer(value)
101
102     def deserialize_modified(self, value):
103         return self.timestamp_deserializer(value)
104
105     def normalize_modified(self, value):
106         return self.datetime_normalizer(value)
107
108     def serialize_start(self, value):
109         return self.timestamp_serializer(value)
110
111     def deserialize_start(self, value):
112         return self.timestamp_deserializer(value)
113
114     def normalize_start(self, value):
115         return self.datetime_normalizer(value)
116
117     def serialize_end(self, value):
118         return self.timestamp_serializer(value)
119
120     def deserialize_end(self, value):
121         return self.timestamp_deserializer(value)
122
123     def normalize_end(self, value):
124         return self.datetime_normalizer(value)
125
126     def serialize_due(self, value):
127         return self.timestamp_serializer(value)
128
129     def deserialize_due(self, value):
130         return self.timestamp_deserializer(value)
131
132     def normalize_due(self, value):
133         return self.datetime_normalizer(value)
134
135     def serialize_scheduled(self, value):
136         return self.timestamp_serializer(value)
137
138     def deserialize_scheduled(self, value):
139         return self.timestamp_deserializer(value)
140
141     def normalize_scheduled(self, value):
142         return self.datetime_normalizer(value)
143
144     def serialize_until(self, value):
145         return self.timestamp_serializer(value)
146
147     def deserialize_until(self, value):
148         return self.timestamp_deserializer(value)
149
150     def normalize_until(self, value):
151         return self.datetime_normalizer(value)
152
153     def serialize_wait(self, value):
154         return self.timestamp_serializer(value)
155
156     def deserialize_wait(self, value):
157         return self.timestamp_deserializer(value)
158
159     def normalize_wait(self, value):
160         return self.datetime_normalizer(value)
161
162     def serialize_annotations(self, value):
163         value = value if value is not None else []
164
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 ''
170
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 []
175
176     def serialize_tags(self, tags):
177         return ','.join(tags) if tags else ''
178
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 [])
183
184     def serialize_parent(self, parent):
185         return parent['uuid'] if parent else ''
186
187     def deserialize_parent(self, uuid):
188         return LazyUUIDTask(self.backend, uuid) if uuid else None
189
190     def serialize_depends(self, value):
191         # Return the list of uuids
192         value = value if value is not None else set()
193
194         if isinstance(value, LazyUUIDTaskSet):
195             return ','.join(value._uuids)
196         else:
197             return ','.join(task['uuid'] for task in value)
198
199     def deserialize_depends(self, raw_uuids):
200         raw_uuids = raw_uuids or []  # Convert None to empty list
201
202         if not raw_uuids:
203             return set()
204
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
209         else:
210             uuids = raw_uuids
211
212         return LazyUUIDTaskSet(self.backend, uuids)
213
214     def datetime_normalizer(self, value):
215         """
216         Normalizes date/datetime value (considered to come from user input)
217         to localized datetime value. Following conversions happen:
218
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)
222         """
223
224         if (
225             isinstance(value, datetime.date)
226             and not isinstance(value, datetime.datetime)
227         ):
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)
235             else:
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.
238                 localized = value
239         elif isinstance(value, six.string_types):
240             localized = self.backend.convert_datetime_string(value)
241         else:
242             raise ValueError("Provided value could not be converted to "
243                              "datetime, its type is not supported: {}"
244                              .format(type(value)))
245
246         return localized
247
248     def normalize_uuid(self, value):
249         # Enforce sane UUID
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))
253
254         return value