2026-03-16 22:45:27 -05:00
|
|
|
"""
|
|
|
|
|
trovedb.py — Concrete implementation of Trove protocols backed by Sqlite3Trove.
|
|
|
|
|
|
|
|
|
|
Implements BlobNote, TreeNote, and Trove protocols defined in trove.py.
|
2026-03-24 22:51:56 -05:00
|
|
|
Depends on db.py (Sqlite3Trove) for storage.
|
2026-03-16 22:45:27 -05:00
|
|
|
"""
|
|
|
|
|
|
2026-03-24 22:51:56 -05:00
|
|
|
from typing import Optional
|
2026-03-16 22:45:27 -05:00
|
|
|
from pathlib import Path
|
2026-03-22 23:57:05 -05:00
|
|
|
import datetime as dt
|
2026-03-16 22:45:27 -05:00
|
|
|
|
2026-03-21 22:25:32 -05:00
|
|
|
from .db import Sqlite3Trove, NOTE_ROOT_ID
|
|
|
|
|
|
|
|
|
|
from . import trove as tr
|
|
|
|
|
|
2026-03-26 00:36:01 -05:00
|
|
|
from .trove import Note, Trove, TreeNote, TreeEntry, NoteNotFound, ObjectId
|
2026-03-16 22:45:27 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class NoteImpl(Note):
|
2026-03-24 22:51:56 -05:00
|
|
|
"""Concrete note implementation."""
|
2026-03-16 22:45:27 -05:00
|
|
|
|
2026-03-24 21:22:24 -05:00
|
|
|
def __init__(self, parent: 'TroveImpl', object_id: ObjectId):
|
2026-03-19 22:43:11 -05:00
|
|
|
self._parent = parent
|
|
|
|
|
self._db = parent.db
|
2026-03-16 22:45:27 -05:00
|
|
|
self._object_id = object_id
|
|
|
|
|
|
|
|
|
|
# Note protocol
|
|
|
|
|
@property
|
2026-03-24 21:22:24 -05:00
|
|
|
def object_id(self) -> ObjectId:
|
2026-03-16 22:45:27 -05:00
|
|
|
return self._object_id
|
|
|
|
|
|
2026-03-22 23:57:05 -05:00
|
|
|
@property
|
|
|
|
|
def readonly(self) -> bool:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def mtime(self) -> dt.datetime:
|
2026-03-24 22:51:56 -05:00
|
|
|
"""Return modification time as UTC datetime."""
|
2026-03-22 23:57:05 -05:00
|
|
|
return self._db.get_mtime(self._object_id)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def mime(self) -> str:
|
2026-03-24 22:51:56 -05:00
|
|
|
"""Return MIME type from the objects table."""
|
|
|
|
|
info = self._db.get_info(self._object_id)
|
|
|
|
|
return info.type if info else "application/octet-stream"
|
2026-03-22 23:57:05 -05:00
|
|
|
|
2026-03-16 22:45:27 -05:00
|
|
|
def get_raw_metadata(self, key: str) -> Optional[bytes]:
|
|
|
|
|
return self._db.read_metadata(self._object_id, key)
|
|
|
|
|
|
|
|
|
|
def set_raw_metadata(self, key: str, value: bytes) -> None:
|
|
|
|
|
self._db.write_metadata(self._object_id, key, value)
|
|
|
|
|
|
2026-03-26 00:36:01 -05:00
|
|
|
def read_content(self) -> bytes:
|
2026-03-16 22:45:27 -05:00
|
|
|
data = self._db.read_object(self._object_id)
|
|
|
|
|
return data if data is not None else b""
|
|
|
|
|
|
2026-03-26 00:36:01 -05:00
|
|
|
def write_content(self, data: bytes) -> None:
|
2026-03-24 22:51:56 -05:00
|
|
|
self._db.write_content(self._object_id, data)
|
2026-03-16 22:45:27 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TreeNoteImpl(NoteImpl, TreeNote):
|
2026-03-24 22:51:56 -05:00
|
|
|
"""Concrete TreeNote: a tree object backed by the tree_entries table."""
|
2026-03-16 22:45:27 -05:00
|
|
|
|
|
|
|
|
# Tree protocol
|
|
|
|
|
def link(self, name: str, note: Note) -> None:
|
2026-03-24 22:51:56 -05:00
|
|
|
"""Link name to an existing note."""
|
|
|
|
|
self._db.link(self._object_id, name, note.object_id)
|
2026-03-16 22:45:27 -05:00
|
|
|
|
|
|
|
|
def unlink(self, name: str) -> None:
|
2026-03-24 22:51:56 -05:00
|
|
|
"""Remove an entry by name."""
|
|
|
|
|
self._db.unlink(self._object_id, name)
|
2026-03-16 22:45:27 -05:00
|
|
|
|
|
|
|
|
def mkdir(self, name: str) -> 'TreeNoteImpl':
|
|
|
|
|
"""Create a new empty tree, link it under name, and return it."""
|
2026-03-24 22:51:56 -05:00
|
|
|
new_id = self._db.write_tree(b"")
|
2026-03-19 22:43:11 -05:00
|
|
|
tree = TreeNoteImpl(self._parent, new_id)
|
2026-03-16 22:45:27 -05:00
|
|
|
self.link(name, tree)
|
|
|
|
|
return tree
|
|
|
|
|
|
2026-03-19 22:43:11 -05:00
|
|
|
def rmdir(self, name: str) -> None:
|
|
|
|
|
"""Remove a directory from the tree."""
|
|
|
|
|
self.unlink(name)
|
|
|
|
|
|
|
|
|
|
def child(self, name: str) -> Note:
|
|
|
|
|
"""Retrieve a child note by name."""
|
2026-03-24 22:51:56 -05:00
|
|
|
entries = self._db.list_tree(self._object_id)
|
2026-03-19 22:43:11 -05:00
|
|
|
if name not in entries:
|
|
|
|
|
raise KeyError(f"Entry '{name}' not found")
|
|
|
|
|
child_id = entries[name]
|
2026-03-21 12:08:54 -05:00
|
|
|
value = self._parent.get_raw_note(child_id)
|
|
|
|
|
if value is None:
|
|
|
|
|
raise KeyError(f"Entry '{name}' has no value")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
def entries(self):
|
|
|
|
|
"""Return all entries as an iterable of TreeEntry."""
|
2026-03-24 22:51:56 -05:00
|
|
|
for name, object_id in self._db.list_tree(self._object_id).items():
|
2026-03-21 12:08:54 -05:00
|
|
|
yield TreeEntry(name, object_id)
|
2026-03-19 22:43:11 -05:00
|
|
|
|
2026-03-24 21:22:24 -05:00
|
|
|
def list(self) -> dict[str, ObjectId]:
|
2026-03-16 22:45:27 -05:00
|
|
|
"""Return all entries as {name: object_id}."""
|
2026-03-24 22:51:56 -05:00
|
|
|
return self._db.list_tree(self._object_id)
|
2026-03-16 22:45:27 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Trove
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TroveImpl:
|
|
|
|
|
"""
|
|
|
|
|
Concrete Trove: top-level API backed by a Sqlite3Trove database.
|
|
|
|
|
|
|
|
|
|
Use TroveImpl.open() to get an instance.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, db: Sqlite3Trove):
|
|
|
|
|
self._db = db
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def open(cls, path: str | Path, create: bool = False) -> "TroveImpl":
|
|
|
|
|
db = Sqlite3Trove.open(path, create=create)
|
2026-03-24 22:51:56 -05:00
|
|
|
return cls(db)
|
2026-03-16 22:45:27 -05:00
|
|
|
|
2026-03-19 22:43:11 -05:00
|
|
|
@property
|
|
|
|
|
def db(self) -> Sqlite3Trove:
|
|
|
|
|
return self._db
|
|
|
|
|
|
2026-03-16 22:45:27 -05:00
|
|
|
def close(self) -> None:
|
|
|
|
|
self._db.close()
|
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def __exit__(self, *_):
|
|
|
|
|
self.close()
|
|
|
|
|
|
|
|
|
|
# Trove protocol
|
2026-03-24 21:22:24 -05:00
|
|
|
def get_raw_note(self, note_id: ObjectId) -> Note:
|
2026-03-16 22:45:27 -05:00
|
|
|
"""Return a BlobNote or TreeNote for the given id, or None if not found."""
|
2026-03-24 22:51:56 -05:00
|
|
|
info = self._db.get_info(note_id)
|
|
|
|
|
if info is None:
|
2026-03-21 12:08:54 -05:00
|
|
|
raise NoteNotFound(note_id)
|
2026-03-24 22:51:56 -05:00
|
|
|
if self._db.is_tree(note_id) or info.type == "inode/directory":
|
2026-03-19 22:43:11 -05:00
|
|
|
return TreeNoteImpl(self, note_id)
|
2026-03-26 00:36:01 -05:00
|
|
|
return NoteImpl(self, note_id)
|
2026-03-16 22:45:27 -05:00
|
|
|
|
2026-03-24 22:51:56 -05:00
|
|
|
def create_blob(self, data: bytes | None = None,
|
2026-03-26 00:36:01 -05:00
|
|
|
dtype: str = "application/octet-stream") -> Note:
|
2026-03-16 22:45:27 -05:00
|
|
|
"""Create a new blob object and return a BlobNote for it."""
|
2026-03-24 22:51:56 -05:00
|
|
|
obj_id = self._db.write_blob(data or b"", dtype=dtype)
|
2026-03-26 00:36:01 -05:00
|
|
|
return NoteImpl(self, obj_id)
|
2026-03-16 22:45:27 -05:00
|
|
|
|
|
|
|
|
def get_root(self) -> TreeNote:
|
2026-03-24 22:51:56 -05:00
|
|
|
"""Return the root TreeNote (always id=NOTE_ROOT_ID)."""
|
2026-03-21 22:25:32 -05:00
|
|
|
return TreeNoteImpl(self, NOTE_ROOT_ID)
|
2026-03-16 22:45:27 -05:00
|
|
|
|
2026-03-24 22:51:56 -05:00
|
|
|
|
2026-03-21 22:25:32 -05:00
|
|
|
def open_db_trove(path: str | Path, create: bool = False, **kwargs: tr.OpenArguments) -> Trove:
|
2026-03-16 22:45:27 -05:00
|
|
|
return TroveImpl.open(path, create=create)
|