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.
Files changed (69) hide show
  1. evolver_tools/__init__.py +2 -0
  2. evolver_tools/__main__.py +3 -0
  3. evolver_tools/cli.py +89 -0
  4. evolver_tools/vendor/b64/__init__.py +2 -0
  5. evolver_tools/vendor/b64/b64.py +176 -0
  6. evolver_tools/vendor/cal_tool/__init__.py +1 -0
  7. evolver_tools/vendor/cal_tool/cli.py +234 -0
  8. evolver_tools/vendor/chart_cli/__init__.py +444 -0
  9. evolver_tools/vendor/chart_cli/__main__.py +3 -0
  10. evolver_tools/vendor/colors/__init__.py +5 -0
  11. evolver_tools/vendor/colors/__main__.py +97 -0
  12. evolver_tools/vendor/csv_stats/__init__.py +5 -0
  13. evolver_tools/vendor/csv_stats/__main__.py +4 -0
  14. evolver_tools/vendor/csv_stats/analyzer.py +258 -0
  15. evolver_tools/vendor/csv_stats/cli.py +45 -0
  16. evolver_tools/vendor/dirsize/__init__.py +183 -0
  17. evolver_tools/vendor/envcheck/__init__.py +426 -0
  18. evolver_tools/vendor/ff/__init__.py +427 -0
  19. evolver_tools/vendor/ff/__main__.py +3 -0
  20. evolver_tools/vendor/find_dups/__init__.py +7 -0
  21. evolver_tools/vendor/find_dups/cli.py +392 -0
  22. evolver_tools/vendor/hashsum/__init__.py +211 -0
  23. evolver_tools/vendor/hashsum/__main__.py +5 -0
  24. evolver_tools/vendor/http_live/__init__.py +265 -0
  25. evolver_tools/vendor/http_live/__main__.py +2 -0
  26. evolver_tools/vendor/ipinfo/__init__.py +3 -0
  27. evolver_tools/vendor/ipinfo/__main__.py +30 -0
  28. evolver_tools/vendor/jq_lite/__init__.py +257 -0
  29. evolver_tools/vendor/jq_lite/__main__.py +5 -0
  30. evolver_tools/vendor/json2csv/__init__.py +3 -0
  31. evolver_tools/vendor/json2csv/__main__.py +82 -0
  32. evolver_tools/vendor/jsonql/__init__.py +326 -0
  33. evolver_tools/vendor/jsonql/__main__.py +5 -0
  34. evolver_tools/vendor/license_cli/__init__.py +1 -0
  35. evolver_tools/vendor/license_cli/__main__.py +4 -0
  36. evolver_tools/vendor/license_cli/cli.py +289 -0
  37. evolver_tools/vendor/markdown_check/__init__.py +211 -0
  38. evolver_tools/vendor/nb/__init__.py +319 -0
  39. evolver_tools/vendor/nb/__main__.py +3 -0
  40. evolver_tools/vendor/passgen/__init__.py +224 -0
  41. evolver_tools/vendor/portcheck/__init__.py +2 -0
  42. evolver_tools/vendor/portcheck/__main__.py +66 -0
  43. evolver_tools/vendor/project_doctor/__init__.py +412 -0
  44. evolver_tools/vendor/project_doctor/__main__.py +3 -0
  45. evolver_tools/vendor/ren/__init__.py +283 -0
  46. evolver_tools/vendor/ren/__main__.py +3 -0
  47. evolver_tools/vendor/siege_lite/__init__.py +250 -0
  48. evolver_tools/vendor/siege_lite/__main__.py +3 -0
  49. evolver_tools/vendor/smellfinder/__init__.py +376 -0
  50. evolver_tools/vendor/smellfinder/__main__.py +3 -0
  51. evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
  52. evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
  53. evolver_tools/vendor/sysmon/__init__.py +299 -0
  54. evolver_tools/vendor/sysmon/__main__.py +3 -0
  55. evolver_tools/vendor/timer/__init__.py +127 -0
  56. evolver_tools/vendor/treedir/__init__.py +2 -0
  57. evolver_tools/vendor/treedir/__main__.py +128 -0
  58. evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
  59. evolver_tools/vendor/urlparse_tool/cli.py +212 -0
  60. evolver_tools/vendor/web_summary/__init__.py +341 -0
  61. evolver_tools/vendor/web_summary/__main__.py +3 -0
  62. evolver_tools/vendor/wordcount/__init__.py +2 -0
  63. evolver_tools/vendor/wordcount/__main__.py +101 -0
  64. evolver_tools-1.4.0.dist-info/METADATA +107 -0
  65. evolver_tools-1.4.0.dist-info/RECORD +69 -0
  66. evolver_tools-1.4.0.dist-info/WHEEL +5 -0
  67. evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
  68. evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
  69. 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,2 @@
1
+ from . import main
2
+ main()
@@ -0,0 +1,3 @@
1
+ from evolver_tools.vendor.ipinfo.__main__ import main
2
+
3
+ __all__ = ['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,5 @@
1
+ """jq-lite entry point."""
2
+ from . import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,3 @@
1
+ from evolver_tools.vendor.json2csv.__main__ import main
2
+
3
+ __all__ = ['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()