]> 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:

SerializingObject: Serialize tags into sets, not lists
[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
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_depends(self, value):
185         # Return the list of uuids
186         value = value if value is not None else set()
187
188         if isinstance(value, LazyUUIDTaskSet):
189             return ','.join(value._uuids)
190         else:
191             return ','.join(task['uuid'] for task in value)
192
193     def deserialize_depends(self, raw_uuids):
194         raw_uuids = raw_uuids or []  # Convert None to empty list
195
196         if not raw_uuids:
197             return set()
198
199         # TW 2.4.4 encodes list of dependencies as a single string
200         if type(raw_uuids) is not list:
201             uuids = raw_uuids.split(',')
202         # TW 2.4.5 and later exports them as a list, no conversion needed
203         else:
204             uuids = raw_uuids
205
206         return LazyUUIDTaskSet(self.backend, uuids)
207
208     def datetime_normalizer(self, value):
209         """
210         Normalizes date/datetime value (considered to come from user input)
211         to localized datetime value. Following conversions happen:
212
213         naive date -> localized datetime with the same date, and time=midnight
214         naive datetime -> localized datetime with the same value
215         localized datetime -> localized datetime (no conversion)
216         """
217
218         if (
219             isinstance(value, datetime.date)
220             and not isinstance(value, datetime.datetime)
221         ):
222             # Convert to local midnight
223             value_full = datetime.datetime.combine(value, datetime.time.min)
224             localized = local_zone.localize(value_full)
225         elif isinstance(value, datetime.datetime):
226             if value.tzinfo is None:
227                 # Convert to localized datetime object
228                 localized = local_zone.localize(value)
229             else:
230                 # If the value is already localized, there is no need to change
231                 # time zone at this point. Also None is a valid value too.
232                 localized = value
233         elif isinstance(value, six.string_types):
234             localized = self.backend.convert_datetime_string(value)
235         else:
236             raise ValueError("Provided value could not be converted to "
237                              "datetime, its type is not supported: {}"
238                              .format(type(value)))
239
240         return localized
241
242     def normalize_uuid(self, value):
243         # Enforce sane UUID
244         if not isinstance(value, six.string_types) or value == '':
245             raise ValueError("UUID must be a valid non-empty string, "
246                              "not: {}".format(value))
247
248         return value