docsif 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.
- docsif-0.1.0.dist-info/METADATA +393 -0
- docsif-0.1.0.dist-info/RECORD +86 -0
- docsif-0.1.0.dist-info/WHEEL +4 -0
- docsif-0.1.0.dist-info/entry_points.txt +3 -0
- docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
- sif/__init__.py +24 -0
- sif/_version.py +3 -0
- sif/cli/__init__.py +11 -0
- sif/cli/commands/__init__.py +18 -0
- sif/cli/commands/bench.py +148 -0
- sif/cli/commands/collection.py +360 -0
- sif/cli/commands/context.py +154 -0
- sif/cli/commands/get.py +200 -0
- sif/cli/commands/index.py +294 -0
- sif/cli/commands/ls.py +67 -0
- sif/cli/commands/mcp.py +93 -0
- sif/cli/commands/pull.py +111 -0
- sif/cli/commands/search.py +574 -0
- sif/cli/config.py +206 -0
- sif/cli/formatters.py +269 -0
- sif/cli/main.py +205 -0
- sif/config/__init__.py +30 -0
- sif/config/constants.py +52 -0
- sif/config/settings.py +217 -0
- sif/core/__init__.py +21 -0
- sif/core/collection.py +161 -0
- sif/core/context.py +167 -0
- sif/core/document.py +125 -0
- sif/core/models.py +298 -0
- sif/database/__init__.py +23 -0
- sif/database/connection.py +139 -0
- sif/database/database.py +136 -0
- sif/database/migrations.py +187 -0
- sif/database/repositories.py +500 -0
- sif/database/repository.py +171 -0
- sif/database/schema.py +362 -0
- sif/embedding/__init__.py +22 -0
- sif/embedding/cache.py +196 -0
- sif/embedding/embedder.py +328 -0
- sif/embedding/factory.py +92 -0
- sif/embedding/manager.py +200 -0
- sif/embedding/model.py +126 -0
- sif/indexing/__init__.py +21 -0
- sif/indexing/chunker.py +226 -0
- sif/indexing/indexer.py +226 -0
- sif/indexing/parser.py +192 -0
- sif/indexing/scanner.py +148 -0
- sif/indexing/watcher.py +156 -0
- sif/mcp/README.md +258 -0
- sif/mcp/__init__.py +9 -0
- sif/mcp/claude_desktop_config.json +16 -0
- sif/mcp/cli.py +92 -0
- sif/mcp/protocol.py +336 -0
- sif/mcp/server.py +433 -0
- sif/mcp/server_http.py +54 -0
- sif/mcp/test_mcp.py +108 -0
- sif/mcp/tools.py +497 -0
- sif/mcp/transport_http.py +424 -0
- sif/mcp/transport_stdio.py +292 -0
- sif/mcp_server/__init__.py +26 -0
- sif/mcp_server/handlers.py +68 -0
- sif/mcp_server/server.py +58 -0
- sif/mcp_server/tools.py +216 -0
- sif/mcp_server/transport.py +221 -0
- sif/models/__init__.py +70 -0
- sif/models/collection.py +51 -0
- sif/models/context.py +45 -0
- sif/models/document.py +76 -0
- sif/models/download.py +208 -0
- sif/models/embedding.py +71 -0
- sif/models/search.py +92 -0
- sif/search/__init__.py +22 -0
- sif/search/benchmark.py +110 -0
- sif/search/bm25.py +216 -0
- sif/search/expansion.py +157 -0
- sif/search/hybrid.py +323 -0
- sif/search/rerank.py +390 -0
- sif/search/rrf.py +186 -0
- sif/search/snippets.py +130 -0
- sif/search/strategy.py +97 -0
- sif/search/vector.py +177 -0
- sif/utils/__init__.py +26 -0
- sif/utils/logging.py +143 -0
- sif/utils/paths.py +82 -0
- sif/utils/progress.py +83 -0
- sif/utils/text.py +128 -0
sif/indexing/parser.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Document parsing utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import frontmatter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ParsedDocument:
|
|
16
|
+
"""Result of parsing a document."""
|
|
17
|
+
|
|
18
|
+
path: str
|
|
19
|
+
content: str
|
|
20
|
+
title: str
|
|
21
|
+
metadata: dict[str, Any]
|
|
22
|
+
checksum: str
|
|
23
|
+
mtime: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MarkdownParser:
|
|
27
|
+
"""Parser for Markdown documents."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self.title_pattern = re.compile(r"^#\s+(.+)$", re.MULTILINE)
|
|
31
|
+
|
|
32
|
+
def parse(self, file_path: Path) -> ParsedDocument:
|
|
33
|
+
"""Parse a markdown file.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
file_path: Path to the markdown file
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Parsed document
|
|
40
|
+
"""
|
|
41
|
+
# Read file
|
|
42
|
+
content = file_path.read_text(encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
# Get file stats
|
|
45
|
+
stat = file_path.stat()
|
|
46
|
+
mtime = stat.st_mtime
|
|
47
|
+
|
|
48
|
+
# Parse frontmatter
|
|
49
|
+
try:
|
|
50
|
+
post = frontmatter.loads(content)
|
|
51
|
+
body = post.content
|
|
52
|
+
metadata = dict(post.metadata)
|
|
53
|
+
except Exception:
|
|
54
|
+
body = content
|
|
55
|
+
metadata = {}
|
|
56
|
+
|
|
57
|
+
# Extract title
|
|
58
|
+
title = metadata.get("title", "")
|
|
59
|
+
if not title:
|
|
60
|
+
# Try to extract from first H1
|
|
61
|
+
match = self.title_pattern.search(body)
|
|
62
|
+
title = match.group(1).strip() if match else file_path.stem
|
|
63
|
+
|
|
64
|
+
# Calculate checksum
|
|
65
|
+
checksum = hashlib.sha256(content.encode()).hexdigest()
|
|
66
|
+
|
|
67
|
+
return ParsedDocument(
|
|
68
|
+
path=str(file_path.resolve()),
|
|
69
|
+
content=body,
|
|
70
|
+
title=title,
|
|
71
|
+
metadata=metadata,
|
|
72
|
+
checksum=checksum,
|
|
73
|
+
mtime=mtime,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TextParser:
|
|
78
|
+
"""Parser for plain text documents."""
|
|
79
|
+
|
|
80
|
+
def parse(self, file_path: Path) -> ParsedDocument:
|
|
81
|
+
"""Parse a text file."""
|
|
82
|
+
content = file_path.read_text(encoding="utf-8")
|
|
83
|
+
stat = file_path.stat()
|
|
84
|
+
|
|
85
|
+
# Use first line as title if not too long
|
|
86
|
+
lines = content.split("\n")
|
|
87
|
+
title = ""
|
|
88
|
+
for line in lines:
|
|
89
|
+
stripped = line.strip()
|
|
90
|
+
if stripped and len(stripped) < 100:
|
|
91
|
+
title = stripped
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if not title:
|
|
95
|
+
title = file_path.stem
|
|
96
|
+
|
|
97
|
+
checksum = hashlib.sha256(content.encode()).hexdigest()
|
|
98
|
+
|
|
99
|
+
return ParsedDocument(
|
|
100
|
+
path=str(file_path.resolve()),
|
|
101
|
+
content=content,
|
|
102
|
+
title=title,
|
|
103
|
+
metadata={},
|
|
104
|
+
checksum=checksum,
|
|
105
|
+
mtime=stat.st_mtime,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CodeParser:
|
|
110
|
+
"""Parser for code files."""
|
|
111
|
+
|
|
112
|
+
# File extensions to language mapping
|
|
113
|
+
LANGUAGE_MAP = {
|
|
114
|
+
".py": "python",
|
|
115
|
+
".js": "javascript",
|
|
116
|
+
".ts": "typescript",
|
|
117
|
+
".jsx": "jsx",
|
|
118
|
+
".tsx": "tsx",
|
|
119
|
+
".rs": "rust",
|
|
120
|
+
".go": "go",
|
|
121
|
+
".java": "java",
|
|
122
|
+
".cpp": "cpp",
|
|
123
|
+
".c": "c",
|
|
124
|
+
".h": "c",
|
|
125
|
+
".hpp": "cpp",
|
|
126
|
+
".rb": "ruby",
|
|
127
|
+
".php": "php",
|
|
128
|
+
".swift": "swift",
|
|
129
|
+
".kt": "kotlin",
|
|
130
|
+
".scala": "scala",
|
|
131
|
+
".r": "r",
|
|
132
|
+
".sh": "bash",
|
|
133
|
+
".yaml": "yaml",
|
|
134
|
+
".yml": "yaml",
|
|
135
|
+
".json": "json",
|
|
136
|
+
".toml": "toml",
|
|
137
|
+
".xml": "xml",
|
|
138
|
+
".sql": "sql",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def parse(self, file_path: Path) -> ParsedDocument:
|
|
142
|
+
"""Parse a code file."""
|
|
143
|
+
content = file_path.read_text(encoding="utf-8")
|
|
144
|
+
stat = file_path.stat()
|
|
145
|
+
|
|
146
|
+
# Detect language
|
|
147
|
+
ext = file_path.suffix.lower()
|
|
148
|
+
language = self.LANGUAGE_MAP.get(ext, "text")
|
|
149
|
+
|
|
150
|
+
# Try to extract title from comments
|
|
151
|
+
title = file_path.stem
|
|
152
|
+
|
|
153
|
+
# For Python, look for module docstring
|
|
154
|
+
if language == "python":
|
|
155
|
+
match = re.search(r'^[\'"]{3}(.+?)[\'"]{3}', content, re.MULTILINE | re.DOTALL)
|
|
156
|
+
if match:
|
|
157
|
+
docstring = match.group(1).strip()
|
|
158
|
+
first_line = docstring.split("\n")[0].strip()
|
|
159
|
+
if first_line:
|
|
160
|
+
title = first_line[:100]
|
|
161
|
+
|
|
162
|
+
checksum = hashlib.sha256(content.encode()).hexdigest()
|
|
163
|
+
|
|
164
|
+
return ParsedDocument(
|
|
165
|
+
path=str(file_path.resolve()),
|
|
166
|
+
content=content,
|
|
167
|
+
title=title,
|
|
168
|
+
metadata={"language": language},
|
|
169
|
+
checksum=checksum,
|
|
170
|
+
mtime=stat.st_mtime,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def create_parser(file_path: Path) -> MarkdownParser | TextParser | CodeParser | None:
|
|
175
|
+
"""Create appropriate parser for a file.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
file_path: Path to the file
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Parser instance or None
|
|
182
|
+
"""
|
|
183
|
+
ext = file_path.suffix.lower()
|
|
184
|
+
|
|
185
|
+
if ext in [".md", ".markdown", ".mdx"]:
|
|
186
|
+
return MarkdownParser()
|
|
187
|
+
if ext in CodeParser.LANGUAGE_MAP:
|
|
188
|
+
return CodeParser()
|
|
189
|
+
if ext in [".txt", ".rst", ".adoc"]:
|
|
190
|
+
return TextParser()
|
|
191
|
+
|
|
192
|
+
return None
|
sif/indexing/scanner.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""File scanning utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ScanResult:
|
|
12
|
+
"""Result of scanning a directory."""
|
|
13
|
+
|
|
14
|
+
files: list[Path] = field(default_factory=list)
|
|
15
|
+
scanned_dirs: set[Path] = field(default_factory=set)
|
|
16
|
+
file_count: int = 0
|
|
17
|
+
|
|
18
|
+
def __post_init__(self):
|
|
19
|
+
self.file_count = len(self.files)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FileScanner:
|
|
23
|
+
"""Scanner for finding files matching patterns."""
|
|
24
|
+
|
|
25
|
+
DEFAULT_IGNORE_PATTERNS = [
|
|
26
|
+
"node_modules",
|
|
27
|
+
".git",
|
|
28
|
+
".svn",
|
|
29
|
+
".hg",
|
|
30
|
+
"__pycache__",
|
|
31
|
+
".pytest_cache",
|
|
32
|
+
".mypy_cache",
|
|
33
|
+
".tox",
|
|
34
|
+
".venv",
|
|
35
|
+
"venv",
|
|
36
|
+
"env",
|
|
37
|
+
".env",
|
|
38
|
+
"dist",
|
|
39
|
+
"build",
|
|
40
|
+
"*.egg-info",
|
|
41
|
+
".DS_Store",
|
|
42
|
+
"Thumbs.db",
|
|
43
|
+
"*.swp",
|
|
44
|
+
"*.swo",
|
|
45
|
+
"*~",
|
|
46
|
+
".idea",
|
|
47
|
+
".vscode",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def scan(
|
|
54
|
+
self,
|
|
55
|
+
root_path: Path,
|
|
56
|
+
pattern: str = "**/*.md",
|
|
57
|
+
ignore_patterns: list[str] | None = None,
|
|
58
|
+
) -> ScanResult:
|
|
59
|
+
"""Scan a directory for files matching a pattern.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
root_path: Root directory to scan
|
|
63
|
+
pattern: Glob pattern to match files
|
|
64
|
+
ignore_patterns: Patterns to ignore
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Scan result with matching files
|
|
68
|
+
"""
|
|
69
|
+
root_path = Path(root_path).resolve()
|
|
70
|
+
|
|
71
|
+
# Combine default and custom ignore patterns
|
|
72
|
+
all_ignore_patterns = list(self.DEFAULT_IGNORE_PATTERNS)
|
|
73
|
+
if ignore_patterns:
|
|
74
|
+
all_ignore_patterns.extend(ignore_patterns)
|
|
75
|
+
|
|
76
|
+
result = ScanResult()
|
|
77
|
+
result.scanned_dirs.add(root_path)
|
|
78
|
+
|
|
79
|
+
# Use rglob for recursive matching
|
|
80
|
+
for file_path in root_path.rglob(pattern):
|
|
81
|
+
if not file_path.is_file():
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Check if file should be ignored
|
|
85
|
+
if self._should_ignore(file_path, root_path, all_ignore_patterns):
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
result.files.append(file_path)
|
|
89
|
+
|
|
90
|
+
result.file_count = len(result.files)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def _should_ignore(
|
|
94
|
+
self,
|
|
95
|
+
file_path: Path,
|
|
96
|
+
root_path: Path,
|
|
97
|
+
ignore_patterns: list[str],
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""Check if a file should be ignored.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
file_path: Path to check
|
|
103
|
+
root_path: Root directory
|
|
104
|
+
ignore_patterns: Patterns to ignore
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if file should be ignored
|
|
108
|
+
"""
|
|
109
|
+
# Get relative path from root
|
|
110
|
+
try:
|
|
111
|
+
rel_path = file_path.relative_to(root_path)
|
|
112
|
+
except ValueError:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Check each part of the path against ignore patterns
|
|
116
|
+
for part in rel_path.parts:
|
|
117
|
+
for pattern in ignore_patterns:
|
|
118
|
+
if fnmatch.fnmatch(part, pattern):
|
|
119
|
+
return True
|
|
120
|
+
if fnmatch.fnmatch(str(rel_path), pattern):
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
# Check filename
|
|
124
|
+
return any(fnmatch.fnmatch(file_path.name, pattern) for pattern in ignore_patterns)
|
|
125
|
+
|
|
126
|
+
def get_stats(self, scan_result: ScanResult) -> dict:
|
|
127
|
+
"""Get statistics about a scan result.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
scan_result: Scan result
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Statistics dictionary
|
|
134
|
+
"""
|
|
135
|
+
total_size = sum(f.stat().st_size for f in scan_result.files if f.exists())
|
|
136
|
+
|
|
137
|
+
# Count by extension
|
|
138
|
+
extensions = {}
|
|
139
|
+
for f in scan_result.files:
|
|
140
|
+
ext = f.suffix.lower() or "(no extension)"
|
|
141
|
+
extensions[ext] = extensions.get(ext, 0) + 1
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"file_count": scan_result.file_count,
|
|
145
|
+
"total_size_bytes": total_size,
|
|
146
|
+
"total_size_mb": round(total_size / (1024 * 1024), 2),
|
|
147
|
+
"extensions": extensions,
|
|
148
|
+
}
|
sif/indexing/watcher.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""File system watcher for auto-indexing."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
6
|
+
from watchdog.observers import Observer
|
|
7
|
+
|
|
8
|
+
from sif.utils.logging import get_logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IndexingEventHandler(FileSystemEventHandler):
|
|
15
|
+
"""Event handler for file system changes."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
collection_id: str,
|
|
20
|
+
indexer: "DocumentIndexer", # noqa: F821
|
|
21
|
+
extensions: set[str] | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize event handler.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
collection_id: Collection ID
|
|
27
|
+
indexer: Document indexer
|
|
28
|
+
extensions: File extensions to watch
|
|
29
|
+
"""
|
|
30
|
+
self._collection_id = collection_id
|
|
31
|
+
self._indexer = indexer
|
|
32
|
+
self._extensions = extensions or {".md", ".markdown"}
|
|
33
|
+
|
|
34
|
+
def on_created(self, event: FileSystemEvent) -> None:
|
|
35
|
+
"""Handle file creation."""
|
|
36
|
+
if event.is_directory:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if self._should_handle(event.src_path):
|
|
40
|
+
logger.info(f"File created: {event.src_path}")
|
|
41
|
+
# Trigger incremental index
|
|
42
|
+
|
|
43
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
|
44
|
+
"""Handle file modification."""
|
|
45
|
+
if event.is_directory:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if self._should_handle(event.src_path):
|
|
49
|
+
logger.info(f"File modified: {event.src_path}")
|
|
50
|
+
# Trigger incremental index
|
|
51
|
+
|
|
52
|
+
def on_deleted(self, event: FileSystemEvent) -> None:
|
|
53
|
+
"""Handle file deletion."""
|
|
54
|
+
if event.is_directory:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if self._should_handle(event.src_path):
|
|
58
|
+
logger.info(f"File deleted: {event.src_path}")
|
|
59
|
+
# Trigger removal from index
|
|
60
|
+
|
|
61
|
+
def on_moved(self, event: FileSystemEvent) -> None:
|
|
62
|
+
"""Handle file move/rename."""
|
|
63
|
+
if event.is_directory:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if self._should_handle(event.src_path):
|
|
67
|
+
logger.info(f"File moved: {event.src_path} -> {event.dest_path}")
|
|
68
|
+
# Trigger update
|
|
69
|
+
|
|
70
|
+
def _should_handle(self, path: str) -> bool:
|
|
71
|
+
"""Check if a path should be handled.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
path: File path
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if path should be handled
|
|
78
|
+
"""
|
|
79
|
+
return any(path.endswith(ext) for ext in self._extensions)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class FileWatcher:
|
|
83
|
+
"""File system watcher for auto-indexing.
|
|
84
|
+
|
|
85
|
+
Watches directories for changes and triggers incremental
|
|
86
|
+
indexing when files are created, modified, or deleted.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
indexer: "DocumentIndexer", # noqa: F821
|
|
92
|
+
extensions: set[str] | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Initialize file watcher.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
indexer: Document indexer
|
|
98
|
+
extensions: File extensions to watch
|
|
99
|
+
"""
|
|
100
|
+
self._indexer = indexer
|
|
101
|
+
self._extensions = extensions or {".md", ".markdown"}
|
|
102
|
+
self._observer: Observer | None = None
|
|
103
|
+
self._handlers: dict[str, IndexingEventHandler] = {}
|
|
104
|
+
|
|
105
|
+
def start(self, collection_id: str, paths: list[str]) -> None:
|
|
106
|
+
"""Start watching paths.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
collection_id: Collection ID
|
|
110
|
+
paths: Paths to watch
|
|
111
|
+
"""
|
|
112
|
+
logger.info(f"Starting file watcher for collection: {collection_id}")
|
|
113
|
+
|
|
114
|
+
self._observer = Observer()
|
|
115
|
+
|
|
116
|
+
handler = IndexingEventHandler(
|
|
117
|
+
collection_id=collection_id,
|
|
118
|
+
indexer=self._indexer,
|
|
119
|
+
extensions=self._extensions,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
for path in paths:
|
|
123
|
+
if Path(path).exists():
|
|
124
|
+
self._observer.schedule(handler, path, recursive=True)
|
|
125
|
+
logger.info(f"Watching: {path}")
|
|
126
|
+
|
|
127
|
+
self._observer.start()
|
|
128
|
+
self._handlers[collection_id] = handler
|
|
129
|
+
|
|
130
|
+
def stop(self, collection_id: str | None = None) -> None:
|
|
131
|
+
"""Stop watching.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
collection_id: Collection to stop watching (None for all)
|
|
135
|
+
"""
|
|
136
|
+
if collection_id and collection_id in self._handlers:
|
|
137
|
+
del self._handlers[collection_id]
|
|
138
|
+
|
|
139
|
+
if not collection_id or not self._handlers:
|
|
140
|
+
if self._observer:
|
|
141
|
+
self._observer.stop()
|
|
142
|
+
self._observer.join()
|
|
143
|
+
self._observer = None
|
|
144
|
+
|
|
145
|
+
logger.info("File watcher stopped")
|
|
146
|
+
|
|
147
|
+
def is_watching(self, collection_id: str) -> bool:
|
|
148
|
+
"""Check if a collection is being watched.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
collection_id: Collection ID
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if collection is being watched
|
|
155
|
+
"""
|
|
156
|
+
return collection_id in self._handlers
|