ragjson 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.
- ragjson/__init__.py +4 -0
- ragjson/__main__.py +5 -0
- ragjson/atomic_ops.py +118 -0
- ragjson/cli.py +178 -0
- ragjson/commands.py +478 -0
- ragjson/config.py +113 -0
- ragjson/doc_parser.py +171 -0
- ragjson/embedding.py +76 -0
- ragjson/ragjson_file.py +262 -0
- ragjson/splitter.py +51 -0
- ragjson/utils.py +63 -0
- ragjson-0.1.0.dist-info/METADATA +259 -0
- ragjson-0.1.0.dist-info/RECORD +15 -0
- ragjson-0.1.0.dist-info/WHEEL +4 -0
- ragjson-0.1.0.dist-info/entry_points.txt +2 -0
ragjson/__init__.py
ADDED
ragjson/__main__.py
ADDED
ragjson/atomic_ops.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Atomic write operations and directory-level file locking.
|
|
2
|
+
|
|
3
|
+
Follows spec Section 3: Atomic Write Protocol.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import fcntl
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DirectoryLock:
|
|
12
|
+
"""Context manager for directory-level exclusive file locking.
|
|
13
|
+
|
|
14
|
+
Uses flock(2) on the .rag/tmp/ directory file descriptor.
|
|
15
|
+
Per spec Section 3.3: same-directory operations are serialized,
|
|
16
|
+
different directories are fully isolated (no global lock).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, rag_dir: str | Path):
|
|
20
|
+
self.rag_dir = Path(rag_dir)
|
|
21
|
+
self.tmp_dir = self.rag_dir / "tmp"
|
|
22
|
+
self._fd: int | None = None
|
|
23
|
+
|
|
24
|
+
def __enter__(self) -> "DirectoryLock":
|
|
25
|
+
# Open the tmp directory for locking
|
|
26
|
+
self._fd = os.open(str(self.tmp_dir), os.O_RDONLY)
|
|
27
|
+
fcntl.flock(self._fd, fcntl.LOCK_EX)
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
31
|
+
if self._fd is not None:
|
|
32
|
+
fcntl.flock(self._fd, fcntl.LOCK_UN)
|
|
33
|
+
os.close(self._fd)
|
|
34
|
+
self._fd = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def atomic_write(rag_dir: str | Path, filename: str, content: str) -> Path:
|
|
38
|
+
"""Atomically write content to a .ragjson file.
|
|
39
|
+
|
|
40
|
+
Follows spec Section 3.1:
|
|
41
|
+
1. Write to .rag/tmp/{filename}.tmp
|
|
42
|
+
2. fsync to flush
|
|
43
|
+
3. os.rename() to .rag/{filename} — atomic on same filesystem
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
rag_dir: Path to the .rag directory
|
|
47
|
+
filename: Target filename (e.g., "readme.md.ragjson")
|
|
48
|
+
content: JSON string content to write
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Path to the final written file
|
|
52
|
+
"""
|
|
53
|
+
rag_dir = Path(rag_dir)
|
|
54
|
+
tmp_dir = rag_dir / "tmp"
|
|
55
|
+
tmp_path = tmp_dir / f"{filename}.tmp"
|
|
56
|
+
final_path = rag_dir / filename
|
|
57
|
+
|
|
58
|
+
# Ensure directories exist
|
|
59
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
# Step 1: Write to temporary file
|
|
62
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
63
|
+
f.write(content)
|
|
64
|
+
# Step 2: fsync to ensure data is on disk
|
|
65
|
+
f.flush()
|
|
66
|
+
os.fsync(f.fileno())
|
|
67
|
+
|
|
68
|
+
# Step 3: Atomic rename
|
|
69
|
+
os.rename(tmp_path, final_path)
|
|
70
|
+
|
|
71
|
+
return final_path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def clean_tmp_files(rag_dir: str | Path) -> list[Path]:
|
|
75
|
+
"""Remove residual .tmp files from .rag/tmp/ directory.
|
|
76
|
+
|
|
77
|
+
Per spec Section 3.2: crash recovery cleans up incomplete writes.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
rag_dir: Path to the .rag directory
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of paths that were cleaned
|
|
84
|
+
"""
|
|
85
|
+
rag_dir = Path(rag_dir)
|
|
86
|
+
tmp_dir = rag_dir / "tmp"
|
|
87
|
+
cleaned = []
|
|
88
|
+
|
|
89
|
+
if not tmp_dir.exists():
|
|
90
|
+
return cleaned
|
|
91
|
+
|
|
92
|
+
for tmp_file in tmp_dir.glob("*.tmp"):
|
|
93
|
+
try:
|
|
94
|
+
tmp_file.unlink()
|
|
95
|
+
cleaned.append(tmp_file)
|
|
96
|
+
except OSError:
|
|
97
|
+
pass # File may already be gone
|
|
98
|
+
|
|
99
|
+
return cleaned
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ensure_rag_dirs(path: str | Path) -> tuple[Path, Path]:
|
|
103
|
+
"""Ensure .rag/ and .rag/tmp/ directories exist.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
path: Source directory to create .rag structure in
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Tuple of (rag_dir, tmp_dir) paths
|
|
110
|
+
"""
|
|
111
|
+
path = Path(path)
|
|
112
|
+
rag_dir = path / ".rag"
|
|
113
|
+
tmp_dir = rag_dir / "tmp"
|
|
114
|
+
|
|
115
|
+
rag_dir.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
return rag_dir, tmp_dir
|
ragjson/cli.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""CLI entry point for the `rag` command.
|
|
2
|
+
|
|
3
|
+
Uses argparse with subcommands per spec Section 6.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from ragjson import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
"""Main entry point for the rag CLI tool."""
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
prog="rag",
|
|
17
|
+
description="RAG 知识库构建工具 — 遵循 RAG-Storage-Spec 规范",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--version",
|
|
21
|
+
action="version",
|
|
22
|
+
version=f"%(prog)s {__version__}",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
26
|
+
|
|
27
|
+
# --- rag init <path> ---
|
|
28
|
+
init_parser = subparsers.add_parser(
|
|
29
|
+
"init",
|
|
30
|
+
help="Initialize .rag/ directory structure at the given path",
|
|
31
|
+
)
|
|
32
|
+
init_parser.add_argument(
|
|
33
|
+
"path",
|
|
34
|
+
help="Directory path to initialize",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# --- rag status [path] [-r] ---
|
|
38
|
+
status_parser = subparsers.add_parser(
|
|
39
|
+
"status",
|
|
40
|
+
help="Show knowledge base status with MD5 comparison",
|
|
41
|
+
)
|
|
42
|
+
status_parser.add_argument(
|
|
43
|
+
"path",
|
|
44
|
+
nargs="?",
|
|
45
|
+
default=".",
|
|
46
|
+
help="Directory path to check (default: current directory)",
|
|
47
|
+
)
|
|
48
|
+
status_parser.add_argument(
|
|
49
|
+
"-r", "--recursive",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Recursively scan subdirectories",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# --- rag update [path] [-r] [-j N] [--splitter TYPE] [--parser TYPE] ---
|
|
55
|
+
update_parser = subparsers.add_parser(
|
|
56
|
+
"update",
|
|
57
|
+
help="Incremental update: process files with changed MD5",
|
|
58
|
+
)
|
|
59
|
+
update_parser.add_argument(
|
|
60
|
+
"path",
|
|
61
|
+
nargs="?",
|
|
62
|
+
default=".",
|
|
63
|
+
help="Directory path to update (default: current directory)",
|
|
64
|
+
)
|
|
65
|
+
update_parser.add_argument(
|
|
66
|
+
"-r", "--recursive",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Recursively process subdirectories",
|
|
69
|
+
)
|
|
70
|
+
update_parser.add_argument(
|
|
71
|
+
"-j", "--jobs",
|
|
72
|
+
type=int,
|
|
73
|
+
default=0,
|
|
74
|
+
help="Number of parallel jobs (default: CPU core count)",
|
|
75
|
+
)
|
|
76
|
+
update_parser.add_argument(
|
|
77
|
+
"--splitter",
|
|
78
|
+
choices=["recursive", "markdown"],
|
|
79
|
+
default="recursive",
|
|
80
|
+
help="Text splitter type (default: recursive)",
|
|
81
|
+
)
|
|
82
|
+
update_parser.add_argument(
|
|
83
|
+
"--parser",
|
|
84
|
+
choices=["markitdown", "mineru"],
|
|
85
|
+
default="markitdown",
|
|
86
|
+
help="Document parser type (default: markitdown)",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# --- rag clean [path] [-r] ---
|
|
90
|
+
clean_parser = subparsers.add_parser(
|
|
91
|
+
"clean",
|
|
92
|
+
help="Clean residual .tmp files from .rag/tmp/",
|
|
93
|
+
)
|
|
94
|
+
clean_parser.add_argument(
|
|
95
|
+
"path",
|
|
96
|
+
nargs="?",
|
|
97
|
+
default=".",
|
|
98
|
+
help="Directory path to clean (default: current directory)",
|
|
99
|
+
)
|
|
100
|
+
clean_parser.add_argument(
|
|
101
|
+
"-r", "--recursive",
|
|
102
|
+
action="store_true",
|
|
103
|
+
help="Recursively clean subdirectories",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# --- rag cleanup [path] [-r] ---
|
|
107
|
+
cleanup_parser = subparsers.add_parser(
|
|
108
|
+
"cleanup",
|
|
109
|
+
help="Physically delete .ragjson files with DELETED status",
|
|
110
|
+
)
|
|
111
|
+
cleanup_parser.add_argument(
|
|
112
|
+
"path",
|
|
113
|
+
nargs="?",
|
|
114
|
+
default=".",
|
|
115
|
+
help="Directory path to cleanup (default: current directory)",
|
|
116
|
+
)
|
|
117
|
+
cleanup_parser.add_argument(
|
|
118
|
+
"-r", "--recursive",
|
|
119
|
+
action="store_true",
|
|
120
|
+
help="Recursively process subdirectories",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# --- rag export <path> <output> ---
|
|
124
|
+
export_parser = subparsers.add_parser(
|
|
125
|
+
"export",
|
|
126
|
+
help="Export all .ragjson files into a tar.gz archive",
|
|
127
|
+
)
|
|
128
|
+
export_parser.add_argument(
|
|
129
|
+
"path",
|
|
130
|
+
help="Source directory path to export from",
|
|
131
|
+
)
|
|
132
|
+
export_parser.add_argument(
|
|
133
|
+
"output",
|
|
134
|
+
help="Output archive path (e.g., export.tar.gz)",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Parse and dispatch
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
|
|
140
|
+
if args.command is None:
|
|
141
|
+
parser.print_help()
|
|
142
|
+
sys.exit(0)
|
|
143
|
+
|
|
144
|
+
# Dispatch to command implementations
|
|
145
|
+
from ragjson.commands import (
|
|
146
|
+
cmd_clean,
|
|
147
|
+
cmd_cleanup,
|
|
148
|
+
cmd_export,
|
|
149
|
+
cmd_init,
|
|
150
|
+
cmd_status,
|
|
151
|
+
cmd_update,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if args.command == "init":
|
|
155
|
+
cmd_init(args.path)
|
|
156
|
+
elif args.command == "status":
|
|
157
|
+
cmd_status(args.path, args.recursive)
|
|
158
|
+
elif args.command == "update":
|
|
159
|
+
cmd_update(
|
|
160
|
+
args.path,
|
|
161
|
+
recursive=args.recursive,
|
|
162
|
+
jobs=args.jobs,
|
|
163
|
+
splitter_type=args.splitter,
|
|
164
|
+
parser_type=args.parser,
|
|
165
|
+
)
|
|
166
|
+
elif args.command == "clean":
|
|
167
|
+
cmd_clean(args.path, args.recursive)
|
|
168
|
+
elif args.command == "cleanup":
|
|
169
|
+
cmd_cleanup(args.path, args.recursive)
|
|
170
|
+
elif args.command == "export":
|
|
171
|
+
cmd_export(args.path, args.output)
|
|
172
|
+
else:
|
|
173
|
+
parser.print_help()
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
main()
|