Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jpp which is an extended superset of the jp command #224

Open
wants to merge 50 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
126621f
Merge branch 'release-0.0.2'
jamesls Mar 22, 2013
fd87696
Merge branch 'release-0.0.3'
jamesls Oct 9, 2013
54d0620
Merge branch 'release-0.1.0'
jamesls Oct 17, 2013
e7df117
Merge branch 'release-0.2.0'
jamesls Dec 6, 2013
fd8af34
Merge branch 'release-0.2.1'
jamesls Dec 19, 2013
e23af23
Merge branch 'release-0.3.0'
jamesls Feb 28, 2014
53999ed
Merge branch 'release-0.3.1'
jamesls Mar 6, 2014
7e1c5b1
Merge branch 'release-0.4.0'
jamesls Apr 23, 2014
10ec1b9
Merge branch 'release-0.4.1'
jamesls May 1, 2014
b445f13
Merge branch 'release-0.5.0'
jamesls Nov 6, 2014
7cdde2b
Merge branch 'release-0.6.0'
jamesls Jan 31, 2015
5f23fbf
Merge branch 'release-0.6.1'
jamesls Feb 3, 2015
dc57769
Merge branch 'release-0.6.2'
jamesls Apr 9, 2015
0466cc1
Merge branch 'release-0.7.0'
jamesls Apr 21, 2015
ee83029
Merge branch 'release-0.7.1'
jamesls Apr 27, 2015
361838d
Merge branch 'release-0.8.0'
jamesls Sep 23, 2015
048f594
Merge branch 'release-0.9.0'
jamesls Oct 1, 2015
79baf74
Merge branch 'release-0.9.1'
jamesls Jan 26, 2017
6cde3b3
Merge branch 'release-0.9.2'
jamesls Mar 10, 2017
b4a3f4e
Merge branch 'release-0.9.3'
jamesls May 26, 2017
4a4f6db
Merge branch 'release-0.9.4'
jamesls Feb 24, 2019
e824eee
Merge branch 'release-0.9.5'
jamesls Feb 24, 2020
1c46efc
Merge branch 'release-0.10.0'
jamesls May 12, 2020
8b59732
init repo
zmedico Jun 13, 2021
6a076a4
Initial import
zmedico Jun 13, 2021
4ca9dd3
jpp: fork jpp command from jpipe
zmedico Jun 15, 2021
9068279
jpp: copy jpipe unit tests
zmedico Jun 15, 2021
ef5e7e3
jpp: Add --compact, -c bool flag to omit nonessential whitespace
zmedico Jun 15, 2021
b64e300
jpp: decode all objects from the input stream
zmedico Jun 15, 2021
f51b65f
jpp: Add --slurp, -s bool flag like jq has
zmedico Jun 15, 2021
0bbc90f
jpp: Add --accumulate, -a option which accumulates all output objects…
zmedico Jun 15, 2021
8acf6dc
version 0.1.3.1
zmedico Jun 15, 2021
72e273e
jpp: fix --accumulate array merge to coalesce duplicates
zmedico Jun 15, 2021
3771021
Add jpp which is an extended superset of the jp command
zmedico Jun 15, 2021
fbd3751
jpp: default to the identity expression @ if no expression is given
zmedico Jun 16, 2021
a61cdea
jpp: Add --read-raw, -R bool flag like jq has
zmedico Jun 16, 2021
073bb88
jpp: Add --raw, -r bool flag like jq has (an alias for unquoted)
zmedico Jun 16, 2021
478b397
jpp: default to the identity expression @ if no expression is given
zmedico Jun 16, 2021
d69e8e4
jpp: Add --read-raw, -R bool flag like jq has
zmedico Jun 16, 2021
827491f
jpp: Add --raw, -r bool flag like jq has (an alias for unquoted)
zmedico Jun 16, 2021
6b5b611
jpp: rename --read-raw to --raw-input for consistency with jq
zmedico Jun 17, 2021
60cd963
jpp: Add --unbox, -u flag (and drop --unquoted to reduce clutter)
zmedico Jun 17, 2021
d811157
jpp: rename --raw to --raw-output for consistency with jq
zmedico Jun 17, 2021
31f18fb
jpp: rename --read-raw to --raw-input for consistency with jq
zmedico Jun 17, 2021
666036f
jpp: Add --unbox, -u flag (and drop --unquoted to reduce clutter)
zmedico Jun 17, 2021
4762f0d
jpp: rename --raw to --raw-output for consistency with jq
zmedico Jun 17, 2021
87036fd
3 files reformatted with black
zmedico Jun 17, 2021
ff641ef
jpp: re-add --unquoted (a short -u means --unbox now)
zmedico Jun 17, 2021
70c5ec2
jpp: reformatted with black
zmedico Jun 17, 2021
379b70b
jpp: re-add --unquoted (a short -u means --unbox now)
zmedico Jun 17, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions bin/jpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env python

import sys
from jmespath.jpp import jpp_main

if __name__ == "__main__":
sys.exit(jpp_main(argv=sys.argv))
291 changes: 291 additions & 0 deletions jmespath/jpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import argparse
import json
import os
import pprint
import sys
import itertools

import jmespath.exceptions

# This 2 space indent matches https://github.com/jmespath/jp behavior.
JP_COMPAT_DUMP_KWARGS = (
("indent", 2),
("ensure_ascii", False),
)


def decode_json_stream(stream):
"""
Decode a text JSON input stream and generate objects until EOF.
"""
eof = False
line_buffer = []
while line_buffer or not eof:
progress = False
if not eof:
line = stream.readline()
if line:
progress = True
line_buffer.append(line)
else:
eof = True

if line_buffer:
chunk = "".join(line_buffer)
del line_buffer[:]

try:
yield json.loads(chunk)
progress = True
except json.JSONDecodeError as e:
if e.pos > 0:
try:
yield json.loads(chunk[: e.pos])
progress = True
except json.JSONDecodeError:
# Raise if there's no progress, since a given
# chunk should be growning if it is not yet
# decodable.
if not progress:
raise
line_buffer.append(chunk)
else:
line_buffer.append(chunk[e.pos :])
else:
raise


def jpp_main(argv=None):
argv = sys.argv if argv is None else argv
parser = argparse.ArgumentParser(
prog=os.path.basename(argv[0]),
)
parser.add_argument("expression", nargs="?", default=None)
parser.add_argument(
"-a",
"--accumulate",
action="store_true",
dest="accumulate",
default=False,
help=(
"Accumulate all output objects into a single recursively merged output object."
),
)
parser.add_argument(
"-c",
"--compact",
action="store_true",
dest="compact",
default=False,
help=("Produce compact JSON output that omits nonessential whitespace."),
)
parser.add_argument(
"-e",
"--expr-file",
dest="expr_file",
default=None,
help=("Read JMESPath expression from the specified file."),
)
parser.add_argument(
"-f",
"--filename",
dest="filename",
default=None,
help=(
"The filename containing the input data. "
"If a filename is not given then data is "
"read from stdin."
),
)
parser.add_argument(
"-r",
"--raw-output",
action="store_false",
dest="quoted",
default=True,
help=(
"If the final result is a string, it will be printed without quotes (an alias for --unquoted)."
),
)
parser.add_argument(
"-R",
"--raw-input",
action="store_true",
dest="raw_input",
default=False,
help=("Read raw string input and box it as JSON strings."),
)
parser.add_argument(
"-s",
"--slurp",
action="store_true",
dest="slurp",
default=False,
help=(
"Read one or more input JSON objects into an array and apply the JMESPath expression to the resulting array."
),
)
parser.add_argument(
"-u",
"--unbox",
action="store_true",
dest="unbox",
default=False,
help=(
"If the final result is a list, unbox it into a stream of output objects that is suitable for consumption by --slurp mode."
),
)
parser.add_argument(
"--unquoted",
action="store_false",
dest="quoted",
default=True,
help=("If the final result is a string, it will be printed without quotes."),
)
parser.add_argument(
"--ast",
action="store_true",
help=(
"Only print the AST of the parsed expression. Do not rely on this output, only useful for debugging purposes."
),
)
parser.usage = "{}\n {}".format(
parser.format_usage().partition("usage: ")[-1],
"jpp is an extended superset of the jp CLI for JMESPath",
)

args = parser.parse_args(argv[1:])
expression = args.expression
if expression == "help":
parser.print_help()
return 1

if expression and args.expr_file:
parser.error("Only use one of the expression or --expr-file arguments.")

dump_kwargs = dict(JP_COMPAT_DUMP_KWARGS)
if args.compact:
dump_kwargs.pop("indent", None)
dump_kwargs["separators"] = (",", ":")

if args.expr_file:
with open(args.expr_file, "rt") as f:
expression = f.read()
if not expression:
raise jmespath.exceptions.EmptyExpressionError()
elif not expression:
expression = "@"

if args.ast:
# Only print the AST
expression = jmespath.compile(args.expression)
sys.stdout.write(pprint.pformat(expression.parsed))
sys.stdout.write("\n")
return 0

if args.filename:
f = open(args.filename, "rt")
else:
f = sys.stdin

accumulator = None
stream_iter = None
eof = False

with f:
if not args.raw_input:
stream_iter = decode_json_stream(f)
while True:
while True:
if args.slurp:
if stream_iter is None:
data = f.read()
else:
data = list(stream_iter)
elif stream_iter is None:
data = f.readline()
else:
try:
data = next(stream_iter)
except StopIteration:
data = None

if not data:
eof = True
break

result = jmespath.search(expression, data)

if args.accumulate:
if accumulator is None:
accumulator = result
else:
accumulator = merge(accumulator, result)
if args.slurp:
break
else:
break

if args.accumulate:
result = accumulator
elif eof:
break

if args.unbox and isinstance(result, list):
for element in result:
output_result(args, dump_kwargs, element)
else:
output_result(args, dump_kwargs, result)

if eof or args.accumulate or args.slurp:
break
return 0


def output_result(args, dump_kwargs, result):
if args.quoted or not isinstance(result, str):
result = json.dumps(result, **dump_kwargs)

sys.stdout.write(result)

if args.quoted or (
not args.quoted and isinstance(result, str) and result[-1:] != "\n"
):
sys.stdout.write("\n")


def merge(base, head):
"""
Recursively merge head onto base.
"""
if isinstance(head, dict):
if not isinstance(base, dict):
return head

result = {}
for k in itertools.chain(head, base):
try:
result[k] = merge(base[k], head[k])
except KeyError:
try:
result[k] = head[k]
except KeyError:
result[k] = base[k]

elif isinstance(head, list):
result = []
if isinstance(base, list):
result.extend(base)
for node in head:
if node not in result:
result.append(node)
else:
result.extend(head)
else:
result = head

return result


if __name__ == "__main__":
sys.exit(jpp_main(argv=sys.argv))
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
author='James Saryerwinnie',
author_email='js@jamesls.com',
url='https://github.com/jmespath/jmespath.py',
scripts=['bin/jp.py'],
scripts=['bin/jp.py', 'bin/jpp'],
packages=find_packages(exclude=['tests']),
license='MIT',
python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*',
Expand Down