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

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