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 ADDED
@@ -0,0 +1,4 @@
1
+ """ragjson - 遵循RAG-Storage-Spec的知识库构建工具"""
2
+
3
+ __version__ = "0.1.0"
4
+ RAGJSON_VERSION = "2.0"
ragjson/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m ragjson"""
2
+
3
+ from ragjson.cli import main
4
+
5
+ main()
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()