Last active
October 13, 2019 20:32
-
-
Save mverleg/3bc2bf83fb7a29d119960e0f7b116920 to your computer and use it in GitHub Desktop.
Convert json list of dicts to a table
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
Render json as a text table. | |
Input should be a list of dictionaries: | |
[{"name": "first", "a": 1, "b": null, "x": true}, {"b": 2, "name": "second", "x": false}, {"a": 123, "name": "third", "x": null}] | |
Output will be a table: | |
NAME A B Y | |
first 1 Y | |
second 2 N | |
third 123 | |
Call like this either of these: | |
echo '[{"column_name": "some value"}]' | python3 json2table.py | |
echo '[{"column_name": "some value"}]' | json2table | |
The second one needs this file to be on PATH, without .py extension, and with execute permission. | |
""" | |
from collections import OrderedDict | |
from json import loads | |
from sys import stdin, stdout, stderr | |
from typing import List, Set, Dict, Tuple, Optional, Union, Any, List, Optional, cast | |
class Column: | |
def __init__(self, name: str, example_value: Any, required: bool): | |
self.name: str = name | |
self.typ: str = get_type(example_value) | |
self.length: int = max(len(self.name), len(render_raw(self.typ, example_value))) | |
self.required: bool = required | |
def update(self, example_value: Any): | |
if self.typ != 'str' and example_value is not None: | |
new_typ = get_type(example_value) | |
if self.typ != new_typ: | |
self.typ = 'str' | |
if example_value is not None: | |
example_length = len(render_raw(self.typ, example_value)) | |
if example_length > self.length: | |
self.length = example_length | |
def get_type(value: Any) -> str: | |
if value is None: | |
return 'unknown' | |
if isinstance(value, bool): | |
return 'bool' | |
if isinstance(value, int): | |
return 'int' | |
if isinstance(value, float): | |
return 'float' | |
return 'str' | |
def render_raw(typ: str, value: Any) -> str: | |
if value is None: | |
return '' | |
# need special case for True and False, because the type could change from bool to str, | |
# which would make their length longer. This might make columns misalign, so keep T and F | |
# as string representation even if the type is string. | |
if typ == 'bool' or value is True or value is False: | |
return 'Y' if value else 'N' | |
if typ == 'int': | |
return str(value) | |
if typ == 'float': | |
return '{0:.3f}'.format(value) | |
if typ == 'str': | |
return str(value).strip() | |
raise NotImplementedError() | |
def render_padded(typ: str, value: Any, length: int) -> str: | |
if typ == 'bool' or typ == 'int' or typ == 'float': | |
return render_raw(typ, value).rjust(length) | |
if typ == 'str' or typ == 'unknown': | |
return render_raw(typ, value).ljust(length) | |
raise NotImplementedError('typ = {}'.format(typ)) | |
def determine_columns(json_li: List[Dict[str, Any]]) -> List[Column]: | |
""" Collect the columns and determine their lengths """ | |
if not isinstance(json_li, list): | |
stderr.write('Input should be a list of maps, but got {} instead of list\n'.format(type(json_li))) | |
exit(1) | |
columns: Map[str, Column] = OrderedDict() | |
if_first = True | |
for entry in json_li: | |
if not isinstance(entry, dict): | |
stderr.write('Input should be a list of maps, but got list of {} instead of map\n'.format(type(dict))) | |
exit(1) | |
for col_name, value in entry.items(): | |
if col_name not in columns: | |
columns[col_name] = Column( | |
name=col_name, | |
example_value=value, | |
required=if_first | |
) | |
else: | |
columns[col_name].update(value) | |
for column in columns.values(): | |
if column.name not in entry: | |
column.required = False | |
if_first = False | |
return list(columns.values()) | |
def print_header(columns: List[Column]): | |
if len(columns) == 0: | |
return | |
line_parts = [] | |
for column in columns: | |
line_parts.append(render_padded(column.typ, column.name.upper(), column.length)) | |
line = ' '.join(line_parts).rstrip() | |
stdout.write(line) | |
stdout.write('\n') | |
def print_rows(columns: List[Column], json_li: List[Dict[str, Any]]): | |
for entry in json_li: | |
line_parts = [] | |
for k, column in enumerate(columns): | |
if column.name in entry: | |
value = render_padded(column.typ, entry[column.name], length=column.length) | |
else: | |
value = ' ' * column.length | |
line_parts.append(value) | |
line = ' '.join(line_parts).rstrip() | |
stdout.write(line) | |
stdout.write('\n') | |
if __name__ == '__main__': | |
json_str = stdin.read() | |
inp_list = None | |
try: | |
inp_list = loads(json_str) | |
except ValueError: | |
stderr.write('failed to parse input as json\n{}\n'.format(json_str)) | |
exit(1) | |
columns = determine_columns(inp_list) | |
print_header(columns) | |
print_rows(columns, inp_list) | |
stdout.flush() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment