Skip to content

Commit 23c18cb

Browse files
authored
Merge pull request #33 from ctomkow/find_value
Find value
2 parents 822572a + 72ca758 commit 23c18cb

File tree

9 files changed

+187
-7
lines changed

9 files changed

+187
-7
lines changed

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
> **jsonparse** is a simple JSON parsing library. Extract what's needed from key:value pairs.
1010
1111
## What's New
12+
- A new function, [find_value](#find_value), has been added. This function will return all keys of the matched value. :grinning:
1213
- [CLI tool](#CLI-tool). Parse json text files or stdin via the command line :tada:
13-
- New public (or hostable) [web API](#web-api)
1414

1515
# Python Library
1616

@@ -23,7 +23,7 @@ pip install jsonparse
2323
Here is a quick example of what jsonparse is able to do.
2424

2525
```python
26-
from jsonparse import find_key, find_keys, find_key_chain, find_key_value
26+
from jsonparse import find_key, find_keys, find_key_chain, find_key_value, find_value
2727

2828
data = [{
2929
"key0":
@@ -69,6 +69,12 @@ Note, `jsonparse` and `jp` are equivalent.
6969

7070
`echo '{"key1": {"key2": 5}}' | jp key key2`
7171

72+
`jp value null --file text.json`
73+
74+
`jp value 42 --file text.json`
75+
76+
`jp value '"strValue"' --file text.json`
77+
7278

7379
# API
7480

@@ -77,6 +83,7 @@ Note, `jsonparse` and `jp` are equivalent.
7783
- [find_keys](#find_keys)
7884
- [find_key_chain](#find_key_chain)
7985
- [find_key_value](#find_key_value)
86+
- [find_value](#find_value)
8087

8188
The API examples using the following test data.
8289

@@ -200,6 +207,22 @@ p.find_key_value(data, 'chain', 'B')
200207
[{'chain': 'B', 'rope': 7, 'string': 0.7, 'cable': True}]
201208
```
202209

210+
---
211+
212+
### find_value
213+
<pre>
214+
<b>find_value(</b><i>data</i>: dict | list, <i>value</i>: str | int | float | bool | None<b>)</b> -> list
215+
</pre>
216+
217+
&nbsp;&nbsp;&nbsp;&nbsp;Will return all keys of the matched value.
218+
219+
```python
220+
p.find_value(data, 'A')
221+
['chain']
222+
223+
p.find_value(data, False)
224+
['cable']
225+
```
203226

204227
# Web API
205228

@@ -215,6 +238,7 @@ POST /v1/key/{key}
215238
POST /v1/keys?key=1&key=2&key=3&key=4...
216239
POST /v1/keychain?key=1&key=2&key=3&key=4...
217240
POST /v1/keyvalue?key=a&value=1
241+
POST /v1/value/{value}
218242
```
219243

220244
## Quickstart

jsonparse/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.13.1
1+
0.14.0

jsonparse/cli.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,18 @@ def _flags(version: str) -> argparse.ArgumentParser:
3939
Parse deeply nested json based on key(s) and value(s)
4040
4141
examples
42-
42+
--------
4343
jsonparse key-chain my key chain --file test.json
4444
jsonparse key-chain my '*' chain --file test.json
45+
4546
jsonparse key-value mykey 42 --file test.json
4647
jsonparse key-value mykey '"strValue"' --file test.json
48+
4749
echo '{"mykey": 42}' | jsonparse key mykey
50+
51+
jp value null --file test.json
52+
jp value 42 --file test.json
53+
jp value '"strValue"' --file test.json
4854
""")
4955

5056
# all flags here
@@ -73,6 +79,10 @@ def _flags(version: str) -> argparse.ArgumentParser:
7379
key_value.add_argument('--file', type=argparse.FileType('r'), nargs='?', default=sys.stdin,
7480
help="json file as input")
7581

82+
value = sub_parser.add_parser('value', description='With no FILE, read standard input.')
83+
value.add_argument('VALUE', action='store', type=str, nargs=1, help='Search for value. Must be a valid JSON value. If searching for a string value, ensure it is quoted. Returns all keys with that value')
84+
value.add_argument('--file', type=argparse.FileType('r'), nargs='?', default=sys.stdin, help="json file as input")
85+
7686
return parser
7787

7888

@@ -97,6 +107,13 @@ def _parse_input(args: argparse.Namespace) -> None:
97107
print('value is not valid json. example valid types: \'"value"\', 5, false, true, null')
98108
raise SystemExit(0)
99109
_print(_jsonify(Parser().find_key_value(data, args.KVKEY[0], value)))
110+
elif 'VALUE' in args:
111+
try:
112+
value = _pythonify(args.VALUE[0])
113+
except json.decoder.JSONDecodeError:
114+
print('value is not valid json. example valid types: \'"value"\', 5, false, true, null')
115+
raise SystemExit(0)
116+
_print(_jsonify(Parser().find_value(data, value)))
100117

101118

102119
def _input(fp: io.TextIOWrapper) -> str:

jsonparse/functions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ def find_key_chain(data: Union[dict, list], keys: list) -> list:
2222
def find_key_value(data: Union[dict, list], key: str, value: Union[str, int, float, bool, None]) -> list:
2323

2424
return Parser().find_key_value(data, key, value)
25+
26+
27+
def find_value(data: Union[dict, list], value: Union[str, int, float, bool, None]) -> list:
28+
29+
return Parser().find_value(data, value)

jsonparse/parser.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class Parser:
2626
2727
find_key_value(data, key, value):
2828
Returns a list of set(s) that contain the key value pair.
29+
30+
find_value(data, value):
31+
Returns a list of key(s) that have the corresponding value.
32+
2933
"""
3034

3135
def __init__(self,
@@ -245,6 +249,47 @@ def find_key_value(self,
245249

246250
return value_list
247251

252+
def find_value(self, data: Union[dict, list], value: Union[str, int, float, bool, None]) -> list:
253+
"""
254+
Search JSON data that consists of key:value pairs for all instances of
255+
provided value, returning the associated key (opposite of find_key). The data can have complex nested dictionaries and lists.
256+
If duplicate values exist in the data (at any layer), all associated
257+
keys will be returned. Data is parsed using a depth first search
258+
with a stack.
259+
260+
Keyword arguments:
261+
262+
data -- The python object representing JSON data with key:value pairs.
263+
This could be a dictionary or a list.
264+
value -- The value that will be searched for in the JSON data.
265+
Must be a valid JSON value.
266+
"""
267+
if not self._valid_value_input(data, value):
268+
raise
269+
270+
self.stack_ref = self._stack_init() # init a new stack every request
271+
self._stack_push(data)
272+
self._stack_trace()
273+
274+
key_list = []
275+
276+
while self._stack_size() >= 1:
277+
278+
elem = self._stack_pop()
279+
280+
if type(elem) is list:
281+
self._stack_push_list_elem(elem)
282+
elif type(elem) is dict:
283+
key = self._stack_all_value_in_dict(value, elem)
284+
if key:
285+
key_list.insert(0, key)
286+
else: # according to RFC 7159, valid JSON can also contain a
287+
# string, number, 'false', 'null', 'true'
288+
pass # discard these other values as they don't have a key
289+
290+
return key_list
291+
292+
248293
# STACK operations
249294

250295
def _stack_init(self) -> list:
@@ -353,6 +398,24 @@ def _stack_all_key_and_value_in_dict(
353398
self._stack_trace()
354399
return False
355400

401+
def _stack_all_value_in_dict(self, value: Union[str, int, float, bool, None], elem: dict) -> str:
402+
403+
if type(elem) is not dict:
404+
raise TypeError
405+
elif not isinstance(value, (str, int, float, bool, type(None))):
406+
raise TypeError
407+
408+
if len(elem) <= 0: # don't want an empty dict on the stack
409+
pass
410+
else:
411+
for e in elem:
412+
if elem[e] == value:
413+
return e
414+
else:
415+
self._stack_push(elem[e])
416+
self._stack_trace()
417+
return False
418+
356419
def _stack_trace(self) -> None:
357420

358421
if self.stack_trace:
@@ -499,3 +562,11 @@ def _valid_key_value_input(
499562
elif not isinstance(value, (str, int, float, bool, type(None))):
500563
raise TypeError
501564
return True
565+
566+
def _valid_value_input(self, data: Union[dict, list], value: Union[str, int, float, bool, None]) -> bool:
567+
568+
if not isinstance(data, (dict, list)):
569+
raise TypeError
570+
elif not isinstance(value, (str, int, float, bool, type(None))):
571+
raise TypeError
572+
return True

jsonparse/webapi.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,18 @@ def _find_key_value():
115115
return (jsonify(error="value must be valid json"), 400)
116116

117117
return jsonify(Parser().find_key_value(request.json, key, value))
118+
119+
120+
# accept a singular value
121+
# /v1/value/myvalue
122+
@app.post('/v1/value/<path:value>')
123+
def _find_value(value: str):
124+
125+
if not value:
126+
return jsonify(error="value must not be empty"), 400
127+
try:
128+
value = json.loads(value)
129+
except json.JSONDecodeError:
130+
return jsonify(error="value must be valid json"), 400
131+
132+
return jsonify(Parser().find_value(request.json, value))

tests/test_cli.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class TestCli:
2121
@pytest.fixture
2222
def version(self):
2323

24-
return '0.13.1'
24+
return '0.14.0'
2525

2626
def test_read_version(self, version):
2727

@@ -66,3 +66,12 @@ def test_flags_keyvalue_invalid_json_input(self):
6666

6767
with pytest.raises(SystemExit):
6868
args = cli._parse_input(args)
69+
70+
71+
def test_flags_value(self):
72+
73+
parser = cli._flags('v0.0.1-test')
74+
args = parser.parse_args(['value', '42', '--file', 'tests.json'])
75+
args = cli._parse_input(args)
76+
77+
assert True

tests/test_parser.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def complex_json(self):
3838
},
3939
"topping":
4040
[
41-
{"id": "5001", "type": "None"},
41+
{"id": "5001", "ty": "None"},
4242
{"id": "5002", "type": "Glazed"},
4343
{"id": "5003", "type": "Sugar"},
4444
{"id": "5004", "type": "Powdered Sugar"},
@@ -63,7 +63,7 @@ def complex_json(self):
6363
},
6464
"top_stuff":
6565
[
66-
{"id": "5001", "type": "None"},
66+
{"id": "5001", "typ": "None"},
6767
{"id": "5002", "type": "Glazed"},
6868
{"id": "5003", "type": "Sugar"},
6969
{"id": "5004", "type": "Chocolate"},
@@ -302,3 +302,18 @@ def test_find_keys_one_not_found(self, parser, complex_json):
302302
)
303303

304304
assert result == [[True, 0.55], [False, 42], [None, 7]]
305+
306+
def test_find_value(self, parser, complex_json):
307+
308+
result = parser.find_value(complex_json, 42)
309+
assert result == ['ppu']
310+
311+
def test_find_value_not_found(self, parser, complex_json):
312+
313+
result = parser.find_value(complex_json, 0.0001)
314+
assert result == []
315+
316+
def test_find_value_multiple(self, parser, complex_json):
317+
318+
result = parser.find_value(complex_json, 'None')
319+
assert result == ['ty', 'typ', 'type']

tests/test_webapi.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,27 @@ def test_keyvalue_bad_json_value(client, data):
116116

117117
response = client.post('/v1/keyvalue?key=a&value=A', json=data)
118118
assert response.status_code == 400
119+
120+
121+
def test_value(client, data):
122+
123+
response = client.post('/v1/value/42', json=data)
124+
assert response.status_code == 200
125+
126+
127+
def test_value_valid_json_str_value(client, data):
128+
129+
response = client.post('/v1/value/"a"', json=data)
130+
assert response.status_code == 200
131+
132+
133+
def test_value_no_value(client, data):
134+
135+
response = client.post('/v1/value/', json=data)
136+
assert response.status_code == 404
137+
138+
139+
def test_value_bad_json_value(client, data):
140+
141+
response = client.post('/v1/value/A', json=data)
142+
assert response.status_code == 400

0 commit comments

Comments
 (0)