Skip to content

Instantly share code, notes, and snippets.

@mverleg
Last active October 13, 2019 20:32
Show Gist options
  • Save mverleg/3bc2bf83fb7a29d119960e0f7b116920 to your computer and use it in GitHub Desktop.
Save mverleg/3bc2bf83fb7a29d119960e0f7b116920 to your computer and use it in GitHub Desktop.
Convert json list of dicts to a table
#!/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