# HG changeset patch # User Pawel Solyga # Date 1243881098 -7200 # Node ID 05ab9393303d197e63ca9cef7147e764177b8104 # Parent 805400745f5751155dbe9624e66d72ef913a2eaa Add google-visualization-python project to Melange repository. This python module will be used with our Statistics module to leverage Google Visualization API and python as a data source. diff -r 805400745f57 -r 05ab9393303d app/gviz/COPYRIGHT --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/gviz/COPYRIGHT Mon Jun 01 20:31:38 2009 +0200 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff -r 805400745f57 -r 05ab9393303d app/gviz/README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/gviz/README Mon Jun 01 20:31:38 2009 +0200 @@ -0,0 +1,20 @@ +Copyright (C) 2008 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Installing the library: + python ./setup.py install + (You might need root privileges to do this) + +Testing the library: + python ./setup.py test diff -r 805400745f57 -r 05ab9393303d app/gviz/__init__.py diff -r 805400745f57 -r 05ab9393303d app/gviz/examples/dynamic_example.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/gviz/examples/dynamic_example.py Mon Jun 01 20:31:38 2009 +0200 @@ -0,0 +1,39 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Example of dynamic use of Google Visualization Python API.""" + +__author__ = "Misha Seltzer" + +import gviz_api + +description = {"name": ("string", "Name"), + "salary": ("number", "Salary"), + "full_time": ("boolean", "Full Time Employee")} +data = [{"name": "Mike", "salary": (10000, "$10,000"), "full_time": True}, + {"name": "Jim", "salary": (800, "$800"), "full_time": False}, + {"name": "Alice", "salary": (12500, "$12,500"), "full_time": True}, + {"name": "Bob", "salary": (7000, "$7,000"), "full_time": True}] + +data_table = gviz_api.DataTable(description) +data_table.LoadData(data) +print "Content-type: text/plain" +print +print data_table.ToJSonResponse(columns_order=("name", "salary", "full_time"), + order_by="salary") + +# Put the url (http://google-visualization.appspot.com/python/dynamic_example) +# as your Google Visualization data source. diff -r 805400745f57 -r 05ab9393303d app/gviz/examples/static_example.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/gviz/examples/static_example.py Mon Jun 01 20:31:38 2009 +0200 @@ -0,0 +1,80 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Example of static use of Google Visualization Python API.""" + +__author__ = "Misha Seltzer" + +import gviz_api + +page_template = """ + + + Static example + + + + +

Table created using ToJSCode

+
+

Table created using ToJSon

+
+ + +""" + + +def main(): + # Creating the data + description = {"name": ("string", "Name"), + "salary": ("number", "Salary"), + "full_time": ("boolean", "Full Time Employee")} + data = [{"name": "Mike", "salary": (10000, "$10,000"), "full_time": True}, + {"name": "Jim", "salary": (800, "$800"), "full_time": False}, + {"name": "Alice", "salary": (12500, "$12,500"), "full_time": True}, + {"name": "Bob", "salary": (7000, "$7,000"), "full_time": True}] + + # Loading it into gviz_api.DataTable + data_table = gviz_api.DataTable(description) + data_table.LoadData(data) + + # Creating a JavaScript code string + jscode = data_table.ToJSCode("jscode_data", + columns_order=("name", "salary", "full_time"), + order_by="salary") + # Creating a JSon string + json = data_table.ToJSon(columns_order=("name", "salary", "full_time"), + order_by="salary") + + # Putting the JS code and JSon string into the template + print page_template % vars() + + +if __name__ == "__main__": + main() diff -r 805400745f57 -r 05ab9393303d app/gviz/gviz_api.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/gviz/gviz_api.py Mon Jun 01 20:31:38 2009 +0200 @@ -0,0 +1,1021 @@ +#!/usr/bin/python +# +# Copyright (C) 2009 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""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, and JavaScript. + +See http://code.google.com/apis/visualization/ for documentation on the +Google Visualization API. +""" + +__author__ = "Amit Weinstein, Misha Seltzer" + +import cgi +import datetime +import types + + +class DataTableException(Exception): + """The general exception object thrown by DataTable.""" + pass + + +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 _EscapeValueForCsv(v): + """Escapes the value for use in a CSV file. + + Puts the string in double-quotes, and escapes any inner double-quotes by + doubling them. + + Args: + v: The value to escape. + + Returns: + The escaped values. + """ + return '"%s"' % v.replace('"', '""') + + @staticmethod + def _EscapeValue(v): + """Puts the string in quotes, and escapes any inner quotes and slashes.""" + if isinstance(v, unicode): + # Here we use repr as in the usual case, but on unicode strings, it + # also escapes the unicode characters (which we want to leave as is). + # So, after repr() we decode using raw-unicode-escape, which decodes + # only the unicode characters, and leaves all the rest (", ', \n and + # more) escaped. + # We don't take the first character, because repr adds a u in the + # beginning of the string (usual repr output for unicode is u'...'). + return repr(v).decode("raw-unicode-escape")[1:] + # Here we use python built-in escaping mechanism for string using repr. + return repr(str(v)) + + @staticmethod + def _EscapeCustomProperties(custom_properties): + """Escapes the custom properties dictionary.""" + l = [] + for key, value in custom_properties.iteritems(): + l.append("%s:%s" % (DataTable._EscapeValue(key), + DataTable._EscapeValue(value))) + return "{%s}" % ",".join(l) + + @staticmethod + def SingleValueToJS(value, value_type, escape_func=None): + """Translates a single value and type into a JS value. + + Internal helper method. + + Args: + value: The value which should be converted + value_type: One of "string", "number", "boolean", "date", "datetime" or + "timeofday". + escape_func: The function to use for escaping strings. + + Returns: + The proper JS format (as string) of the given value according to the + given value_type. For None, we simply return "null". + 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: + SingleValueToJS(None, "boolean") returns "null" + SingleValueToJS(False, "boolean") returns "false" + SingleValueToJS((5, "5$"), "number") returns ("5", "'5$'") + SingleValueToJS((None, "5$"), "number") returns ("null", "'5$'") + + Raises: + DataTableException: The value and type did not match in a not-recoverable + way, for example given value 'abc' for type 'number'. + """ + if escape_func is None: + escape_func = DataTable._EscapeValue + 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.SingleValueToJS(value[0], value_type) + if value[1] is None: + return (js_value, None) + return (js_value, escape_func(value[1])) + + # The standard case - no formatting. + t_value = type(value) + if value is None: + return "null" + if value_type == "boolean": + if value: + return "true" + return "false" + + elif value_type == "number": + if isinstance(value, (int, long, float)): + return str(value) + raise DataTableException("Wrong type %s when expected number" % t_value) + + elif value_type == "string": + if isinstance(value, tuple): + raise DataTableException("Tuple is not allowed as string value.") + return escape_func(value) + + elif value_type == "date": + if not isinstance(value, (datetime.date, datetime.datetime)): + raise DataTableException("Wrong type %s when expected date" % t_value) + # We need to shift the month by 1 to match JS Date format + return "new Date(%d,%d,%d)" % (value.year, value.month - 1, value.day) + + elif value_type == "timeofday": + if not isinstance(value, (datetime.time, datetime.datetime)): + raise DataTableException("Wrong type %s when expected time" % t_value) + return "[%d,%d,%d]" % (value.hour, value.minute, value.second) + + elif value_type == "datetime": + if not isinstance(value, datetime.datetime): + raise DataTableException("Wrong type %s when expected datetime" % + t_value) + 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) + # 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 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. + """ + 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") + 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': {}}] + """ + # 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") + + # The number of keys in the dictionary separates between the two cases of + # more levels below or this is the most inner dictionary. + if len(table_description) != 1: + # 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 SingleValueToJS() 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. + """ + 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, DataTable._EscapeCustomProperties(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, + col_dict[col]["type"], + col_dict[col]["label"], + col_dict[col]["id"]) + if col_dict[col]["custom_properties"]: + jscode += "%s.setColumnProperties(%d, %s);\n" % ( + name, i, DataTable._EscapeCustomProperties( + 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 + cell_cp = "" + if isinstance(row[col], tuple) and len(row[col]) == 3: + cell_cp = ", %s" % DataTable._EscapeCustomProperties(row[col][2]) + value = self.SingleValueToJS(row[col], col_dict[col]["type"]) + if isinstance(value, tuple): + # We have a formatted value or custom property as well + if value[1] is None: + value = (value[0], "null") + jscode += ("%s.setCell(%d, %d, %s, %s%s);\n" % + (name, i, j, value[0], value[1], cell_cp)) + else: + jscode += "%s.setCell(%d, %d, %s);\n" % (name, i, j, value) + if cp: + jscode += "%s.setRowProperties(%d, %s);\n" % ( + name, i, DataTable._EscapeCustomProperties(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): + + + + + + +
abc
1"z"2
"3$""w"
+ + Raises: + DataTableException: The data does not match the type. + """ + table_template = "%s
" + columns_template = "%s" + rows_template = "%s" + row_template = "%s" + header_cell_template = "%s" + cell_template = "%s" + + 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 % 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.SingleValueToJS(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(value[1])) + else: + cells_list.append(cell_template % cgi.escape(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. + + 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. + """ + 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(DataTable._EscapeValueForCsv(col_dict[col]["label"])) + columns_line = separator.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: + value = '""' + if col in row and row[col] is not None: + value = self.SingleValueToJS(row[col], col_dict[col]["type"], + DataTable._EscapeValueForCsv) + 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(value[1]) + else: + cells_list.append(value[0]) + else: + # We need to quote date types, because they contain commas. + if (col_dict[col]["type"] in ["date", "datetime", "timeofday"] and + value != '""'): + value = '"%s"' % value + cells_list.append(value) + rows_list.append(separator.join(cells_list)) + rows = "\n".join(rows_list) + + return "%s\n%s" % (columns_line, rows) + + 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").encode("UTF-16LE") + + def ToJSon(self, columns_order=None, order_by=()): + """Writes a JSON 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. + """ + 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 columns jsons + cols_jsons = [] + for col_id in columns_order: + d = dict(col_dict[col_id]) + d["cp"] = "" + if col_dict[col_id]["custom_properties"]: + d["cp"] = ",p:%s" % DataTable._EscapeCustomProperties( + col_dict[col_id]["custom_properties"]) + cols_jsons.append( + "{id:'%(id)s',label:'%(label)s',type:'%(type)s'%(cp)s}" % d) + + # Creating the rows jsons + rows_jsons = [] + for row, cp in self._PreparedData(order_by): + cells_jsons = [] + for col in columns_order: + # We omit the {v:null} for a None value of the not last column + value = row.get(col, None) + if value is None and col != columns_order[-1]: + cells_jsons.append("") + else: + value = self.SingleValueToJS(value, col_dict[col]["type"]) + if isinstance(value, tuple): + # We have a formatted value or custom property as well + if len(row.get(col)) == 3: + if value[1] is None: + cells_jsons.append("{v:%s,p:%s}" % ( + value[0], + DataTable._EscapeCustomProperties(row.get(col)[2]))) + else: + cells_jsons.append("{v:%s,f:%s,p:%s}" % (value + ( + DataTable._EscapeCustomProperties(row.get(col)[2]),))) + else: + cells_jsons.append("{v:%s,f:%s}" % value) + else: + cells_jsons.append("{v:%s}" % value) + if cp: + rows_jsons.append("{c:[%s],p:%s}" % ( + ",".join(cells_jsons), DataTable._EscapeCustomProperties(cp))) + else: + rows_jsons.append("{c:[%s]}" % ",".join(cells_jsons)) + + general_custom_properties = "" + if self.custom_properties: + general_custom_properties = ( + ",p:%s" % DataTable._EscapeCustomProperties(self.custom_properties)) + + # We now join the columns jsons and the rows jsons + json = "{cols:[%s],rows:[%s]%s}" % (",".join(cols_jsons), + ",".join(rows_jsons), + general_custom_properties) + return json + + 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. + """ + table = self.ToJSon(columns_order, order_by) + return ("%s({'version':'0.6', 'reqId':'%s', 'status':'OK', " + "'table': %s});") % (response_handler, req_id, table) + + 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"]) diff -r 805400745f57 -r 05ab9393303d app/gviz/gviz_api_test.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/gviz/gviz_api_test.py Mon Jun 01 20:31:38 2009 +0200 @@ -0,0 +1,518 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the gviz_api module.""" + +__author__ = "Amit Weinstein" + +from datetime import date +from datetime import datetime +from datetime import time +import re +import unittest + +from gviz_api import DataTable +from gviz_api import DataTableException + + +class DataTableTest(unittest.TestCase): + + def testSingleValueToJS(self): + # We first check that given an unknown type it raises exception + self.assertRaises(DataTableException, + DataTable.SingleValueToJS, 1, "no_such_type") + + # If we give a type which does not match the value, we expect it to fail + self.assertRaises(DataTableException, + DataTable.SingleValueToJS, "a", "number") + self.assertRaises(DataTableException, + DataTable.SingleValueToJS, "b", "timeofday") + self.assertRaises(DataTableException, + DataTable.SingleValueToJS, 10, "date") + + # A tuple for value and formatted value should be of length 2 + self.assertRaises(DataTableException, + DataTable.SingleValueToJS, (5, "5$", "6$"), "string") + + # Some good examples from all the different types + self.assertEqual("true", DataTable.SingleValueToJS(True, "boolean")) + self.assertEqual("false", DataTable.SingleValueToJS(False, "boolean")) + self.assertEqual("true", DataTable.SingleValueToJS(1, "boolean")) + self.assertEqual("null", DataTable.SingleValueToJS(None, "boolean")) + self.assertEqual(("false", "'a'"), + DataTable.SingleValueToJS((False, "a"), "boolean")) + + self.assertEqual("1", DataTable.SingleValueToJS(1, "number")) + self.assertEqual("1.0", DataTable.SingleValueToJS(1., "number")) + self.assertEqual("-5", DataTable.SingleValueToJS(-5, "number")) + self.assertEqual("null", DataTable.SingleValueToJS(None, "number")) + self.assertEqual(("5", "'5$'"), + DataTable.SingleValueToJS((5, "5$"), "number")) + + self.assertEqual("'-5'", DataTable.SingleValueToJS(-5, "string")) + self.assertEqual("'abc'", DataTable.SingleValueToJS("abc", "string")) + self.assertEqual("null", DataTable.SingleValueToJS(None, "string")) + + self.assertEqual("new Date(2010,0,2)", + DataTable.SingleValueToJS(date(2010, 1, 2), "date")) + self.assertEqual("new Date(2001,1,3)", + DataTable.SingleValueToJS(datetime(2001, 2, 3, 4, 5, 6), + "date")) + self.assertEqual("null", DataTable.SingleValueToJS(None, "date")) + + self.assertEqual("[10,11,12]", + DataTable.SingleValueToJS(time(10, 11, 12), "timeofday")) + self.assertEqual("[3,4,5]", + DataTable.SingleValueToJS(datetime(2010, 1, 2, 3, 4, 5), + "timeofday")) + self.assertEqual("null", DataTable.SingleValueToJS(None, "timeofday")) + + self.assertEqual("new Date(2001,1,3,4,5,6)", + DataTable.SingleValueToJS(datetime(2001, 2, 3, 4, 5, 6), + "datetime")) + self.assertEqual("null", DataTable.SingleValueToJS(None, "datetime")) + self.assertEqual(("null", "'none'"), + DataTable.SingleValueToJS((None, "none"), "string")) + + def testDifferentStrings(self): + # Checking escaping of strings + problematic_strings = ["control", "new\nline", "", + "single'quote", 'double"quote', + r"one\slash", r"two\\slash", u"unicode eng", + u"unicode \u05e2\u05d1\u05e8\u05d9\u05ea"] + for s in problematic_strings: + js_value = DataTable.SingleValueToJS(s, "string") + if isinstance(js_value, unicode): + js_value = "u%s" % js_value + self.assertEquals(s, eval(js_value)) + + js_value = DataTable.SingleValueToJS(("str", s), "string")[1] + if isinstance(js_value, unicode): + js_value = "u%s" % js_value + self.assertEquals(s, eval(js_value)) + + def testDifferentCustomProperties(self): + # Checking escaping of custom properties + problematic_cps = [{"control": "test"}, {u"unicode": "value"}, + {"key": u"unicode"}, {u"unicode": u"unicode"}, + {"regular": "single'quote"}, {"unicode": u"s'quote"}] + for cp in problematic_cps: + js_value = DataTable._EscapeCustomProperties(cp) + self.assertEquals(cp, eval(js_value)) + + def testColumnTypeParser(self): + # Checking several wrong formats + self.assertRaises(DataTableException, + DataTable.ColumnTypeParser, 5) + self.assertRaises(DataTableException, + DataTable.ColumnTypeParser, ("a", "b", "c", "d")) + self.assertRaises(DataTableException, + DataTable.ColumnTypeParser, ("a", 5, "c")) + + # Checking several legal formats + self.assertEqual({"id": "abc", "label": "abc", "type": "string", + "custom_properties": {}}, + DataTable.ColumnTypeParser("abc")) + self.assertEqual({"id": "abc", "label": "abc", "type": "string", + "custom_properties": {}}, + DataTable.ColumnTypeParser(("abc",))) + self.assertEqual({"id": "abc", "label": "bcd", "type": "string", + "custom_properties": {}}, + DataTable.ColumnTypeParser(("abc", "string", "bcd"))) + self.assertEqual({"id": "a", "label": "b", "type": "number", + "custom_properties": {}}, + DataTable.ColumnTypeParser(("a", "number", "b"))) + self.assertEqual({"id": "a", "label": "a", "type": "number", + "custom_properties": {}}, + DataTable.ColumnTypeParser(("a", "number"))) + self.assertEqual({"id": "i", "label": "l", "type": "string", + "custom_properties": {"key": "value"}}, + DataTable.ColumnTypeParser(("i", "string", "l", + {"key": "value"}))) + + def testTableDescriptionParser(self): + # We expect it to fail with empty lists or dictionaries + self.assertRaises(DataTableException, + DataTable.TableDescriptionParser, {}) + self.assertRaises(DataTableException, + DataTable.TableDescriptionParser, []) + self.assertRaises(DataTableException, + DataTable.TableDescriptionParser, {"a": []}) + self.assertRaises(DataTableException, + DataTable.TableDescriptionParser, {"a": {"b": {}}}) + + # We expect it to fail if we give a non-string at the lowest level + self.assertRaises(DataTableException, + DataTable.TableDescriptionParser, {"a": 5}) + self.assertRaises(DataTableException, + DataTable.TableDescriptionParser, [("a", "number"), 6]) + + # Some valid examples which mixes both dictionaries and lists + self.assertEqual( + [{"id": "a", "label": "a", "type": "date", + "depth": 0, "container": "iter", "custom_properties": {}}, + {"id": "b", "label": "b", "type": "timeofday", + "depth": 0, "container": "iter", "custom_properties": {}}], + DataTable.TableDescriptionParser([("a", "date"), ("b", "timeofday")])) + + self.assertEqual( + [{"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": {}}], + DataTable.TableDescriptionParser({"a": [("b", "number"), + ("c", "string", "column c")]})) + + self.assertEqual( + [{"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": {}}], + DataTable.TableDescriptionParser({("a", "number", "column a"): + {"b": "number", "c": "string"}})) + + self.assertEqual( + [{"id": "a", "label": "column a", "type": "number", + "depth": 0, "container": "dict", "custom_properties": {}}, + {"id": "b", "label": "column b", "type": "string", + "depth": 1, "container": "scalar", "custom_properties": {}}], + DataTable.TableDescriptionParser({("a", "number", "column a"): + ("b", "string", "column b")})) + + def testAppendData(self): + # We check a few examples where the format of the data does not match the + # description and hen a few valid examples. The test for the content itself + # is done inside the ToJSCode and ToJSon functions. + table = DataTable([("a", "number"), ("b", "string")]) + self.assertEqual(0, table.NumberOfRows()) + self.assertRaises(DataTableException, + table.AppendData, [[1, "a", True]]) + self.assertRaises(DataTableException, + table.AppendData, {1: ["a"], 2: ["b"]}) + self.assertEquals(None, table.AppendData([[1, "a"], [2, "b"]])) + self.assertEqual(2, table.NumberOfRows()) + self.assertEquals(None, table.AppendData([[3, "c"], [4]])) + self.assertEqual(4, table.NumberOfRows()) + + table = DataTable({"a": "number", "b": "string"}) + self.assertEqual(0, table.NumberOfRows()) + self.assertRaises(DataTableException, + table.AppendData, [[1, "a"]]) + self.assertRaises(DataTableException, + table.AppendData, {5: {"b": "z"}}) + self.assertEquals(None, table.AppendData([{"a": 1, "b": "z"}])) + self.assertEqual(1, table.NumberOfRows()) + + table = DataTable({("a", "number"): [("b", "string")]}) + self.assertEqual(0, table.NumberOfRows()) + self.assertRaises(DataTableException, + table.AppendData, [[1, "a"]]) + self.assertRaises(DataTableException, + table.AppendData, {5: {"b": "z"}}) + self.assertEquals(None, table.AppendData({5: ["z"], 6: ["w"]})) + self.assertEqual(2, table.NumberOfRows()) + + table = DataTable({("a", "number"): {"b": "string", "c": "number"}}) + self.assertEqual(0, table.NumberOfRows()) + self.assertRaises(DataTableException, + table.AppendData, [[1, "a"]]) + self.assertRaises(DataTableException, + table.AppendData, {1: ["a", 2]}) + self.assertEquals(None, table.AppendData({5: {"b": "z", "c": 6}, + 7: {"c": 8}, + 9: {}})) + self.assertEqual(3, table.NumberOfRows()) + + def testToJSCode(self): + table = DataTable([("a", "number", "A"), "b", ("c", "timeofday")], + [[1], + [None, "z", time(1, 2, 3)], + [(2, "2$"), "w", time(2, 3, 4)]]) + self.assertEqual(3, table.NumberOfRows()) + self.assertEqual(("var mytab = new google.visualization.DataTable();\n" + "mytab.addColumn('number', 'A', 'a');\n" + "mytab.addColumn('string', 'b', 'b');\n" + "mytab.addColumn('timeofday', 'c', 'c');\n" + "mytab.addRows(3);\n" + "mytab.setCell(0, 0, 1);\n" + "mytab.setCell(1, 1, 'z');\n" + "mytab.setCell(1, 2, [1,2,3]);\n" + "mytab.setCell(2, 0, 2, '2$');\n" + "mytab.setCell(2, 1, 'w');\n" + "mytab.setCell(2, 2, [2,3,4]);\n"), + table.ToJSCode("mytab")) + + table = DataTable({("a", "number"): {"b": "date", "c": "datetime"}}, + {1: {}, + 2: {"b": date(1, 2, 3)}, + 3: {"c": datetime(1, 2, 3, 4, 5, 6)}}) + self.assertEqual(3, table.NumberOfRows()) + self.assertEqual(("var mytab2 = new google.visualization.DataTable();\n" + "mytab2.addColumn('datetime', 'c', 'c');\n" + "mytab2.addColumn('date', 'b', 'b');\n" + "mytab2.addColumn('number', 'a', 'a');\n" + "mytab2.addRows(3);\n" + "mytab2.setCell(0, 2, 1);\n" + "mytab2.setCell(1, 1, new Date(1,1,3));\n" + "mytab2.setCell(1, 2, 2);\n" + "mytab2.setCell(2, 0, new Date(1,1,3,4,5,6));\n" + "mytab2.setCell(2, 2, 3);\n"), + table.ToJSCode("mytab2", columns_order=["c", "b", "a"])) + + def testToJSon(self): + # The json of the initial data we load to the table. + init_data_json = ("{cols:" + "[{id:'a',label:'A',type:'number'}," + "{id:'b',label:'b',type:'string'}," + "{id:'c',label:'c',type:'boolean'}]," + "rows:[" + "{c:[{v:1},,{v:null}]}," + "{c:[,{v:'z'},{v:true}]}" + "]}") + table = DataTable([("a", "number", "A"), "b", ("c", "boolean")], + [[1], + [None, "z", True]]) + self.assertEqual(2, table.NumberOfRows()) + self.assertEqual(init_data_json, + table.ToJSon()) + table.AppendData([[-1, "w", False]]) + self.assertEqual(3, table.NumberOfRows()) + self.assertEqual(init_data_json[:-2] + ",{c:[{v:-1},{v:'w'},{v:false}]}]}", + table.ToJSon()) + + cols_json = ("{cols:" + "[{id:'t',label:'T',type:'timeofday'}," + "{id:'d',label:'d',type:'date'}," + "{id:'dt',label:'dt',type:'datetime'}],") + table = DataTable({("d", "date"): [("t", "timeofday", "T"), + ("dt", "datetime")]}) + table.LoadData({date(1, 2, 3): [time(1, 2, 3)]}) + self.assertEqual(1, table.NumberOfRows()) + self.assertEqual(cols_json + + "rows:[{c:[{v:[1,2,3]},{v:new Date(1,1,3)},{v:null}]}]}", + table.ToJSon(columns_order=["t", "d", "dt"])) + table.LoadData({date(2, 3, 4): [(time(2, 3, 4), "time 2 3 4"), + datetime(1, 2, 3, 4, 5, 6)], + date(3, 4, 5): []}) + self.assertEqual(2, table.NumberOfRows()) + self.assertEqual((cols_json + "rows:[" + "{c:[{v:[2,3,4],f:'time 2 3 4'},{v:new Date(2,2,4)}," + "{v:new Date(1,1,3,4,5,6)}]}," + "{c:[,{v:new Date(3,3,5)},{v:null}]}]}"), + table.ToJSon(columns_order=["t", "d", "dt"])) + + json = ("{cols:[{id:'a',label:'a',type:'string'}," + "{id:'b',label:'b',type:'number'}]," + "rows:[{c:[{v:'a1'},{v:1}]},{c:[{v:'a2'},{v:2}]}," + "{c:[{v:'a3'},{v:3}]}]}") + table = DataTable({"a": ("b", "number")}, + {"a1": 1, "a2": 2, "a3": 3}) + self.assertEqual(3, table.NumberOfRows()) + self.assertEqual(json, + table.ToJSon()) + + def testCustomProperties(self): + # The json of the initial data we load to the table. + json = ("{cols:" + "[{id:'a',label:'A',type:'number',p:{'col_cp':'col_v'}}," + "{id:'b',label:'b',type:'string'}," + "{id:'c',label:'c',type:'boolean'}]," + "rows:[" + "{c:[{v:1},,{v:null,p:{'null_cp':'null_v'}}],p:{'row_cp':'row_v'}}," + "{c:[,{v:'z',p:{'cell_cp':'cell_v'}},{v:true}]}," + "{c:[{v:3},,{v:null}],p:{'row_cp2':'row_v2'}}]," + "p:{'global_cp':'global_v'}" + "}") + jscode = ("var mytab = new google.visualization.DataTable();\n" + "mytab.setTableProperties({'global_cp':'global_v'});\n" + "mytab.addColumn('number', 'A', 'a');\n" + "mytab.setColumnProperties(0, {'col_cp':'col_v'});\n" + "mytab.addColumn('string', 'b', 'b');\n" + "mytab.addColumn('boolean', 'c', 'c');\n" + "mytab.addRows(3);\n" + "mytab.setCell(0, 0, 1);\n" + "mytab.setCell(0, 2, null, null, {'null_cp':'null_v'});\n" + "mytab.setRowProperties(0, {'row_cp':'row_v'});\n" + "mytab.setCell(1, 1, 'z', null, {'cell_cp':'cell_v'});\n" + "mytab.setCell(1, 2, true);\n" + "mytab.setCell(2, 0, 3);\n" + "mytab.setRowProperties(2, {'row_cp2':'row_v2'});\n") + + table = DataTable([("a", "number", "A", {"col_cp": "col_v"}), "b", + ("c", "boolean")], + custom_properties={"global_cp": "global_v"}) + table.AppendData([[1, None, (None, None, {"null_cp": "null_v"})]], + custom_properties={"row_cp": "row_v"}) + table.AppendData([[None, ("z", None, {"cell_cp": "cell_v"}), True], [3]]) + table.SetRowsCustomProperties(2, {"row_cp2": "row_v2"}) + self.assertEqual(json, table.ToJSon()) + self.assertEqual(jscode, table.ToJSCode("mytab")) + + def testToCsv(self): + init_data_csv = "\n".join(['"A", "b", "c"', + '1, "", ""', + '"", "zz\'top", true']) + table = DataTable([("a", "number", "A"), "b", ("c", "boolean")], + [[(1, "$1")], [None, "zz'top", True]]) + self.assertEqual(init_data_csv, table.ToCsv()) + table.AppendData([[-1, "w", False]]) + init_data_csv = "%s\n%s" % (init_data_csv, '-1, "w", false') + self.assertEquals(init_data_csv, table.ToCsv()) + + init_data_csv = "\n".join([ + '"T", "d", "dt"', + '"[1,2,3]", "new Date(1,1,3)", ""', + '"time ""2 3 4""", "new Date(2,2,4)", "new Date(1,1,3,4,5,6)"', + '"", "new Date(3,3,5)", ""']) + table = DataTable({("d", "date"): [("t", "timeofday", "T"), + ("dt", "datetime")]}) + table.LoadData({date(1, 2, 3): [time(1, 2, 3)], + date(2, 3, 4): [(time(2, 3, 4), 'time "2 3 4"'), + datetime(1, 2, 3, 4, 5, 6)], + date(3, 4, 5): []}) + self.assertEqual(init_data_csv, table.ToCsv(columns_order=["t", "d", "dt"])) + + def testToTsvExcel(self): + table = DataTable({("d", "date"): [("t", "timeofday", "T"), + ("dt", "datetime")]}) + table.LoadData({date(1, 2, 3): [time(1, 2, 3)], + date(2, 3, 4): [(time(2, 3, 4), 'time "2 3 4"'), + datetime(1, 2, 3, 4, 5, 6)], + date(3, 4, 5): []}) + self.assertEqual(table.ToCsv().replace(", ", "\t").encode("UTF-16LE"), + table.ToTsvExcel()) + + def testToHtml(self): + html_table_header = "" + html_table_footer = "
" + init_data_html = html_table_header + ( + "" + "Abc" + "" + "" + "'$1'" + "'<z>'true" + "") + html_table_footer + table = DataTable([("a", "number", "A"), "b", ("c", "boolean")], + [[(1, "$1")], [None, "", True]]) + self.assertEqual(init_data_html.replace("\n", ""), table.ToHtml()) + + init_data_html = html_table_header + ( + "" + "Tddt" + "" + "" + "[1,2,3]new Date(1,1,3)" + "'time 2 3 4'new Date(2,2,4)" + "new Date(1,1,3,4,5,6)" + "new Date(3,3,5)" + "") + html_table_footer + table = DataTable({("d", "date"): [("t", "timeofday", "T"), + ("dt", "datetime")]}) + table.LoadData({date(1, 2, 3): [time(1, 2, 3)], + date(2, 3, 4): [(time(2, 3, 4), "time 2 3 4"), + datetime(1, 2, 3, 4, 5, 6)], + date(3, 4, 5): []}) + self.assertEqual(init_data_html.replace("\n", ""), + table.ToHtml(columns_order=["t", "d", "dt"])) + + def testOrderBy(self): + data = [("b", 3), ("a", 3), ("a", 2), ("b", 1)] + description = ["col1", ("col2", "number", "Second Column")] + table = DataTable(description, data) + + table_num_sorted = DataTable(description, + sorted(data, key=lambda x: (x[1], x[0]))) + + table_str_sorted = DataTable(description, + sorted(data, key=lambda x: x[0])) + + table_diff_sorted = DataTable(description, + sorted(sorted(data, key=lambda x: x[1]), + key=lambda x: x[0], reverse=True)) + + self.assertEqual(table_num_sorted.ToJSon(), + table.ToJSon(order_by=("col2", "col1"))) + self.assertEqual(table_num_sorted.ToJSCode("mytab"), + table.ToJSCode("mytab", order_by=("col2", "col1"))) + + self.assertEqual(table_str_sorted.ToJSon(), table.ToJSon(order_by="col1")) + self.assertEqual(table_str_sorted.ToJSCode("mytab"), + table.ToJSCode("mytab", order_by="col1")) + + self.assertEqual(table_diff_sorted.ToJSon(), + table.ToJSon(order_by=[("col1", "desc"), "col2"])) + self.assertEqual(table_diff_sorted.ToJSCode("mytab"), + table.ToJSCode("mytab", + order_by=[("col1", "desc"), "col2"])) + + def testToJSonResponse(self): + description = ["col1", "col2", "col3"] + data = [("1", "2", "3"), ("a", "b", "c"), ("One", "Two", "Three")] + req_id = 4 + table = DataTable(description, data) + + start_str_default = r"google.visualization.Query.setResponse" + start_str_handler = r"MyHandlerFunction" + default_params = (r"\s*'version'\s*:\s*'0.6'\s*,\s*'reqId'\s*:\s*'%s'\s*," + r"\s*'status'\s*:\s*'OK'\s*" % req_id) + regex1 = re.compile("%s\(\s*\{%s,\s*'table'\s*:\s*{(.*)}\s*\}\s*\);" % + (start_str_default, default_params)) + regex2 = re.compile("%s\(\s*\{%s,\s*'table'\s*:\s*{(.*)}\s*\}\s*\);" % + (start_str_handler, default_params)) + + json_str = table.ToJSon().strip() + + json_response = table.ToJSonResponse(req_id=req_id) + match = regex1.findall(json_response) + self.assertEquals(len(match), 1) + # We want to match against the json_str without the curly brackets. + self.assertEquals(match[0], json_str[1:-1]) + + json_response = table.ToJSonResponse(req_id=req_id, + response_handler=start_str_handler) + match = regex2.findall(json_response) + self.assertEquals(len(match), 1) + # We want to match against the json_str without the curly brackets. + self.assertEquals(match[0], json_str[1:-1]) + + def testToResponse(self): + description = ["col1", "col2", "col3"] + data = [("1", "2", "3"), ("a", "b", "c"), ("One", "Two", "Three")] + table = DataTable(description, data) + + self.assertEquals(table.ToResponse(), table.ToJSonResponse()) + self.assertEquals(table.ToResponse(tqx="out:csv"), table.ToCsv()) + self.assertEquals(table.ToResponse(tqx="out:html"), table.ToHtml()) + self.assertRaises(DataTableException, table.ToResponse, tqx="version:0.1") + self.assertEquals(table.ToResponse(tqx="reqId:4;responseHandler:handle"), + table.ToJSonResponse(req_id=4, response_handler="handle")) + self.assertEquals(table.ToResponse(tqx="out:csv;reqId:4"), table.ToCsv()) + self.assertEquals(table.ToResponse(order_by="col2"), + table.ToJSonResponse(order_by="col2")) + self.assertEquals(table.ToResponse(tqx="out:html", + columns_order=("col3", "col2", "col1")), + table.ToHtml(columns_order=("col3", "col2", "col1"))) + self.assertRaises(ValueError, table.ToResponse, tqx="SomeWrongTqxFormat") + self.assertRaises(DataTableException, table.ToResponse, tqx="out:bad") + + +if __name__ == "__main__": + unittest.main() diff -r 805400745f57 -r 05ab9393303d app/gviz/setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/gviz/setup.py Mon Jun 01 20:31:38 2009 +0200 @@ -0,0 +1,59 @@ +#!/usr/bin/python +# +# Copyright (C) 2009 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Setup module for Google Visualization Python API.""" + +__author__ = "Misha Seltzer" + +import distutils.core +import unittest +import gviz_api_test + + +class TestCommand(distutils.core.Command): + """Class that provides the 'test' command for setup.""" + user_options = [] + + def initialize_options(self): + """Must override this method in the Command class.""" + pass + + def finalize_options(self): + """Must override this method in the Command class.""" + pass + + def run(self): + """The run method - running the tests on invocation.""" + suite = unittest.TestLoader().loadTestsFromTestCase( + gviz_api_test.DataTableTest) + unittest.TextTestRunner().run(suite) + + +distutils.core.setup( + name="gviz_api.py", + version="1.6", + description="Python API for Google Visualization", + long_description=""" +The Python API for Google Visualization makes it easy to convert python data +structures into Google Visualization JS code, DataTable JSon construction +string or JSon response for Query object. +""".strip(), + author="Amit Weinstein, Misha Seltzer", + license="Apache 2.0", + url="http://code.google.com/p/google-visualization-python/", + py_modules=["gviz_api"], + cmdclass={"test": TestCommand}, +)