evolver-tools 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evolver_tools/__init__.py +2 -0
- evolver_tools/__main__.py +3 -0
- evolver_tools/cli.py +89 -0
- evolver_tools/vendor/b64/__init__.py +2 -0
- evolver_tools/vendor/b64/b64.py +176 -0
- evolver_tools/vendor/cal_tool/__init__.py +1 -0
- evolver_tools/vendor/cal_tool/cli.py +234 -0
- evolver_tools/vendor/chart_cli/__init__.py +444 -0
- evolver_tools/vendor/chart_cli/__main__.py +3 -0
- evolver_tools/vendor/colors/__init__.py +5 -0
- evolver_tools/vendor/colors/__main__.py +97 -0
- evolver_tools/vendor/csv_stats/__init__.py +5 -0
- evolver_tools/vendor/csv_stats/__main__.py +4 -0
- evolver_tools/vendor/csv_stats/analyzer.py +258 -0
- evolver_tools/vendor/csv_stats/cli.py +45 -0
- evolver_tools/vendor/dirsize/__init__.py +183 -0
- evolver_tools/vendor/envcheck/__init__.py +426 -0
- evolver_tools/vendor/ff/__init__.py +427 -0
- evolver_tools/vendor/ff/__main__.py +3 -0
- evolver_tools/vendor/find_dups/__init__.py +7 -0
- evolver_tools/vendor/find_dups/cli.py +392 -0
- evolver_tools/vendor/hashsum/__init__.py +211 -0
- evolver_tools/vendor/hashsum/__main__.py +5 -0
- evolver_tools/vendor/http_live/__init__.py +265 -0
- evolver_tools/vendor/http_live/__main__.py +2 -0
- evolver_tools/vendor/ipinfo/__init__.py +3 -0
- evolver_tools/vendor/ipinfo/__main__.py +30 -0
- evolver_tools/vendor/jq_lite/__init__.py +257 -0
- evolver_tools/vendor/jq_lite/__main__.py +5 -0
- evolver_tools/vendor/json2csv/__init__.py +3 -0
- evolver_tools/vendor/json2csv/__main__.py +82 -0
- evolver_tools/vendor/jsonql/__init__.py +326 -0
- evolver_tools/vendor/jsonql/__main__.py +5 -0
- evolver_tools/vendor/license_cli/__init__.py +1 -0
- evolver_tools/vendor/license_cli/__main__.py +4 -0
- evolver_tools/vendor/license_cli/cli.py +289 -0
- evolver_tools/vendor/markdown_check/__init__.py +211 -0
- evolver_tools/vendor/nb/__init__.py +319 -0
- evolver_tools/vendor/nb/__main__.py +3 -0
- evolver_tools/vendor/passgen/__init__.py +224 -0
- evolver_tools/vendor/portcheck/__init__.py +2 -0
- evolver_tools/vendor/portcheck/__main__.py +66 -0
- evolver_tools/vendor/project_doctor/__init__.py +412 -0
- evolver_tools/vendor/project_doctor/__main__.py +3 -0
- evolver_tools/vendor/ren/__init__.py +283 -0
- evolver_tools/vendor/ren/__main__.py +3 -0
- evolver_tools/vendor/siege_lite/__init__.py +250 -0
- evolver_tools/vendor/siege_lite/__main__.py +3 -0
- evolver_tools/vendor/smellfinder/__init__.py +376 -0
- evolver_tools/vendor/smellfinder/__main__.py +3 -0
- evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
- evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
- evolver_tools/vendor/sysmon/__init__.py +299 -0
- evolver_tools/vendor/sysmon/__main__.py +3 -0
- evolver_tools/vendor/timer/__init__.py +127 -0
- evolver_tools/vendor/treedir/__init__.py +2 -0
- evolver_tools/vendor/treedir/__main__.py +128 -0
- evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
- evolver_tools/vendor/urlparse_tool/cli.py +212 -0
- evolver_tools/vendor/web_summary/__init__.py +341 -0
- evolver_tools/vendor/web_summary/__main__.py +3 -0
- evolver_tools/vendor/wordcount/__init__.py +2 -0
- evolver_tools/vendor/wordcount/__main__.py +101 -0
- evolver_tools-1.4.0.dist-info/METADATA +107 -0
- evolver_tools-1.4.0.dist-info/RECORD +69 -0
- evolver_tools-1.4.0.dist-info/WHEEL +5 -0
- evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
- evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
- evolver_tools-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
http-live — 零依赖热重载 HTTP 服务器
|
|
3
|
+
|
|
4
|
+
一个纯 Python 的静态文件服务器,当文件变化时自动刷新浏览器。
|
|
5
|
+
零外部依赖,仅使用 Python 标准库。
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
http-live # 当前目录,端口 8000
|
|
9
|
+
http-live --port 3000 # 自定义端口
|
|
10
|
+
http-live --dir ./dist # 自定义目录
|
|
11
|
+
http-live --poll 1.0 # 轮询间隔(秒)
|
|
12
|
+
http-live --no-browser # 不自动打开浏览器
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import http.server
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import queue
|
|
20
|
+
import socket
|
|
21
|
+
import sys
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
import webbrowser
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
REFRESH_SCRIPT = """
|
|
29
|
+
<script>
|
|
30
|
+
(function() {
|
|
31
|
+
var es = new EventSource('/__http_live/reload');
|
|
32
|
+
es.onmessage = function(e) {
|
|
33
|
+
if (e.data === 'refresh') {
|
|
34
|
+
location.reload();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
es.onerror = function() {
|
|
38
|
+
// 如果 SSE 连接断开,静默重连
|
|
39
|
+
es.close();
|
|
40
|
+
setTimeout(function() {
|
|
41
|
+
location.reload();
|
|
42
|
+
}, 2000);
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
</script>
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LiveHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
50
|
+
"""在 HTML 响应中注入热重载脚本的请求处理器"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, *args, **kwargs):
|
|
53
|
+
self._directory = kwargs.pop("live_dir", None) or os.getcwd()
|
|
54
|
+
self._event_queue = kwargs.pop("event_queue")
|
|
55
|
+
super().__init__(*args, directory=self._directory, **kwargs)
|
|
56
|
+
|
|
57
|
+
def send_head(self):
|
|
58
|
+
path = self.translate_path(self.path)
|
|
59
|
+
if self.path.endswith("/__http_live/reload"):
|
|
60
|
+
return self._handle_sse()
|
|
61
|
+
return super().send_head()
|
|
62
|
+
|
|
63
|
+
def _handle_sse(self):
|
|
64
|
+
"""处理 SSE 连接"""
|
|
65
|
+
self.send_response(200)
|
|
66
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
67
|
+
self.send_header("Cache-Control", "no-cache")
|
|
68
|
+
self.send_header("Connection", "keep-alive")
|
|
69
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
70
|
+
self.end_headers()
|
|
71
|
+
|
|
72
|
+
def stream():
|
|
73
|
+
try:
|
|
74
|
+
while True:
|
|
75
|
+
try:
|
|
76
|
+
msg = self._event_queue.get(timeout=30)
|
|
77
|
+
self.wfile.write(f"data: {msg}\n\n".encode())
|
|
78
|
+
self.wfile.flush()
|
|
79
|
+
except queue.Empty:
|
|
80
|
+
# 发送心跳保持连接
|
|
81
|
+
self.wfile.write(b": heartbeat\n\n")
|
|
82
|
+
self.wfile.flush()
|
|
83
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
threading.Thread(target=stream, daemon=True).start()
|
|
87
|
+
return None # 阻止默认响应
|
|
88
|
+
|
|
89
|
+
def copyfile(self, source, outputfile):
|
|
90
|
+
content = source.read()
|
|
91
|
+
source.seek(0)
|
|
92
|
+
path = self.translate_path(self.path)
|
|
93
|
+
|
|
94
|
+
if self.path.endswith(".html"):
|
|
95
|
+
modified = source.read().decode("utf-8", errors="replace")
|
|
96
|
+
# 在 </body> 前注入刷新脚本
|
|
97
|
+
if "</body>" in modified:
|
|
98
|
+
modified = modified.replace("</body>", REFRESH_SCRIPT + "\n</body>")
|
|
99
|
+
else:
|
|
100
|
+
modified += REFRESH_SCRIPT
|
|
101
|
+
outputfile.write(modified.encode("utf-8"))
|
|
102
|
+
else:
|
|
103
|
+
super().copyfile(source, outputfile)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class FileWatcher(threading.Thread):
|
|
107
|
+
"""基于轮询的文件变化检测器"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, watch_dir, poll_interval=0.5, event_queue=None):
|
|
110
|
+
super().__init__(daemon=True)
|
|
111
|
+
self.watch_dir = Path(watch_dir).resolve()
|
|
112
|
+
self.poll_interval = poll_interval
|
|
113
|
+
self.event_queue = event_queue
|
|
114
|
+
self._mtime_cache = {}
|
|
115
|
+
self._running = True
|
|
116
|
+
self.name = "FileWatcher"
|
|
117
|
+
|
|
118
|
+
def run(self):
|
|
119
|
+
self._scan_initial()
|
|
120
|
+
while self._running:
|
|
121
|
+
time.sleep(self.poll_interval)
|
|
122
|
+
self._check_changes()
|
|
123
|
+
|
|
124
|
+
def stop(self):
|
|
125
|
+
self._running = False
|
|
126
|
+
|
|
127
|
+
def _scan_initial(self):
|
|
128
|
+
for path in self._walk():
|
|
129
|
+
try:
|
|
130
|
+
self._mtime_cache[str(path)] = path.stat().st_mtime
|
|
131
|
+
except OSError:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
def _walk(self):
|
|
135
|
+
for root, dirs, files in os.walk(self.watch_dir):
|
|
136
|
+
# 跳过隐藏目录和 __pycache__
|
|
137
|
+
dirs[:] = [d for d in dirs
|
|
138
|
+
if not d.startswith(".")
|
|
139
|
+
and d != "__pycache__"
|
|
140
|
+
and d != "node_modules"
|
|
141
|
+
and d != ".git"]
|
|
142
|
+
for f in files:
|
|
143
|
+
if f.startswith("."):
|
|
144
|
+
continue
|
|
145
|
+
yield Path(root) / f
|
|
146
|
+
|
|
147
|
+
def _check_changes(self):
|
|
148
|
+
current = {}
|
|
149
|
+
changed = False
|
|
150
|
+
for path in self._walk():
|
|
151
|
+
try:
|
|
152
|
+
mtime = path.stat().st_mtime
|
|
153
|
+
current[str(path)] = mtime
|
|
154
|
+
cached = self._mtime_cache.get(str(path))
|
|
155
|
+
if cached is None or abs(mtime - cached) > 0.01:
|
|
156
|
+
changed = True
|
|
157
|
+
# 打印日志到 stderr(不干扰 SSE 流)
|
|
158
|
+
relative = path.relative_to(self.watch_dir)
|
|
159
|
+
print(f" \u2728 Changed: {relative}", file=sys.stderr)
|
|
160
|
+
except OSError:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# 检测文件删除
|
|
164
|
+
for path_str in list(self._mtime_cache.keys()):
|
|
165
|
+
if path_str not in current:
|
|
166
|
+
changed = True
|
|
167
|
+
try:
|
|
168
|
+
rel = Path(path_str).relative_to(self.watch_dir)
|
|
169
|
+
print(f" \u274c Removed: {rel}", file=sys.stderr)
|
|
170
|
+
except ValueError:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
if changed:
|
|
174
|
+
self._mtime_cache = current
|
|
175
|
+
if self.event_queue:
|
|
176
|
+
self.event_queue.put("refresh")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _find_open_port(start=8000, end=9999):
|
|
180
|
+
"""找到可用端口"""
|
|
181
|
+
for port in range(start, end + 1):
|
|
182
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
183
|
+
try:
|
|
184
|
+
s.bind(("127.0.0.1", port))
|
|
185
|
+
return port
|
|
186
|
+
except OSError:
|
|
187
|
+
continue
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def main():
|
|
192
|
+
parser = argparse.ArgumentParser(
|
|
193
|
+
description="http-live — 零依赖热重载 HTTP 服务器",
|
|
194
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
195
|
+
epilog="""
|
|
196
|
+
Examples:
|
|
197
|
+
http-live # 当前目录,端口 8000
|
|
198
|
+
http-live --port 3000 # 自定义端口
|
|
199
|
+
http-live --dir ./dist # 自定义目录
|
|
200
|
+
http-live --poll 1.0 # 轮询间隔 1 秒
|
|
201
|
+
""",
|
|
202
|
+
)
|
|
203
|
+
parser.add_argument("--port", "-p", type=int, default=8000, help="端口号 (默认: 8000)")
|
|
204
|
+
parser.add_argument("--dir", "-d", type=str, default=".", help="服务目录 (默认: 当前目录)")
|
|
205
|
+
parser.add_argument("--poll", type=float, default=0.5, help="文件轮询间隔秒数 (默认: 0.5)")
|
|
206
|
+
parser.add_argument("--no-browser", action="store_true", help="不自动打开浏览器")
|
|
207
|
+
parser.add_argument("--version", "-v", action="version", version="http-live 1.0.0")
|
|
208
|
+
|
|
209
|
+
args = parser.parse_args()
|
|
210
|
+
|
|
211
|
+
serve_dir = os.path.abspath(args.dir)
|
|
212
|
+
if not os.path.isdir(serve_dir):
|
|
213
|
+
print(f"错误: 目录不存在: {serve_dir}", file=sys.stderr)
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
|
|
216
|
+
port = _find_open_port(args.port)
|
|
217
|
+
if port == 0:
|
|
218
|
+
print("错误: 找不到可用端口", file=sys.stderr)
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
event_queue = queue.Queue()
|
|
222
|
+
|
|
223
|
+
# 启动文件监控
|
|
224
|
+
watcher = FileWatcher(
|
|
225
|
+
watch_dir=serve_dir,
|
|
226
|
+
poll_interval=args.poll,
|
|
227
|
+
event_queue=event_queue,
|
|
228
|
+
)
|
|
229
|
+
watcher.start()
|
|
230
|
+
|
|
231
|
+
# 创建服务器
|
|
232
|
+
handler = lambda *h_args, **h_kwargs: LiveHTTPRequestHandler(
|
|
233
|
+
*h_args, live_dir=serve_dir, event_queue=event_queue, **h_kwargs
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
server = http.server.HTTPServer(("127.0.0.1", port), handler)
|
|
237
|
+
|
|
238
|
+
url = f"http://127.0.0.1:{port}"
|
|
239
|
+
|
|
240
|
+
print(f"")
|
|
241
|
+
print(f" \U0001f525 http-live started")
|
|
242
|
+
print(f" \ud83d\udcc1 Serving: {serve_dir}")
|
|
243
|
+
print(f" \ud83c\udf10 URL: {url}")
|
|
244
|
+
print(f" \u23f3 Watching: every {args.poll}s")
|
|
245
|
+
print(f" \u241b Quit: Ctrl+C")
|
|
246
|
+
print(f"")
|
|
247
|
+
|
|
248
|
+
if not args.no_browser:
|
|
249
|
+
try:
|
|
250
|
+
webbrowser.open(url)
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
server.serve_forever()
|
|
256
|
+
except KeyboardInterrupt:
|
|
257
|
+
print("\n \ud83d\udc4b Shutting down...")
|
|
258
|
+
watcher.stop()
|
|
259
|
+
server.shutdown()
|
|
260
|
+
print(" Done.")
|
|
261
|
+
sys.exit(0)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
def main():
|
|
3
|
+
import urllib.request
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
resp = urllib.request.urlopen("http://ip-api.com/json/", timeout=5)
|
|
9
|
+
data = json.loads(resp.read().decode())
|
|
10
|
+
if data.get("status") == "fail":
|
|
11
|
+
# fallback to ipify
|
|
12
|
+
ip = urllib.request.urlopen("https://api.ipify.org?format=json", timeout=5).read().decode()
|
|
13
|
+
data2 = json.loads(ip)
|
|
14
|
+
print(f"IP: {data2.get('ip', 'unknown')}")
|
|
15
|
+
print("Location: (geo lookup failed)")
|
|
16
|
+
else:
|
|
17
|
+
print(f"IP: {data.get('query', '?')}")
|
|
18
|
+
print(f"Country: {data.get('country', '?')}")
|
|
19
|
+
print(f"Region: {data.get('regionName', '?')}")
|
|
20
|
+
print(f"City: {data.get('city', '?')}")
|
|
21
|
+
print(f"ISP: {data.get('isp', '?')}")
|
|
22
|
+
print(f"Org: {data.get('org', '?')}")
|
|
23
|
+
print(f"AS: {data.get('as', '?')}")
|
|
24
|
+
print(f"Lat/Lon: {data.get('lat', '?')}, {data.get('lon', '?')}")
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""jq-lite: Zero-dependency JSON query tool (pure Python stdlib jq alternative)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import operator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.0"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_query(query: str):
|
|
14
|
+
"""Parse a jq-like query string into a chain of filters."""
|
|
15
|
+
if not query:
|
|
16
|
+
return [("identity",)]
|
|
17
|
+
|
|
18
|
+
# Split by pipe
|
|
19
|
+
stages = []
|
|
20
|
+
for part in query.split("|"):
|
|
21
|
+
part = part.strip()
|
|
22
|
+
if not part:
|
|
23
|
+
continue
|
|
24
|
+
stages.append(part)
|
|
25
|
+
|
|
26
|
+
if not stages:
|
|
27
|
+
return [("identity",)]
|
|
28
|
+
|
|
29
|
+
filters = []
|
|
30
|
+
for stage in stages:
|
|
31
|
+
filters.append(parse_stage(stage))
|
|
32
|
+
return filters
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_stage(stage: str):
|
|
36
|
+
"""Parse a single pipeline stage."""
|
|
37
|
+
stage = stage.strip()
|
|
38
|
+
|
|
39
|
+
if stage == ".":
|
|
40
|
+
return ("identity",)
|
|
41
|
+
|
|
42
|
+
# Array iteration: .[]
|
|
43
|
+
m = re.match(r'^\.\[\]$', stage)
|
|
44
|
+
if m:
|
|
45
|
+
return ("iterate",)
|
|
46
|
+
|
|
47
|
+
# Array iteration with index: .[0], .[1:3], .[-1]
|
|
48
|
+
m = re.match(r'^\.\[([^\]]+)\]$', stage)
|
|
49
|
+
if m:
|
|
50
|
+
idx_str = m.group(1)
|
|
51
|
+
return parse_index(idx_str)
|
|
52
|
+
|
|
53
|
+
# Object key access: .key, .key.subkey, .key[0].subkey
|
|
54
|
+
if stage.startswith("."):
|
|
55
|
+
parts = stage[1:].split(".")
|
|
56
|
+
# Parse each part, handling key[index] patterns
|
|
57
|
+
chain = []
|
|
58
|
+
for p in parts:
|
|
59
|
+
# Check for array index suffix like [0], [1:3]
|
|
60
|
+
bracket_match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)(\[([^\]]+)\])?$', p)
|
|
61
|
+
if bracket_match and bracket_match.group(2):
|
|
62
|
+
# Part has both key and index: e.g. "users[0]"
|
|
63
|
+
chain.append(("key", bracket_match.group(1)))
|
|
64
|
+
idx_part = parse_index(bracket_match.group(3))
|
|
65
|
+
if isinstance(idx_part, tuple):
|
|
66
|
+
chain.append(idx_part)
|
|
67
|
+
else:
|
|
68
|
+
chain.append(idx_part)
|
|
69
|
+
else:
|
|
70
|
+
chain.append(("key", p))
|
|
71
|
+
if len(chain) == 1:
|
|
72
|
+
return chain[0]
|
|
73
|
+
return ("chain", chain)
|
|
74
|
+
|
|
75
|
+
# Key access without dot prefix
|
|
76
|
+
m = re.match(r'^"([^"]+)"$', stage)
|
|
77
|
+
if m:
|
|
78
|
+
return ("key", m.group(1))
|
|
79
|
+
|
|
80
|
+
return ("identity",)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_index(idx_str: str):
|
|
84
|
+
"""Parse array index expression."""
|
|
85
|
+
idx_str = idx_str.strip()
|
|
86
|
+
|
|
87
|
+
# Slice: 0:2, :3, 1:, -1:
|
|
88
|
+
if ":" in idx_str:
|
|
89
|
+
parts = idx_str.split(":")
|
|
90
|
+
start = int(parts[0]) if parts[0] else None
|
|
91
|
+
end = int(parts[1]) if len(parts) > 1 and parts[1] else None
|
|
92
|
+
return ("slice", start, end)
|
|
93
|
+
|
|
94
|
+
# Negative index
|
|
95
|
+
return ("index", int(idx_str))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def apply_filters(data, filters):
|
|
99
|
+
"""Apply a chain of filters to data, yielding results."""
|
|
100
|
+
results = [data]
|
|
101
|
+
|
|
102
|
+
for filter_type, *args in filters:
|
|
103
|
+
new_results = []
|
|
104
|
+
for item in results:
|
|
105
|
+
try:
|
|
106
|
+
if filter_type == "identity":
|
|
107
|
+
new_results.append(item)
|
|
108
|
+
elif filter_type == "key":
|
|
109
|
+
key = args[0]
|
|
110
|
+
if isinstance(item, dict):
|
|
111
|
+
new_results.append(item[key])
|
|
112
|
+
else:
|
|
113
|
+
return []
|
|
114
|
+
elif filter_type == "index":
|
|
115
|
+
idx = args[0]
|
|
116
|
+
if isinstance(item, (list, tuple)):
|
|
117
|
+
new_results.append(item[idx])
|
|
118
|
+
else:
|
|
119
|
+
return []
|
|
120
|
+
elif filter_type == "slice":
|
|
121
|
+
start, end = args
|
|
122
|
+
if isinstance(item, (list, tuple)):
|
|
123
|
+
new_results.extend(item[start:end])
|
|
124
|
+
else:
|
|
125
|
+
return []
|
|
126
|
+
elif filter_type == "iterate":
|
|
127
|
+
if isinstance(item, (list, tuple)):
|
|
128
|
+
new_results.extend(item)
|
|
129
|
+
elif isinstance(item, dict):
|
|
130
|
+
new_results.extend(item.values())
|
|
131
|
+
else:
|
|
132
|
+
return []
|
|
133
|
+
elif filter_type == "chain":
|
|
134
|
+
# Apply sub-filters in sequence
|
|
135
|
+
sub_results = [item]
|
|
136
|
+
for sub_filter in args[0]:
|
|
137
|
+
tmp = []
|
|
138
|
+
for sr in sub_results:
|
|
139
|
+
ft, *fa = sub_filter
|
|
140
|
+
try:
|
|
141
|
+
if ft == "key":
|
|
142
|
+
if isinstance(sr, dict):
|
|
143
|
+
tmp.append(sr[fa[0]])
|
|
144
|
+
elif ft == "index":
|
|
145
|
+
if isinstance(sr, (list, tuple)):
|
|
146
|
+
tmp.append(sr[fa[0]])
|
|
147
|
+
else:
|
|
148
|
+
tmp.append(sr)
|
|
149
|
+
except (KeyError, IndexError, TypeError):
|
|
150
|
+
return []
|
|
151
|
+
sub_results = tmp
|
|
152
|
+
new_results.extend(sub_results)
|
|
153
|
+
except (KeyError, IndexError, TypeError):
|
|
154
|
+
return []
|
|
155
|
+
results = new_results
|
|
156
|
+
if not results:
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
return results
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def format_result(value, raw=False):
|
|
163
|
+
"""Format a single result value."""
|
|
164
|
+
if raw:
|
|
165
|
+
if isinstance(value, str):
|
|
166
|
+
return value
|
|
167
|
+
elif isinstance(value, (int, float)):
|
|
168
|
+
return str(value)
|
|
169
|
+
elif value is None:
|
|
170
|
+
return "null"
|
|
171
|
+
elif isinstance(value, bool):
|
|
172
|
+
return "true" if value else "false"
|
|
173
|
+
else:
|
|
174
|
+
return json.dumps(value, ensure_ascii=False)
|
|
175
|
+
else:
|
|
176
|
+
if isinstance(value, str):
|
|
177
|
+
return value
|
|
178
|
+
elif isinstance(value, (int, float, bool, type(None))):
|
|
179
|
+
return json.dumps(value, ensure_ascii=False)
|
|
180
|
+
else:
|
|
181
|
+
return json.dumps(value, ensure_ascii=False)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main():
|
|
185
|
+
import argparse
|
|
186
|
+
|
|
187
|
+
parser = argparse.ArgumentParser(
|
|
188
|
+
description="jq-lite — Zero-dependency JSON query tool",
|
|
189
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
190
|
+
epilog="""
|
|
191
|
+
Examples:
|
|
192
|
+
jq-lite '.name' data.json # Extract key
|
|
193
|
+
jq-lite '.users[0].name' data.json # Nested + array
|
|
194
|
+
jq-lite '.items[] | .id' data.json # Iterate + pipe
|
|
195
|
+
echo '{"a":1,"b":2}' | jq-lite '.[]' # Iterate values
|
|
196
|
+
jq-lite '.' data.json # Pretty-print entire file
|
|
197
|
+
jq-lite -r '.name' data.json # Raw output (no string quotes)
|
|
198
|
+
"""
|
|
199
|
+
)
|
|
200
|
+
parser.add_argument("query", help="jq-like query expression (e.g. '.key', '.key[0]', '.[] | .name')")
|
|
201
|
+
parser.add_argument("input", nargs="?", help="Input file (default: stdin)")
|
|
202
|
+
parser.add_argument("-r", "--raw-output", action="store_true", help="Output raw strings without JSON quoting")
|
|
203
|
+
parser.add_argument("--compact", action="store_true", help="Compact output (no pretty-print for objects)")
|
|
204
|
+
|
|
205
|
+
args = parser.parse_args()
|
|
206
|
+
|
|
207
|
+
# Read input
|
|
208
|
+
if args.input:
|
|
209
|
+
try:
|
|
210
|
+
with open(args.input, "r") as f:
|
|
211
|
+
data = json.load(f)
|
|
212
|
+
except FileNotFoundError:
|
|
213
|
+
print(f"Error: file not found: {args.input}", file=sys.stderr)
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
except json.JSONDecodeError as e:
|
|
216
|
+
print(f"Error: invalid JSON: {e}", file=sys.stderr)
|
|
217
|
+
sys.exit(1)
|
|
218
|
+
else:
|
|
219
|
+
stdin_data = sys.stdin.read()
|
|
220
|
+
if not stdin_data.strip():
|
|
221
|
+
print("Error: no input (pipe JSON or provide a file)", file=sys.stderr)
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
try:
|
|
224
|
+
data = json.loads(stdin_data)
|
|
225
|
+
except json.JSONDecodeError as e:
|
|
226
|
+
print(f"Error: invalid JSON from stdin: {e}", file=sys.stderr)
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
# Parse and apply query
|
|
230
|
+
try:
|
|
231
|
+
filters = parse_query(args.query)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(f"Error: invalid query: {e}", file=sys.stderr)
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
results = apply_filters(data, filters)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
print(f"Error: query failed: {e}", file=sys.stderr)
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
if not results:
|
|
243
|
+
sys.exit(0)
|
|
244
|
+
|
|
245
|
+
# Format and output
|
|
246
|
+
output_lines = []
|
|
247
|
+
for r in results:
|
|
248
|
+
formatted = format_result(r, raw=args.raw_output)
|
|
249
|
+
if isinstance(r, (dict, list)) and not args.compact:
|
|
250
|
+
formatted = json.dumps(r, ensure_ascii=False, indent=2)
|
|
251
|
+
output_lines.append(formatted)
|
|
252
|
+
|
|
253
|
+
print("\n".join(output_lines))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import csv
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
def flatten(obj, prefix=""):
|
|
8
|
+
"""Flatten nested dict to single level"""
|
|
9
|
+
items = {}
|
|
10
|
+
if isinstance(obj, dict):
|
|
11
|
+
for k, v in obj.items():
|
|
12
|
+
key = f"{prefix}.{k}" if prefix else k
|
|
13
|
+
if isinstance(v, (dict,)):
|
|
14
|
+
items.update(flatten(v, key))
|
|
15
|
+
elif isinstance(v, (list, tuple)):
|
|
16
|
+
items[key] = json.dumps(v, ensure_ascii=False)
|
|
17
|
+
else:
|
|
18
|
+
items[key] = v
|
|
19
|
+
else:
|
|
20
|
+
items[prefix or "value"] = obj
|
|
21
|
+
return items
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
import argparse
|
|
25
|
+
|
|
26
|
+
parser = argparse.ArgumentParser(description="Convert JSON to CSV")
|
|
27
|
+
parser.add_argument("input", nargs="?", help="Input file (default: stdin)")
|
|
28
|
+
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
|
|
29
|
+
parser.add_argument("--flatten", action="store_true", help="Flatten nested objects")
|
|
30
|
+
parser.add_argument("--delimiter", default=",", help="CSV delimiter (default: comma)")
|
|
31
|
+
parser.add_argument("--no-header", action="store_true", help="Do not write header row")
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
# Read input
|
|
35
|
+
if args.input:
|
|
36
|
+
with open(args.input, "r") as f:
|
|
37
|
+
data = json.load(f)
|
|
38
|
+
else:
|
|
39
|
+
data = json.load(sys.stdin)
|
|
40
|
+
|
|
41
|
+
# Normalize to list
|
|
42
|
+
if isinstance(data, dict):
|
|
43
|
+
data = [data]
|
|
44
|
+
if not isinstance(data, list):
|
|
45
|
+
print("Error: JSON must be an array or object", file=sys.stderr)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
# Flatten if requested
|
|
49
|
+
if args.flatten:
|
|
50
|
+
data = [flatten(item) for item in data]
|
|
51
|
+
|
|
52
|
+
# Get all field names
|
|
53
|
+
fields = []
|
|
54
|
+
for item in data:
|
|
55
|
+
if isinstance(item, dict):
|
|
56
|
+
for k in item:
|
|
57
|
+
if k not in fields:
|
|
58
|
+
fields.append(k)
|
|
59
|
+
|
|
60
|
+
if not fields:
|
|
61
|
+
# Simple values list
|
|
62
|
+
fields = ["value"]
|
|
63
|
+
data = [{"value": v} for v in data]
|
|
64
|
+
|
|
65
|
+
# Write CSV
|
|
66
|
+
out = open(args.output, "w", newline="") if args.output else sys.stdout
|
|
67
|
+
try:
|
|
68
|
+
writer = csv.DictWriter(out, fieldnames=fields, delimiter=args.delimiter,
|
|
69
|
+
extrasaction="ignore", quoting=csv.QUOTE_MINIMAL)
|
|
70
|
+
if not args.no_header:
|
|
71
|
+
writer.writeheader()
|
|
72
|
+
for item in data:
|
|
73
|
+
if isinstance(item, dict):
|
|
74
|
+
writer.writerow(item)
|
|
75
|
+
else:
|
|
76
|
+
writer.writerow({"value": item})
|
|
77
|
+
finally:
|
|
78
|
+
if args.output:
|
|
79
|
+
out.close()
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|