• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2#
3# Copyright (c) 2016, Alliance for Open Media. All rights reserved
4#
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.
11#
12
13"""Converts Python data into data for Google Visualization API clients.
14
15This library can be used to create a google.visualization.DataTable usable by
16visualizations built on the Google Visualization API. Output formats are raw
17JSON, JSON response, JavaScript, CSV, and HTML table.
18
19See http://code.google.com/apis/visualization/ for documentation on the
20Google Visualization API.
21"""
22
23__author__ = "Amit Weinstein, Misha Seltzer, Jacob Baskin"
24
25import cgi
26import cStringIO
27import csv
28import datetime
29try:
30  import json
31except ImportError:
32  import simplejson as json
33import types
34
35
36class DataTableException(Exception):
37  """The general exception object thrown by DataTable."""
38  pass
39
40
41class DataTableJSONEncoder(json.JSONEncoder):
42  """JSON encoder that handles date/time/datetime objects correctly."""
43
44  def __init__(self):
45    json.JSONEncoder.__init__(self,
46                              separators=(",", ":"),
47                              ensure_ascii=False)
48
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)
66
67
68class DataTable(object):
69  """Wraps the data to convert to a Google Visualization API DataTable.
70
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.
73
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.
77
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__().
80
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.
85
86  The data and the table_description are closely tied, as described here:
87
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.
93
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')]}
100
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.
103
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:
109
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
115    1  z
116    2  w
117    4  o
118    5  k
119
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
128
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
137  """
138
139  def __init__(self, table_description, data=None, custom_properties=None):
140    """Initialize the data table from a table schema and (optionally) data.
141
142    See the class documentation for more information on table schema and data
143    values.
144
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.
157
158    Raises:
159      DataTableException: Raised if the data and the description did not match,
160                          or did not use the supported formats.
161    """
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)
169
170  @staticmethod
171  def CoerceValue(value, value_type):
172    """Coerces a single value into the type expected for its column.
173
174    Internal helper method.
175
176    Args:
177      value: The value which should be converted
178      value_type: One of "string", "number", "boolean", "date", "datetime" or
179                  "timeofday".
180
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
204
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'.
208    """
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:]
221
222    t_value = type(value)
223    if value is None:
224      return value
225    if value_type == "boolean":
226      return bool(value)
227
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)
232
233    elif value_type == "string":
234      if isinstance(value, unicode):
235        return value
236      else:
237        return str(value).decode("utf-8")
238
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)
246
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)
254
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)
264
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)
290
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")
305
306  @staticmethod
307  def ColumnTypeParser(description):
308    """Parses a single column description. Internal helper method.
309
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.
324
325    Raises:
326      DataTableException: The column description did not match the RE, or
327          unsupported type was passed.
328    """
329    if not description:
330      raise DataTableException("Description error: empty description given")
331
332    if not isinstance(description, (types.StringTypes, tuple)):
333      raise DataTableException("Description error: expected either string or "
334                               "tuple, got %s." % type(description))
335
336    if isinstance(description, types.StringTypes):
337      description = (description,)
338
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
367
368  @staticmethod
369  def TableDescriptionParser(table_description, depth=0):
370    """Parses the table_description object for internal use.
371
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.
374
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.
380
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.
392
393    Raises:
394      DataTableException: Error in a column description or in the description
395                          structure.
396
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.
412
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'}}]
419
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': {}}]
427
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': {}}]
435
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': {}}]
441
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': {}}
447
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.
457    """
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]
464
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")
485
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))
518
519  @property
520  def columns(self):
521    """Returns the parsed table description."""
522    return self.__columns
523
524  def NumberOfRows(self):
525    """Returns the number of rows in the current data stored in the table."""
526    return len(self.__data)
527
528  def SetRowsCustomProperties(self, rows, custom_properties):
529    """Sets the custom properties for given row(s).
530
531    Can accept a single row or an iterable of rows.
532    Sets the given custom properties for all specified rows.
533
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.
538    """
539    if not hasattr(rows, "__iter__"):
540      rows = [rows]
541    for row in rows:
542      self.__data[row] = (self.__data[row][0], custom_properties)
543
544  def LoadData(self, data, custom_properties=None):
545    """Loads new rows to the data table, clearing existing rows.
546
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.
550
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.
555    """
556    self.__data = []
557    self.AppendData(data, custom_properties)
558
559  def AppendData(self, data, custom_properties=None):
560    """Appends new data to the table.
561
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.
566
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.
572
573    Raises:
574      DataTableException: The data structure does not match the description.
575    """
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)
584
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")
590
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
596
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
610
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
623
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)
635
636  def _PreparedData(self, order_by=()):
637    """Prepares the data for enumeration - sorting it by order_by.
638
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").
647
648    Returns:
649      The data sorted by the keys given.
650
651    Raises:
652      DataTableException: Sort direction not in 'asc' or 'desc'
653    """
654    if not order_by:
655      return self.__data
656
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'")
671
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
679
680    return sorted(self.__data, cmp=SortCmpFunc)
681
682  def ToJSCode(self, name, columns_order=None, order_by=()):
683    """Writes the data table as a JS code string.
684
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.
688
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.
699
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);
712         ...
713         tab1.setCell(9, 0, "c");
714         tab1.setCell(9, 1, 3, "3$");
715         tab1.setCell(9, 2, false);"
716
717    Raises:
718      DataTableException: The data does not match the type.
719    """
720
721    encoder = DataTableJSONEncoder()
722
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])
726
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))
732
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))
744
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
768
769  def ToHtml(self, columns_order=None, order_by=()):
770    """Writes the data table as an HTML table code string.
771
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.
780
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>
791
792    Raises:
793      DataTableException: The data does not match the type.
794    """
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>"
801
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])
805
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)
811
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)
829
830    return table_template % (columns_html + rows_html)
831
832  def ToCsv(self, columns_order=None, order_by=(), separator=","):
833    """Writes the data table as a CSV string.
834
835    Output is encoded in UTF-8 because the Python "csv" module can't handle
836    Unicode properly according to its documentation.
837
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.
847
848    Returns:
849      A CSV string representing the table.
850      Example result:
851       'a','b','c'
852       1,'z',2
853       3,'w',''
854
855    Raises:
856      DataTableException: The data does not match the type.
857    """
858
859    csv_buffer = cStringIO.StringIO()
860    writer = csv.writer(csv_buffer, delimiter=separator)
861
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])
865
866    writer.writerow([col_dict[col]["label"].encode("utf-8")
867                     for col in columns_order])
868
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()
887
888  def ToTsvExcel(self, columns_order=None, order_by=()):
889    """Returns a file in tab-separated-format readable by MS Excel.
890
891    Returns a file in UTF-16 little endian encoding, with tabs separating the
892    values.
893
894    Args:
895      columns_order: Delegated to ToCsv.
896      order_by: Delegated to ToCsv.
897
898    Returns:
899      A tab-separated little endian UTF16 file representing the table.
900    """
901    return (self.ToCsv(columns_order, order_by, separator="\t")
902            .decode("utf-8").encode("UTF-16LE"))
903
904  def _ToJSonObj(self, columns_order=None, order_by=()):
905    """Returns an object suitable to be converted to JSON.
906
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().
913
914    Returns:
915      A dictionary object for use by ToJSon or ToJSonResponse.
916    """
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])
920
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)
930
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)
952
953    json_obj = {"cols": col_objs, "rows": row_objs}
954    if self.custom_properties:
955      json_obj["p"] = self.custom_properties
956
957    return json_obj
958
959  def ToJSon(self, columns_order=None, order_by=()):
960    """Returns a string that can be used in a JS DataTable constructor.
961
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);
972      }
973
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().
982
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'}}
992
993    Raises:
994      DataTableException: The data does not match the type.
995    """
996
997    encoder = DataTableJSONEncoder()
998    return encoder.encode(
999        self._ToJSonObj(columns_order, order_by)).encode("utf-8")
1000
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.
1004
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.
1009
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.
1016
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: [...]}});
1025
1026    Note: The URL returning this string can be used as a data source by Google
1027          Visualization Gadgets or from JS code.
1028    """
1029
1030    response_obj = {
1031        "version": "0.6",
1032        "reqId": str(req_id),
1033        "table": self._ToJSonObj(columns_order, order_by),
1034        "status": "ok"
1035    }
1036    encoder = DataTableJSONEncoder()
1037    return "%s(%s);" % (response_handler,
1038                        encoder.encode(response_obj).encode("utf-8"))
1039
1040  def ToResponse(self, columns_order=None, order_by=(), tqx=""):
1041    """Writes the right response according to the request string passed in tqx.
1042
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.
1050
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).
1058
1059    Returns:
1060      A response string, as returned by the relevant response function.
1061
1062    Raises:
1063      DataTableException: One of the parameters passed in tqx is not supported.
1064    """
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"])
1072
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"])
1088