├── 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 | '
'
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 = ""
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 = ""
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 | | a | b | c | <"\
87 | "tr>1 | 2 | 3 | | 5 | 6 | 7 | "\
88 | "
|
|---|
"
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 | | id | file |
|---|
| me"\
94 | "nuitem | | onclick | value |
|---|
| CreateNew"\
95 | "Doc() | New | | OpenDoc() | Open | | Clo"\
96 | "seDoc() | Close |
|
|---|
| value | File |
|---|
|
|---|
"
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 |
|---|
| id | menuitem | value
|---|
| file | | onclick | value |
|---|
| 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 | ""
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 = ""
210 | for item in entry:
211 | list_markup += "- {:s}
".format(self._markup(item))
212 | 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 |
--------------------------------------------------------------------------------