├── tests ├── __init__.py └── test_json2table.py ├── MANIFEST.in ├── setup.cfg ├── json2table ├── __init__.py └── json2table.py ├── .gitignore ├── .travis.yml ├── setup.py ├── LICENSE.txt └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /json2table/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | from .json2table import convert 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # byte-compiled files 2 | __pycache__/ 3 | *.pyc 4 | 5 | # distribution 6 | build/ 7 | dist/ 8 | sdist/ 9 | *.egg-info/ 10 | *.egg 11 | 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.5 5 | install: 6 | - pip install -U pip 7 | - pip install coveralls 8 | script: coverage run --source=json2table -m unittest tests.test_json2table 9 | after_success: 10 | coveralls 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name = "json2table", 5 | packages = ["json2table"], 6 | version = "1.1.5", 7 | description = "Convert JSON to an HTML table", 8 | long_description=open("README.rst").read(), 9 | author = "Ryan Latture", 10 | author_email = "ryan.latture@gmail.com", 11 | url = "https://github.com/latture/json2table", 12 | download_url = "https://github.com/latture/json2table/tarball/master", 13 | keywords = ["json", "HTML", "convert", "table"], 14 | license = "MIT", 15 | classifiers = ( 16 | "Programming Language :: Python", 17 | "Programming Language :: Python :: 2", 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | ), 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan Latture 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | json2table 2 | ========== 3 | 4 | |Build Status| |Coverage Status| 5 | 6 | .. |Build Status| image:: https://travis-ci.org/latture/json2table.svg?branch=master 7 | :target: https://travis-ci.org/latture/json2table 8 | .. |Coverage Status| image:: https://coveralls.io/repos/github/latture/json2table/badge.svg?branch=master 9 | :target: https://coveralls.io/github/latture/json2table?branch=master 10 | 11 | This is a simple Python package that allows a JSON object to be converted to HTML. It provides a ``convert`` function that accepts a ``dict`` instance and returns a string of converted HTML. For example, the simple JSON object ``{"key" : "value"}`` can be converted to HTML via: 12 | 13 | .. code:: python 14 | 15 | >>> from json2table import convert 16 | >>> json_object = {"key" : "value"} 17 | >>> build_direction = "LEFT_TO_RIGHT" 18 | >>> table_attributes = {"style" : "width:100%"} 19 | >>> html = convert(json_object, build_direction=build_direction, table_attributes=table_attributes) 20 | >>> print(html) 21 | '
keyvalue
' 22 | 23 | The resulting table will resemble 24 | 25 | +---------+-------+ 26 | | **key** | value | 27 | +---------+-------+ 28 | 29 | More complex parsing is also possible. If a list of ``dict``'s provides the same list of keys, the generated HTML with gather items by key and display them in the same column. 30 | 31 | .. code:: json 32 | 33 | {"menu": { 34 | "id": "file", 35 | "value": "File", 36 | "menuitem": [ 37 | {"value": "New", "onclick": "CreateNewDoc()"}, 38 | {"value": "Open", "onclick": "OpenDoc()"}, 39 | {"value": "Close", "onclick": "CloseDoc()"} 40 | ] 41 | } 42 | } 43 | 44 | Output: 45 | 46 | +----------+--------------+----------------+-----------+ 47 | | **menu** | **menuitem** | **onclick** | **value** | 48 | + + +----------------+-----------+ 49 | | | | CreateNewDoc() | New | 50 | + + +----------------+-----------+ 51 | | | | OpenDoc() | Open | 52 | + + +----------------+-----------+ 53 | | | | CloseDoc() | Close | 54 | + +--------------+----------------+-----------+ 55 | | | **id** | file | 56 | + +--------------+----------------+-----------+ 57 | | | **value** | File | 58 | +----------+--------------+----------------+-----------+ 59 | 60 | It might, however, be more readable if we were able to build the table from top-to-bottom instead of the default left-to-right. Changing the ``build_direction`` to ``"TOP_TO_BOTTOM"`` yields: 61 | 62 | +----------------+-----------+-------+-----------+ 63 | | **menu** | 64 | +----------------+-----------+-------+-----------+ 65 | | **menuitem** | **id**| **value** | 66 | +----------------+-----------+-------+-----------+ 67 | | **onclick** | **value** | file | File | 68 | +----------------+-----------+ + + 69 | | CreateNewDoc() | New | | | 70 | +----------------+-----------+ + + 71 | | OpenDoc() | Open | | | 72 | +----------------+-----------+ + + 73 | | CloseDoc() | Close | | | 74 | +----------------+-----------+-------+-----------+ 75 | 76 | Table attributes are added via the ``table_attributes`` parameter. This parameter should be a ``dict`` of ``(key, value)`` pairs to apply to the table in the form ``key="value"``. If in our simple example before we additionally wanted to apply a class attribute of ``"table table-striped"`` we would use the following: 77 | 78 | .. code:: python 79 | 80 | >>> table_attributes = {"style" : "width:100%", "class" : "table table-striped"} 81 | 82 | and convert just as before: 83 | 84 | .. code:: python 85 | 86 | >>> html = convert(json_object, build_direction=build_direction, table_attributes=table_attributes) 87 | 88 | Details 89 | ------- 90 | This module provides a single ``convert`` function. It takes as input the JSON object (represented as a Python ``dict``) and, optionally, a build direction and a dictionary of table attributes to customize the generated table: 91 | 92 | ``convert(json_input, build_direction="LEFT_TO_RIGHT", table_attributes=None)`` 93 | 94 | **Parameters** 95 | 96 | json_input : dict 97 | 98 | JSON object to convert into HTML. 99 | 100 | build_direction : ``{"TOP_TO_BOTTOM", "LEFT_TO_RIGHT"}``, optional 101 | 102 | String denoting the build direction of the table. If ``"TOP_TO_BOTTOM"`` child 103 | objects will be appended below parents, i.e. in the subsequent row. If ``"LEFT_TO_RIGHT"`` 104 | child objects will be appended to the right of parents, i.e. in the subsequent column. 105 | Default is ``"LEFT_TO_RIGHT"``. 106 | 107 | table_attributes : ``dict``, optional 108 | 109 | Dictionary of ``(key, value)`` pairs describing attributes to add to the table. 110 | Each attribute is added according to the template ``key="value"``. For example, 111 | the table ``{ "border" : 1 }`` modifies the generated table tags to include 112 | ``border="1"`` as an attribute. The generated opening tag would look like 113 | ````. Default is ``None``. 114 | 115 | **Returns** 116 | 117 | ``str`` 118 | 119 | String of converted HTML. 120 | 121 | Installation 122 | ------------ 123 | The easiest method on installation is to use ``pip``. Simply run: 124 | 125 | :: 126 | 127 | >>> pip install json2table 128 | 129 | If instead the repo was cloned, navigate to the root directory of the ``json2table`` package from the command line and execute: 130 | 131 | :: 132 | 133 | >>> python setup.py install 134 | 135 | Tests 136 | ----- 137 | 138 | In order to verify the code is working, from the command line navigate to the ``json2table`` root directory and run: 139 | 140 | :: 141 | 142 | >>> python -m unittest tests.test_json2table 143 | -------------------------------------------------------------------------------- /tests/test_json2table.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests to verify proper conversion of JSON to HTML. 3 | """ 4 | from __future__ import division, print_function, absolute_import 5 | from collections import OrderedDict 6 | import unittest 7 | from json2table.json2table import convert, JsonConverter 8 | 9 | 10 | def _to_ordered_dict(d): 11 | """ 12 | Recursively converts a dict to OrderedDict. 13 | This is needed to preserve `(key, value)` ordering 14 | with iterating through `dict`'s between Python 2 and Python 3. 15 | 16 | Parameters 17 | ---------- 18 | d : dict 19 | Dictionary to order. 20 | 21 | Returns 22 | ------- 23 | OrderedDict 24 | Recursively order representation of the input dictionary. 25 | """ 26 | d_ordered = OrderedDict() 27 | for key, value in sorted(d.items()): 28 | if isinstance(value, dict): 29 | d_ordered[key] = _to_ordered_dict(value) 30 | elif isinstance(value, list) and (all(isinstance(item, dict) for item in value)): 31 | list_of_ordered_dicts = [] 32 | for item in value: 33 | list_of_ordered_dicts.append(_to_ordered_dict(item)) 34 | d_ordered[key] = list_of_ordered_dicts 35 | else: 36 | d_ordered[key] = value 37 | return d_ordered 38 | 39 | 40 | class TestConvert(unittest.TestCase): 41 | def setUp(self): 42 | self.simple_json = {"key" : "value"} 43 | self.custom_table_attributes = {"border" : 1} 44 | nested_json = { 45 | "menu": { 46 | "id": "file", 47 | "value": "File", 48 | "menuitem": [{"value": "New", "onclick": "CreateNewDoc()"}, 49 | {"value": "Open", "onclick": "OpenDoc()"}, 50 | {"value": "Close", "onclick": "CloseDoc()"}] 51 | }} 52 | self.nested_json = _to_ordered_dict(nested_json) 53 | self.maxDiff = None 54 | 55 | def test_invalid_build_direction(self): 56 | with self.assertRaises(ValueError) as context: 57 | convert(None, build_direction=None) 58 | self.assertTrue("Invalid build direction" in context.exception) 59 | 60 | def test_invalid_table_attributes(self): 61 | with self.assertRaises(TypeError) as context: 62 | convert(None, table_attributes=0) 63 | self.assertTrue("Table attributes must be either" in context.exception) 64 | 65 | def test_invalid_json(self): 66 | with self.assertRaises(AttributeError) as context: 67 | convert(None) 68 | 69 | def test_simple(self): 70 | result = convert(self.simple_json) 71 | simple_table = "
keyvalue
" 72 | self.assertEqual(result, simple_table) 73 | 74 | def test_custom_table_attributes(self): 75 | result = convert({}, table_attributes=self.custom_table_attributes) 76 | self.assertTrue("border=\"1\"" in result) 77 | 78 | def test_build_direction_top_to_bottom(self): 79 | result = convert(self.simple_json, build_direction="TOP_TO_BOTTOM") 80 | simple_table = "
key
value
" 81 | self.assertEqual(result, simple_table) 82 | 83 | def test_clubbed_json(self): 84 | clubbed_json = _to_ordered_dict({"sample": [ {"a":1, "b":2, "c":3}, {"a":5, "b":6, "c":7} ] }) 85 | result = convert(clubbed_json) 86 | clubbed_table = "
sample<"\ 87 | "tr>"\ 88 | "
abc
123
567
" 89 | self.assertEqual(result, clubbed_table) 90 | 91 | def test_nested_left_to_right(self): 92 | result = convert(self.nested_json, build_direction="LEFT_TO_RIGHT") 93 | nested_table = "
menu
idfile
me"\ 94 | "nuitem
onclickvalue
CreateNew"\ 95 | "Doc()New
OpenDoc()Open
Clo"\ 96 | "seDoc()Close
valueFile
" 98 | self.assertEqual(result, nested_table) 99 | 100 | def test_nested_top_to_bottom(self): 101 | result = convert(self.nested_json, build_direction="TOP_TO_BOTTOM") 102 | nested_table = "
menu
value
idmenuitem
file
onclickvalue
CreateNewDoc()New
OpenDoc()Op"\ 105 | "en
CloseDoc()Close
File
" 107 | self.assertEqual(result, nested_table) 108 | 109 | 110 | class TestJsonConverter(unittest.TestCase): 111 | def setUp(self): 112 | self.json_converter = JsonConverter() 113 | 114 | def test_empty_list_of_dicts_to_column_headers(self): 115 | result = self.json_converter._list_of_dicts_to_column_headers([]) 116 | self.assertIs(result, None) 117 | 118 | def test_short_list_of_dicts_to_column_headers(self): 119 | result = self.json_converter._list_of_dicts_to_column_headers([{"key" : "value"}]) 120 | self.assertIs(result, None) 121 | 122 | def test_noncollapsible_list_of_dicts_to_column_headers(self): 123 | result = self.json_converter._list_of_dicts_to_column_headers([{"key" : "value"}, {"value" : "key"}]) 124 | self.assertIs(result, None) 125 | 126 | def test_none_markup(self): 127 | result = self.json_converter._markup(None) 128 | self.assertEqual(result, "") 129 | 130 | def test_list_markup(self): 131 | result = self.json_converter._markup([1, 2, 3]) 132 | self.assertEqual(result, "") 133 | 134 | def test_uncommon_headers_maybe_club(self): 135 | result = self.json_converter._maybe_club([None]) 136 | self.assertEqual(result, "") 137 | 138 | if __name__ == "__main__": 139 | unittest.main() 140 | -------------------------------------------------------------------------------- /json2table/json2table.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple tool for converting JSON to an HTML table. 3 | This is based off of the `json2html` project. 4 | Their code can be found at https://github.com/softvar/json2html 5 | """ 6 | from __future__ import division, print_function, absolute_import 7 | import json 8 | 9 | __all__ = ["convert"] 10 | 11 | 12 | def convert(json_input, build_direction="LEFT_TO_RIGHT", table_attributes=None): 13 | """ 14 | Converts JSON to HTML Table format. 15 | 16 | Parameters 17 | ---------- 18 | json_input : dict 19 | JSON object to convert into HTML. 20 | build_direction : {"TOP_TO_BOTTOM", "LEFT_TO_RIGHT"} 21 | String denoting the build direction of the table. If ``"TOP_TO_BOTTOM"`` child 22 | objects will be appended below parents, i.e. in the subsequent row. If ``"LEFT_TO_RIGHT"`` 23 | child objects will be appended to the right of parents, i.e. in the subsequent column. 24 | Default is ``"LEFT_TO_RIGHT"``. 25 | table_attributes : dict, optional 26 | Dictionary of ``(key, value)`` pairs describing attributes to add to the table. 27 | Each attribute is added according to the template ``key="value". For example, 28 | the table ``{ "border" : 1 }`` modifies the generated table tags to include 29 | ``border="1"`` as an attribute. The generated opening tag would look like 30 | ````. Default is ``None``. 31 | 32 | Returns 33 | ------- 34 | str 35 | String of converted HTML. 36 | 37 | An example usage is shown below: 38 | 39 | >>> json_object = {"key" : "value"} 40 | >>> build_direction = "TOP_TO_BOTTOM" 41 | >>> table_attributes = {"border" : 1} 42 | >>> html = convert(json_object, build_direction=build_direction, table_attributes=table_attributes) 43 | >>> print(html) 44 | "
keyvalue
" 45 | 46 | """ 47 | json_converter = JsonConverter(build_direction=build_direction, table_attributes=table_attributes) 48 | return json_converter.convert(json_input) 49 | 50 | 51 | class JsonConverter(object): 52 | """ 53 | Class that manages the conversion of a JSON object to a string of HTML. 54 | 55 | Methods 56 | ------- 57 | convert(json_input) 58 | Converts JSON to HTML. 59 | """ 60 | 61 | def __init__(self, build_direction="LEFT_TO_RIGHT", table_attributes=None): 62 | valid_build_directions = ("TOP_TO_BOTTOM", "LEFT_TO_RIGHT") 63 | if build_direction not in valid_build_directions: 64 | raise ValueError( 65 | "Invalid build direction {}. Must be either \"TOP_TO_BOTTOM\" or \"LEFT_TO_RIGHT\".".format(build_direction)) 66 | if table_attributes is not None and not isinstance(table_attributes, dict): 67 | raise TypeError("Table attributes must be either a `dict` or `None`.") 68 | 69 | build_direction = build_direction.upper() 70 | self._build_top_to_bottom = True if build_direction == "TOP_TO_BOTTOM" else False 71 | self._table_opening_tag = "".format(JsonConverter._dict_to_html_attributes(table_attributes)) 72 | 73 | def convert(self, json_input): 74 | """ 75 | Converts JSON to HTML Table format. 76 | 77 | Parameters 78 | ---------- 79 | json_input : dict 80 | JSON object to convert into HTML. 81 | 82 | Returns 83 | ------- 84 | str 85 | String of converted HTML. 86 | """ 87 | html_output = self._table_opening_tag 88 | if self._build_top_to_bottom: 89 | html_output += self._markup_header_row(json_input.keys()) 90 | html_output += "" 91 | for value in json_input.values(): 92 | if isinstance(value, list): 93 | # check if all keys in the list are identical 94 | # and group all values under a common column 95 | # heading if so, if not default to normal markup 96 | html_output += self._maybe_club(value) 97 | else: 98 | html_output += self._markup_table_cell(value) 99 | html_output += "" 100 | else: 101 | for key, value in iter(json_input.items()): 102 | html_output += "{:s}".format(self._markup(key)) 103 | if isinstance(value, list): 104 | html_output += self._maybe_club(value) 105 | else: 106 | html_output += self._markup_table_cell(value) 107 | html_output += "" 108 | html_output += "" 109 | return html_output 110 | 111 | def _markup_table_cell(self, value): 112 | """ 113 | Wraps the generated HTML in table cell `` tags. 114 | 115 | Parameters 116 | ---------- 117 | value : object 118 | Object to place in the table cell. 119 | 120 | Returns 121 | ------- 122 | str 123 | String of HTML wrapped in table cell tags. 124 | """ 125 | return "{:s}".format(self._markup(value)) 126 | 127 | def _markup_header_row(self, headers): 128 | """ 129 | Creates a row of table header items. 130 | 131 | Parameters 132 | ---------- 133 | headers : list 134 | List of column headers. Each will be wrapped in `` tags. 135 | 136 | Returns 137 | ------- 138 | str 139 | Table row of headers. 140 | """ 141 | return "" + "".join(headers) + "" 142 | 143 | @staticmethod 144 | def _dict_to_html_attributes(d): 145 | """ 146 | Converts a dictionary to a string of ``key=\"value\"`` pairs. 147 | If ``None`` is provided as the dictionary an empty string is returned, 148 | i.e. no html attributes are generated. 149 | 150 | Parameters 151 | ---------- 152 | d : dict 153 | Dictionary to convert to html attributes. 154 | 155 | Returns 156 | ------- 157 | str 158 | String of HTML attributes in the form ``key_i=\"value_i\" ... key_N=\"value_N\"``, 159 | where ``N`` is the total number of ``(key, value)`` pairs. 160 | """ 161 | if d is None: 162 | return "" 163 | 164 | return "".join(" {}=\"{}\"".format(key, value) for key, value in iter(d.items())) 165 | 166 | @staticmethod 167 | def _list_of_dicts_to_column_headers(list_of_dicts): 168 | """ 169 | Detects if all entries in an list of ``dict``'s have identical keys. 170 | Returns the keys if all keys are the same and ``None`` otherwise. 171 | 172 | Parameters 173 | ---------- 174 | list_of_dicts : list 175 | List of dictionaries to test for identical keys. 176 | 177 | Returns 178 | ------- 179 | list or None 180 | List of column headers if all dictionary posessed the same keys. Returns ``None`` otherwise. 181 | """ 182 | 183 | if len(list_of_dicts) < 2 or not all(isinstance(item, dict) for item in list_of_dicts): 184 | return None 185 | 186 | column_headers = list_of_dicts[0].keys() 187 | for d in list_of_dicts[1:]: 188 | if len(d.keys()) != len(column_headers) or not all(header in d for header in column_headers): 189 | return None 190 | return column_headers 191 | 192 | def _markup(self, entry): 193 | """ 194 | Recursively generates HTML for the current entry. 195 | 196 | Parameters 197 | ---------- 198 | entry : object 199 | Object to convert to HTML. Maybe be a single entity or contain multiple and/or nested objects. 200 | 201 | Returns 202 | ------- 203 | str 204 | String of HTML formatted json. 205 | """ 206 | if entry is None: 207 | return "" 208 | if isinstance(entry, list): 209 | list_markup = "" 213 | return list_markup 214 | if isinstance(entry, dict): 215 | return self.convert(entry) 216 | 217 | # default to stringifying entry 218 | return str(entry) 219 | 220 | def _maybe_club(self, list_of_dicts): 221 | """ 222 | If all keys in a list of dicts are identical, values from each ``dict`` 223 | are clubbed, i.e. inserted under a common column heading. If the keys 224 | are not identical ``None`` is returned, and the list should be converted 225 | to HTML per the normal ``convert`` function. 226 | 227 | Parameters 228 | ---------- 229 | list_of_dicts : list 230 | List to attempt to club. 231 | 232 | Returns 233 | ------- 234 | str or None 235 | String of HTML if list was successfully clubbed. Returns ``None`` otherwise. 236 | 237 | Example 238 | ------- 239 | Given the following json object:: 240 | 241 | { 242 | "sampleData": [ 243 | {"a":1, "b":2, "c":3}, 244 | {"a":5, "b":6, "c":7}] 245 | } 246 | 247 | 248 | Calling ``_maybe_club`` would result in the following HTML table: 249 | _____________________________ 250 | | | | | | 251 | | | a | c | b | 252 | | sampleData |---|---|---| 253 | | | 1 | 3 | 2 | 254 | | | 5 | 7 | 6 | 255 | ----------------------------- 256 | 257 | Adapted from a contribution from @muellermichel to ``json2html``. 258 | """ 259 | column_headers = JsonConverter._list_of_dicts_to_column_headers(list_of_dicts) 260 | if column_headers is None: 261 | # common headers not found, return normal markup 262 | html_output = self._markup(list_of_dicts) 263 | else: 264 | html_output = self._table_opening_tag 265 | html_output += self._markup_header_row(column_headers) 266 | for list_entry in list_of_dicts: 267 | html_output += "" 268 | html_output += "".join(self._markup(list_entry[column_header]) for column_header in column_headers) 269 | html_output += "" 270 | html_output += "" 271 | 272 | return self._markup_table_cell(html_output) 273 | --------------------------------------------------------------------------------