| #!/usr/bin/python | |
| # | |
| # Copyright (c) 2016, Alliance for Open Media. All rights reserved | |
| # | |
| # This source code is subject to the terms of the BSD 2 Clause License and | |
| # the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License | |
| # was not distributed with this source code in the LICENSE file, you can | |
| # obtain it at www.aomedia.org/license/software. If the Alliance for Open | |
| # Media Patent License 1.0 was not distributed with this source code in the | |
| # PATENTS file, you can obtain it at www.aomedia.org/license/patent. | |
| # | |
| """Converts Python data into data for Google Visualization API clients. | |
| This library can be used to create a google.visualization.DataTable usable by | |
| visualizations built on the Google Visualization API. Output formats are raw | |
| JSON, JSON response, JavaScript, CSV, and HTML table. | |
| See http://code.google.com/apis/visualization/ for documentation on the | |
| Google Visualization API. | |
| """ | |
| __author__ = "Amit Weinstein, Misha Seltzer, Jacob Baskin" | |
| import cgi | |
| import cStringIO | |
| import csv | |
| import datetime | |
| try: | |
| import json | |
| except ImportError: | |
| import simplejson as json | |
| import types | |
| class DataTableException(Exception): | |
| """The general exception object thrown by DataTable.""" | |
| pass | |
| class DataTableJSONEncoder(json.JSONEncoder): | |
| """JSON encoder that handles date/time/datetime objects correctly.""" | |
| def __init__(self): | |
| json.JSONEncoder.__init__(self, | |
| separators=(",", ":"), | |
| ensure_ascii=False) | |
| def default(self, o): | |
| if isinstance(o, datetime.datetime): | |
| if o.microsecond == 0: | |
| # If the time doesn't have ms-resolution, leave it out to keep | |
| # things smaller. | |
| return "Date(%d,%d,%d,%d,%d,%d)" % ( | |
| o.year, o.month - 1, o.day, o.hour, o.minute, o.second) | |
| else: | |
| return "Date(%d,%d,%d,%d,%d,%d,%d)" % ( | |
| o.year, o.month - 1, o.day, o.hour, o.minute, o.second, | |
| o.microsecond / 1000) | |
| elif isinstance(o, datetime.date): | |
| return "Date(%d,%d,%d)" % (o.year, o.month - 1, o.day) | |
| elif isinstance(o, datetime.time): | |
| return [o.hour, o.minute, o.second] | |
| else: | |
| return super(DataTableJSONEncoder, self).default(o) | |
| class DataTable(object): | |
| """Wraps the data to convert to a Google Visualization API DataTable. | |
| Create this object, populate it with data, then call one of the ToJS... | |
| methods to return a string representation of the data in the format described. | |
| You can clear all data from the object to reuse it, but you cannot clear | |
| individual cells, rows, or columns. You also cannot modify the table schema | |
| specified in the class constructor. | |
| You can add new data one or more rows at a time. All data added to an | |
| instantiated DataTable must conform to the schema passed in to __init__(). | |
| You can reorder the columns in the output table, and also specify row sorting | |
| order by column. The default column order is according to the original | |
| table_description parameter. Default row sort order is ascending, by column | |
| 1 values. For a dictionary, we sort the keys for order. | |
| The data and the table_description are closely tied, as described here: | |
| The table schema is defined in the class constructor's table_description | |
| parameter. The user defines each column using a tuple of | |
| (id[, type[, label[, custom_properties]]]). The default value for type is | |
| string, label is the same as ID if not specified, and custom properties is | |
| an empty dictionary if not specified. | |
| table_description is a dictionary or list, containing one or more column | |
| descriptor tuples, nested dictionaries, and lists. Each dictionary key, list | |
| element, or dictionary element must eventually be defined as | |
| a column description tuple. Here's an example of a dictionary where the key | |
| is a tuple, and the value is a list of two tuples: | |
| {('a', 'number'): [('b', 'number'), ('c', 'string')]} | |
| This flexibility in data entry enables you to build and manipulate your data | |
| in a Python structure that makes sense for your program. | |
| Add data to the table using the same nested design as the table's | |
| table_description, replacing column descriptor tuples with cell data, and | |
| each row is an element in the top level collection. This will be a bit | |
| clearer after you look at the following examples showing the | |
| table_description, matching data, and the resulting table: | |
| Columns as list of tuples [col1, col2, col3] | |
| table_description: [('a', 'number'), ('b', 'string')] | |
| AppendData( [[1, 'z'], [2, 'w'], [4, 'o'], [5, 'k']] ) | |
| Table: | |
| a b <--- these are column ids/labels | |
| 1 z | |
| 2 w | |
| 4 o | |
| 5 k | |
| Dictionary of columns, where key is a column, and value is a list of | |
| columns {col1: [col2, col3]} | |
| table_description: {('a', 'number'): [('b', 'number'), ('c', 'string')]} | |
| AppendData( data: {1: [2, 'z'], 3: [4, 'w']} | |
| Table: | |
| a b c | |
| 1 2 z | |
| 3 4 w | |
| Dictionary where key is a column, and the value is itself a dictionary of | |
| columns {col1: {col2, col3}} | |
| table_description: {('a', 'number'): {'b': 'number', 'c': 'string'}} | |
| AppendData( data: {1: {'b': 2, 'c': 'z'}, 3: {'b': 4, 'c': 'w'}} | |
| Table: | |
| a b c | |
| 1 2 z | |
| 3 4 w | |
| """ | |
| def __init__(self, table_description, data=None, custom_properties=None): | |
| """Initialize the data table from a table schema and (optionally) data. | |
| See the class documentation for more information on table schema and data | |
| values. | |
| Args: | |
| table_description: A table schema, following one of the formats described | |
| in TableDescriptionParser(). Schemas describe the | |
| column names, data types, and labels. See | |
| TableDescriptionParser() for acceptable formats. | |
| data: Optional. If given, fills the table with the given data. The data | |
| structure must be consistent with schema in table_description. See | |
| the class documentation for more information on acceptable data. You | |
| can add data later by calling AppendData(). | |
| custom_properties: Optional. A dictionary from string to string that | |
| goes into the table's custom properties. This can be | |
| later changed by changing self.custom_properties. | |
| Raises: | |
| DataTableException: Raised if the data and the description did not match, | |
| or did not use the supported formats. | |
| """ | |
| self.__columns = self.TableDescriptionParser(table_description) | |
| self.__data = [] | |
| self.custom_properties = {} | |
| if custom_properties is not None: | |
| self.custom_properties = custom_properties | |
| if data: | |
| self.LoadData(data) | |
| @staticmethod | |
| def CoerceValue(value, value_type): | |
| """Coerces a single value into the type expected for its column. | |
| Internal helper method. | |
| Args: | |
| value: The value which should be converted | |
| value_type: One of "string", "number", "boolean", "date", "datetime" or | |
| "timeofday". | |
| Returns: | |
| An item of the Python type appropriate to the given value_type. Strings | |
| are also converted to Unicode using UTF-8 encoding if necessary. | |
| If a tuple is given, it should be in one of the following forms: | |
| - (value, formatted value) | |
| - (value, formatted value, custom properties) | |
| where the formatted value is a string, and custom properties is a | |
| dictionary of the custom properties for this cell. | |
| To specify custom properties without specifying formatted value, one can | |
| pass None as the formatted value. | |
| One can also have a null-valued cell with formatted value and/or custom | |
| properties by specifying None for the value. | |
| This method ignores the custom properties except for checking that it is a | |
| dictionary. The custom properties are handled in the ToJSon and ToJSCode | |
| methods. | |
| The real type of the given value is not strictly checked. For example, | |
| any type can be used for string - as we simply take its str( ) and for | |
| boolean value we just check "if value". | |
| Examples: | |
| CoerceValue(None, "string") returns None | |
| CoerceValue((5, "5$"), "number") returns (5, "5$") | |
| CoerceValue(100, "string") returns "100" | |
| CoerceValue(0, "boolean") returns False | |
| Raises: | |
| DataTableException: The value and type did not match in a not-recoverable | |
| way, for example given value 'abc' for type 'number'. | |
| """ | |
| if isinstance(value, tuple): | |
| # In case of a tuple, we run the same function on the value itself and | |
| # add the formatted value. | |
| if (len(value) not in [2, 3] or | |
| (len(value) == 3 and not isinstance(value[2], dict))): | |
| raise DataTableException("Wrong format for value and formatting - %s." % | |
| str(value)) | |
| if not isinstance(value[1], types.StringTypes + (types.NoneType,)): | |
| raise DataTableException("Formatted value is not string, given %s." % | |
| type(value[1])) | |
| js_value = DataTable.CoerceValue(value[0], value_type) | |
| return (js_value,) + value[1:] | |
| t_value = type(value) | |
| if value is None: | |
| return value | |
| if value_type == "boolean": | |
| return bool(value) | |
| elif value_type == "number": | |
| if isinstance(value, (int, long, float)): | |
| return value | |
| raise DataTableException("Wrong type %s when expected number" % t_value) | |
| elif value_type == "string": | |
| if isinstance(value, unicode): | |
| return value | |
| else: | |
| return str(value).decode("utf-8") | |
| elif value_type == "date": | |
| if isinstance(value, datetime.datetime): | |
| return datetime.date(value.year, value.month, value.day) | |
| elif isinstance(value, datetime.date): | |
| return value | |
| else: | |
| raise DataTableException("Wrong type %s when expected date" % t_value) | |
| elif value_type == "timeofday": | |
| if isinstance(value, datetime.datetime): | |
| return datetime.time(value.hour, value.minute, value.second) | |
| elif isinstance(value, datetime.time): | |
| return value | |
| else: | |
| raise DataTableException("Wrong type %s when expected time" % t_value) | |
| elif value_type == "datetime": | |
| if isinstance(value, datetime.datetime): | |
| return value | |
| else: | |
| raise DataTableException("Wrong type %s when expected datetime" % | |
| t_value) | |
| # If we got here, it means the given value_type was not one of the | |
| # supported types. | |
| raise DataTableException("Unsupported type %s" % value_type) | |
| @staticmethod | |
| def EscapeForJSCode(encoder, value): | |
| if value is None: | |
| return "null" | |
| elif isinstance(value, datetime.datetime): | |
| if value.microsecond == 0: | |
| # If it's not ms-resolution, leave that out to save space. | |
| return "new Date(%d,%d,%d,%d,%d,%d)" % (value.year, | |
| value.month - 1, # To match JS | |
| value.day, | |
| value.hour, | |
| value.minute, | |
| value.second) | |
| else: | |
| return "new Date(%d,%d,%d,%d,%d,%d,%d)" % (value.year, | |
| value.month - 1, # match JS | |
| value.day, | |
| value.hour, | |
| value.minute, | |
| value.second, | |
| value.microsecond / 1000) | |
| elif isinstance(value, datetime.date): | |
| return "new Date(%d,%d,%d)" % (value.year, value.month - 1, value.day) | |
| else: | |
| return encoder.encode(value) | |
| @staticmethod | |
| def ToString(value): | |
| if value is None: | |
| return "(empty)" | |
| elif isinstance(value, (datetime.datetime, | |
| datetime.date, | |
| datetime.time)): | |
| return str(value) | |
| elif isinstance(value, unicode): | |
| return value | |
| elif isinstance(value, bool): | |
| return str(value).lower() | |
| else: | |
| return str(value).decode("utf-8") | |
| @staticmethod | |
| def ColumnTypeParser(description): | |
| """Parses a single column description. Internal helper method. | |
| Args: | |
| description: a column description in the possible formats: | |
| 'id' | |
| ('id',) | |
| ('id', 'type') | |
| ('id', 'type', 'label') | |
| ('id', 'type', 'label', {'custom_prop1': 'custom_val1'}) | |
| Returns: | |
| Dictionary with the following keys: id, label, type, and | |
| custom_properties where: | |
| - If label not given, it equals the id. | |
| - If type not given, string is used by default. | |
| - If custom properties are not given, an empty dictionary is used by | |
| default. | |
| Raises: | |
| DataTableException: The column description did not match the RE, or | |
| unsupported type was passed. | |
| """ | |
| if not description: | |
| raise DataTableException("Description error: empty description given") | |
| if not isinstance(description, (types.StringTypes, tuple)): | |
| raise DataTableException("Description error: expected either string or " | |
| "tuple, got %s." % type(description)) | |
| if isinstance(description, types.StringTypes): | |
| description = (description,) | |
| # According to the tuple's length, we fill the keys | |
| # We verify everything is of type string | |
| for elem in description[:3]: | |
| if not isinstance(elem, types.StringTypes): | |
| raise DataTableException("Description error: expected tuple of " | |
| "strings, current element of type %s." % | |
| type(elem)) | |
| desc_dict = {"id": description[0], | |
| "label": description[0], | |
| "type": "string", | |
| "custom_properties": {}} | |
| if len(description) > 1: | |
| desc_dict["type"] = description[1].lower() | |
| if len(description) > 2: | |
| desc_dict["label"] = description[2] | |
| if len(description) > 3: | |
| if not isinstance(description[3], dict): | |
| raise DataTableException("Description error: expected custom " | |
| "properties of type dict, current element " | |
| "of type %s." % type(description[3])) | |
| desc_dict["custom_properties"] = description[3] | |
| if len(description) > 4: | |
| raise DataTableException("Description error: tuple of length > 4") | |
| if desc_dict["type"] not in ["string", "number", "boolean", | |
| "date", "datetime", "timeofday"]: | |
| raise DataTableException( | |
| "Description error: unsupported type '%s'" % desc_dict["type"]) | |
| return desc_dict | |
| @staticmethod | |
| def TableDescriptionParser(table_description, depth=0): | |
| """Parses the table_description object for internal use. | |
| Parses the user-submitted table description into an internal format used | |
| by the Python DataTable class. Returns the flat list of parsed columns. | |
| Args: | |
| table_description: A description of the table which should comply | |
| with one of the formats described below. | |
| depth: Optional. The depth of the first level in the current description. | |
| Used by recursive calls to this function. | |
| Returns: | |
| List of columns, where each column represented by a dictionary with the | |
| keys: id, label, type, depth, container which means the following: | |
| - id: the id of the column | |
| - name: The name of the column | |
| - type: The datatype of the elements in this column. Allowed types are | |
| described in ColumnTypeParser(). | |
| - depth: The depth of this column in the table description | |
| - container: 'dict', 'iter' or 'scalar' for parsing the format easily. | |
| - custom_properties: The custom properties for this column. | |
| The returned description is flattened regardless of how it was given. | |
| Raises: | |
| DataTableException: Error in a column description or in the description | |
| structure. | |
| Examples: | |
| A column description can be of the following forms: | |
| 'id' | |
| ('id',) | |
| ('id', 'type') | |
| ('id', 'type', 'label') | |
| ('id', 'type', 'label', {'custom_prop1': 'custom_val1'}) | |
| or as a dictionary: | |
| 'id': 'type' | |
| 'id': ('type',) | |
| 'id': ('type', 'label') | |
| 'id': ('type', 'label', {'custom_prop1': 'custom_val1'}) | |
| If the type is not specified, we treat it as string. | |
| If no specific label is given, the label is simply the id. | |
| If no custom properties are given, we use an empty dictionary. | |
| input: [('a', 'date'), ('b', 'timeofday', 'b', {'foo': 'bar'})] | |
| output: [{'id': 'a', 'label': 'a', 'type': 'date', | |
| 'depth': 0, 'container': 'iter', 'custom_properties': {}}, | |
| {'id': 'b', 'label': 'b', 'type': 'timeofday', | |
| 'depth': 0, 'container': 'iter', | |
| 'custom_properties': {'foo': 'bar'}}] | |
| input: {'a': [('b', 'number'), ('c', 'string', 'column c')]} | |
| output: [{'id': 'a', 'label': 'a', 'type': 'string', | |
| 'depth': 0, 'container': 'dict', 'custom_properties': {}}, | |
| {'id': 'b', 'label': 'b', 'type': 'number', | |
| 'depth': 1, 'container': 'iter', 'custom_properties': {}}, | |
| {'id': 'c', 'label': 'column c', 'type': 'string', | |
| 'depth': 1, 'container': 'iter', 'custom_properties': {}}] | |
| input: {('a', 'number', 'column a'): { 'b': 'number', 'c': 'string'}} | |
| output: [{'id': 'a', 'label': 'column a', 'type': 'number', | |
| 'depth': 0, 'container': 'dict', 'custom_properties': {}}, | |
| {'id': 'b', 'label': 'b', 'type': 'number', | |
| 'depth': 1, 'container': 'dict', 'custom_properties': {}}, | |
| {'id': 'c', 'label': 'c', 'type': 'string', | |
| 'depth': 1, 'container': 'dict', 'custom_properties': {}}] | |
| input: { ('w', 'string', 'word'): ('c', 'number', 'count') } | |
| output: [{'id': 'w', 'label': 'word', 'type': 'string', | |
| 'depth': 0, 'container': 'dict', 'custom_properties': {}}, | |
| {'id': 'c', 'label': 'count', 'type': 'number', | |
| 'depth': 1, 'container': 'scalar', 'custom_properties': {}}] | |
| input: {'a': ('number', 'column a'), 'b': ('string', 'column b')} | |
| output: [{'id': 'a', 'label': 'column a', 'type': 'number', 'depth': 0, | |
| 'container': 'dict', 'custom_properties': {}}, | |
| {'id': 'b', 'label': 'column b', 'type': 'string', 'depth': 0, | |
| 'container': 'dict', 'custom_properties': {}} | |
| NOTE: there might be ambiguity in the case of a dictionary representation | |
| of a single column. For example, the following description can be parsed | |
| in 2 different ways: {'a': ('b', 'c')} can be thought of a single column | |
| with the id 'a', of type 'b' and the label 'c', or as 2 columns: one named | |
| 'a', and the other named 'b' of type 'c'. We choose the first option by | |
| default, and in case the second option is the right one, it is possible to | |
| make the key into a tuple (i.e. {('a',): ('b', 'c')}) or add more info | |
| into the tuple, thus making it look like this: {'a': ('b', 'c', 'b', {})} | |
| -- second 'b' is the label, and {} is the custom properties field. | |
| """ | |
| # For the recursion step, we check for a scalar object (string or tuple) | |
| if isinstance(table_description, (types.StringTypes, tuple)): | |
| parsed_col = DataTable.ColumnTypeParser(table_description) | |
| parsed_col["depth"] = depth | |
| parsed_col["container"] = "scalar" | |
| return [parsed_col] | |
| # Since it is not scalar, table_description must be iterable. | |
| if not hasattr(table_description, "__iter__"): | |
| raise DataTableException("Expected an iterable object, got %s" % | |
| type(table_description)) | |
| if not isinstance(table_description, dict): | |
| # We expects a non-dictionary iterable item. | |
| columns = [] | |
| for desc in table_description: | |
| parsed_col = DataTable.ColumnTypeParser(desc) | |
| parsed_col["depth"] = depth | |
| parsed_col["container"] = "iter" | |
| columns.append(parsed_col) | |
| if not columns: | |
| raise DataTableException("Description iterable objects should not" | |
| " be empty.") | |
| return columns | |
| # The other case is a dictionary | |
| if not table_description: | |
| raise DataTableException("Empty dictionaries are not allowed inside" | |
| " description") | |
| # To differentiate between the two cases of more levels below or this is | |
| # the most inner dictionary, we consider the number of keys (more then one | |
| # key is indication for most inner dictionary) and the type of the key and | |
| # value in case of only 1 key (if the type of key is string and the type of | |
| # the value is a tuple of 0-3 items, we assume this is the most inner | |
| # dictionary). | |
| # NOTE: this way of differentiating might create ambiguity. See docs. | |
| if (len(table_description) != 1 or | |
| (isinstance(table_description.keys()[0], types.StringTypes) and | |
| isinstance(table_description.values()[0], tuple) and | |
| len(table_description.values()[0]) < 4)): | |
| # This is the most inner dictionary. Parsing types. | |
| columns = [] | |
| # We sort the items, equivalent to sort the keys since they are unique | |
| for key, value in sorted(table_description.items()): | |
| # We parse the column type as (key, type) or (key, type, label) using | |
| # ColumnTypeParser. | |
| if isinstance(value, tuple): | |
| parsed_col = DataTable.ColumnTypeParser((key,) + value) | |
| else: | |
| parsed_col = DataTable.ColumnTypeParser((key, value)) | |
| parsed_col["depth"] = depth | |
| parsed_col["container"] = "dict" | |
| columns.append(parsed_col) | |
| return columns | |
| # This is an outer dictionary, must have at most one key. | |
| parsed_col = DataTable.ColumnTypeParser(table_description.keys()[0]) | |
| parsed_col["depth"] = depth | |
| parsed_col["container"] = "dict" | |
| return ([parsed_col] + | |
| DataTable.TableDescriptionParser(table_description.values()[0], | |
| depth=depth + 1)) | |
| @property | |
| def columns(self): | |
| """Returns the parsed table description.""" | |
| return self.__columns | |
| def NumberOfRows(self): | |
| """Returns the number of rows in the current data stored in the table.""" | |
| return len(self.__data) | |
| def SetRowsCustomProperties(self, rows, custom_properties): | |
| """Sets the custom properties for given row(s). | |
| Can accept a single row or an iterable of rows. | |
| Sets the given custom properties for all specified rows. | |
| Args: | |
| rows: The row, or rows, to set the custom properties for. | |
| custom_properties: A string to string dictionary of custom properties to | |
| set for all rows. | |
| """ | |
| if not hasattr(rows, "__iter__"): | |
| rows = [rows] | |
| for row in rows: | |
| self.__data[row] = (self.__data[row][0], custom_properties) | |
| def LoadData(self, data, custom_properties=None): | |
| """Loads new rows to the data table, clearing existing rows. | |
| May also set the custom_properties for the added rows. The given custom | |
| properties dictionary specifies the dictionary that will be used for *all* | |
| given rows. | |
| Args: | |
| data: The rows that the table will contain. | |
| custom_properties: A dictionary of string to string to set as the custom | |
| properties for all rows. | |
| """ | |
| self.__data = [] | |
| self.AppendData(data, custom_properties) | |
| def AppendData(self, data, custom_properties=None): | |
| """Appends new data to the table. | |
| Data is appended in rows. Data must comply with | |
| the table schema passed in to __init__(). See CoerceValue() for a list | |
| of acceptable data types. See the class documentation for more information | |
| and examples of schema and data values. | |
| Args: | |
| data: The row to add to the table. The data must conform to the table | |
| description format. | |
| custom_properties: A dictionary of string to string, representing the | |
| custom properties to add to all the rows. | |
| Raises: | |
| DataTableException: The data structure does not match the description. | |
| """ | |
| # If the maximal depth is 0, we simply iterate over the data table | |
| # lines and insert them using _InnerAppendData. Otherwise, we simply | |
| # let the _InnerAppendData handle all the levels. | |
| if not self.__columns[-1]["depth"]: | |
| for row in data: | |
| self._InnerAppendData(({}, custom_properties), row, 0) | |
| else: | |
| self._InnerAppendData(({}, custom_properties), data, 0) | |
| def _InnerAppendData(self, prev_col_values, data, col_index): | |
| """Inner function to assist LoadData.""" | |
| # We first check that col_index has not exceeded the columns size | |
| if col_index >= len(self.__columns): | |
| raise DataTableException("The data does not match description, too deep") | |
| # Dealing with the scalar case, the data is the last value. | |
| if self.__columns[col_index]["container"] == "scalar": | |
| prev_col_values[0][self.__columns[col_index]["id"]] = data | |
| self.__data.append(prev_col_values) | |
| return | |
| if self.__columns[col_index]["container"] == "iter": | |
| if not hasattr(data, "__iter__") or isinstance(data, dict): | |
| raise DataTableException("Expected iterable object, got %s" % | |
| type(data)) | |
| # We only need to insert the rest of the columns | |
| # If there are less items than expected, we only add what there is. | |
| for value in data: | |
| if col_index >= len(self.__columns): | |
| raise DataTableException("Too many elements given in data") | |
| prev_col_values[0][self.__columns[col_index]["id"]] = value | |
| col_index += 1 | |
| self.__data.append(prev_col_values) | |
| return | |
| # We know the current level is a dictionary, we verify the type. | |
| if not isinstance(data, dict): | |
| raise DataTableException("Expected dictionary at current level, got %s" % | |
| type(data)) | |
| # We check if this is the last level | |
| if self.__columns[col_index]["depth"] == self.__columns[-1]["depth"]: | |
| # We need to add the keys in the dictionary as they are | |
| for col in self.__columns[col_index:]: | |
| if col["id"] in data: | |
| prev_col_values[0][col["id"]] = data[col["id"]] | |
| self.__data.append(prev_col_values) | |
| return | |
| # We have a dictionary in an inner depth level. | |
| if not data.keys(): | |
| # In case this is an empty dictionary, we add a record with the columns | |
| # filled only until this point. | |
| self.__data.append(prev_col_values) | |
| else: | |
| for key in sorted(data): | |
| col_values = dict(prev_col_values[0]) | |
| col_values[self.__columns[col_index]["id"]] = key | |
| self._InnerAppendData((col_values, prev_col_values[1]), | |
| data[key], col_index + 1) | |
| def _PreparedData(self, order_by=()): | |
| """Prepares the data for enumeration - sorting it by order_by. | |
| Args: | |
| order_by: Optional. Specifies the name of the column(s) to sort by, and | |
| (optionally) which direction to sort in. Default sort direction | |
| is asc. Following formats are accepted: | |
| "string_col_name" -- For a single key in default (asc) order. | |
| ("string_col_name", "asc|desc") -- For a single key. | |
| [("col_1","asc|desc"), ("col_2","asc|desc")] -- For more than | |
| one column, an array of tuples of (col_name, "asc|desc"). | |
| Returns: | |
| The data sorted by the keys given. | |
| Raises: | |
| DataTableException: Sort direction not in 'asc' or 'desc' | |
| """ | |
| if not order_by: | |
| return self.__data | |
| proper_sort_keys = [] | |
| if isinstance(order_by, types.StringTypes) or ( | |
| isinstance(order_by, tuple) and len(order_by) == 2 and | |
| order_by[1].lower() in ["asc", "desc"]): | |
| order_by = (order_by,) | |
| for key in order_by: | |
| if isinstance(key, types.StringTypes): | |
| proper_sort_keys.append((key, 1)) | |
| elif (isinstance(key, (list, tuple)) and len(key) == 2 and | |
| key[1].lower() in ("asc", "desc")): | |
| proper_sort_keys.append((key[0], key[1].lower() == "asc" and 1 or -1)) | |
| else: | |
| raise DataTableException("Expected tuple with second value: " | |
| "'asc' or 'desc'") | |
| def SortCmpFunc(row1, row2): | |
| """cmp function for sorted. Compares by keys and 'asc'/'desc' keywords.""" | |
| for key, asc_mult in proper_sort_keys: | |
| cmp_result = asc_mult * cmp(row1[0].get(key), row2[0].get(key)) | |
| if cmp_result: | |
| return cmp_result | |
| return 0 | |
| return sorted(self.__data, cmp=SortCmpFunc) | |
| def ToJSCode(self, name, columns_order=None, order_by=()): | |
| """Writes the data table as a JS code string. | |
| This method writes a string of JS code that can be run to | |
| generate a DataTable with the specified data. Typically used for debugging | |
| only. | |
| Args: | |
| name: The name of the table. The name would be used as the DataTable's | |
| variable name in the created JS code. | |
| columns_order: Optional. Specifies the order of columns in the | |
| output table. Specify a list of all column IDs in the order | |
| in which you want the table created. | |
| Note that you must list all column IDs in this parameter, | |
| if you use it. | |
| order_by: Optional. Specifies the name of the column(s) to sort by. | |
| Passed as is to _PreparedData. | |
| Returns: | |
| A string of JS code that, when run, generates a DataTable with the given | |
| name and the data stored in the DataTable object. | |
| Example result: | |
| "var tab1 = new google.visualization.DataTable(); | |
| tab1.addColumn("string", "a", "a"); | |
| tab1.addColumn("number", "b", "b"); | |
| tab1.addColumn("boolean", "c", "c"); | |
| tab1.addRows(10); | |
| tab1.setCell(0, 0, "a"); | |
| tab1.setCell(0, 1, 1, null, {"foo": "bar"}); | |
| tab1.setCell(0, 2, true); | |
| ... | |
| tab1.setCell(9, 0, "c"); | |
| tab1.setCell(9, 1, 3, "3$"); | |
| tab1.setCell(9, 2, false);" | |
| Raises: | |
| DataTableException: The data does not match the type. | |
| """ | |
| encoder = DataTableJSONEncoder() | |
| if columns_order is None: | |
| columns_order = [col["id"] for col in self.__columns] | |
| col_dict = dict([(col["id"], col) for col in self.__columns]) | |
| # We first create the table with the given name | |
| jscode = "var %s = new google.visualization.DataTable();\n" % name | |
| if self.custom_properties: | |
| jscode += "%s.setTableProperties(%s);\n" % ( | |
| name, encoder.encode(self.custom_properties)) | |
| # We add the columns to the table | |
| for i, col in enumerate(columns_order): | |
| jscode += "%s.addColumn(%s, %s, %s);\n" % ( | |
| name, | |
| encoder.encode(col_dict[col]["type"]), | |
| encoder.encode(col_dict[col]["label"]), | |
| encoder.encode(col_dict[col]["id"])) | |
| if col_dict[col]["custom_properties"]: | |
| jscode += "%s.setColumnProperties(%d, %s);\n" % ( | |
| name, i, encoder.encode(col_dict[col]["custom_properties"])) | |
| jscode += "%s.addRows(%d);\n" % (name, len(self.__data)) | |
| # We now go over the data and add each row | |
| for (i, (row, cp)) in enumerate(self._PreparedData(order_by)): | |
| # We add all the elements of this row by their order | |
| for (j, col) in enumerate(columns_order): | |
| if col not in row or row[col] is None: | |
| continue | |
| value = self.CoerceValue(row[col], col_dict[col]["type"]) | |
| if isinstance(value, tuple): | |
| cell_cp = "" | |
| if len(value) == 3: | |
| cell_cp = ", %s" % encoder.encode(row[col][2]) | |
| # We have a formatted value or custom property as well | |
| jscode += ("%s.setCell(%d, %d, %s, %s%s);\n" % | |
| (name, i, j, | |
| self.EscapeForJSCode(encoder, value[0]), | |
| self.EscapeForJSCode(encoder, value[1]), cell_cp)) | |
| else: | |
| jscode += "%s.setCell(%d, %d, %s);\n" % ( | |
| name, i, j, self.EscapeForJSCode(encoder, value)) | |
| if cp: | |
| jscode += "%s.setRowProperties(%d, %s);\n" % ( | |
| name, i, encoder.encode(cp)) | |
| return jscode | |
| def ToHtml(self, columns_order=None, order_by=()): | |
| """Writes the data table as an HTML table code string. | |
| Args: | |
| columns_order: Optional. Specifies the order of columns in the | |
| output table. Specify a list of all column IDs in the order | |
| in which you want the table created. | |
| Note that you must list all column IDs in this parameter, | |
| if you use it. | |
| order_by: Optional. Specifies the name of the column(s) to sort by. | |
| Passed as is to _PreparedData. | |
| Returns: | |
| An HTML table code string. | |
| Example result (the result is without the newlines): | |
| <html><body><table border="1"> | |
| <thead><tr><th>a</th><th>b</th><th>c</th></tr></thead> | |
| <tbody> | |
| <tr><td>1</td><td>"z"</td><td>2</td></tr> | |
| <tr><td>"3$"</td><td>"w"</td><td></td></tr> | |
| </tbody> | |
| </table></body></html> | |
| Raises: | |
| DataTableException: The data does not match the type. | |
| """ | |
| table_template = "<html><body><table border=\"1\">%s</table></body></html>" | |
| columns_template = "<thead><tr>%s</tr></thead>" | |
| rows_template = "<tbody>%s</tbody>" | |
| row_template = "<tr>%s</tr>" | |
| header_cell_template = "<th>%s</th>" | |
| cell_template = "<td>%s</td>" | |
| if columns_order is None: | |
| columns_order = [col["id"] for col in self.__columns] | |
| col_dict = dict([(col["id"], col) for col in self.__columns]) | |
| columns_list = [] | |
| for col in columns_order: | |
| columns_list.append(header_cell_template % | |
| cgi.escape(col_dict[col]["label"])) | |
| columns_html = columns_template % "".join(columns_list) | |
| rows_list = [] | |
| # We now go over the data and add each row | |
| for row, unused_cp in self._PreparedData(order_by): | |
| cells_list = [] | |
| # We add all the elements of this row by their order | |
| for col in columns_order: | |
| # For empty string we want empty quotes (""). | |
| value = "" | |
| if col in row and row[col] is not None: | |
| value = self.CoerceValue(row[col], col_dict[col]["type"]) | |
| if isinstance(value, tuple): | |
| # We have a formatted value and we're going to use it | |
| cells_list.append(cell_template % cgi.escape(self.ToString(value[1]))) | |
| else: | |
| cells_list.append(cell_template % cgi.escape(self.ToString(value))) | |
| rows_list.append(row_template % "".join(cells_list)) | |
| rows_html = rows_template % "".join(rows_list) | |
| return table_template % (columns_html + rows_html) | |
| def ToCsv(self, columns_order=None, order_by=(), separator=","): | |
| """Writes the data table as a CSV string. | |
| Output is encoded in UTF-8 because the Python "csv" module can't handle | |
| Unicode properly according to its documentation. | |
| Args: | |
| columns_order: Optional. Specifies the order of columns in the | |
| output table. Specify a list of all column IDs in the order | |
| in which you want the table created. | |
| Note that you must list all column IDs in this parameter, | |
| if you use it. | |
| order_by: Optional. Specifies the name of the column(s) to sort by. | |
| Passed as is to _PreparedData. | |
| separator: Optional. The separator to use between the values. | |
| Returns: | |
| A CSV string representing the table. | |
| Example result: | |
| 'a','b','c' | |
| 1,'z',2 | |
| 3,'w','' | |
| Raises: | |
| DataTableException: The data does not match the type. | |
| """ | |
| csv_buffer = cStringIO.StringIO() | |
| writer = csv.writer(csv_buffer, delimiter=separator) | |
| if columns_order is None: | |
| columns_order = [col["id"] for col in self.__columns] | |
| col_dict = dict([(col["id"], col) for col in self.__columns]) | |
| writer.writerow([col_dict[col]["label"].encode("utf-8") | |
| for col in columns_order]) | |
| # We now go over the data and add each row | |
| for row, unused_cp in self._PreparedData(order_by): | |
| cells_list = [] | |
| # We add all the elements of this row by their order | |
| for col in columns_order: | |
| value = "" | |
| if col in row and row[col] is not None: | |
| value = self.CoerceValue(row[col], col_dict[col]["type"]) | |
| if isinstance(value, tuple): | |
| # We have a formatted value. Using it only for date/time types. | |
| if col_dict[col]["type"] in ["date", "datetime", "timeofday"]: | |
| cells_list.append(self.ToString(value[1]).encode("utf-8")) | |
| else: | |
| cells_list.append(self.ToString(value[0]).encode("utf-8")) | |
| else: | |
| cells_list.append(self.ToString(value).encode("utf-8")) | |
| writer.writerow(cells_list) | |
| return csv_buffer.getvalue() | |
| def ToTsvExcel(self, columns_order=None, order_by=()): | |
| """Returns a file in tab-separated-format readable by MS Excel. | |
| Returns a file in UTF-16 little endian encoding, with tabs separating the | |
| values. | |
| Args: | |
| columns_order: Delegated to ToCsv. | |
| order_by: Delegated to ToCsv. | |
| Returns: | |
| A tab-separated little endian UTF16 file representing the table. | |
| """ | |
| return (self.ToCsv(columns_order, order_by, separator="\t") | |
| .decode("utf-8").encode("UTF-16LE")) | |
| def _ToJSonObj(self, columns_order=None, order_by=()): | |
| """Returns an object suitable to be converted to JSON. | |
| Args: | |
| columns_order: Optional. A list of all column IDs in the order in which | |
| you want them created in the output table. If specified, | |
| all column IDs must be present. | |
| order_by: Optional. Specifies the name of the column(s) to sort by. | |
| Passed as is to _PreparedData(). | |
| Returns: | |
| A dictionary object for use by ToJSon or ToJSonResponse. | |
| """ | |
| if columns_order is None: | |
| columns_order = [col["id"] for col in self.__columns] | |
| col_dict = dict([(col["id"], col) for col in self.__columns]) | |
| # Creating the column JSON objects | |
| col_objs = [] | |
| for col_id in columns_order: | |
| col_obj = {"id": col_dict[col_id]["id"], | |
| "label": col_dict[col_id]["label"], | |
| "type": col_dict[col_id]["type"]} | |
| if col_dict[col_id]["custom_properties"]: | |
| col_obj["p"] = col_dict[col_id]["custom_properties"] | |
| col_objs.append(col_obj) | |
| # Creating the rows jsons | |
| row_objs = [] | |
| for row, cp in self._PreparedData(order_by): | |
| cell_objs = [] | |
| for col in columns_order: | |
| value = self.CoerceValue(row.get(col, None), col_dict[col]["type"]) | |
| if value is None: | |
| cell_obj = None | |
| elif isinstance(value, tuple): | |
| cell_obj = {"v": value[0]} | |
| if len(value) > 1 and value[1] is not None: | |
| cell_obj["f"] = value[1] | |
| if len(value) == 3: | |
| cell_obj["p"] = value[2] | |
| else: | |
| cell_obj = {"v": value} | |
| cell_objs.append(cell_obj) | |
| row_obj = {"c": cell_objs} | |
| if cp: | |
| row_obj["p"] = cp | |
| row_objs.append(row_obj) | |
| json_obj = {"cols": col_objs, "rows": row_objs} | |
| if self.custom_properties: | |
| json_obj["p"] = self.custom_properties | |
| return json_obj | |
| def ToJSon(self, columns_order=None, order_by=()): | |
| """Returns a string that can be used in a JS DataTable constructor. | |
| This method writes a JSON string that can be passed directly into a Google | |
| Visualization API DataTable constructor. Use this output if you are | |
| hosting the visualization HTML on your site, and want to code the data | |
| table in Python. Pass this string into the | |
| google.visualization.DataTable constructor, e.g,: | |
| ... on my page that hosts my visualization ... | |
| google.setOnLoadCallback(drawTable); | |
| function drawTable() { | |
| var data = new google.visualization.DataTable(_my_JSon_string, 0.6); | |
| myTable.draw(data); | |
| } | |
| Args: | |
| columns_order: Optional. Specifies the order of columns in the | |
| output table. Specify a list of all column IDs in the order | |
| in which you want the table created. | |
| Note that you must list all column IDs in this parameter, | |
| if you use it. | |
| order_by: Optional. Specifies the name of the column(s) to sort by. | |
| Passed as is to _PreparedData(). | |
| Returns: | |
| A JSon constructor string to generate a JS DataTable with the data | |
| stored in the DataTable object. | |
| Example result (the result is without the newlines): | |
| {cols: [{id:"a",label:"a",type:"number"}, | |
| {id:"b",label:"b",type:"string"}, | |
| {id:"c",label:"c",type:"number"}], | |
| rows: [{c:[{v:1},{v:"z"},{v:2}]}, c:{[{v:3,f:"3$"},{v:"w"},{v:null}]}], | |
| p: {'foo': 'bar'}} | |
| Raises: | |
| DataTableException: The data does not match the type. | |
| """ | |
| encoder = DataTableJSONEncoder() | |
| return encoder.encode( | |
| self._ToJSonObj(columns_order, order_by)).encode("utf-8") | |
| def ToJSonResponse(self, columns_order=None, order_by=(), req_id=0, | |
| response_handler="google.visualization.Query.setResponse"): | |
| """Writes a table as a JSON response that can be returned as-is to a client. | |
| This method writes a JSON response to return to a client in response to a | |
| Google Visualization API query. This string can be processed by the calling | |
| page, and is used to deliver a data table to a visualization hosted on | |
| a different page. | |
| Args: | |
| columns_order: Optional. Passed straight to self.ToJSon(). | |
| order_by: Optional. Passed straight to self.ToJSon(). | |
| req_id: Optional. The response id, as retrieved by the request. | |
| response_handler: Optional. The response handler, as retrieved by the | |
| request. | |
| Returns: | |
| A JSON response string to be received by JS the visualization Query | |
| object. This response would be translated into a DataTable on the | |
| client side. | |
| Example result (newlines added for readability): | |
| google.visualization.Query.setResponse({ | |
| 'version':'0.6', 'reqId':'0', 'status':'OK', | |
| 'table': {cols: [...], rows: [...]}}); | |
| Note: The URL returning this string can be used as a data source by Google | |
| Visualization Gadgets or from JS code. | |
| """ | |
| response_obj = { | |
| "version": "0.6", | |
| "reqId": str(req_id), | |
| "table": self._ToJSonObj(columns_order, order_by), | |
| "status": "ok" | |
| } | |
| encoder = DataTableJSONEncoder() | |
| return "%s(%s);" % (response_handler, | |
| encoder.encode(response_obj).encode("utf-8")) | |
| def ToResponse(self, columns_order=None, order_by=(), tqx=""): | |
| """Writes the right response according to the request string passed in tqx. | |
| This method parses the tqx request string (format of which is defined in | |
| the documentation for implementing a data source of Google Visualization), | |
| and returns the right response according to the request. | |
| It parses out the "out" parameter of tqx, calls the relevant response | |
| (ToJSonResponse() for "json", ToCsv() for "csv", ToHtml() for "html", | |
| ToTsvExcel() for "tsv-excel") and passes the response function the rest of | |
| the relevant request keys. | |
| Args: | |
| columns_order: Optional. Passed as is to the relevant response function. | |
| order_by: Optional. Passed as is to the relevant response function. | |
| tqx: Optional. The request string as received by HTTP GET. Should be in | |
| the format "key1:value1;key2:value2...". All keys have a default | |
| value, so an empty string will just do the default (which is calling | |
| ToJSonResponse() with no extra parameters). | |
| Returns: | |
| A response string, as returned by the relevant response function. | |
| Raises: | |
| DataTableException: One of the parameters passed in tqx is not supported. | |
| """ | |
| tqx_dict = {} | |
| if tqx: | |
| tqx_dict = dict(opt.split(":") for opt in tqx.split(";")) | |
| if tqx_dict.get("version", "0.6") != "0.6": | |
| raise DataTableException( | |
| "Version (%s) passed by request is not supported." | |
| % tqx_dict["version"]) | |
| if tqx_dict.get("out", "json") == "json": | |
| response_handler = tqx_dict.get("responseHandler", | |
| "google.visualization.Query.setResponse") | |
| return self.ToJSonResponse(columns_order, order_by, | |
| req_id=tqx_dict.get("reqId", 0), | |
| response_handler=response_handler) | |
| elif tqx_dict["out"] == "html": | |
| return self.ToHtml(columns_order, order_by) | |
| elif tqx_dict["out"] == "csv": | |
| return self.ToCsv(columns_order, order_by) | |
| elif tqx_dict["out"] == "tsv-excel": | |
| return self.ToTsvExcel(columns_order, order_by) | |
| else: | |
| raise DataTableException( | |
| "'out' parameter: '%s' is not supported" % tqx_dict["out"]) |