##// END OF EJS Templates
allow datestamps to exclude microseconds...
MinRK -
Show More
@@ -1,256 +1,259 b''
1 1 """Utilities to manipulate JSON objects.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010-2011 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING.txt, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13 # stdlib
14 14 import math
15 15 import re
16 16 import types
17 17 from datetime import datetime
18 18
19 19 try:
20 20 # base64.encodestring is deprecated in Python 3.x
21 21 from base64 import encodebytes
22 22 except ImportError:
23 23 # Python 2.x
24 24 from base64 import encodestring as encodebytes
25 25
26 26 from IPython.utils import py3compat
27 27 from IPython.utils.py3compat import string_types, unicode_type, iteritems
28 28 from IPython.utils.encoding import DEFAULT_ENCODING
29 29 next_attr_name = '__next__' if py3compat.PY3 else 'next'
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Globals and constants
33 33 #-----------------------------------------------------------------------------
34 34
35 35 # timestamp formats
36 36 ISO8601 = "%Y-%m-%dT%H:%M:%S.%f"
37 ISO8601_PAT=re.compile(r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,6})Z?([\+\-]\d{2}:?\d{2})?$")
37 ISO8601_PAT=re.compile(r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d{1,6})?Z?([\+\-]\d{2}:?\d{2})?$")
38 38
39 39 #-----------------------------------------------------------------------------
40 40 # Classes and functions
41 41 #-----------------------------------------------------------------------------
42 42
43 43 def rekey(dikt):
44 44 """Rekey a dict that has been forced to use str keys where there should be
45 45 ints by json."""
46 46 for k in dikt:
47 47 if isinstance(k, string_types):
48 48 ik=fk=None
49 49 try:
50 50 ik = int(k)
51 51 except ValueError:
52 52 try:
53 53 fk = float(k)
54 54 except ValueError:
55 55 continue
56 56 if ik is not None:
57 57 nk = ik
58 58 else:
59 59 nk = fk
60 60 if nk in dikt:
61 61 raise KeyError("already have key %r"%nk)
62 62 dikt[nk] = dikt.pop(k)
63 63 return dikt
64 64
65 65 def parse_date(s):
66 66 """parse an ISO8601 date string
67 67
68 68 If it is None or not a valid ISO8601 timestamp,
69 69 it will be returned unmodified.
70 70 Otherwise, it will return a datetime object.
71 71 """
72 72 if s is None:
73 73 return s
74 74 m = ISO8601_PAT.match(s)
75 75 if m:
76 76 # FIXME: add actual timezone support
77 77 # this just drops the timezone info
78 notz = m.groups()[0]
78 notz, ms, tz = m.groups()
79 if not ms:
80 ms = '.0'
81 notz = notz + ms
79 82 return datetime.strptime(notz, ISO8601)
80 83 return s
81 84
82 85 def extract_dates(obj):
83 86 """extract ISO8601 dates from unpacked JSON"""
84 87 if isinstance(obj, dict):
85 88 new_obj = {} # don't clobber
86 89 for k,v in iteritems(obj):
87 90 new_obj[k] = extract_dates(v)
88 91 obj = new_obj
89 92 elif isinstance(obj, (list, tuple)):
90 93 obj = [ extract_dates(o) for o in obj ]
91 94 elif isinstance(obj, string_types):
92 95 obj = parse_date(obj)
93 96 return obj
94 97
95 98 def squash_dates(obj):
96 99 """squash datetime objects into ISO8601 strings"""
97 100 if isinstance(obj, dict):
98 101 obj = dict(obj) # don't clobber
99 102 for k,v in iteritems(obj):
100 103 obj[k] = squash_dates(v)
101 104 elif isinstance(obj, (list, tuple)):
102 105 obj = [ squash_dates(o) for o in obj ]
103 106 elif isinstance(obj, datetime):
104 107 obj = obj.isoformat()
105 108 return obj
106 109
107 110 def date_default(obj):
108 111 """default function for packing datetime objects in JSON."""
109 112 if isinstance(obj, datetime):
110 113 return obj.isoformat()
111 114 else:
112 115 raise TypeError("%r is not JSON serializable"%obj)
113 116
114 117
115 118 # constants for identifying png/jpeg data
116 119 PNG = b'\x89PNG\r\n\x1a\n'
117 120 # front of PNG base64-encoded
118 121 PNG64 = b'iVBORw0KG'
119 122 JPEG = b'\xff\xd8'
120 123 # front of JPEG base64-encoded
121 124 JPEG64 = b'/9'
122 125 # front of PDF base64-encoded
123 126 PDF64 = b'JVBER'
124 127
125 128 def encode_images(format_dict):
126 129 """b64-encodes images in a displaypub format dict
127 130
128 131 Perhaps this should be handled in json_clean itself?
129 132
130 133 Parameters
131 134 ----------
132 135
133 136 format_dict : dict
134 137 A dictionary of display data keyed by mime-type
135 138
136 139 Returns
137 140 -------
138 141
139 142 format_dict : dict
140 143 A copy of the same dictionary,
141 144 but binary image data ('image/png', 'image/jpeg' or 'application/pdf')
142 145 is base64-encoded.
143 146
144 147 """
145 148 encoded = format_dict.copy()
146 149
147 150 pngdata = format_dict.get('image/png')
148 151 if isinstance(pngdata, bytes):
149 152 # make sure we don't double-encode
150 153 if not pngdata.startswith(PNG64):
151 154 pngdata = encodebytes(pngdata)
152 155 encoded['image/png'] = pngdata.decode('ascii')
153 156
154 157 jpegdata = format_dict.get('image/jpeg')
155 158 if isinstance(jpegdata, bytes):
156 159 # make sure we don't double-encode
157 160 if not jpegdata.startswith(JPEG64):
158 161 jpegdata = encodebytes(jpegdata)
159 162 encoded['image/jpeg'] = jpegdata.decode('ascii')
160 163
161 164 pdfdata = format_dict.get('application/pdf')
162 165 if isinstance(pdfdata, bytes):
163 166 # make sure we don't double-encode
164 167 if not pdfdata.startswith(PDF64):
165 168 pdfdata = encodebytes(pdfdata)
166 169 encoded['application/pdf'] = pdfdata.decode('ascii')
167 170
168 171 return encoded
169 172
170 173
171 174 def json_clean(obj):
172 175 """Clean an object to ensure it's safe to encode in JSON.
173 176
174 177 Atomic, immutable objects are returned unmodified. Sets and tuples are
175 178 converted to lists, lists are copied and dicts are also copied.
176 179
177 180 Note: dicts whose keys could cause collisions upon encoding (such as a dict
178 181 with both the number 1 and the string '1' as keys) will cause a ValueError
179 182 to be raised.
180 183
181 184 Parameters
182 185 ----------
183 186 obj : any python object
184 187
185 188 Returns
186 189 -------
187 190 out : object
188 191
189 192 A version of the input which will not cause an encoding error when
190 193 encoded as JSON. Note that this function does not *encode* its inputs,
191 194 it simply sanitizes it so that there will be no encoding errors later.
192 195
193 196 Examples
194 197 --------
195 198 >>> json_clean(4)
196 199 4
197 200 >>> json_clean(list(range(10)))
198 201 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
199 202 >>> sorted(json_clean(dict(x=1, y=2)).items())
200 203 [('x', 1), ('y', 2)]
201 204 >>> sorted(json_clean(dict(x=1, y=2, z=[1,2,3])).items())
202 205 [('x', 1), ('y', 2), ('z', [1, 2, 3])]
203 206 >>> json_clean(True)
204 207 True
205 208 """
206 209 # types that are 'atomic' and ok in json as-is.
207 210 atomic_ok = (unicode_type, type(None))
208 211
209 212 # containers that we need to convert into lists
210 213 container_to_list = (tuple, set, types.GeneratorType)
211 214
212 215 if isinstance(obj, float):
213 216 # cast out-of-range floats to their reprs
214 217 if math.isnan(obj) or math.isinf(obj):
215 218 return repr(obj)
216 219 return float(obj)
217 220
218 221 if isinstance(obj, int):
219 222 # cast int to int, in case subclasses override __str__ (e.g. boost enum, #4598)
220 223 if isinstance(obj, bool):
221 224 # bools are ints, but we don't want to cast them to 0,1
222 225 return obj
223 226 return int(obj)
224 227
225 228 if isinstance(obj, atomic_ok):
226 229 return obj
227 230
228 231 if isinstance(obj, bytes):
229 232 return obj.decode(DEFAULT_ENCODING, 'replace')
230 233
231 234 if isinstance(obj, container_to_list) or (
232 235 hasattr(obj, '__iter__') and hasattr(obj, next_attr_name)):
233 236 obj = list(obj)
234 237
235 238 if isinstance(obj, list):
236 239 return [json_clean(x) for x in obj]
237 240
238 241 if isinstance(obj, dict):
239 242 # First, validate that the dict won't lose data in conversion due to
240 243 # key collisions after stringification. This can happen with keys like
241 244 # True and 'true' or 1 and '1', which collide in JSON.
242 245 nkeys = len(obj)
243 246 nkeys_collapsed = len(set(map(str, obj)))
244 247 if nkeys != nkeys_collapsed:
245 248 raise ValueError('dict can not be safely converted to JSON: '
246 249 'key collision would lead to dropped values')
247 250 # If all OK, proceed by making the new dict that will be json-safe
248 251 out = {}
249 252 for k,v in iteritems(obj):
250 253 out[str(k)] = json_clean(v)
251 254 return out
252 255
253 256 # If we get here, we don't know how to handle the object, so we just get
254 257 # its repr and return that. This will catch lambdas, open sockets, class
255 258 # objects, and any other complicated contraption that json can't encode
256 259 return repr(obj)
General Comments 0
You need to be logged in to leave comments. Login now