Initial SFERA platform baseline
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# sfera-storage-core
|
||||
|
||||
File-backed storage for SFERA semantic snapshots and local stand state.
|
||||
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "sfera-storage-core"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"pydantic>=2.0",
|
||||
"sfera-sir",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sir import SirSnapshot, snapshot_from_json, snapshot_to_json
|
||||
|
||||
|
||||
class StoredSnapshotInfo(BaseModel):
|
||||
project_id: str
|
||||
snapshot_id: str
|
||||
snapshot_hash: str | None = None
|
||||
path: str
|
||||
|
||||
|
||||
class FileStorage:
|
||||
def __init__(self, root: str | Path = ".sfera/storage") -> None:
|
||||
self.root = Path(root)
|
||||
self.snapshots_dir = self.root / "snapshots"
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.snapshots_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def save_snapshot(self, snapshot: SirSnapshot) -> StoredSnapshotInfo:
|
||||
self.initialize()
|
||||
path = self._snapshot_path(snapshot.project_id)
|
||||
path.write_bytes(snapshot_to_json(snapshot))
|
||||
return StoredSnapshotInfo(
|
||||
project_id=snapshot.project_id,
|
||||
snapshot_id=snapshot.snapshot_id,
|
||||
snapshot_hash=snapshot.snapshot_hash,
|
||||
path=path.as_posix(),
|
||||
)
|
||||
|
||||
def load_snapshot(self, project_id: str) -> SirSnapshot:
|
||||
path = self._snapshot_path(project_id)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(project_id)
|
||||
return snapshot_from_json(path.read_bytes())
|
||||
|
||||
def list_snapshots(self) -> list[StoredSnapshotInfo]:
|
||||
self.initialize()
|
||||
result: list[StoredSnapshotInfo] = []
|
||||
for path in sorted(self.snapshots_dir.glob("*.json")):
|
||||
snapshot = snapshot_from_json(path.read_bytes())
|
||||
result.append(
|
||||
StoredSnapshotInfo(
|
||||
project_id=snapshot.project_id,
|
||||
snapshot_id=snapshot.snapshot_id,
|
||||
snapshot_hash=snapshot.snapshot_hash,
|
||||
path=path.as_posix(),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def has_snapshot(self, project_id: str) -> bool:
|
||||
return self._snapshot_path(project_id).exists()
|
||||
|
||||
def write_document(self, collection: str, document_id: str, payload: dict[str, Any]) -> Path:
|
||||
directory = self._collection_dir(collection)
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
path = directory / f"{self._safe_name(document_id)}.json"
|
||||
path.write_bytes(orjson.dumps(payload, option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS))
|
||||
return path
|
||||
|
||||
def read_document(self, collection: str, document_id: str) -> dict[str, Any]:
|
||||
path = self._collection_dir(collection) / f"{self._safe_name(document_id)}.json"
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"{collection}/{document_id}")
|
||||
return orjson.loads(path.read_bytes())
|
||||
|
||||
def delete_document(self, collection: str, document_id: str) -> bool:
|
||||
path = self._collection_dir(collection) / f"{self._safe_name(document_id)}.json"
|
||||
if not path.exists():
|
||||
return False
|
||||
path.unlink()
|
||||
return True
|
||||
|
||||
def delete_snapshot(self, project_id: str) -> bool:
|
||||
path = self._snapshot_path(project_id)
|
||||
if not path.exists():
|
||||
return False
|
||||
path.unlink()
|
||||
return True
|
||||
|
||||
def delete_documents_matching(self, collection: str, predicate) -> int:
|
||||
directory = self._collection_dir(collection)
|
||||
if not directory.exists():
|
||||
return 0
|
||||
deleted = 0
|
||||
for path in sorted(directory.glob("*.json")):
|
||||
try:
|
||||
payload = orjson.loads(path.read_bytes())
|
||||
except (FileNotFoundError, orjson.JSONDecodeError):
|
||||
continue
|
||||
if predicate(payload):
|
||||
path.unlink()
|
||||
deleted += 1
|
||||
return deleted
|
||||
|
||||
def list_documents(self, collection: str, *, limit: int | None = None) -> list[dict[str, Any]]:
|
||||
directory = self._collection_dir(collection)
|
||||
if not directory.exists():
|
||||
return []
|
||||
documents: list[dict[str, Any]] = []
|
||||
for path in sorted(directory.glob("*.json")):
|
||||
try:
|
||||
documents.append(orjson.loads(path.read_bytes()))
|
||||
except (FileNotFoundError, orjson.JSONDecodeError):
|
||||
continue
|
||||
if limit is not None and len(documents) >= limit:
|
||||
break
|
||||
return documents
|
||||
|
||||
def _snapshot_path(self, project_id: str) -> Path:
|
||||
return self.snapshots_dir / f"{self._safe_name(project_id)}.json"
|
||||
|
||||
def _collection_dir(self, collection: str) -> Path:
|
||||
return self.root / self._safe_name(collection)
|
||||
|
||||
def _safe_name(self, value: str) -> str:
|
||||
return "".join(
|
||||
character if character.isalnum() or character in {"-", "_", "."} else "_"
|
||||
for character in value
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["FileStorage", "StoredSnapshotInfo"]
|
||||
@@ -0,0 +1,28 @@
|
||||
from pathlib import Path
|
||||
|
||||
from semantic_kernel import index_project
|
||||
from storage_core import FileStorage
|
||||
|
||||
|
||||
def test_file_storage_saves_and_loads_snapshot(tmp_path: Path):
|
||||
source_dir = tmp_path / "src"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "module.bsl").write_text("Процедура Test()\nКонецПроцедуры\n", encoding="utf-8")
|
||||
snapshot = index_project(source_dir, project_id="demo")
|
||||
storage = FileStorage(tmp_path / "storage")
|
||||
|
||||
info = storage.save_snapshot(snapshot)
|
||||
restored = storage.load_snapshot("demo")
|
||||
|
||||
assert info.project_id == "demo"
|
||||
assert restored.snapshot_hash == snapshot.snapshot_hash
|
||||
assert storage.list_snapshots()[0].snapshot_id == snapshot.snapshot_id
|
||||
|
||||
|
||||
def test_file_storage_generic_documents(tmp_path: Path):
|
||||
storage = FileStorage(tmp_path / "storage")
|
||||
|
||||
storage.write_document("knowledge", "record/1", {"record_id": "record/1", "title": "Demo"})
|
||||
|
||||
assert storage.read_document("knowledge", "record/1")["title"] == "Demo"
|
||||
assert storage.list_documents("knowledge")[0]["record_id"] == "record/1"
|
||||
Reference in New Issue
Block a user