From b9fb7fad9ceebf64059b380680a2eed086c90abe Mon Sep 17 00:00:00 2001 From: Andreas Weiden Date: Mon, 12 Dec 2022 15:10:59 +0100 Subject: [PATCH 1/6] Add parser for cbt --- jc/parsers/cbt.py | 175 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_cbt.py | 145 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 jc/parsers/cbt.py create mode 100644 tests/test_cbt.py diff --git a/jc/parsers/cbt.py b/jc/parsers/cbt.py new file mode 100644 index 00000000..a0a25b07 --- /dev/null +++ b/jc/parsers/cbt.py @@ -0,0 +1,175 @@ +"""jc - JSON Convert `foo` command output parser + +Parses the human-, but not machine-, readable output of the cbt command (for Google's BigTable). + +No effort is made to convert the data types of the values in the cells. + +The timestamps of the cells are converted to Python's isoformat. + +Raw output contains all cells for each column (including timestamps in converted to Python's isoformat), +while the normal output contains only the latest value for each column. + +Usage (cli): + + $ cbt | jc --cbt + +or + + $ jc cbt + +Usage (module): + + import jc + result = jc.parse('cbt', cbt_command_output) + +Schema: + + [ + { + "key": string, + "cells": { + string: { + string: string + } + } + } + ] + +Examples: + + $ cbt -project=$PROJECT -instance=$INSTANCE lookup $TABLE foo | jc --cbt -p + [ + { + "key": "foo", + "cells": { + "foo": { + "bar": "baz" + } + } + } + ] + + $ cbt -project=$PROJECT -instance=$INSTANCE lookup $TABLE foo | jc --cbt -p -r + [ + { + "key": "foo", + "cells": [ + { + "column_family": "foo", + "column": "bar", + "timestamp": "1970-01-01T01:00:00", + "value": "baz" + } + ] + } + ] +""" +import datetime +from itertools import groupby +from typing import List, Dict +from jc.jc_types import JSONDictType +import jc.utils + + +class info(): + """Provides parser metadata (version, author, etc.)""" + version = '1.0' + description = '`cbt` command parser' + author = 'Andreas Weiden' + author_email = 'andreas.weiden@gmail.com' + # details = 'enter any other details here' + + # compatible options: linux, darwin, cygwin, win32, aix, freebsd + compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd'] + magic_commands = ['cbt'] + + +__version__ = info.version + + +def _process(proc_data: List[JSONDictType]) -> List[JSONDictType]: + """ + Final processing to conform to the schema. + + Parameters: + + proc_data: (List of Dictionaries) raw structured data to process + + Returns: + + List of Dictionaries. Structured to conform to the schema. + """ + + # process the data here + # rebuild output for added semantic information + # use helper functions in jc.utils for int, float, bool + # conversions and timestamps + out_data = [] + for row in proc_data: + cells = {} + key_func = lambda cell: (cell["column_family"], cell["column"]) + all_cells = sorted(row["cells"], key=key_func) + for (column_family, column), group in groupby(all_cells, key=key_func): + group = sorted(group, key=lambda cell: cell["timestamp"], reverse=True) + if column_family not in cells: + cells[column_family] = {} + cells[column_family][column] = group[0]["value"] + row["cells"] = cells + out_data.append(row) + return out_data + + +def parse( + data: str, + raw: bool = False, + quiet: bool = False +) -> List[JSONDictType]: + """ + Main text parsing function + + Parameters: + + data: (string) text data to parse + raw: (boolean) unprocessed output if True + quiet: (boolean) suppress warning messages if True + + Returns: + + List of Dictionaries. Raw or processed structured data. + """ + jc.utils.compatibility(__name__, info.compatible, quiet) + jc.utils.input_type_check(data) + + raw_output: List[Dict] = [] + + if jc.utils.has_data(data): + for line in filter(None, data.split("-" * 40)): + # parse the content here + # check out helper functions in jc.utils + # and jc.parsers.universal + key = None + cells = [] + column_name = "" + timestamp = None + value_next = False + for field in line.splitlines(): + if not field.strip(): + continue + if field.startswith(" " * 4): + value = field.strip(' "') + if value_next: + cells.append({ + "column_family": column_name.split(":", 1)[0], + "column": column_name.split(":", 1)[1], + "timestamp": datetime.datetime.strptime(timestamp, "%Y/%m/%d-%H:%M:%S.%f").isoformat(), + "value": value + }) + elif field.startswith(" " * 2): + column_name, timestamp = map(str.strip, field.split("@")) + value_next = True + else: + key = field + if key is not None: + raw_output.append({"key": key, "cells": cells}) + + return raw_output if raw else _process(raw_output) diff --git a/tests/test_cbt.py b/tests/test_cbt.py new file mode 100644 index 00000000..a3e3ad3b --- /dev/null +++ b/tests/test_cbt.py @@ -0,0 +1,145 @@ +import os +import unittest +from jc.exceptions import ParseError +import jc.parsers.cbt + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class MyTests(unittest.TestCase): + + def test_cbt_nodata(self): + """ + Test 'cbt' with no data + """ + self.assertEqual(jc.parsers.cbt.parse('', quiet=True), []) + + def test_cbt_single_row(self): + """ + Test 'cbt' with a single row + """ + input = ''' +---------------------------------------- +foo + foo:bar @ 1970/01/01-01:00:00.000000 + "baz" + ''' + expected = [ + { + "key": "foo", + "cells": { + "foo": { + "bar": "baz" + } + } + } + ] + self.assertEqual(jc.parsers.cbt.parse(input, quiet=True), expected) + + def test_cbt_multiple_column_families(self): + """ + Test 'cbt' with multiple column families + """ + input = ''' +---------------------------------------- +foo + foo:bar1 @ 1970/01/01-01:00:00.000000 + "baz1" + foo:bar2 @ 1970/01/01-01:00:00.000000 + "baz2" + bat:bar @ 1970/01/01-01:00:00.000000 + "baz" + ''' + expected = [ + { + "key": "foo", + "cells": { + "foo": { + "bar1": "baz1", + "bar2": "baz2", + }, + "bat": { + "bar": "baz" + } + } + } + ] + self.assertEqual(jc.parsers.cbt.parse(input, quiet=True), expected) + + def test_cbt_multiple_rows(self): + """ + Test 'cbt' with multiple rows + """ + input = ''' +---------------------------------------- +foo + foo:bar @ 1970/01/01-01:00:00.000000 + "baz1" +---------------------------------------- +bar + foo:bar @ 1970/01/01-01:00:00.000000 + "baz2" + ''' + expected = [ + { + "key": "foo", + "cells": { + "foo": { + "bar": "baz1", + } + } + }, + { + "key": "bar", + "cells": { + "foo": { + "bar": "baz2", + } + } + } + ] + self.assertEqual(jc.parsers.cbt.parse(input, quiet=True), expected) + + def test_cbt_multiple_rows_raw(self): + """ + Test 'cbt' with multiple rows raw + """ + input = ''' +---------------------------------------- +foo + foo:bar @ 1970/01/01-01:00:00.000000 + "baz1" +---------------------------------------- +bar + foo:bar @ 1970/01/01-01:00:00.000000 + "baz2" + ''' + expected = [ + { + "key": "foo", + "cells": [ + { + "column_family": "foo", + "column": "bar", + "timestamp": "1970-01-01T01:00:00", + "value": "baz1", + } + ] + }, + { + "key": "bar", + "cells": [ + { + "column_family": "foo", + "column": "bar", + "timestamp": "1970-01-01T01:00:00", + "value": "baz2", + } + ] + } + ] + self.assertEqual(jc.parsers.cbt.parse(input, quiet=True, raw=True), expected) + + +if __name__ == '__main__': + unittest.main() From 7f4a9510655f81b70ad2565290665ecb5cbca4c3 Mon Sep 17 00:00:00 2001 From: Andreas Weiden Date: Mon, 12 Dec 2022 15:15:08 +0100 Subject: [PATCH 2/6] Add parser to table in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9cc8b396..3102ed71 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ option. | ` --asciitable` | ASCII and Unicode table parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/asciitable) | | ` --asciitable-m` | multi-line ASCII and Unicode table parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/asciitable_m) | | ` --blkid` | `blkid` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/blkid) | +| ` --cbt` | `cbt` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/cbt) | | ` --cef` | CEF string parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/cef) | | ` --cef-s` | CEF string streaming parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/cef_s) | | ` --chage` | `chage --list` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/chage) | From 532c37140c87a3369803290996bd46a6966c15d4 Mon Sep 17 00:00:00 2001 From: Andreas Weiden Date: Mon, 12 Dec 2022 15:33:40 +0100 Subject: [PATCH 3/6] Move test stuff to fixtures --- .../generic/cbt-multiple-columns.json | 1 + .../fixtures/generic/cbt-multiple-columns.out | 8 ++ .../generic/cbt-multiple-rows-raw.json | 1 + tests/fixtures/generic/cbt-multiple-rows.json | 1 + tests/fixtures/generic/cbt-multiple-rows.out | 8 ++ tests/fixtures/generic/cbt-single.json | 1 + tests/fixtures/generic/cbt-single.out | 4 + tests/test_cbt.py | 133 ++++-------------- 8 files changed, 50 insertions(+), 107 deletions(-) create mode 100644 tests/fixtures/generic/cbt-multiple-columns.json create mode 100644 tests/fixtures/generic/cbt-multiple-columns.out create mode 100644 tests/fixtures/generic/cbt-multiple-rows-raw.json create mode 100644 tests/fixtures/generic/cbt-multiple-rows.json create mode 100644 tests/fixtures/generic/cbt-multiple-rows.out create mode 100644 tests/fixtures/generic/cbt-single.json create mode 100644 tests/fixtures/generic/cbt-single.out diff --git a/tests/fixtures/generic/cbt-multiple-columns.json b/tests/fixtures/generic/cbt-multiple-columns.json new file mode 100644 index 00000000..8de855f4 --- /dev/null +++ b/tests/fixtures/generic/cbt-multiple-columns.json @@ -0,0 +1 @@ +[{"key":"foo","cells":{"bat":{"bar":"baz"},"foo":{"bar1":"baz1","bar2":"baz2"}}}] \ No newline at end of file diff --git a/tests/fixtures/generic/cbt-multiple-columns.out b/tests/fixtures/generic/cbt-multiple-columns.out new file mode 100644 index 00000000..e4e0655a --- /dev/null +++ b/tests/fixtures/generic/cbt-multiple-columns.out @@ -0,0 +1,8 @@ +---------------------------------------- +foo + foo:bar1 @ 1970/01/01-01:00:00.000000 + "baz1" + foo:bar2 @ 1970/01/01-01:00:00.000000 + "baz2" + bat:bar @ 1970/01/01-01:00:00.000000 + "baz" \ No newline at end of file diff --git a/tests/fixtures/generic/cbt-multiple-rows-raw.json b/tests/fixtures/generic/cbt-multiple-rows-raw.json new file mode 100644 index 00000000..4fcb195e --- /dev/null +++ b/tests/fixtures/generic/cbt-multiple-rows-raw.json @@ -0,0 +1 @@ +[{"key":"foo","cells":[{"column_family":"foo","column":"bar","timestamp":"1970-01-01T01:00:00","value":"baz1"}]},{"key":"bar","cells":[{"column_family":"foo","column":"bar","timestamp":"1970-01-01T01:00:00","value":"baz2"}]}] \ No newline at end of file diff --git a/tests/fixtures/generic/cbt-multiple-rows.json b/tests/fixtures/generic/cbt-multiple-rows.json new file mode 100644 index 00000000..499d597b --- /dev/null +++ b/tests/fixtures/generic/cbt-multiple-rows.json @@ -0,0 +1 @@ +[{"key":"foo","cells":{"foo":{"bar":"baz1"}}},{"key":"bar","cells":{"foo":{"bar":"baz2"}}}] \ No newline at end of file diff --git a/tests/fixtures/generic/cbt-multiple-rows.out b/tests/fixtures/generic/cbt-multiple-rows.out new file mode 100644 index 00000000..a23a6ec4 --- /dev/null +++ b/tests/fixtures/generic/cbt-multiple-rows.out @@ -0,0 +1,8 @@ +---------------------------------------- +foo + foo:bar @ 1970/01/01-01:00:00.000000 + "baz1" +---------------------------------------- +bar + foo:bar @ 1970/01/01-01:00:00.000000 + "baz2" \ No newline at end of file diff --git a/tests/fixtures/generic/cbt-single.json b/tests/fixtures/generic/cbt-single.json new file mode 100644 index 00000000..2be41c8b --- /dev/null +++ b/tests/fixtures/generic/cbt-single.json @@ -0,0 +1 @@ +[{"key":"foo","cells":{"foo":{"bar":"baz"}}}] \ No newline at end of file diff --git a/tests/fixtures/generic/cbt-single.out b/tests/fixtures/generic/cbt-single.out new file mode 100644 index 00000000..9eb4f1f0 --- /dev/null +++ b/tests/fixtures/generic/cbt-single.out @@ -0,0 +1,4 @@ +---------------------------------------- +foo + foo:bar @ 1970/01/01-01:00:00.000000 + "baz" diff --git a/tests/test_cbt.py b/tests/test_cbt.py index a3e3ad3b..3dbf1a3e 100644 --- a/tests/test_cbt.py +++ b/tests/test_cbt.py @@ -1,3 +1,4 @@ +import json import os import unittest from jc.exceptions import ParseError @@ -7,6 +8,26 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) class MyTests(unittest.TestCase): + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/cbt-single.out'), 'r', encoding='utf-8') as f: + single = f.read() + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/cbt-multiple-columns.out'), 'r', encoding='utf-8') as f: + multiple_columns = f.read() + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/cbt-multiple-rows.out'), 'r', encoding='utf-8') as f: + multiple_rows = f.read() + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/cbt-single.json'), 'r', encoding='utf-8') as f: + single_json = json.loads(f.read()) + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/cbt-multiple-columns.json'), 'r', encoding='utf-8') as f: + multiple_columns_json = json.loads(f.read()) + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/cbt-multiple-rows.json'), 'r', encoding='utf-8') as f: + multiple_rows_json = json.loads(f.read()) + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/cbt-multiple-rows-raw.json'), 'r', encoding='utf-8') as f: + multiple_rows_raw_json = json.loads(f.read()) def test_cbt_nodata(self): """ @@ -18,127 +39,25 @@ class MyTests(unittest.TestCase): """ Test 'cbt' with a single row """ - input = ''' ----------------------------------------- -foo - foo:bar @ 1970/01/01-01:00:00.000000 - "baz" - ''' - expected = [ - { - "key": "foo", - "cells": { - "foo": { - "bar": "baz" - } - } - } - ] - self.assertEqual(jc.parsers.cbt.parse(input, quiet=True), expected) + self.assertEqual(jc.parsers.cbt.parse(self.single, quiet=True), self.single_json) def test_cbt_multiple_column_families(self): """ - Test 'cbt' with multiple column families + Test 'cbt' with multiple columns from multiple column families """ - input = ''' ----------------------------------------- -foo - foo:bar1 @ 1970/01/01-01:00:00.000000 - "baz1" - foo:bar2 @ 1970/01/01-01:00:00.000000 - "baz2" - bat:bar @ 1970/01/01-01:00:00.000000 - "baz" - ''' - expected = [ - { - "key": "foo", - "cells": { - "foo": { - "bar1": "baz1", - "bar2": "baz2", - }, - "bat": { - "bar": "baz" - } - } - } - ] - self.assertEqual(jc.parsers.cbt.parse(input, quiet=True), expected) + self.assertEqual(jc.parsers.cbt.parse(self.multiple_columns, quiet=True), self.multiple_columns_json) def test_cbt_multiple_rows(self): """ Test 'cbt' with multiple rows """ - input = ''' ----------------------------------------- -foo - foo:bar @ 1970/01/01-01:00:00.000000 - "baz1" ----------------------------------------- -bar - foo:bar @ 1970/01/01-01:00:00.000000 - "baz2" - ''' - expected = [ - { - "key": "foo", - "cells": { - "foo": { - "bar": "baz1", - } - } - }, - { - "key": "bar", - "cells": { - "foo": { - "bar": "baz2", - } - } - } - ] - self.assertEqual(jc.parsers.cbt.parse(input, quiet=True), expected) + self.assertEqual(jc.parsers.cbt.parse(self.multiple_rows, quiet=True), self.multiple_rows_json) def test_cbt_multiple_rows_raw(self): """ Test 'cbt' with multiple rows raw """ - input = ''' ----------------------------------------- -foo - foo:bar @ 1970/01/01-01:00:00.000000 - "baz1" ----------------------------------------- -bar - foo:bar @ 1970/01/01-01:00:00.000000 - "baz2" - ''' - expected = [ - { - "key": "foo", - "cells": [ - { - "column_family": "foo", - "column": "bar", - "timestamp": "1970-01-01T01:00:00", - "value": "baz1", - } - ] - }, - { - "key": "bar", - "cells": [ - { - "column_family": "foo", - "column": "bar", - "timestamp": "1970-01-01T01:00:00", - "value": "baz2", - } - ] - } - ] - self.assertEqual(jc.parsers.cbt.parse(input, quiet=True, raw=True), expected) + self.assertEqual(jc.parsers.cbt.parse(self.multiple_rows, quiet=True, raw=True), self.multiple_rows_raw_json) if __name__ == '__main__': From 27acedf8b719e1b87db4f33d02eca8da26175ab6 Mon Sep 17 00:00:00 2001 From: Andreas Weiden Date: Mon, 12 Dec 2022 15:43:22 +0100 Subject: [PATCH 4/6] Add newlines at end of files --- tests/fixtures/generic/cbt-multiple-columns.json | 2 +- tests/fixtures/generic/cbt-multiple-columns.out | 2 +- tests/fixtures/generic/cbt-multiple-rows-raw.json | 2 +- tests/fixtures/generic/cbt-multiple-rows.json | 2 +- tests/fixtures/generic/cbt-multiple-rows.out | 2 +- tests/fixtures/generic/cbt-single.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/fixtures/generic/cbt-multiple-columns.json b/tests/fixtures/generic/cbt-multiple-columns.json index 8de855f4..f410bf37 100644 --- a/tests/fixtures/generic/cbt-multiple-columns.json +++ b/tests/fixtures/generic/cbt-multiple-columns.json @@ -1 +1 @@ -[{"key":"foo","cells":{"bat":{"bar":"baz"},"foo":{"bar1":"baz1","bar2":"baz2"}}}] \ No newline at end of file +[{"key":"foo","cells":{"bat":{"bar":"baz"},"foo":{"bar1":"baz1","bar2":"baz2"}}}] diff --git a/tests/fixtures/generic/cbt-multiple-columns.out b/tests/fixtures/generic/cbt-multiple-columns.out index e4e0655a..fa876b10 100644 --- a/tests/fixtures/generic/cbt-multiple-columns.out +++ b/tests/fixtures/generic/cbt-multiple-columns.out @@ -5,4 +5,4 @@ foo foo:bar2 @ 1970/01/01-01:00:00.000000 "baz2" bat:bar @ 1970/01/01-01:00:00.000000 - "baz" \ No newline at end of file + "baz" diff --git a/tests/fixtures/generic/cbt-multiple-rows-raw.json b/tests/fixtures/generic/cbt-multiple-rows-raw.json index 4fcb195e..8f17f235 100644 --- a/tests/fixtures/generic/cbt-multiple-rows-raw.json +++ b/tests/fixtures/generic/cbt-multiple-rows-raw.json @@ -1 +1 @@ -[{"key":"foo","cells":[{"column_family":"foo","column":"bar","timestamp":"1970-01-01T01:00:00","value":"baz1"}]},{"key":"bar","cells":[{"column_family":"foo","column":"bar","timestamp":"1970-01-01T01:00:00","value":"baz2"}]}] \ No newline at end of file +[{"key":"foo","cells":[{"column_family":"foo","column":"bar","timestamp":"1970-01-01T01:00:00","value":"baz1"}]},{"key":"bar","cells":[{"column_family":"foo","column":"bar","timestamp":"1970-01-01T01:00:00","value":"baz2"}]}] diff --git a/tests/fixtures/generic/cbt-multiple-rows.json b/tests/fixtures/generic/cbt-multiple-rows.json index 499d597b..3d443138 100644 --- a/tests/fixtures/generic/cbt-multiple-rows.json +++ b/tests/fixtures/generic/cbt-multiple-rows.json @@ -1 +1 @@ -[{"key":"foo","cells":{"foo":{"bar":"baz1"}}},{"key":"bar","cells":{"foo":{"bar":"baz2"}}}] \ No newline at end of file +[{"key":"foo","cells":{"foo":{"bar":"baz1"}}},{"key":"bar","cells":{"foo":{"bar":"baz2"}}}] diff --git a/tests/fixtures/generic/cbt-multiple-rows.out b/tests/fixtures/generic/cbt-multiple-rows.out index a23a6ec4..686cd2b2 100644 --- a/tests/fixtures/generic/cbt-multiple-rows.out +++ b/tests/fixtures/generic/cbt-multiple-rows.out @@ -5,4 +5,4 @@ foo ---------------------------------------- bar foo:bar @ 1970/01/01-01:00:00.000000 - "baz2" \ No newline at end of file + "baz2" diff --git a/tests/fixtures/generic/cbt-single.json b/tests/fixtures/generic/cbt-single.json index 2be41c8b..d3e578cf 100644 --- a/tests/fixtures/generic/cbt-single.json +++ b/tests/fixtures/generic/cbt-single.json @@ -1 +1 @@ -[{"key":"foo","cells":{"foo":{"bar":"baz"}}}] \ No newline at end of file +[{"key":"foo","cells":{"foo":{"bar":"baz"}}}] From 79011465afae2474ea18a470abb56b2c49dc6dbb Mon Sep 17 00:00:00 2001 From: Andreas Weiden Date: Mon, 12 Dec 2022 19:46:45 +0100 Subject: [PATCH 5/6] Fix docstring --- jc/parsers/cbt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jc/parsers/cbt.py b/jc/parsers/cbt.py index a0a25b07..5b75e8d0 100644 --- a/jc/parsers/cbt.py +++ b/jc/parsers/cbt.py @@ -1,6 +1,6 @@ -"""jc - JSON Convert `foo` command output parser +"""jc - JSON Convert `cbt` command output parser -Parses the human-, but not machine-, readable output of the cbt command (for Google's BigTable). +Parses the human-, but not machine-, friendly output of the cbt command (for Google's BigTable). No effort is made to convert the data types of the values in the cells. From fd61e19135ce4282ab5c51f8fa80009ddaff40c5 Mon Sep 17 00:00:00 2001 From: Andreas Weiden Date: Mon, 12 Dec 2022 19:48:33 +0100 Subject: [PATCH 6/6] Add raw schema --- jc/parsers/cbt.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/jc/parsers/cbt.py b/jc/parsers/cbt.py index 5b75e8d0..e0559485 100644 --- a/jc/parsers/cbt.py +++ b/jc/parsers/cbt.py @@ -35,6 +35,22 @@ Schema: } ] +Schema (raw): + + [ + { + "key": string, + "cells": [ + { + "column_family": string, + "column": string, + "timestamp": string, + "value": string + } + ] + } + ] + Examples: $ cbt -project=$PROJECT -instance=$INSTANCE lookup $TABLE foo | jc --cbt -p