Merge with Pawel's head.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/gviz/COPYRIGHT Mon Jun 01 20:38: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.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/gviz/README Mon Jun 01 20:38: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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/gviz/examples/dynamic_example.py Mon Jun 01 20:38: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.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/gviz/examples/static_example.py Mon Jun 01 20:38: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 = """
+<html>
+ <head>
+ <title>Static example</title>
+ <script src="http://www.google.com/jsapi" type="text/javascript"></script>
+ <script>
+ google.load("visualization", "1", {packages:["table"]});
+
+ google.setOnLoadCallback(drawTable);
+ function drawTable() {
+ %(jscode)s
+ var jscode_table = new google.visualization.Table(document.getElementById('table_div_jscode'));
+ jscode_table.draw(jscode_data, {showRowNumber: true});
+
+ var json_table = new google.visualization.Table(document.getElementById('table_div_json'));
+ var json_data = new google.visualization.DataTable(%(json)s, 0.5);
+ json_table.draw(json_data, {showRowNumber: true});
+ }
+ </script>
+ </head>
+ <body>
+ <H1>Table created using ToJSCode</H1>
+ <div id="table_div_jscode"></div>
+ <H1>Table created using ToJSon</H1>
+ <div id="table_div_json"></div>
+ </body>
+</html>
+"""
+
+
+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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/gviz/gviz_api.py Mon Jun 01 20:38: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):
+ <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 % 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"])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/gviz/gviz_api_test.py Mon Jun 01 20:38: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><body><table border='1'>"
+ html_table_footer = "</table></body></html>"
+ init_data_html = html_table_header + (
+ "<thead><tr>"
+ "<th>A</th><th>b</th><th>c</th>"
+ "</tr></thead>"
+ "<tbody>"
+ "<tr><td>'$1'</td><td></td><td></td></tr>"
+ "<tr><td></td><td>'<z>'</td><td>true</td></tr>"
+ "</tbody>") + html_table_footer
+ table = DataTable([("a", "number", "A"), "b", ("c", "boolean")],
+ [[(1, "$1")], [None, "<z>", True]])
+ self.assertEqual(init_data_html.replace("\n", ""), table.ToHtml())
+
+ init_data_html = html_table_header + (
+ "<thead><tr>"
+ "<th>T</th><th>d</th><th>dt</th>"
+ "</tr></thead>"
+ "<tbody>"
+ "<tr><td>[1,2,3]</td><td>new Date(1,1,3)</td><td></td></tr>"
+ "<tr><td>'time 2 3 4'</td><td>new Date(2,2,4)</td>"
+ "<td>new Date(1,1,3,4,5,6)</td></tr>"
+ "<tr><td></td><td>new Date(3,3,5)</td><td></td></tr>"
+ "</tbody>") + 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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/gviz/setup.py Mon Jun 01 20:38: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},
+)