Skip to content

Commit 689a8b4

Browse files
authored
Merge pull request #27 from ctomkow/cli
Cli
2 parents 7583fff + c24fe4b commit 689a8b4

File tree

7 files changed

+281
-2
lines changed

7 files changed

+281
-2
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ dist
1010
docs/_build
1111
docs/_static
1212
docs/_templates
13-
docs/make.bat
13+
docs/make.bat
14+
test.json

jsonparse/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.11.3
1+
0.12.0.dev1

jsonparse/cli.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env python3
2+
# Craig Tomkow
3+
# 2022-10-03
4+
5+
# local imports
6+
from .parser import Parser
7+
8+
# python imports
9+
from typing import Any
10+
import sys
11+
import argparse
12+
import io
13+
import os
14+
import json
15+
16+
17+
def entrypoint():
18+
19+
version = _read_version()
20+
parser = _flags(version)
21+
args = parser.parse_args()
22+
_parse_input(args)
23+
24+
25+
def _read_version() -> str:
26+
27+
# read from the VERSION file
28+
with open(os.path.join(os.path.dirname(__file__), 'VERSION')) as version_file:
29+
version = version_file.read().strip()
30+
return version
31+
32+
33+
def _flags(version: str) -> argparse.ArgumentParser:
34+
35+
parser = argparse.ArgumentParser(
36+
prog='jsonparse',
37+
formatter_class=argparse.RawDescriptionHelpFormatter,
38+
description="""
39+
Parse deeply nested json based on key(s) and value(s)
40+
41+
examples
42+
43+
jsonparse key-chain my key chain --file test.json
44+
45+
jsonparse key-value mykey 42 --file test.json
46+
jsonparse key-value mykey \"strValue\" --file test.json
47+
48+
echo '{"mykey": 42}' | jsonparse key mykey
49+
""")
50+
51+
# all flags here
52+
parser.add_argument('-v', '--version', action='version', version=f"%(prog)s {version}")
53+
54+
sub_parser = parser.add_subparsers()
55+
56+
key = sub_parser.add_parser('key', description='With no FILE, read standard input.')
57+
key.add_argument('KEY', action='store', type=str, nargs=1, help='search for key')
58+
key.add_argument('--file', type=argparse.FileType('r'), nargs='?', default=sys.stdin, help="json file as input")
59+
60+
keys = sub_parser.add_parser('keys', description='With no FILE, read standard input.')
61+
keys.add_argument('--ungroup', action='store_false', help='return a one dimensional list')
62+
keys.add_argument('KEYS', metavar='KEY', action='store', type=str, nargs='+', help='search for keys')
63+
keys.add_argument('--file', type=argparse.FileType('r'), nargs='?', default=sys.stdin, help="json file as input")
64+
65+
key_chain = sub_parser.add_parser('key-chain', description='With no FILE, read standard input.')
66+
key_chain.add_argument('KEYCHAIN', metavar='KEY', action='store', type=str, nargs='+', help='search for key chain')
67+
key_chain.add_argument('--file', type=argparse.FileType('r'), nargs='?', default=sys.stdin,
68+
help="json file as input")
69+
70+
key_value = sub_parser.add_parser('key-value', description='With no FILE, read standard input.')
71+
key_value.add_argument('KVKEY', metavar='KEY', action='store', type=str, nargs=1, help='search key part of key:value')
72+
key_value.add_argument('KVVALUE', metavar='VALUE', action='store', type=str, nargs=1,
73+
help='must be valid json. String must have escaped double quotes. e.g. \\"asdf\\"')
74+
key_value.add_argument('--file', type=argparse.FileType('r'), nargs='?', default=sys.stdin,
75+
help="json file as input")
76+
77+
return parser
78+
79+
80+
def _parse_input(args: argparse.Namespace) -> None:
81+
82+
try:
83+
data = _pythonify(_input(args.file))
84+
except json.decoder.JSONDecodeError:
85+
print('input json not valid.')
86+
raise SystemExit(0)
87+
88+
if 'KEY' in args:
89+
print(_jsonify(Parser().find_key(data, args.KEY[0])))
90+
elif 'KEYS' in args:
91+
print(_jsonify(Parser().find_keys(data, args.KEYS, group=args.ungroup)))
92+
elif 'KEYCHAIN' in args:
93+
print(_jsonify(Parser().find_key_chain(data, args.KEYCHAIN)))
94+
elif ('KVKEY' in args) and ('KVVALUE' in args):
95+
try:
96+
value = _pythonify(args.KVVALUE[0])
97+
except json.decoder.JSONDecodeError:
98+
print('value is not valid json. example valid types: \\"value\\", 5, false, true, null')
99+
raise SystemExit(0)
100+
print(_jsonify(Parser().find_key_value(data, args.KVKEY[0], value)))
101+
102+
103+
def _input(fp: io.TextIOWrapper) -> str:
104+
105+
data = ''
106+
for line in fp:
107+
data += line.rstrip()
108+
return data
109+
110+
111+
def _pythonify(data: json) -> Any:
112+
113+
try:
114+
return json.loads(data)
115+
except json.decoder.JSONDecodeError:
116+
raise
117+
118+
119+
def _jsonify(data: Any) -> json:
120+
121+
return json.dumps(data)
122+
123+
124+
if __name__ == "__main__":
125+
126+
entrypoint()

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
max-line-length = 128

setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
'gunicorn>=20.1.0,<21.0.0'
4747
]
4848
},
49+
entry_points={
50+
'console_scripts': [
51+
'jsonparse=jsonparse.cli:entrypoint',
52+
],
53+
},
4954
packages=find_namespace_packages(where="."),
5055
package_dir={"": "."},
5156
package_data={

tests/test_cli.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Craig Tomkow
2+
#
3+
# Tests only public methods
4+
5+
6+
# local imports
7+
from jsonparse import cli
8+
9+
# python imports
10+
import os
11+
12+
# 3rd part imports
13+
import pytest
14+
15+
16+
class TestCli:
17+
18+
# set working dir to tests directory to read from json file
19+
os.chdir(os.path.dirname(os.path.realpath(__file__)))
20+
21+
@pytest.fixture
22+
def version(self):
23+
24+
return '0.12.0.dev1'
25+
26+
def test_read_version(self, version):
27+
28+
assert cli._read_version() == version
29+
30+
def test_flags_key(self):
31+
32+
parser = cli._flags('v0.0.1-test')
33+
args = parser.parse_args(['key', 'ppu', '--file', 'tests.json'])
34+
args = cli._parse_input(args)
35+
36+
assert True
37+
38+
def test_flags_keys(self):
39+
40+
parser = cli._flags('v0.0.1-test')
41+
args = parser.parse_args(['keys', 'ppu', '--file', 'tests.json'])
42+
args = cli._parse_input(args)
43+
44+
assert True
45+
46+
def test_flags_keychain(self):
47+
48+
parser = cli._flags('v0.0.1-test')
49+
args = parser.parse_args(['key-chain', 'ppu', '--file', 'tests.json'])
50+
args = cli._parse_input(args)
51+
52+
assert True
53+
54+
def test_flags_keyvalue(self):
55+
56+
parser = cli._flags('v0.0.1-test')
57+
args = parser.parse_args(['key-value', 'ppu', '0.55', '--file', 'tests.json'])
58+
args = cli._parse_input(args)
59+
60+
assert True
61+
62+
def test_flags_keyvalue_invalid_json_input(self):
63+
64+
parser = cli._flags('v0.0.1-test')
65+
args = parser.parse_args(['key-value', 'exists', 'False', '--file', 'tests.json'])
66+
67+
with pytest.raises(SystemExit):
68+
args = cli._parse_input(args)

tests/tests.json

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
[
2+
{
3+
"id": "0001",
4+
"type": "donut",
5+
"exists": true,
6+
"ppu": 0.55,
7+
"batters":
8+
{
9+
"batter":
10+
[
11+
{"id": "1001", "type": "Reg"},
12+
{"id": "1002", "type": "Chocolate"},
13+
{"id": "1003", "type": "Blueberry"},
14+
{"id": "1004", "type": "Devil's Food"},
15+
{"start": 5, "end": 8}
16+
]
17+
},
18+
"topping":
19+
[
20+
{"id": "5001", "type": "None"},
21+
{"id": "5002", "type": "Glazed"},
22+
{"id": "5003", "type": "Sugar"},
23+
{"id": "5004", "type": "Powdered Sugar"},
24+
{"id": "5005", "type": "Chocolate with Sprinkles"},
25+
{"id": "5006", "type": "Chocolate"},
26+
{"id": "5007", "type": "Maple"}
27+
],
28+
"start": 22,
29+
"end": 99
30+
},
31+
{
32+
"id": "0002",
33+
"type": "donut",
34+
"exists": false,
35+
"ppu": 42,
36+
"batters":
37+
{
38+
"batter":
39+
[
40+
{"id": "1001", "type": "Rul"}
41+
]
42+
},
43+
"top_stuff":
44+
[
45+
{"id": "5001", "type": "None"},
46+
{"id": "5002", "type": "Glazed"},
47+
{"id": "5003", "type": "Sugar"},
48+
{"id": "5004", "type": "Chocolate"},
49+
{"id": "5005", "type": "Maple"}
50+
],
51+
"start": 1,
52+
"end": 9
53+
},
54+
{
55+
"id": "0003",
56+
"type": "donut",
57+
"exists": null,
58+
"ppu": 7,
59+
"batters":
60+
{
61+
"batter":
62+
[
63+
{"id": "1001", "type": "Lar"},
64+
{"id": "1002", "type": "Chocolate"}
65+
]
66+
},
67+
"on_top_thing":
68+
[
69+
{"id": "5001", "type": "None"},
70+
{"id": "5002", "type": "Glazed"},
71+
{"id": "5003", "type": "Chocolate"},
72+
{"id": "5004", "type": "Maple"}
73+
],
74+
"start": 4,
75+
"end": 7
76+
}
77+
]

0 commit comments

Comments
 (0)