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

TaskQuerySet: Always require backend set properly
[etc/taskwarrior.git] / tasklib / serializing.py
1 import datetime
2 import json
3 import pytz
4 import six
5 import tzlocal
6
7 DATE_FORMAT = '%Y%m%dT%H%M%SZ'
8 local_zone = tzlocal.get_localzone()
9
10 class SerializingObject(object):
11     """
12     Common ancestor for TaskResource & TaskWarriorFilter, since they both
13     need to serialize arguments.
14
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
19
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.
26
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
34         to raise ValueError.
35     """
36
37     def __init__(self, backend):
38         self.backend = backend
39
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)
44
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)
49
50     def _normalize(self, key, value):
51         """
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.
55         """
56
57         # None value should not be converted by normalizer
58         if value is None:
59             return None
60
61         normalize_func = getattr(self, 'normalize_{0}'.format(key),
62                                  lambda x: x)
63
64         return normalize_func(value)
65
66     def timestamp_serializer(self, date):
67         if not date:
68             return ''
69
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)
73
74         return date.strftime(DATE_FORMAT)
75
76     def timestamp_deserializer(self, date_str):
77         if not date_str:
78             return None
79
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)
84
85     def serialize_entry(self, value):
86         return self.timestamp_serializer(value)
87
88     def deserialize_entry(self, value):
89         return self.timestamp_deserializer(value)
90
91     def normalize_entry(self, value):
92         return self.datetime_normalizer(value)
93
94     def serialize_modified(self, value):
95         return self.timestamp_serializer(value)
96
97     def deserialize_modified(self, value):
98         return self.timestamp_deserializer(value)
99
100     def normalize_modified(self, value):
101         return self.datetime_normalizer(value)
102
103     def serialize_start(self, value):
104         return self.timestamp_serializer(value)
105
106     def deserialize_start(self, value):
107         return self.timestamp_deserializer(value)
108
109     def normalize_start(self, value):
110         return self.datetime_normalizer(value)
111
112     def serialize_end(self, value):
113         return self.timestamp_serializer(value)
114
115     def deserialize_end(self, value):
116         return self.timestamp_deserializer(value)
117
118     def normalize_end(self, value):
119         return self.datetime_normalizer(value)
120
121     def serialize_due(self, value):
122         return self.timestamp_serializer(value)
123
124     def deserialize_due(self, value):
125         return self.timestamp_deserializer(value)
126
127     def normalize_due(self, value):
128         return self.datetime_normalizer(value)
129
130     def serialize_scheduled(self, value):
131         return self.timestamp_serializer(value)
132
133     def deserialize_scheduled(self, value):
134         return self.timestamp_deserializer(value)
135
136     def normalize_scheduled(self, value):
137         return self.datetime_normalizer(value)
138
139     def serialize_until(self, value):
140         return self.timestamp_serializer(value)
141
142     def deserialize_until(self, value):
143         return self.timestamp_deserializer(value)
144
145     def normalize_until(self, value):
146         return self.datetime_normalizer(value)
147
148     def serialize_wait(self, value):
149         return self.timestamp_serializer(value)
150
151     def deserialize_wait(self, value):
152         return self.timestamp_deserializer(value)
153
154     def normalize_wait(self, value):
155         return self.datetime_normalizer(value)
156
157     def serialize_annotations(self, value):
158         value = value if value is not None else []
159
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 ''
165
166     def deserialize_annotations(self, data):
167         return [TaskAnnotation(self, d) for d in data] if data else []
168
169     def serialize_tags(self, tags):
170         return ','.join(tags) if tags else ''
171
172     def deserialize_tags(self, tags):
173         if isinstance(tags, six.string_types):
174             return tags.split(',') if tags else []
175         return tags or []
176
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)
181
182     def deserialize_depends(self, raw_uuids):
183         raw_uuids = raw_uuids or []  # Convert None to empty list
184
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
189         else:
190             uuids = raw_uuids
191
192         return set(self.backend.tasks.get(uuid=uuid) for uuid in uuids if uuid)
193
194     def datetime_normalizer(self, value):
195         """
196         Normalizes date/datetime value (considered to come from user input)
197         to localized datetime value. Following conversions happen:
198
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)
202         """
203
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)
213             else:
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.
216                 localized = value
217         elif isinstance(value, six.string_types):
218             localized = self.backend.convert_datetime_string(value)
219         else:
220             raise ValueError("Provided value could not be converted to "
221                              "datetime, its type is not supported: {}"
222                              .format(type(value)))
223
224         return localized
225
226     def normalize_uuid(self, value):
227         # Enforce sane UUID
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))
231
232         return value
233
234