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