update project knowledge function
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""Sync state persistence for Jira Project Knowledge.
|
||||
|
||||
A ``SyncManifest`` records the operational state of one project's Jira sync:
|
||||
when it last succeeded, how many issues were processed or failed, and the
|
||||
incremental checkpoint (Jira ``updated > timestamp``) for the next run.
|
||||
|
||||
Persistence uses atomic JSON writes so a crash mid-sync cannot corrupt the
|
||||
manifest and cause duplicate or lost work on recovery.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncManifest:
|
||||
"""Operational state of one project's Jira knowledge sync.
|
||||
|
||||
All timestamps are ISO-8601 UTC strings. ``sync_cursor`` is the Jira
|
||||
``updated`` timestamp watermark; the next incremental sync fetches issues
|
||||
with ``updated >= sync_cursor``.
|
||||
"""
|
||||
project_id: str
|
||||
jira_project_key: str = ""
|
||||
last_successful_sync: str = ""
|
||||
last_attempted_sync: str = ""
|
||||
sync_cursor: str = ""
|
||||
processed_count: int = 0
|
||||
failed_count: int = 0
|
||||
error_category: str = ""
|
||||
total_issues_indexed: int = 0
|
||||
sync_duration_seconds: float = 0.0
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def mark_attempt(self) -> None:
|
||||
"""Record that a sync attempt has started."""
|
||||
self.last_attempted_sync = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def mark_success(
|
||||
self,
|
||||
*,
|
||||
processed: int,
|
||||
failed: int,
|
||||
cursor: str,
|
||||
duration: float,
|
||||
total_indexed: int,
|
||||
) -> None:
|
||||
"""Record a completed sync with its outcomes."""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self.last_successful_sync = now
|
||||
self.last_attempted_sync = now
|
||||
self.processed_count = processed
|
||||
self.failed_count = failed
|
||||
self.sync_cursor = cursor
|
||||
self.sync_duration_seconds = round(duration, 2)
|
||||
self.total_issues_indexed = total_indexed
|
||||
self.error_category = ""
|
||||
|
||||
def mark_failure(self, category: str, failed: int = 0) -> None:
|
||||
"""Record a failed sync attempt without losing the previous cursor."""
|
||||
self.last_attempted_sync = datetime.now(timezone.utc).isoformat()
|
||||
self.error_category = category
|
||||
if failed:
|
||||
self.failed_count = failed
|
||||
|
||||
|
||||
def _manifest_path(index_root: Path, project_id: str) -> Path:
|
||||
"""Deterministic manifest path for one project."""
|
||||
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in project_id)
|
||||
return index_root / safe / "manifest.json"
|
||||
|
||||
|
||||
def load_manifest(index_root: Path, project_id: str) -> SyncManifest:
|
||||
"""Load the manifest for ``project_id``, returning a fresh one if absent.
|
||||
|
||||
Never raises on missing or corrupt files — a missing manifest simply means
|
||||
"first sync", and a corrupt one is treated the same way (the operator can
|
||||
inspect the file manually if needed).
|
||||
"""
|
||||
path = _manifest_path(index_root, project_id)
|
||||
if not path.exists():
|
||||
return SyncManifest(project_id=project_id)
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
known = {f.name for f in SyncManifest.__dataclass_fields__.values()}
|
||||
return SyncManifest(**{k: v for k, v in data.items() if k in known})
|
||||
except (OSError, json.JSONDecodeError, TypeError):
|
||||
return SyncManifest(project_id=project_id)
|
||||
|
||||
|
||||
def save_manifest(index_root: Path, manifest: SyncManifest) -> None:
|
||||
"""Atomically persist ``manifest`` to disk.
|
||||
|
||||
Creates the project directory if it does not exist. Uses the shared
|
||||
atomic-write helper so a crash between truncate and write cannot leave
|
||||
a half-written manifest.
|
||||
"""
|
||||
path = _manifest_path(index_root, manifest.project_id)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
from ...infrastructure.persistence.json.atomic_write import write_json
|
||||
write_json(path, asdict(manifest))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SyncManifest",
|
||||
"load_manifest",
|
||||
"save_manifest",
|
||||
]
|
||||
Reference in New Issue
Block a user