jsonseek 0.1.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.
- jsonseek/__init__.py +3 -0
- jsonseek/cli.py +162 -0
- jsonseek/commands/__init__.py +14 -0
- jsonseek/commands/add_cmd.py +148 -0
- jsonseek/commands/append_cmd.py +116 -0
- jsonseek/commands/concat_cmd.py +73 -0
- jsonseek/commands/cutline_cmd.py +45 -0
- jsonseek/commands/del_cmd.py +182 -0
- jsonseek/commands/extend_cmd.py +80 -0
- jsonseek/commands/extract_cmd.py +57 -0
- jsonseek/commands/fields_cmd.py +59 -0
- jsonseek/commands/get_cmd.py +42 -0
- jsonseek/commands/ls_cmd.py +45 -0
- jsonseek/commands/query_cmd.py +58 -0
- jsonseek/commands/replaceline_cmd.py +57 -0
- jsonseek/commands/set_cmd.py +153 -0
- jsonseek/commands/shape_cmd.py +146 -0
- jsonseek/detect.py +57 -0
- jsonseek/errors.py +23 -0
- jsonseek/formatters.py +260 -0
- jsonseek/io/__init__.py +0 -0
- jsonseek/io/encoding.py +83 -0
- jsonseek/io/json_file.py +122 -0
- jsonseek/io/jsonl_file.py +154 -0
- jsonseek/io/rewrite.py +60 -0
- jsonseek/patch/__init__.py +0 -0
- jsonseek/patch/array_ops.py +22 -0
- jsonseek/patch/locator.py +101 -0
- jsonseek/patch/object_ops.py +24 -0
- jsonseek/path_parser.py +136 -0
- jsonseek/types.py +65 -0
- jsonseek/value_utils.py +98 -0
- jsonseek/walkers/__init__.py +0 -0
- jsonseek/walkers/field_scan.py +59 -0
- jsonseek/walkers/query_scan.py +140 -0
- jsonseek/walkers/tree_walk.py +48 -0
- jsonseek-0.1.0.data/data/docs/README.md +557 -0
- jsonseek-0.1.0.data/data/docs/README_ZH.md +556 -0
- jsonseek-0.1.0.data/data/skills/jsonseek/SKILL.md +285 -0
- jsonseek-0.1.0.data/data/skills/jsonseek/commands.md +221 -0
- jsonseek-0.1.0.data/data/skills/jsonseek/path-syntax.md +67 -0
- jsonseek-0.1.0.dist-info/METADATA +588 -0
- jsonseek-0.1.0.dist-info/RECORD +47 -0
- jsonseek-0.1.0.dist-info/WHEEL +5 -0
- jsonseek-0.1.0.dist-info/entry_points.txt +2 -0
- jsonseek-0.1.0.dist-info/licenses/LICENSE +21 -0
- jsonseek-0.1.0.dist-info/top_level.txt +1 -0
jsonseek/__init__.py
ADDED
jsonseek/cli.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any, List, Optional
|
|
4
|
+
|
|
5
|
+
from . import commands
|
|
6
|
+
from .detect import detect_file_kind
|
|
7
|
+
from .errors import JsonseekError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
11
|
+
parser = argparse.ArgumentParser(prog="jsonseek", description="Query and patch JSON/JSONL files from the command line.")
|
|
12
|
+
parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
|
|
13
|
+
|
|
14
|
+
sub = parser.add_subparsers(dest="command", help="Available commands")
|
|
15
|
+
|
|
16
|
+
# Common arguments helper
|
|
17
|
+
def add_common(p: argparse.ArgumentParser) -> None:
|
|
18
|
+
p.add_argument("file", help="Target JSON or JSONL file")
|
|
19
|
+
p.add_argument("--kind", choices=["json", "jsonl"], default=None, help="Force file kind (auto-detect by default)")
|
|
20
|
+
p.add_argument("--output", choices=["pretty", "json"], default="pretty", help="Output format")
|
|
21
|
+
p.add_argument("--backup", action="store_true", default=False, help="Create .bak backup before writing")
|
|
22
|
+
p.add_argument("--encoding", default=None, help="File encoding (auto-detect by default)")
|
|
23
|
+
p.add_argument("--dry-run", action="store_true", default=False, help="Preview changes without applying")
|
|
24
|
+
p.add_argument("--context", type=int, default=2, help="Lines of context around target line (JSONL only)")
|
|
25
|
+
|
|
26
|
+
# shape
|
|
27
|
+
shape_p = sub.add_parser("shape", help="Show structure/shape of the file")
|
|
28
|
+
add_common(shape_p)
|
|
29
|
+
shape_p.add_argument("--max-depth", type=int, default=None, help="Maximum depth to traverse")
|
|
30
|
+
shape_p.add_argument("--array-mode", choices=["sample", "full"], default="sample", help="Array traversal mode")
|
|
31
|
+
shape_p.add_argument("--sample-size", type=int, default=100, help="Number of records to sample for JSONL")
|
|
32
|
+
|
|
33
|
+
# fields
|
|
34
|
+
fields_p = sub.add_parser("fields", help="List fields and their types")
|
|
35
|
+
add_common(fields_p)
|
|
36
|
+
fields_p.add_argument("keyword", nargs="?", default=None, help="Filter fields by keyword")
|
|
37
|
+
fields_p.add_argument("--top", action="store_true", default=False, help="Show only top-level fields")
|
|
38
|
+
|
|
39
|
+
# ls
|
|
40
|
+
ls_p = sub.add_parser("ls", help="List children at a path")
|
|
41
|
+
add_common(ls_p)
|
|
42
|
+
ls_p.add_argument("path", nargs="?", default="", help="Path to list (default: root)")
|
|
43
|
+
|
|
44
|
+
# get
|
|
45
|
+
get_p = sub.add_parser("get", help="Get value at a path")
|
|
46
|
+
add_common(get_p)
|
|
47
|
+
get_p.add_argument("path", help="Path to retrieve")
|
|
48
|
+
|
|
49
|
+
# query
|
|
50
|
+
query_p = sub.add_parser("query", help="Search for keys or values")
|
|
51
|
+
add_common(query_p)
|
|
52
|
+
query_p.add_argument("term", help="Search term")
|
|
53
|
+
query_p.add_argument("--case-sensitive", action="store_true", default=False, help="Case-sensitive matching")
|
|
54
|
+
query_p.add_argument("--exact", action="store_true", default=False, help="Exact match only")
|
|
55
|
+
query_p.add_argument("--match-mode", choices=["key", "value", "both"], default="both", help="What to match against")
|
|
56
|
+
query_p.add_argument("--max-results", type=int, default=None, help="Limit number of results")
|
|
57
|
+
query_p.add_argument("--record-id-field", default=None, help="Field to use as record ID in JSONL output")
|
|
58
|
+
query_p.add_argument("--preview-field", default=None, help="Field to preview in JSONL output")
|
|
59
|
+
|
|
60
|
+
# add
|
|
61
|
+
add_p = sub.add_parser("add", help="Add a key/value to an object")
|
|
62
|
+
add_common(add_p)
|
|
63
|
+
add_p.add_argument("path", help="Target path")
|
|
64
|
+
add_p.add_argument("value", nargs="?", default=None, help="Value to add (JSON string or literal)")
|
|
65
|
+
add_p.add_argument("--create-missing", action="store_true", default=False, help="Create missing intermediate keys")
|
|
66
|
+
add_p.add_argument("--from-file", default=None, help="Read value from file")
|
|
67
|
+
|
|
68
|
+
# del
|
|
69
|
+
del_p = sub.add_parser("del", help="Delete a key or array element")
|
|
70
|
+
add_common(del_p)
|
|
71
|
+
del_p.add_argument("path", help="Target path to delete")
|
|
72
|
+
del_p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
|
|
73
|
+
|
|
74
|
+
# set
|
|
75
|
+
set_p = sub.add_parser("set", help="Set a value at a path")
|
|
76
|
+
add_common(set_p)
|
|
77
|
+
set_p.add_argument("path", help="Target path")
|
|
78
|
+
set_p.add_argument("value", nargs="?", default=None, help="Value to set (JSON string or literal)")
|
|
79
|
+
set_p.add_argument("--create-missing", action="store_true", default=False, help="Create missing intermediate keys")
|
|
80
|
+
set_p.add_argument("--from-file", default=None, help="Read value from file")
|
|
81
|
+
|
|
82
|
+
# append
|
|
83
|
+
append_p = sub.add_parser("append", help="Append a value to an array or JSONL file")
|
|
84
|
+
add_common(append_p)
|
|
85
|
+
append_p.add_argument("path", help="Target array path (JSON) or value (JSONL)")
|
|
86
|
+
append_p.add_argument("value", nargs="?", default=None, help="Value to append (JSON only)")
|
|
87
|
+
|
|
88
|
+
# extract
|
|
89
|
+
extract_p = sub.add_parser("extract", help="Extract a path from multiple JSON files")
|
|
90
|
+
extract_p.add_argument("pattern", help="Glob pattern to match files (e.g. '*.json' or 'data/*.json')")
|
|
91
|
+
extract_p.add_argument("path", help="Path to extract from each file")
|
|
92
|
+
extract_p.add_argument("--output", choices=["pretty", "json"], default="pretty", help="Output format")
|
|
93
|
+
extract_p.add_argument("--kind", choices=["json", "jsonl"], default=None, help="Force file kind")
|
|
94
|
+
extract_p.add_argument("--include-missing", action="store_true", default=False, help="Include files where path is missing")
|
|
95
|
+
|
|
96
|
+
# extend
|
|
97
|
+
extend_p = sub.add_parser("extend", help="Extend an array with multiple values (JSON only)")
|
|
98
|
+
add_common(extend_p)
|
|
99
|
+
extend_p.add_argument("path", help="Target array path")
|
|
100
|
+
extend_p.add_argument("value", help="JSON array string to extend with")
|
|
101
|
+
|
|
102
|
+
# concat
|
|
103
|
+
concat_p = sub.add_parser("concat", help="Concatenate multiple JSON files into JSONL")
|
|
104
|
+
concat_p.add_argument("pattern", help="Glob pattern to match JSON files (e.g. '*.json' or 'data/*.json')")
|
|
105
|
+
concat_p.add_argument("--output-file", "-o", default=None, help="Output JSONL file (default: stdout)")
|
|
106
|
+
concat_p.add_argument("--encoding", default=None, help="File encoding (auto-detect by default)")
|
|
107
|
+
concat_p.add_argument("--no-sort", action="store_true", default=False, help="Preserve glob order instead of sorting by filename")
|
|
108
|
+
|
|
109
|
+
# cutline
|
|
110
|
+
cutline_p = sub.add_parser("cutline", help="Extract a specific line from a file")
|
|
111
|
+
cutline_p.add_argument("file", help="Target file")
|
|
112
|
+
cutline_p.add_argument("line", type=int, help="Line number to extract (1-indexed)")
|
|
113
|
+
cutline_p.add_argument("--encoding", default=None, help="File encoding")
|
|
114
|
+
cutline_p.add_argument("--save-temp", action="store_true", help="Save to temp file and return path")
|
|
115
|
+
|
|
116
|
+
# replaceline
|
|
117
|
+
replaceline_p = sub.add_parser("replaceline", help="Replace a line in a file")
|
|
118
|
+
replaceline_p.add_argument("file", help="Target file")
|
|
119
|
+
replaceline_p.add_argument("line", type=int, help="Line number (1-indexed)")
|
|
120
|
+
replaceline_p.add_argument("content", nargs='?', default=None, help="Content to insert")
|
|
121
|
+
replaceline_p.add_argument("--encoding", default=None, help="File encoding")
|
|
122
|
+
replaceline_p.add_argument("--from-file", default=None, help="Read content from file")
|
|
123
|
+
|
|
124
|
+
return parser
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
128
|
+
parser = build_parser()
|
|
129
|
+
args = parser.parse_args(argv)
|
|
130
|
+
if not args.command:
|
|
131
|
+
parser.print_help()
|
|
132
|
+
return 1
|
|
133
|
+
return dispatch_command(args)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def dispatch_command(args: argparse.Namespace) -> int:
|
|
137
|
+
command = args.command
|
|
138
|
+
handlers = {
|
|
139
|
+
"shape": commands.shape_cmd.handle_shape,
|
|
140
|
+
"fields": commands.fields_cmd.handle_fields,
|
|
141
|
+
"ls": commands.ls_cmd.handle_ls,
|
|
142
|
+
"get": commands.get_cmd.handle_get,
|
|
143
|
+
"query": commands.query_cmd.handle_query,
|
|
144
|
+
"add": commands.add_cmd.handle_add,
|
|
145
|
+
"del": commands.del_cmd.handle_del,
|
|
146
|
+
"set": commands.set_cmd.handle_set,
|
|
147
|
+
"append": commands.append_cmd.handle_append,
|
|
148
|
+
"extract": commands.extract_cmd.handle_extract,
|
|
149
|
+
"extend": commands.extend_cmd.handle_extend,
|
|
150
|
+
"concat": commands.concat_cmd.handle_concat,
|
|
151
|
+
"cutline": commands.cutline_cmd.handle_cutline,
|
|
152
|
+
"replaceline": commands.replaceline_cmd.handle_replaceline,
|
|
153
|
+
}
|
|
154
|
+
handler = handlers.get(command)
|
|
155
|
+
if handler is None:
|
|
156
|
+
print(f"Unknown command: {command}", file=sys.stderr)
|
|
157
|
+
return 1
|
|
158
|
+
return handler(args)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
sys.exit(main())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from . import shape_cmd
|
|
2
|
+
from . import fields_cmd
|
|
3
|
+
from . import ls_cmd
|
|
4
|
+
from . import get_cmd
|
|
5
|
+
from . import query_cmd
|
|
6
|
+
from . import add_cmd
|
|
7
|
+
from . import del_cmd
|
|
8
|
+
from . import set_cmd
|
|
9
|
+
from . import append_cmd
|
|
10
|
+
from . import extract_cmd
|
|
11
|
+
from . import extend_cmd
|
|
12
|
+
from . import concat_cmd
|
|
13
|
+
from . import cutline_cmd
|
|
14
|
+
from . import replaceline_cmd
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from typing import Tuple, Any
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
from ..detect import detect_file_kind
|
|
5
|
+
from ..io.json_file import load_json_file, save_json_file
|
|
6
|
+
from ..io.rewrite import rewrite_jsonl_file
|
|
7
|
+
from ..io.jsonl_file import get_jsonl_record_by_index, get_line_context
|
|
8
|
+
from ..path_parser import parse_path
|
|
9
|
+
from ..patch.locator import resolve_parent_and_key, resolve_record_and_inner_path, resolve_value_at_path
|
|
10
|
+
from ..patch.object_ops import apply_add_to_object
|
|
11
|
+
from ..value_utils import coerce_input_value
|
|
12
|
+
from ..formatters import format_patch_preview
|
|
13
|
+
from ..errors import JsonseekError, PatchError, PathError
|
|
14
|
+
from ..types import KeyToken, IndexToken
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def handle_add(args: argparse.Namespace) -> int:
|
|
18
|
+
try:
|
|
19
|
+
is_dry_run = getattr(args, "dry_run", False)
|
|
20
|
+
output = getattr(args, "output", "pretty")
|
|
21
|
+
enc = getattr(args, "encoding", None)
|
|
22
|
+
context = getattr(args, "context", 2)
|
|
23
|
+
|
|
24
|
+
# Get value from file or command line
|
|
25
|
+
if getattr(args, "from_file", None):
|
|
26
|
+
from ..io.encoding import resolve_encoding
|
|
27
|
+
file_enc = args.encoding or resolve_encoding(args.file, None)
|
|
28
|
+
with open(args.from_file, 'r', encoding=file_enc) as f:
|
|
29
|
+
raw = f.read().strip()
|
|
30
|
+
value = coerce_input_value(raw)
|
|
31
|
+
else:
|
|
32
|
+
value = coerce_input_value(args.value)
|
|
33
|
+
|
|
34
|
+
kind = detect_file_kind(args.file, kind_hint=getattr(args, "kind", None))
|
|
35
|
+
|
|
36
|
+
if kind == "jsonl":
|
|
37
|
+
record_index, inner_tokens = resolve_record_and_inner_path(args.path)
|
|
38
|
+
record = get_jsonl_record_by_index(args.file, record_index, encoding=enc)
|
|
39
|
+
target_line = record.line_number
|
|
40
|
+
before_context = get_line_context(args.file, target_line, context=context, encoding=enc)
|
|
41
|
+
|
|
42
|
+
if is_dry_run:
|
|
43
|
+
print(format_patch_preview(
|
|
44
|
+
path=args.path,
|
|
45
|
+
output=output,
|
|
46
|
+
dry_run=True,
|
|
47
|
+
jsonl_before=before_context,
|
|
48
|
+
jsonl_target_line=target_line,
|
|
49
|
+
operation="modified",
|
|
50
|
+
))
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
rewrite_jsonl_file(
|
|
54
|
+
args.file,
|
|
55
|
+
transform_record=lambda rec: patch_jsonl_record_add(
|
|
56
|
+
rec, record_index, inner_tokens, value, create_missing=getattr(args, "create_missing", False)
|
|
57
|
+
),
|
|
58
|
+
backup=getattr(args, "backup", False),
|
|
59
|
+
encoding=enc,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
after_context = get_line_context(args.file, target_line, context=context, encoding=enc)
|
|
63
|
+
print(format_patch_preview(
|
|
64
|
+
path=args.path,
|
|
65
|
+
output=output,
|
|
66
|
+
dry_run=False,
|
|
67
|
+
jsonl_before=before_context,
|
|
68
|
+
jsonl_after=after_context,
|
|
69
|
+
jsonl_target_line=target_line,
|
|
70
|
+
operation="modified",
|
|
71
|
+
))
|
|
72
|
+
else:
|
|
73
|
+
data = load_json_file(args.file, encoding=enc)
|
|
74
|
+
try:
|
|
75
|
+
before_value = resolve_value_at_path(data, parse_path(args.path))
|
|
76
|
+
except PathError:
|
|
77
|
+
before_value = "<not found>"
|
|
78
|
+
|
|
79
|
+
if is_dry_run:
|
|
80
|
+
print(format_patch_preview(
|
|
81
|
+
path=args.path,
|
|
82
|
+
before=before_value,
|
|
83
|
+
after=value,
|
|
84
|
+
output=output,
|
|
85
|
+
dry_run=True,
|
|
86
|
+
))
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
patched = patch_json_add(data, args.path, value, create_missing=getattr(args, "create_missing", False))
|
|
90
|
+
save_json_file(args.file, patched, backup=getattr(args, "backup", False), encoding=enc or "utf-8")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
after_value = resolve_value_at_path(patched, parse_path(args.path))
|
|
94
|
+
except PathError:
|
|
95
|
+
after_value = "<not found>"
|
|
96
|
+
|
|
97
|
+
print(format_patch_preview(
|
|
98
|
+
path=args.path,
|
|
99
|
+
before=before_value,
|
|
100
|
+
after=after_value,
|
|
101
|
+
output=output,
|
|
102
|
+
dry_run=False,
|
|
103
|
+
))
|
|
104
|
+
return 0
|
|
105
|
+
except (JsonseekError, PatchError) as e:
|
|
106
|
+
print(f"Error: {e}", file=__import__("sys").stderr)
|
|
107
|
+
return 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def patch_json_add(data: Any, target_path: str, value: Any, create_missing: bool = False) -> Any:
|
|
111
|
+
tokens = parse_path(target_path)
|
|
112
|
+
parent, last_token = resolve_parent_and_key(data, tokens)
|
|
113
|
+
if isinstance(last_token, KeyToken):
|
|
114
|
+
if not isinstance(parent, dict):
|
|
115
|
+
raise PatchError(f"Expected object parent at {target_path}, got {type(parent).__name__}")
|
|
116
|
+
apply_add_to_object(parent, last_token.key, value, create_missing=create_missing)
|
|
117
|
+
else:
|
|
118
|
+
raise PatchError(f"Cannot add to array by index; use set or append: {target_path}")
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def patch_jsonl_record_add(
|
|
123
|
+
record: Any, target_index: int, inner_tokens: list, value: Any, create_missing: bool = False
|
|
124
|
+
) -> Tuple[bool, Any]:
|
|
125
|
+
from ..types import JsonlRecord
|
|
126
|
+
if not isinstance(record, JsonlRecord):
|
|
127
|
+
raise PatchError("Expected JsonlRecord")
|
|
128
|
+
if record.record_index != target_index:
|
|
129
|
+
return True, record.data
|
|
130
|
+
data = record.data
|
|
131
|
+
if not inner_tokens:
|
|
132
|
+
raise PatchError("Cannot add to root record; specify an inner path")
|
|
133
|
+
parent, last_token = resolve_parent_and_key(data, inner_tokens)
|
|
134
|
+
if isinstance(last_token, KeyToken):
|
|
135
|
+
if not isinstance(parent, dict):
|
|
136
|
+
raise PatchError(f"Expected object parent, got {type(parent).__name__}")
|
|
137
|
+
apply_add_to_object(parent, last_token.key, value, create_missing=create_missing)
|
|
138
|
+
else:
|
|
139
|
+
raise PatchError(f"Cannot add to array by index; use set or append")
|
|
140
|
+
return True, data
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def add_value(path: str, target_path: str, value: Any, create_missing: bool = False, encoding: str = "utf-8") -> None:
|
|
144
|
+
"""Add a value at a path in a JSON file. Python API version."""
|
|
145
|
+
from ..io.json_file import load_json_file, save_json_file
|
|
146
|
+
data = load_json_file(path, encoding=encoding)
|
|
147
|
+
patched = patch_json_add(data, target_path, value, create_missing=create_missing)
|
|
148
|
+
save_json_file(path, patched, backup=False, encoding=encoding)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
from ..detect import detect_file_kind
|
|
5
|
+
from ..io.json_file import load_json_file, save_json_file
|
|
6
|
+
from ..io.jsonl_file import append_jsonl_record, get_line_context
|
|
7
|
+
from ..io.encoding import resolve_encoding
|
|
8
|
+
from ..path_parser import parse_path
|
|
9
|
+
from ..patch.locator import resolve_value_at_path
|
|
10
|
+
from ..patch.array_ops import apply_append_to_array
|
|
11
|
+
from ..value_utils import coerce_input_value
|
|
12
|
+
from ..formatters import format_patch_preview
|
|
13
|
+
from ..errors import JsonseekError, PatchError, PathError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def handle_append(args: argparse.Namespace) -> int:
|
|
17
|
+
try:
|
|
18
|
+
is_dry_run = getattr(args, "dry_run", False)
|
|
19
|
+
output = getattr(args, "output", "pretty")
|
|
20
|
+
enc = getattr(args, "encoding", None)
|
|
21
|
+
context = getattr(args, "context", 2)
|
|
22
|
+
|
|
23
|
+
kind = detect_file_kind(args.file, kind_hint=getattr(args, "kind", None))
|
|
24
|
+
|
|
25
|
+
if kind == "jsonl":
|
|
26
|
+
# JSONL append: args.path is the value when args.value is omitted
|
|
27
|
+
raw_value = args.value if args.value is not None else args.path
|
|
28
|
+
value = coerce_input_value(raw_value)
|
|
29
|
+
|
|
30
|
+
# Get file end context
|
|
31
|
+
detected = resolve_encoding(args.file, enc)
|
|
32
|
+
with open(args.file, 'r', encoding=detected) as f:
|
|
33
|
+
total_lines = sum(1 for _ in f)
|
|
34
|
+
target_line = total_lines if total_lines > 0 else 1
|
|
35
|
+
before_context = get_line_context(args.file, target_line, context=context, encoding=enc)
|
|
36
|
+
|
|
37
|
+
if is_dry_run:
|
|
38
|
+
print(format_patch_preview(
|
|
39
|
+
path=args.file,
|
|
40
|
+
output=output,
|
|
41
|
+
dry_run=True,
|
|
42
|
+
jsonl_before=before_context,
|
|
43
|
+
jsonl_target_line=target_line,
|
|
44
|
+
operation="appended",
|
|
45
|
+
))
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
append_jsonl_record(args.file, value, encoding=enc or "utf-8")
|
|
49
|
+
|
|
50
|
+
after_target_line = total_lines + 1
|
|
51
|
+
after_context = get_line_context(args.file, after_target_line, context=context, encoding=enc)
|
|
52
|
+
print(format_patch_preview(
|
|
53
|
+
path=args.file,
|
|
54
|
+
output=output,
|
|
55
|
+
dry_run=False,
|
|
56
|
+
jsonl_before=before_context,
|
|
57
|
+
jsonl_after=after_context,
|
|
58
|
+
jsonl_target_line=after_target_line,
|
|
59
|
+
operation="appended",
|
|
60
|
+
))
|
|
61
|
+
else:
|
|
62
|
+
if args.value is None:
|
|
63
|
+
raise PatchError("append for JSON requires both path and value arguments")
|
|
64
|
+
value = coerce_input_value(args.value)
|
|
65
|
+
data = load_json_file(args.file, encoding=enc)
|
|
66
|
+
try:
|
|
67
|
+
before_value = resolve_value_at_path(data, parse_path(args.path))
|
|
68
|
+
except PathError:
|
|
69
|
+
before_value = "<not found>"
|
|
70
|
+
|
|
71
|
+
if is_dry_run:
|
|
72
|
+
print(format_patch_preview(
|
|
73
|
+
path=args.path,
|
|
74
|
+
before=before_value,
|
|
75
|
+
after=value,
|
|
76
|
+
output=output,
|
|
77
|
+
dry_run=True,
|
|
78
|
+
))
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
patched = patch_json_append(data, args.path, value)
|
|
82
|
+
save_json_file(args.file, patched, backup=getattr(args, "backup", False), encoding=enc or "utf-8")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
after_value = resolve_value_at_path(patched, parse_path(args.path))
|
|
86
|
+
except PathError:
|
|
87
|
+
after_value = "<not found>"
|
|
88
|
+
|
|
89
|
+
print(format_patch_preview(
|
|
90
|
+
path=args.path,
|
|
91
|
+
before=before_value,
|
|
92
|
+
after=after_value,
|
|
93
|
+
output=output,
|
|
94
|
+
dry_run=False,
|
|
95
|
+
))
|
|
96
|
+
return 0
|
|
97
|
+
except (JsonseekError, PatchError) as e:
|
|
98
|
+
print(f"Error: {e}", file=__import__("sys").stderr)
|
|
99
|
+
return 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def patch_json_append(data: Any, target_path: str, value: Any) -> Any:
|
|
103
|
+
tokens = parse_path(target_path)
|
|
104
|
+
arr = resolve_value_at_path(data, tokens)
|
|
105
|
+
if not isinstance(arr, list):
|
|
106
|
+
raise PatchError(f"Expected array at {target_path}, got {type(arr).__name__}")
|
|
107
|
+
apply_append_to_array(arr, value)
|
|
108
|
+
return data
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def append_value(path: str, target_path: str, value: Any, encoding: str = "utf-8") -> None:
|
|
112
|
+
"""Append a value to an array in a JSON file. Python API version."""
|
|
113
|
+
from ..io.json_file import load_json_file, save_json_file
|
|
114
|
+
data = load_json_file(path, encoding=encoding)
|
|
115
|
+
patched = patch_json_append(data, target_path, value)
|
|
116
|
+
save_json_file(path, patched, backup=False, encoding=encoding)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import glob
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from typing import Any, List
|
|
7
|
+
|
|
8
|
+
from ..errors import JsonseekError
|
|
9
|
+
from ..io.encoding import resolve_encoding
|
|
10
|
+
from ..io.json_file import load_json_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_concat(args: argparse.Namespace) -> int:
|
|
14
|
+
try:
|
|
15
|
+
pattern = args.pattern
|
|
16
|
+
output_file = getattr(args, "output_file", None)
|
|
17
|
+
no_sort = getattr(args, "no_sort", False)
|
|
18
|
+
|
|
19
|
+
# Expand glob pattern
|
|
20
|
+
files = glob.glob(pattern, recursive=True)
|
|
21
|
+
if not no_sort:
|
|
22
|
+
files = sorted(files)
|
|
23
|
+
if not files:
|
|
24
|
+
if os.path.isfile(pattern):
|
|
25
|
+
files = [pattern]
|
|
26
|
+
else:
|
|
27
|
+
print(f"No files matched pattern: {pattern}", file=__import__("sys").stderr)
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
enc = getattr(args, "encoding", None)
|
|
31
|
+
lines: List[str] = []
|
|
32
|
+
|
|
33
|
+
for path in files:
|
|
34
|
+
try:
|
|
35
|
+
data = load_json_file(path, encoding=enc)
|
|
36
|
+
line = json.dumps(data, ensure_ascii=False)
|
|
37
|
+
lines.append(line)
|
|
38
|
+
except JsonseekError as e:
|
|
39
|
+
print(f"Error processing {path}: {e}", file=__import__("sys").stderr)
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
output = "\n".join(lines)
|
|
43
|
+
if output_file:
|
|
44
|
+
_write_file(output_file, output, enc or "utf-8")
|
|
45
|
+
print(f"Wrote {len(lines)} record(s) to {output_file}")
|
|
46
|
+
else:
|
|
47
|
+
print(output)
|
|
48
|
+
|
|
49
|
+
return 0
|
|
50
|
+
except JsonseekError as e:
|
|
51
|
+
print(f"Error: {e}", file=__import__("sys").stderr)
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _write_file(path: str, content: str, encoding: str) -> None:
|
|
56
|
+
"""Write content to a file atomically."""
|
|
57
|
+
temp_path = path + ".tmp"
|
|
58
|
+
try:
|
|
59
|
+
with open(temp_path, "w", encoding=encoding, newline="\n") as f:
|
|
60
|
+
f.write(content)
|
|
61
|
+
f.write("\n")
|
|
62
|
+
try:
|
|
63
|
+
os.replace(temp_path, path)
|
|
64
|
+
except PermissionError:
|
|
65
|
+
if os.name == "nt" and os.path.exists(path):
|
|
66
|
+
os.remove(path)
|
|
67
|
+
os.rename(temp_path, path)
|
|
68
|
+
else:
|
|
69
|
+
raise
|
|
70
|
+
except OSError as e:
|
|
71
|
+
if os.path.exists(temp_path):
|
|
72
|
+
os.remove(temp_path)
|
|
73
|
+
raise JsonseekError(f"Failed to write {path}: {e}")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ..io.encoding import resolve_encoding
|
|
5
|
+
from ..errors import JsonseekError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_cutline(args: argparse.Namespace) -> int:
|
|
9
|
+
try:
|
|
10
|
+
line_number = args.line
|
|
11
|
+
path = args.file
|
|
12
|
+
enc = args.encoding or resolve_encoding(path, None)
|
|
13
|
+
|
|
14
|
+
content = cut_line(path, line_number, enc)
|
|
15
|
+
|
|
16
|
+
if args.save_temp:
|
|
17
|
+
import tempfile
|
|
18
|
+
import os
|
|
19
|
+
suffix = '.jsonline'
|
|
20
|
+
fd, tmp_path = tempfile.mkstemp(suffix=suffix, text=True)
|
|
21
|
+
with os.fdopen(fd, 'w', encoding=enc) as f:
|
|
22
|
+
f.write(content)
|
|
23
|
+
print(tmp_path)
|
|
24
|
+
else:
|
|
25
|
+
print(content)
|
|
26
|
+
return 0
|
|
27
|
+
except JsonseekError as e:
|
|
28
|
+
print(f"Error: {e}", file=__import__("sys").stderr)
|
|
29
|
+
return 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cut_line(path: str, line_number: int, encoding: Optional[str] = None) -> str:
|
|
33
|
+
"""Extract a specific line from a file (1-indexed)."""
|
|
34
|
+
enc = encoding or resolve_encoding(path, None)
|
|
35
|
+
try:
|
|
36
|
+
with open(path, "r", encoding=enc) as f:
|
|
37
|
+
for i, line in enumerate(f, 1):
|
|
38
|
+
if i == line_number:
|
|
39
|
+
return line.rstrip('\n\r')
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
raise JsonseekError(f"File not found: {path}")
|
|
42
|
+
except OSError as e:
|
|
43
|
+
raise JsonseekError(f"Failed to read {path}: {e}")
|
|
44
|
+
|
|
45
|
+
raise JsonseekError(f"Line {line_number} not found in {path}")
|