|
1 #!/usr/bin/python |
|
2 # |
|
3 # Copyright (C) 2009 Google Inc. |
|
4 # |
|
5 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 # you may not use this file except in compliance with the License. |
|
7 # You may obtain a copy of the License at |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
11 # Unless required by applicable law or agreed to in writing, software |
|
12 # distributed under the License is distributed on an "AS IS" BASIS, |
|
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 # See the License for the specific language governing permissions and |
|
15 # limitations under the License. |
|
16 |
|
17 """Converts Python data into data for Google Visualization API clients. |
|
18 |
|
19 This library can be used to create a google.visualization.DataTable usable by |
|
20 visualizations built on the Google Visualization API. Output formats are raw |
|
21 JSON, JSON response, and JavaScript. |
|
22 |
|
23 See http://code.google.com/apis/visualization/ for documentation on the |
|
24 Google Visualization API. |
|
25 """ |
|
26 |
|
27 __author__ = "Amit Weinstein, Misha Seltzer" |
|
28 |
|
29 import cgi |
|
30 import datetime |
|
31 import types |
|
32 |
|
33 |
|
34 class DataTableException(Exception): |
|
35 """The general exception object thrown by DataTable.""" |
|
36 pass |
|
37 |
|
38 |
|
39 class DataTable(object): |
|
40 """Wraps the data to convert to a Google Visualization API DataTable. |
|
41 |
|
42 Create this object, populate it with data, then call one of the ToJS... |
|
43 methods to return a string representation of the data in the format described. |
|
44 |
|
45 You can clear all data from the object to reuse it, but you cannot clear |
|
46 individual cells, rows, or columns. You also cannot modify the table schema |
|
47 specified in the class constructor. |
|
48 |
|
49 You can add new data one or more rows at a time. All data added to an |
|
50 instantiated DataTable must conform to the schema passed in to __init__(). |
|
51 |
|
52 You can reorder the columns in the output table, and also specify row sorting |
|
53 order by column. The default column order is according to the original |
|
54 table_description parameter. Default row sort order is ascending, by column |
|
55 1 values. For a dictionary, we sort the keys for order. |
|
56 |
|
57 The data and the table_description are closely tied, as described here: |
|
58 |
|
59 The table schema is defined in the class constructor's table_description |
|
60 parameter. The user defines each column using a tuple of |
|
61 (id[, type[, label[, custom_properties]]]). The default value for type is |
|
62 string, label is the same as ID if not specified, and custom properties is |
|
63 an empty dictionary if not specified. |
|
64 |
|
65 table_description is a dictionary or list, containing one or more column |
|
66 descriptor tuples, nested dictionaries, and lists. Each dictionary key, list |
|
67 element, or dictionary element must eventually be defined as |
|
68 a column description tuple. Here's an example of a dictionary where the key |
|
69 is a tuple, and the value is a list of two tuples: |
|
70 {('a', 'number'): [('b', 'number'), ('c', 'string')]} |
|
71 |
|
72 This flexibility in data entry enables you to build and manipulate your data |
|
73 in a Python structure that makes sense for your program. |
|
74 |
|
75 Add data to the table using the same nested design as the table's |
|
76 table_description, replacing column descriptor tuples with cell data, and |
|
77 each row is an element in the top level collection. This will be a bit |
|
78 clearer after you look at the following examples showing the |
|
79 table_description, matching data, and the resulting table: |
|
80 |
|
81 Columns as list of tuples [col1, col2, col3] |
|
82 table_description: [('a', 'number'), ('b', 'string')] |
|
83 AppendData( [[1, 'z'], [2, 'w'], [4, 'o'], [5, 'k']] ) |
|
84 Table: |
|
85 a b <--- these are column ids/labels |
|
86 1 z |
|
87 2 w |
|
88 4 o |
|
89 5 k |
|
90 |
|
91 Dictionary of columns, where key is a column, and value is a list of |
|
92 columns {col1: [col2, col3]} |
|
93 table_description: {('a', 'number'): [('b', 'number'), ('c', 'string')]} |
|
94 AppendData( data: {1: [2, 'z'], 3: [4, 'w']} |
|
95 Table: |
|
96 a b c |
|
97 1 2 z |
|
98 3 4 w |
|
99 |
|
100 Dictionary where key is a column, and the value is itself a dictionary of |
|
101 columns {col1: {col2, col3}} |
|
102 table_description: {('a', 'number'): {'b': 'number', 'c': 'string'}} |
|
103 AppendData( data: {1: {'b': 2, 'c': 'z'}, 3: {'b': 4, 'c': 'w'}} |
|
104 Table: |
|
105 a b c |
|
106 1 2 z |
|
107 3 4 w |
|
108 """ |
|
109 |
|
110 def __init__(self, table_description, data=None, custom_properties=None): |
|
111 """Initialize the data table from a table schema and (optionally) data. |
|
112 |
|
113 See the class documentation for more information on table schema and data |
|
114 values. |
|
115 |
|
116 Args: |
|
117 table_description: A table schema, following one of the formats described |
|
118 in TableDescriptionParser(). Schemas describe the |
|
119 column names, data types, and labels. See |
|
120 TableDescriptionParser() for acceptable formats. |
|
121 data: Optional. If given, fills the table with the given data. The data |
|
122 structure must be consistent with schema in table_description. See |
|
123 the class documentation for more information on acceptable data. You |
|
124 can add data later by calling AppendData(). |
|
125 custom_properties: Optional. A dictionary from string to string that |
|
126 goes into the table's custom properties. This can be |
|
127 later changed by changing self.custom_properties. |
|
128 |
|
129 Raises: |
|
130 DataTableException: Raised if the data and the description did not match, |
|
131 or did not use the supported formats. |
|
132 """ |
|
133 self.__columns = self.TableDescriptionParser(table_description) |
|
134 self.__data = [] |
|
135 self.custom_properties = {} |
|
136 if custom_properties is not None: |
|
137 self.custom_properties = custom_properties |
|
138 if data: |
|
139 self.LoadData(data) |
|
140 |
|
141 @staticmethod |
|
142 def _EscapeValueForCsv(v): |
|
143 """Escapes the value for use in a CSV file. |
|
144 |
|
145 Puts the string in double-quotes, and escapes any inner double-quotes by |
|
146 doubling them. |
|
147 |
|
148 Args: |
|
149 v: The value to escape. |
|
150 |
|
151 Returns: |
|
152 The escaped values. |
|
153 """ |
|
154 return '"%s"' % v.replace('"', '""') |
|
155 |
|
156 @staticmethod |
|
157 def _EscapeValue(v): |
|
158 """Puts the string in quotes, and escapes any inner quotes and slashes.""" |
|
159 if isinstance(v, unicode): |
|
160 # Here we use repr as in the usual case, but on unicode strings, it |
|
161 # also escapes the unicode characters (which we want to leave as is). |
|
162 # So, after repr() we decode using raw-unicode-escape, which decodes |
|
163 # only the unicode characters, and leaves all the rest (", ', \n and |
|
164 # more) escaped. |
|
165 # We don't take the first character, because repr adds a u in the |
|
166 # beginning of the string (usual repr output for unicode is u'...'). |
|
167 return repr(v).decode("raw-unicode-escape")[1:] |
|
168 # Here we use python built-in escaping mechanism for string using repr. |
|
169 return repr(str(v)) |
|
170 |
|
171 @staticmethod |
|
172 def _EscapeCustomProperties(custom_properties): |
|
173 """Escapes the custom properties dictionary.""" |
|
174 l = [] |
|
175 for key, value in custom_properties.iteritems(): |
|
176 l.append("%s:%s" % (DataTable._EscapeValue(key), |
|
177 DataTable._EscapeValue(value))) |
|
178 return "{%s}" % ",".join(l) |
|
179 |
|
180 @staticmethod |
|
181 def SingleValueToJS(value, value_type, escape_func=None): |
|
182 """Translates a single value and type into a JS value. |
|
183 |
|
184 Internal helper method. |
|
185 |
|
186 Args: |
|
187 value: The value which should be converted |
|
188 value_type: One of "string", "number", "boolean", "date", "datetime" or |
|
189 "timeofday". |
|
190 escape_func: The function to use for escaping strings. |
|
191 |
|
192 Returns: |
|
193 The proper JS format (as string) of the given value according to the |
|
194 given value_type. For None, we simply return "null". |
|
195 If a tuple is given, it should be in one of the following forms: |
|
196 - (value, formatted value) |
|
197 - (value, formatted value, custom properties) |
|
198 where the formatted value is a string, and custom properties is a |
|
199 dictionary of the custom properties for this cell. |
|
200 To specify custom properties without specifying formatted value, one can |
|
201 pass None as the formatted value. |
|
202 One can also have a null-valued cell with formatted value and/or custom |
|
203 properties by specifying None for the value. |
|
204 This method ignores the custom properties except for checking that it is a |
|
205 dictionary. The custom properties are handled in the ToJSon and ToJSCode |
|
206 methods. |
|
207 The real type of the given value is not strictly checked. For example, |
|
208 any type can be used for string - as we simply take its str( ) and for |
|
209 boolean value we just check "if value". |
|
210 Examples: |
|
211 SingleValueToJS(None, "boolean") returns "null" |
|
212 SingleValueToJS(False, "boolean") returns "false" |
|
213 SingleValueToJS((5, "5$"), "number") returns ("5", "'5$'") |
|
214 SingleValueToJS((None, "5$"), "number") returns ("null", "'5$'") |
|
215 |
|
216 Raises: |
|
217 DataTableException: The value and type did not match in a not-recoverable |
|
218 way, for example given value 'abc' for type 'number'. |
|
219 """ |
|
220 if escape_func is None: |
|
221 escape_func = DataTable._EscapeValue |
|
222 if isinstance(value, tuple): |
|
223 # In case of a tuple, we run the same function on the value itself and |
|
224 # add the formatted value. |
|
225 if (len(value) not in [2, 3] or |
|
226 (len(value) == 3 and not isinstance(value[2], dict))): |
|
227 raise DataTableException("Wrong format for value and formatting - %s." % |
|
228 str(value)) |
|
229 if not isinstance(value[1], types.StringTypes + (types.NoneType,)): |
|
230 raise DataTableException("Formatted value is not string, given %s." % |
|
231 type(value[1])) |
|
232 js_value = DataTable.SingleValueToJS(value[0], value_type) |
|
233 if value[1] is None: |
|
234 return (js_value, None) |
|
235 return (js_value, escape_func(value[1])) |
|
236 |
|
237 # The standard case - no formatting. |
|
238 t_value = type(value) |
|
239 if value is None: |
|
240 return "null" |
|
241 if value_type == "boolean": |
|
242 if value: |
|
243 return "true" |
|
244 return "false" |
|
245 |
|
246 elif value_type == "number": |
|
247 if isinstance(value, (int, long, float)): |
|
248 return str(value) |
|
249 raise DataTableException("Wrong type %s when expected number" % t_value) |
|
250 |
|
251 elif value_type == "string": |
|
252 if isinstance(value, tuple): |
|
253 raise DataTableException("Tuple is not allowed as string value.") |
|
254 return escape_func(value) |
|
255 |
|
256 elif value_type == "date": |
|
257 if not isinstance(value, (datetime.date, datetime.datetime)): |
|
258 raise DataTableException("Wrong type %s when expected date" % t_value) |
|
259 # We need to shift the month by 1 to match JS Date format |
|
260 return "new Date(%d,%d,%d)" % (value.year, value.month - 1, value.day) |
|
261 |
|
262 elif value_type == "timeofday": |
|
263 if not isinstance(value, (datetime.time, datetime.datetime)): |
|
264 raise DataTableException("Wrong type %s when expected time" % t_value) |
|
265 return "[%d,%d,%d]" % (value.hour, value.minute, value.second) |
|
266 |
|
267 elif value_type == "datetime": |
|
268 if not isinstance(value, datetime.datetime): |
|
269 raise DataTableException("Wrong type %s when expected datetime" % |
|
270 t_value) |
|
271 return "new Date(%d,%d,%d,%d,%d,%d)" % (value.year, |
|
272 value.month - 1, # To match JS |
|
273 value.day, |
|
274 value.hour, |
|
275 value.minute, |
|
276 value.second) |
|
277 # If we got here, it means the given value_type was not one of the |
|
278 # supported types. |
|
279 raise DataTableException("Unsupported type %s" % value_type) |
|
280 |
|
281 @staticmethod |
|
282 def ColumnTypeParser(description): |
|
283 """Parses a single column description. Internal helper method. |
|
284 |
|
285 Args: |
|
286 description: a column description in the possible formats: |
|
287 'id' |
|
288 ('id',) |
|
289 ('id', 'type') |
|
290 ('id', 'type', 'label') |
|
291 ('id', 'type', 'label', {'custom_prop1': 'custom_val1'}) |
|
292 Returns: |
|
293 Dictionary with the following keys: id, label, type, and |
|
294 custom_properties where: |
|
295 - If label not given, it equals the id. |
|
296 - If type not given, string is used by default. |
|
297 - If custom properties are not given, an empty dictionary is used by |
|
298 default. |
|
299 |
|
300 Raises: |
|
301 DataTableException: The column description did not match the RE. |
|
302 """ |
|
303 if not description: |
|
304 raise DataTableException("Description error: empty description given") |
|
305 |
|
306 if not isinstance(description, (types.StringTypes, tuple)): |
|
307 raise DataTableException("Description error: expected either string or " |
|
308 "tuple, got %s." % type(description)) |
|
309 |
|
310 if isinstance(description, types.StringTypes): |
|
311 description = (description,) |
|
312 |
|
313 # According to the tuple's length, we fill the keys |
|
314 # We verify everything is of type string |
|
315 for elem in description[:3]: |
|
316 if not isinstance(elem, types.StringTypes): |
|
317 raise DataTableException("Description error: expected tuple of " |
|
318 "strings, current element of type %s." % |
|
319 type(elem)) |
|
320 desc_dict = {"id": description[0], |
|
321 "label": description[0], |
|
322 "type": "string", |
|
323 "custom_properties": {}} |
|
324 if len(description) > 1: |
|
325 desc_dict["type"] = description[1].lower() |
|
326 if len(description) > 2: |
|
327 desc_dict["label"] = description[2] |
|
328 if len(description) > 3: |
|
329 if not isinstance(description[3], dict): |
|
330 raise DataTableException("Description error: expected custom " |
|
331 "properties of type dict, current element " |
|
332 "of type %s." % type(description[3])) |
|
333 desc_dict["custom_properties"] = description[3] |
|
334 if len(description) > 4: |
|
335 raise DataTableException("Description error: tuple of length > 4") |
|
336 return desc_dict |
|
337 |
|
338 @staticmethod |
|
339 def TableDescriptionParser(table_description, depth=0): |
|
340 """Parses the table_description object for internal use. |
|
341 |
|
342 Parses the user-submitted table description into an internal format used |
|
343 by the Python DataTable class. Returns the flat list of parsed columns. |
|
344 |
|
345 Args: |
|
346 table_description: A description of the table which should comply |
|
347 with one of the formats described below. |
|
348 depth: Optional. The depth of the first level in the current description. |
|
349 Used by recursive calls to this function. |
|
350 |
|
351 Returns: |
|
352 List of columns, where each column represented by a dictionary with the |
|
353 keys: id, label, type, depth, container which means the following: |
|
354 - id: the id of the column |
|
355 - name: The name of the column |
|
356 - type: The datatype of the elements in this column. Allowed types are |
|
357 described in ColumnTypeParser(). |
|
358 - depth: The depth of this column in the table description |
|
359 - container: 'dict', 'iter' or 'scalar' for parsing the format easily. |
|
360 - custom_properties: The custom properties for this column. |
|
361 The returned description is flattened regardless of how it was given. |
|
362 |
|
363 Raises: |
|
364 DataTableException: Error in a column description or in the description |
|
365 structure. |
|
366 |
|
367 Examples: |
|
368 A column description can be of the following forms: |
|
369 'id' |
|
370 ('id',) |
|
371 ('id', 'type') |
|
372 ('id', 'type', 'label') |
|
373 ('id', 'type', 'label', {'custom_prop1': 'custom_val1'}) |
|
374 or as a dictionary: |
|
375 'id': 'type' |
|
376 'id': ('type',) |
|
377 'id': ('type', 'label') |
|
378 'id': ('type', 'label', {'custom_prop1': 'custom_val1'}) |
|
379 If the type is not specified, we treat it as string. |
|
380 If no specific label is given, the label is simply the id. |
|
381 If no custom properties are given, we use an empty dictionary. |
|
382 |
|
383 input: [('a', 'date'), ('b', 'timeofday', 'b', {'foo': 'bar'})] |
|
384 output: [{'id': 'a', 'label': 'a', 'type': 'date', |
|
385 'depth': 0, 'container': 'iter', 'custom_properties': {}}, |
|
386 {'id': 'b', 'label': 'b', 'type': 'timeofday', |
|
387 'depth': 0, 'container': 'iter', |
|
388 'custom_properties': {'foo': 'bar'}}] |
|
389 |
|
390 input: {'a': [('b', 'number'), ('c', 'string', 'column c')]} |
|
391 output: [{'id': 'a', 'label': 'a', 'type': 'string', |
|
392 'depth': 0, 'container': 'dict', 'custom_properties': {}}, |
|
393 {'id': 'b', 'label': 'b', 'type': 'number', |
|
394 'depth': 1, 'container': 'iter', 'custom_properties': {}}, |
|
395 {'id': 'c', 'label': 'column c', 'type': 'string', |
|
396 'depth': 1, 'container': 'iter', 'custom_properties': {}}] |
|
397 |
|
398 input: {('a', 'number', 'column a'): { 'b': 'number', 'c': 'string'}} |
|
399 output: [{'id': 'a', 'label': 'column a', 'type': 'number', |
|
400 'depth': 0, 'container': 'dict', 'custom_properties': {}}, |
|
401 {'id': 'b', 'label': 'b', 'type': 'number', |
|
402 'depth': 1, 'container': 'dict', 'custom_properties': {}}, |
|
403 {'id': 'c', 'label': 'c', 'type': 'string', |
|
404 'depth': 1, 'container': 'dict', 'custom_properties': {}}] |
|
405 |
|
406 input: { ('w', 'string', 'word'): ('c', 'number', 'count') } |
|
407 output: [{'id': 'w', 'label': 'word', 'type': 'string', |
|
408 'depth': 0, 'container': 'dict', 'custom_properties': {}}, |
|
409 {'id': 'c', 'label': 'count', 'type': 'number', |
|
410 'depth': 1, 'container': 'scalar', 'custom_properties': {}}] |
|
411 """ |
|
412 # For the recursion step, we check for a scalar object (string or tuple) |
|
413 if isinstance(table_description, (types.StringTypes, tuple)): |
|
414 parsed_col = DataTable.ColumnTypeParser(table_description) |
|
415 parsed_col["depth"] = depth |
|
416 parsed_col["container"] = "scalar" |
|
417 return [parsed_col] |
|
418 |
|
419 # Since it is not scalar, table_description must be iterable. |
|
420 if not hasattr(table_description, "__iter__"): |
|
421 raise DataTableException("Expected an iterable object, got %s" % |
|
422 type(table_description)) |
|
423 if not isinstance(table_description, dict): |
|
424 # We expects a non-dictionary iterable item. |
|
425 columns = [] |
|
426 for desc in table_description: |
|
427 parsed_col = DataTable.ColumnTypeParser(desc) |
|
428 parsed_col["depth"] = depth |
|
429 parsed_col["container"] = "iter" |
|
430 columns.append(parsed_col) |
|
431 if not columns: |
|
432 raise DataTableException("Description iterable objects should not" |
|
433 " be empty.") |
|
434 return columns |
|
435 # The other case is a dictionary |
|
436 if not table_description: |
|
437 raise DataTableException("Empty dictionaries are not allowed inside" |
|
438 " description") |
|
439 |
|
440 # The number of keys in the dictionary separates between the two cases of |
|
441 # more levels below or this is the most inner dictionary. |
|
442 if len(table_description) != 1: |
|
443 # This is the most inner dictionary. Parsing types. |
|
444 columns = [] |
|
445 # We sort the items, equivalent to sort the keys since they are unique |
|
446 for key, value in sorted(table_description.items()): |
|
447 # We parse the column type as (key, type) or (key, type, label) using |
|
448 # ColumnTypeParser. |
|
449 if isinstance(value, tuple): |
|
450 parsed_col = DataTable.ColumnTypeParser((key,) + value) |
|
451 else: |
|
452 parsed_col = DataTable.ColumnTypeParser((key, value)) |
|
453 parsed_col["depth"] = depth |
|
454 parsed_col["container"] = "dict" |
|
455 columns.append(parsed_col) |
|
456 return columns |
|
457 # This is an outer dictionary, must have at most one key. |
|
458 parsed_col = DataTable.ColumnTypeParser(table_description.keys()[0]) |
|
459 parsed_col["depth"] = depth |
|
460 parsed_col["container"] = "dict" |
|
461 return ([parsed_col] + |
|
462 DataTable.TableDescriptionParser(table_description.values()[0], |
|
463 depth=depth + 1)) |
|
464 |
|
465 @property |
|
466 def columns(self): |
|
467 """Returns the parsed table description.""" |
|
468 return self.__columns |
|
469 |
|
470 def NumberOfRows(self): |
|
471 """Returns the number of rows in the current data stored in the table.""" |
|
472 return len(self.__data) |
|
473 |
|
474 def SetRowsCustomProperties(self, rows, custom_properties): |
|
475 """Sets the custom properties for given row(s). |
|
476 |
|
477 Can accept a single row or an iterable of rows. |
|
478 Sets the given custom properties for all specified rows. |
|
479 |
|
480 Args: |
|
481 rows: The row, or rows, to set the custom properties for. |
|
482 custom_properties: A string to string dictionary of custom properties to |
|
483 set for all rows. |
|
484 """ |
|
485 if not hasattr(rows, "__iter__"): |
|
486 rows = [rows] |
|
487 for row in rows: |
|
488 self.__data[row] = (self.__data[row][0], custom_properties) |
|
489 |
|
490 def LoadData(self, data, custom_properties=None): |
|
491 """Loads new rows to the data table, clearing existing rows. |
|
492 |
|
493 May also set the custom_properties for the added rows. The given custom |
|
494 properties dictionary specifies the dictionary that will be used for *all* |
|
495 given rows. |
|
496 |
|
497 Args: |
|
498 data: The rows that the table will contain. |
|
499 custom_properties: A dictionary of string to string to set as the custom |
|
500 properties for all rows. |
|
501 """ |
|
502 self.__data = [] |
|
503 self.AppendData(data, custom_properties) |
|
504 |
|
505 def AppendData(self, data, custom_properties=None): |
|
506 """Appends new data to the table. |
|
507 |
|
508 Data is appended in rows. Data must comply with |
|
509 the table schema passed in to __init__(). See SingleValueToJS() for a list |
|
510 of acceptable data types. See the class documentation for more information |
|
511 and examples of schema and data values. |
|
512 |
|
513 Args: |
|
514 data: The row to add to the table. The data must conform to the table |
|
515 description format. |
|
516 custom_properties: A dictionary of string to string, representing the |
|
517 custom properties to add to all the rows. |
|
518 |
|
519 Raises: |
|
520 DataTableException: The data structure does not match the description. |
|
521 """ |
|
522 # If the maximal depth is 0, we simply iterate over the data table |
|
523 # lines and insert them using _InnerAppendData. Otherwise, we simply |
|
524 # let the _InnerAppendData handle all the levels. |
|
525 if not self.__columns[-1]["depth"]: |
|
526 for row in data: |
|
527 self._InnerAppendData(({}, custom_properties), row, 0) |
|
528 else: |
|
529 self._InnerAppendData(({}, custom_properties), data, 0) |
|
530 |
|
531 def _InnerAppendData(self, prev_col_values, data, col_index): |
|
532 """Inner function to assist LoadData.""" |
|
533 # We first check that col_index has not exceeded the columns size |
|
534 if col_index >= len(self.__columns): |
|
535 raise DataTableException("The data does not match description, too deep") |
|
536 |
|
537 # Dealing with the scalar case, the data is the last value. |
|
538 if self.__columns[col_index]["container"] == "scalar": |
|
539 prev_col_values[0][self.__columns[col_index]["id"]] = data |
|
540 self.__data.append(prev_col_values) |
|
541 return |
|
542 |
|
543 if self.__columns[col_index]["container"] == "iter": |
|
544 if not hasattr(data, "__iter__") or isinstance(data, dict): |
|
545 raise DataTableException("Expected iterable object, got %s" % |
|
546 type(data)) |
|
547 # We only need to insert the rest of the columns |
|
548 # If there are less items than expected, we only add what there is. |
|
549 for value in data: |
|
550 if col_index >= len(self.__columns): |
|
551 raise DataTableException("Too many elements given in data") |
|
552 prev_col_values[0][self.__columns[col_index]["id"]] = value |
|
553 col_index += 1 |
|
554 self.__data.append(prev_col_values) |
|
555 return |
|
556 |
|
557 # We know the current level is a dictionary, we verify the type. |
|
558 if not isinstance(data, dict): |
|
559 raise DataTableException("Expected dictionary at current level, got %s" % |
|
560 type(data)) |
|
561 # We check if this is the last level |
|
562 if self.__columns[col_index]["depth"] == self.__columns[-1]["depth"]: |
|
563 # We need to add the keys in the dictionary as they are |
|
564 for col in self.__columns[col_index:]: |
|
565 if col["id"] in data: |
|
566 prev_col_values[0][col["id"]] = data[col["id"]] |
|
567 self.__data.append(prev_col_values) |
|
568 return |
|
569 |
|
570 # We have a dictionary in an inner depth level. |
|
571 if not data.keys(): |
|
572 # In case this is an empty dictionary, we add a record with the columns |
|
573 # filled only until this point. |
|
574 self.__data.append(prev_col_values) |
|
575 else: |
|
576 for key in sorted(data): |
|
577 col_values = dict(prev_col_values[0]) |
|
578 col_values[self.__columns[col_index]["id"]] = key |
|
579 self._InnerAppendData((col_values, prev_col_values[1]), |
|
580 data[key], col_index + 1) |
|
581 |
|
582 def _PreparedData(self, order_by=()): |
|
583 """Prepares the data for enumeration - sorting it by order_by. |
|
584 |
|
585 Args: |
|
586 order_by: Optional. Specifies the name of the column(s) to sort by, and |
|
587 (optionally) which direction to sort in. Default sort direction |
|
588 is asc. Following formats are accepted: |
|
589 "string_col_name" -- For a single key in default (asc) order. |
|
590 ("string_col_name", "asc|desc") -- For a single key. |
|
591 [("col_1","asc|desc"), ("col_2","asc|desc")] -- For more than |
|
592 one column, an array of tuples of (col_name, "asc|desc"). |
|
593 |
|
594 Returns: |
|
595 The data sorted by the keys given. |
|
596 |
|
597 Raises: |
|
598 DataTableException: Sort direction not in 'asc' or 'desc' |
|
599 """ |
|
600 if not order_by: |
|
601 return self.__data |
|
602 |
|
603 proper_sort_keys = [] |
|
604 if isinstance(order_by, types.StringTypes) or ( |
|
605 isinstance(order_by, tuple) and len(order_by) == 2 and |
|
606 order_by[1].lower() in ["asc", "desc"]): |
|
607 order_by = (order_by,) |
|
608 for key in order_by: |
|
609 if isinstance(key, types.StringTypes): |
|
610 proper_sort_keys.append((key, 1)) |
|
611 elif (isinstance(key, (list, tuple)) and len(key) == 2 and |
|
612 key[1].lower() in ("asc", "desc")): |
|
613 proper_sort_keys.append((key[0], key[1].lower() == "asc" and 1 or -1)) |
|
614 else: |
|
615 raise DataTableException("Expected tuple with second value: " |
|
616 "'asc' or 'desc'") |
|
617 |
|
618 def SortCmpFunc(row1, row2): |
|
619 """cmp function for sorted. Compares by keys and 'asc'/'desc' keywords.""" |
|
620 for key, asc_mult in proper_sort_keys: |
|
621 cmp_result = asc_mult * cmp(row1[0].get(key), row2[0].get(key)) |
|
622 if cmp_result: |
|
623 return cmp_result |
|
624 return 0 |
|
625 |
|
626 return sorted(self.__data, cmp=SortCmpFunc) |
|
627 |
|
628 def ToJSCode(self, name, columns_order=None, order_by=()): |
|
629 """Writes the data table as a JS code string. |
|
630 |
|
631 This method writes a string of JS code that can be run to |
|
632 generate a DataTable with the specified data. Typically used for debugging |
|
633 only. |
|
634 |
|
635 Args: |
|
636 name: The name of the table. The name would be used as the DataTable's |
|
637 variable name in the created JS code. |
|
638 columns_order: Optional. Specifies the order of columns in the |
|
639 output table. Specify a list of all column IDs in the order |
|
640 in which you want the table created. |
|
641 Note that you must list all column IDs in this parameter, |
|
642 if you use it. |
|
643 order_by: Optional. Specifies the name of the column(s) to sort by. |
|
644 Passed as is to _PreparedData. |
|
645 |
|
646 Returns: |
|
647 A string of JS code that, when run, generates a DataTable with the given |
|
648 name and the data stored in the DataTable object. |
|
649 Example result: |
|
650 "var tab1 = new google.visualization.DataTable(); |
|
651 tab1.addColumn('string', 'a', 'a'); |
|
652 tab1.addColumn('number', 'b', 'b'); |
|
653 tab1.addColumn('boolean', 'c', 'c'); |
|
654 tab1.addRows(10); |
|
655 tab1.setCell(0, 0, 'a'); |
|
656 tab1.setCell(0, 1, 1, null, {'foo': 'bar'}); |
|
657 tab1.setCell(0, 2, true); |
|
658 ... |
|
659 tab1.setCell(9, 0, 'c'); |
|
660 tab1.setCell(9, 1, 3, '3$'); |
|
661 tab1.setCell(9, 2, false);" |
|
662 |
|
663 Raises: |
|
664 DataTableException: The data does not match the type. |
|
665 """ |
|
666 if columns_order is None: |
|
667 columns_order = [col["id"] for col in self.__columns] |
|
668 col_dict = dict([(col["id"], col) for col in self.__columns]) |
|
669 |
|
670 # We first create the table with the given name |
|
671 jscode = "var %s = new google.visualization.DataTable();\n" % name |
|
672 if self.custom_properties: |
|
673 jscode += "%s.setTableProperties(%s);\n" % ( |
|
674 name, DataTable._EscapeCustomProperties(self.custom_properties)) |
|
675 |
|
676 # We add the columns to the table |
|
677 for i, col in enumerate(columns_order): |
|
678 jscode += "%s.addColumn('%s', '%s', '%s');\n" % (name, |
|
679 col_dict[col]["type"], |
|
680 col_dict[col]["label"], |
|
681 col_dict[col]["id"]) |
|
682 if col_dict[col]["custom_properties"]: |
|
683 jscode += "%s.setColumnProperties(%d, %s);\n" % ( |
|
684 name, i, DataTable._EscapeCustomProperties( |
|
685 col_dict[col]["custom_properties"])) |
|
686 jscode += "%s.addRows(%d);\n" % (name, len(self.__data)) |
|
687 |
|
688 # We now go over the data and add each row |
|
689 for (i, (row, cp)) in enumerate(self._PreparedData(order_by)): |
|
690 # We add all the elements of this row by their order |
|
691 for (j, col) in enumerate(columns_order): |
|
692 if col not in row or row[col] is None: |
|
693 continue |
|
694 cell_cp = "" |
|
695 if isinstance(row[col], tuple) and len(row[col]) == 3: |
|
696 cell_cp = ", %s" % DataTable._EscapeCustomProperties(row[col][2]) |
|
697 value = self.SingleValueToJS(row[col], col_dict[col]["type"]) |
|
698 if isinstance(value, tuple): |
|
699 # We have a formatted value or custom property as well |
|
700 if value[1] is None: |
|
701 value = (value[0], "null") |
|
702 jscode += ("%s.setCell(%d, %d, %s, %s%s);\n" % |
|
703 (name, i, j, value[0], value[1], cell_cp)) |
|
704 else: |
|
705 jscode += "%s.setCell(%d, %d, %s);\n" % (name, i, j, value) |
|
706 if cp: |
|
707 jscode += "%s.setRowProperties(%d, %s);\n" % ( |
|
708 name, i, DataTable._EscapeCustomProperties(cp)) |
|
709 return jscode |
|
710 |
|
711 def ToHtml(self, columns_order=None, order_by=()): |
|
712 """Writes the data table as an HTML table code string. |
|
713 |
|
714 Args: |
|
715 columns_order: Optional. Specifies the order of columns in the |
|
716 output table. Specify a list of all column IDs in the order |
|
717 in which you want the table created. |
|
718 Note that you must list all column IDs in this parameter, |
|
719 if you use it. |
|
720 order_by: Optional. Specifies the name of the column(s) to sort by. |
|
721 Passed as is to _PreparedData. |
|
722 |
|
723 Returns: |
|
724 An HTML table code string. |
|
725 Example result (the result is without the newlines): |
|
726 <html><body><table border='1'> |
|
727 <thead><tr><th>a</th><th>b</th><th>c</th></tr></thead> |
|
728 <tbody> |
|
729 <tr><td>1</td><td>"z"</td><td>2</td></tr> |
|
730 <tr><td>"3$"</td><td>"w"</td><td></td></tr> |
|
731 </tbody> |
|
732 </table></body></html> |
|
733 |
|
734 Raises: |
|
735 DataTableException: The data does not match the type. |
|
736 """ |
|
737 table_template = "<html><body><table border='1'>%s</table></body></html>" |
|
738 columns_template = "<thead><tr>%s</tr></thead>" |
|
739 rows_template = "<tbody>%s</tbody>" |
|
740 row_template = "<tr>%s</tr>" |
|
741 header_cell_template = "<th>%s</th>" |
|
742 cell_template = "<td>%s</td>" |
|
743 |
|
744 if columns_order is None: |
|
745 columns_order = [col["id"] for col in self.__columns] |
|
746 col_dict = dict([(col["id"], col) for col in self.__columns]) |
|
747 |
|
748 columns_list = [] |
|
749 for col in columns_order: |
|
750 columns_list.append(header_cell_template % col_dict[col]["label"]) |
|
751 columns_html = columns_template % "".join(columns_list) |
|
752 |
|
753 rows_list = [] |
|
754 # We now go over the data and add each row |
|
755 for row, unused_cp in self._PreparedData(order_by): |
|
756 cells_list = [] |
|
757 # We add all the elements of this row by their order |
|
758 for col in columns_order: |
|
759 # For empty string we want empty quotes (""). |
|
760 value = "" |
|
761 if col in row and row[col] is not None: |
|
762 value = self.SingleValueToJS(row[col], col_dict[col]["type"]) |
|
763 if isinstance(value, tuple): |
|
764 # We have a formatted value and we're going to use it |
|
765 cells_list.append(cell_template % cgi.escape(value[1])) |
|
766 else: |
|
767 cells_list.append(cell_template % cgi.escape(value)) |
|
768 rows_list.append(row_template % "".join(cells_list)) |
|
769 rows_html = rows_template % "".join(rows_list) |
|
770 |
|
771 return table_template % (columns_html + rows_html) |
|
772 |
|
773 def ToCsv(self, columns_order=None, order_by=(), separator=", "): |
|
774 """Writes the data table as a CSV string. |
|
775 |
|
776 Args: |
|
777 columns_order: Optional. Specifies the order of columns in the |
|
778 output table. Specify a list of all column IDs in the order |
|
779 in which you want the table created. |
|
780 Note that you must list all column IDs in this parameter, |
|
781 if you use it. |
|
782 order_by: Optional. Specifies the name of the column(s) to sort by. |
|
783 Passed as is to _PreparedData. |
|
784 separator: Optional. The separator to use between the values. |
|
785 |
|
786 Returns: |
|
787 A CSV string representing the table. |
|
788 Example result: |
|
789 'a', 'b', 'c' |
|
790 1, 'z', 2 |
|
791 3, 'w', '' |
|
792 |
|
793 Raises: |
|
794 DataTableException: The data does not match the type. |
|
795 """ |
|
796 if columns_order is None: |
|
797 columns_order = [col["id"] for col in self.__columns] |
|
798 col_dict = dict([(col["id"], col) for col in self.__columns]) |
|
799 |
|
800 columns_list = [] |
|
801 for col in columns_order: |
|
802 columns_list.append(DataTable._EscapeValueForCsv(col_dict[col]["label"])) |
|
803 columns_line = separator.join(columns_list) |
|
804 |
|
805 rows_list = [] |
|
806 # We now go over the data and add each row |
|
807 for row, unused_cp in self._PreparedData(order_by): |
|
808 cells_list = [] |
|
809 # We add all the elements of this row by their order |
|
810 for col in columns_order: |
|
811 value = '""' |
|
812 if col in row and row[col] is not None: |
|
813 value = self.SingleValueToJS(row[col], col_dict[col]["type"], |
|
814 DataTable._EscapeValueForCsv) |
|
815 if isinstance(value, tuple): |
|
816 # We have a formatted value. Using it only for date/time types. |
|
817 if col_dict[col]["type"] in ["date", "datetime", "timeofday"]: |
|
818 cells_list.append(value[1]) |
|
819 else: |
|
820 cells_list.append(value[0]) |
|
821 else: |
|
822 # We need to quote date types, because they contain commas. |
|
823 if (col_dict[col]["type"] in ["date", "datetime", "timeofday"] and |
|
824 value != '""'): |
|
825 value = '"%s"' % value |
|
826 cells_list.append(value) |
|
827 rows_list.append(separator.join(cells_list)) |
|
828 rows = "\n".join(rows_list) |
|
829 |
|
830 return "%s\n%s" % (columns_line, rows) |
|
831 |
|
832 def ToTsvExcel(self, columns_order=None, order_by=()): |
|
833 """Returns a file in tab-separated-format readable by MS Excel. |
|
834 |
|
835 Returns a file in UTF-16 little endian encoding, with tabs separating the |
|
836 values. |
|
837 |
|
838 Args: |
|
839 columns_order: Delegated to ToCsv. |
|
840 order_by: Delegated to ToCsv. |
|
841 |
|
842 Returns: |
|
843 A tab-separated little endian UTF16 file representing the table. |
|
844 """ |
|
845 return self.ToCsv( |
|
846 columns_order, order_by, separator="\t").encode("UTF-16LE") |
|
847 |
|
848 def ToJSon(self, columns_order=None, order_by=()): |
|
849 """Writes a JSON string that can be used in a JS DataTable constructor. |
|
850 |
|
851 This method writes a JSON string that can be passed directly into a Google |
|
852 Visualization API DataTable constructor. Use this output if you are |
|
853 hosting the visualization HTML on your site, and want to code the data |
|
854 table in Python. Pass this string into the |
|
855 google.visualization.DataTable constructor, e.g,: |
|
856 ... on my page that hosts my visualization ... |
|
857 google.setOnLoadCallback(drawTable); |
|
858 function drawTable() { |
|
859 var data = new google.visualization.DataTable(_my_JSon_string, 0.6); |
|
860 myTable.draw(data); |
|
861 } |
|
862 |
|
863 Args: |
|
864 columns_order: Optional. Specifies the order of columns in the |
|
865 output table. Specify a list of all column IDs in the order |
|
866 in which you want the table created. |
|
867 Note that you must list all column IDs in this parameter, |
|
868 if you use it. |
|
869 order_by: Optional. Specifies the name of the column(s) to sort by. |
|
870 Passed as is to _PreparedData(). |
|
871 |
|
872 Returns: |
|
873 A JSon constructor string to generate a JS DataTable with the data |
|
874 stored in the DataTable object. |
|
875 Example result (the result is without the newlines): |
|
876 {cols: [{id:'a',label:'a',type:'number'}, |
|
877 {id:'b',label:'b',type:'string'}, |
|
878 {id:'c',label:'c',type:'number'}], |
|
879 rows: [{c:[{v:1},{v:'z'},{v:2}]}, c:{[{v:3,f:'3$'},{v:'w'},{v:null}]}], |
|
880 p: {'foo': 'bar'}} |
|
881 |
|
882 Raises: |
|
883 DataTableException: The data does not match the type. |
|
884 """ |
|
885 if columns_order is None: |
|
886 columns_order = [col["id"] for col in self.__columns] |
|
887 col_dict = dict([(col["id"], col) for col in self.__columns]) |
|
888 |
|
889 # Creating the columns jsons |
|
890 cols_jsons = [] |
|
891 for col_id in columns_order: |
|
892 d = dict(col_dict[col_id]) |
|
893 d["cp"] = "" |
|
894 if col_dict[col_id]["custom_properties"]: |
|
895 d["cp"] = ",p:%s" % DataTable._EscapeCustomProperties( |
|
896 col_dict[col_id]["custom_properties"]) |
|
897 cols_jsons.append( |
|
898 "{id:'%(id)s',label:'%(label)s',type:'%(type)s'%(cp)s}" % d) |
|
899 |
|
900 # Creating the rows jsons |
|
901 rows_jsons = [] |
|
902 for row, cp in self._PreparedData(order_by): |
|
903 cells_jsons = [] |
|
904 for col in columns_order: |
|
905 # We omit the {v:null} for a None value of the not last column |
|
906 value = row.get(col, None) |
|
907 if value is None and col != columns_order[-1]: |
|
908 cells_jsons.append("") |
|
909 else: |
|
910 value = self.SingleValueToJS(value, col_dict[col]["type"]) |
|
911 if isinstance(value, tuple): |
|
912 # We have a formatted value or custom property as well |
|
913 if len(row.get(col)) == 3: |
|
914 if value[1] is None: |
|
915 cells_jsons.append("{v:%s,p:%s}" % ( |
|
916 value[0], |
|
917 DataTable._EscapeCustomProperties(row.get(col)[2]))) |
|
918 else: |
|
919 cells_jsons.append("{v:%s,f:%s,p:%s}" % (value + ( |
|
920 DataTable._EscapeCustomProperties(row.get(col)[2]),))) |
|
921 else: |
|
922 cells_jsons.append("{v:%s,f:%s}" % value) |
|
923 else: |
|
924 cells_jsons.append("{v:%s}" % value) |
|
925 if cp: |
|
926 rows_jsons.append("{c:[%s],p:%s}" % ( |
|
927 ",".join(cells_jsons), DataTable._EscapeCustomProperties(cp))) |
|
928 else: |
|
929 rows_jsons.append("{c:[%s]}" % ",".join(cells_jsons)) |
|
930 |
|
931 general_custom_properties = "" |
|
932 if self.custom_properties: |
|
933 general_custom_properties = ( |
|
934 ",p:%s" % DataTable._EscapeCustomProperties(self.custom_properties)) |
|
935 |
|
936 # We now join the columns jsons and the rows jsons |
|
937 json = "{cols:[%s],rows:[%s]%s}" % (",".join(cols_jsons), |
|
938 ",".join(rows_jsons), |
|
939 general_custom_properties) |
|
940 return json |
|
941 |
|
942 def ToJSonResponse(self, columns_order=None, order_by=(), req_id=0, |
|
943 response_handler="google.visualization.Query.setResponse"): |
|
944 """Writes a table as a JSON response that can be returned as-is to a client. |
|
945 |
|
946 This method writes a JSON response to return to a client in response to a |
|
947 Google Visualization API query. This string can be processed by the calling |
|
948 page, and is used to deliver a data table to a visualization hosted on |
|
949 a different page. |
|
950 |
|
951 Args: |
|
952 columns_order: Optional. Passed straight to self.ToJSon(). |
|
953 order_by: Optional. Passed straight to self.ToJSon(). |
|
954 req_id: Optional. The response id, as retrieved by the request. |
|
955 response_handler: Optional. The response handler, as retrieved by the |
|
956 request. |
|
957 |
|
958 Returns: |
|
959 A JSON response string to be received by JS the visualization Query |
|
960 object. This response would be translated into a DataTable on the |
|
961 client side. |
|
962 Example result (newlines added for readability): |
|
963 google.visualization.Query.setResponse({ |
|
964 'version':'0.6', 'reqId':'0', 'status':'OK', |
|
965 'table': {cols: [...], rows: [...]}}); |
|
966 |
|
967 Note: The URL returning this string can be used as a data source by Google |
|
968 Visualization Gadgets or from JS code. |
|
969 """ |
|
970 table = self.ToJSon(columns_order, order_by) |
|
971 return ("%s({'version':'0.6', 'reqId':'%s', 'status':'OK', " |
|
972 "'table': %s});") % (response_handler, req_id, table) |
|
973 |
|
974 def ToResponse(self, columns_order=None, order_by=(), tqx=""): |
|
975 """Writes the right response according to the request string passed in tqx. |
|
976 |
|
977 This method parses the tqx request string (format of which is defined in |
|
978 the documentation for implementing a data source of Google Visualization), |
|
979 and returns the right response according to the request. |
|
980 It parses out the "out" parameter of tqx, calls the relevant response |
|
981 (ToJSonResponse() for "json", ToCsv() for "csv", ToHtml() for "html", |
|
982 ToTsvExcel() for "tsv-excel") and passes the response function the rest of |
|
983 the relevant request keys. |
|
984 |
|
985 Args: |
|
986 columns_order: Optional. Passed as is to the relevant response function. |
|
987 order_by: Optional. Passed as is to the relevant response function. |
|
988 tqx: Optional. The request string as received by HTTP GET. Should be in |
|
989 the format "key1:value1;key2:value2...". All keys have a default |
|
990 value, so an empty string will just do the default (which is calling |
|
991 ToJSonResponse() with no extra parameters). |
|
992 |
|
993 Returns: |
|
994 A response string, as returned by the relevant response function. |
|
995 |
|
996 Raises: |
|
997 DataTableException: One of the parameters passed in tqx is not supported. |
|
998 """ |
|
999 tqx_dict = {} |
|
1000 if tqx: |
|
1001 tqx_dict = dict(opt.split(":") for opt in tqx.split(";")) |
|
1002 if tqx_dict.get("version", "0.6") != "0.6": |
|
1003 raise DataTableException( |
|
1004 "Version (%s) passed by request is not supported." |
|
1005 % tqx_dict["version"]) |
|
1006 |
|
1007 if tqx_dict.get("out", "json") == "json": |
|
1008 response_handler = tqx_dict.get("responseHandler", |
|
1009 "google.visualization.Query.setResponse") |
|
1010 return self.ToJSonResponse(columns_order, order_by, |
|
1011 req_id=tqx_dict.get("reqId", 0), |
|
1012 response_handler=response_handler) |
|
1013 elif tqx_dict["out"] == "html": |
|
1014 return self.ToHtml(columns_order, order_by) |
|
1015 elif tqx_dict["out"] == "csv": |
|
1016 return self.ToCsv(columns_order, order_by) |
|
1017 elif tqx_dict["out"] == "tsv-excel": |
|
1018 return self.ToTsvExcel(columns_order, order_by) |
|
1019 else: |
|
1020 raise DataTableException( |
|
1021 "'out' parameter: '%s' is not supported" % tqx_dict["out"]) |