Files
ankn a5f8fb49ab Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
2026-06-30 20:39:52 +02:00

125 lines
3.9 KiB
Python

"""Drop-folder watcher using watchdog for continuous file ingestion.
Monitors the inbox folder for new files and triggers the processing pipeline.
Moves processed files to archive/ and failed files to failed/ subdirectory.
Ignores temporary files (names starting with '.' or ending with '.tmp').
"""
import logging
import shutil
import time
from pathlib import Path
from watchdog.events import FileCreatedEvent, FileSystemEventHandler
from watchdog.observers import Observer
from ingestion.config import IngestionConfig
from ingestion.discovery import SUPPORTED_EXTENSIONS
logger = logging.getLogger(__name__)
def should_ignore(filename: str) -> bool:
"""Check if a file should be ignored by the watcher.
Ignores files starting with '.' or ending with '.tmp'.
"""
return filename.startswith(".") or filename.endswith(".tmp")
class InboxHandler(FileSystemEventHandler):
"""Handle new files in the inbox directory."""
def __init__(self, config: IngestionConfig):
self.config = config
self.inbox_dir = Path(config.inbox_dir)
self.archive_dir = self.inbox_dir / "archive"
self.failed_dir = self.inbox_dir / "failed"
self.archive_dir.mkdir(parents=True, exist_ok=True)
self.failed_dir.mkdir(parents=True, exist_ok=True)
def on_created(self, event: FileCreatedEvent):
"""Triggered when a new file is created in the inbox."""
if event.is_directory:
return
file_path = Path(event.src_path)
self._process_file(file_path)
def _process_file(self, file_path: Path) -> None:
"""Process a single file and move to archive or failed."""
if should_ignore(file_path.name):
logger.debug("Ignoring temporary file: %s", file_path.name)
return
if file_path.suffix.lower() not in SUPPORTED_EXTENSIONS:
logger.debug("Ignoring unsupported file: %s", file_path.name)
return
# Small delay to ensure file is fully written
time.sleep(0.5)
logger.info("Processing new file: %s", file_path.name)
try:
from ingestion.pipeline import process_file
result = process_file(file_path=file_path, config=self.config)
if result.success:
# Move to archive
dest = self.archive_dir / file_path.name
shutil.move(str(file_path), str(dest))
logger.info("Archived: %s%s", file_path.name, dest)
else:
# Move to failed
dest = self.failed_dir / file_path.name
shutil.move(str(file_path), str(dest))
logger.warning(
"Failed: %s%s (error: %s)",
file_path.name,
dest,
result.error,
)
except Exception as e:
logger.error("Unhandled error processing %s: %s", file_path.name, e)
dest = self.failed_dir / file_path.name
try:
shutil.move(str(file_path), str(dest))
except Exception:
pass
def start_watcher(config: IngestionConfig) -> None:
"""Start the drop-folder watcher.
Processes existing files first, then watches for new ones.
Blocks until interrupted (Ctrl+C).
"""
inbox_dir = Path(config.inbox_dir)
inbox_dir.mkdir(parents=True, exist_ok=True)
handler = InboxHandler(config)
# Process existing files on startup
existing = sorted(inbox_dir.iterdir())
for f in existing:
if f.is_file() and not should_ignore(f.name):
handler._process_file(f)
# Start watching
observer = Observer()
observer.schedule(handler, str(inbox_dir), recursive=False)
observer.start()
logger.info("Watcher started on: %s", inbox_dir)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()