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