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.
Files changed (47) hide show
  1. jsonseek/__init__.py +3 -0
  2. jsonseek/cli.py +162 -0
  3. jsonseek/commands/__init__.py +14 -0
  4. jsonseek/commands/add_cmd.py +148 -0
  5. jsonseek/commands/append_cmd.py +116 -0
  6. jsonseek/commands/concat_cmd.py +73 -0
  7. jsonseek/commands/cutline_cmd.py +45 -0
  8. jsonseek/commands/del_cmd.py +182 -0
  9. jsonseek/commands/extend_cmd.py +80 -0
  10. jsonseek/commands/extract_cmd.py +57 -0
  11. jsonseek/commands/fields_cmd.py +59 -0
  12. jsonseek/commands/get_cmd.py +42 -0
  13. jsonseek/commands/ls_cmd.py +45 -0
  14. jsonseek/commands/query_cmd.py +58 -0
  15. jsonseek/commands/replaceline_cmd.py +57 -0
  16. jsonseek/commands/set_cmd.py +153 -0
  17. jsonseek/commands/shape_cmd.py +146 -0
  18. jsonseek/detect.py +57 -0
  19. jsonseek/errors.py +23 -0
  20. jsonseek/formatters.py +260 -0
  21. jsonseek/io/__init__.py +0 -0
  22. jsonseek/io/encoding.py +83 -0
  23. jsonseek/io/json_file.py +122 -0
  24. jsonseek/io/jsonl_file.py +154 -0
  25. jsonseek/io/rewrite.py +60 -0
  26. jsonseek/patch/__init__.py +0 -0
  27. jsonseek/patch/array_ops.py +22 -0
  28. jsonseek/patch/locator.py +101 -0
  29. jsonseek/patch/object_ops.py +24 -0
  30. jsonseek/path_parser.py +136 -0
  31. jsonseek/types.py +65 -0
  32. jsonseek/value_utils.py +98 -0
  33. jsonseek/walkers/__init__.py +0 -0
  34. jsonseek/walkers/field_scan.py +59 -0
  35. jsonseek/walkers/query_scan.py +140 -0
  36. jsonseek/walkers/tree_walk.py +48 -0
  37. jsonseek-0.1.0.data/data/docs/README.md +557 -0
  38. jsonseek-0.1.0.data/data/docs/README_ZH.md +556 -0
  39. jsonseek-0.1.0.data/data/skills/jsonseek/SKILL.md +285 -0
  40. jsonseek-0.1.0.data/data/skills/jsonseek/commands.md +221 -0
  41. jsonseek-0.1.0.data/data/skills/jsonseek/path-syntax.md +67 -0
  42. jsonseek-0.1.0.dist-info/METADATA +588 -0
  43. jsonseek-0.1.0.dist-info/RECORD +47 -0
  44. jsonseek-0.1.0.dist-info/WHEEL +5 -0
  45. jsonseek-0.1.0.dist-info/entry_points.txt +2 -0
  46. jsonseek-0.1.0.dist-info/licenses/LICENSE +21 -0
  47. jsonseek-0.1.0.dist-info/top_level.txt +1 -0
jsonseek/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """jsonseek - Query and patch JSON/JSONL files from the command line."""
2
+
3
+ __version__ = "0.1.0"
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}")