-
Notifications
You must be signed in to change notification settings - Fork 1
/
jqpy.py
195 lines (159 loc) · 5.71 KB
/
jqpy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
"""JQpy is Python binding for JQ (JSON processing language) that simply works on any platform (even Windows) and does not require compilation."""
from __future__ import annotations
import json
import os
import shutil
import subprocess
from dataclasses import dataclass
from json import JSONDecodeError
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Dict, List, Optional, Union
__version__ = '1.0.0'
JSON = Union[None, bool, int, float, str, Dict, List]
"""
JSON serializable type.
"""
class _TEmptyInput:
pass
EmptyInput = _TEmptyInput()
"""
Constant that represents no data input.
None can not be used because it is valid JSON value.
"""
class JQError(Exception):
"""
Base jqpy exception.
"""
pass
class JQMissingError(JQError):
"""
Jq binary executable (typically jq.exe in Windows and /usr/bin/jq in Unix) ws not found.
Jq binary can be installed via system package manager or from https://jqlang.github.io/jq/download official website.
"""
pass
class JQProcessingError(JQError):
"""
Processing was not finished successfully.
"""
pass
class JQRuntimeError(JQProcessingError):
"""
Processing was failed.
"""
pass
class JQResultError(JQRuntimeError):
"""
Result of processing was corrupted.
"""
pass
class JQUsageError(JQRuntimeError):
"""
Processing was aborted of finished with errors.
"""
pass
class JQTimeoutError(JQProcessingError):
"""
Processing took too much time.
By default, processing longer then 5 minutes will rise this error.
"""
pass
@dataclass
class RawOutput:
"""
Raw jq binary evaluation results.
"""
stdout: str
"""
Standard output text of jq evaluation.
"""
stderr: str
"""
Standard error text of jq evaluation.
"""
code: int
"""
Return code of jq evaluation.
"""
filter: str
"""
Processed jq filtration program.
"""
@property
def text(self) -> str:
"""
Combined stdout then stderr texts.
Result is joined with new line and stripped.
"""
return f'{self.stdout}\n{self.stderr}'.strip()
@property
def json_output(self) -> List[JSON]:
"""
List of JSON parsed results from jq evaluation one per each line.
:raise JQResultError: jq evaluation resulted in bad output.
"""
return _parse_output(self.stdout, self.filter)
def _parse_output(text: str, jq_filter: str) -> List[JSON]:
text = text.replace('\r\n', '\n').strip()
if not text:
return []
try:
return [json.loads(data) for data in text.split('\n') if data.strip()]
except JSONDecodeError as e:
raise JQResultError(f'Received bad output from jq. Can not detect result from string: {text}\n\nFiltration command: {jq_filter}') from e
def jq(filter: str = '', data: Union[JSON, _TEmptyInput] = EmptyInput, *, timeout: Optional[float] = 300, raw_output: bool = False) -> Union[List[JSON], RawOutput]:
"""
Evaluate jq with provided input JSON data and filtration program.
:param filter: jq filtration program. By default, is empty.
:param data: JSON compatible input data to be passed to jq. By default, is empty.
:param timeout: jq evaluation maximum time. By default, processing longer then 5 minutes will rise JQTimeoutError error.
:param raw_output: whether to return raw results of jq evaluation. In the most cases integrated JSON data results parser should be used instead.
:return: List of JSON parsed results from jq evaluation one per each line.
:raise ValueError: invalid input data was provided.
:raise JQMissingError: jq binary dependency not found.
:raise JQUsageError: jq evaluation failed.
:raise JQTimeoutError: jq evaluation took too much time.
:raise JQResultError: jq evaluation resulted in bad output.
"""
if not filter.strip():
if data is EmptyInput:
return []
return [data]
args = ['--monochrome-output', '--compact-output']
if data is EmptyInput:
args.append('--null-input')
data_str = ''
else:
try:
data_str = json.dumps(data, ensure_ascii=False, indent='')
except Exception as e:
raise ValueError(f'Bad input data passed to jq. Input must be any JSON compatible object (see https://docs.python.org/3/library/json.html). Given input: {data}') from e
with NamedTemporaryFile('w+', encoding='utf8', delete=False) as f:
tmp = Path(f.name)
try:
f.write(filter)
f.close()
args.extend(['--from-file', str(tmp.resolve())])
try:
p = subprocess.Popen(args, executable=shutil.which('jq'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, encoding='utf8')
p.stdin.write(data_str)
p.stdin.close()
except FileNotFoundError as e:
installation = 'choco install jq' if os.name == 'nt' else 'apt-get install jq'
raise JQMissingError(f'Can not find jq binary executable (typically jq.exe in Windows and /usr/bin/jq in Unix). Install jq binary via your system package manager or from https://jqlang.github.io/jq/download then restart the program. If you have already installed jq ensure you have jq in PATH environment variable.\nYou can try to install jq binary executable via running "{installation}" command.') from e
except Exception as e:
raise JQUsageError(f'Unexpected usage of jq. Filtration was unexpectedly aborted: {e}.\n\nFiltration command: {filter}') from e
try:
p.wait(timeout=timeout)
except subprocess.TimeoutExpired as e:
p.kill()
raise JQTimeoutError(f'Filtration timed out and was killed.\n\nFiltration command: {filter}') from e
finally:
if tmp.exists():
tmp.unlink()
if raw_output:
return RawOutput(stdout=p.stdout.read(), stderr=p.stderr.read(), code=p.returncode, filter=filter)
err = p.stderr.read()
if p.returncode != 0 or err:
raise JQUsageError(f'Unexpected usage of jq. Filtration process finished with error code {p.returncode} with error message: {err}\n\nFiltration command: {filter}')
return _parse_output(p.stdout.read(), filter)