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