trove/trovedb/trovedb.py

195 lines
6.3 KiB
Python
Raw Normal View History

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.
Depends on db.py (Sqlite3Trove) for storage.
2026-03-16 22:45:27 -05:00
"""
from typing import Optional, Iterator
2026-03-16 22:45:27 -05:00
from pathlib import Path
import datetime as dt
import uuid
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
from .trove import Note, Trove, TreeNote, TreeEntry, NoteNotFound, ObjectId
2026-03-16 22:45:27 -05:00
class NoteImpl(Note):
"""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):
if not isinstance(object_id, uuid.UUID):
object_id = uuid.UUID(str(object_id))
assert isinstance(object_id, uuid.UUID)
2026-03-19 22:43:11 -05:00
self._parent = parent
self._db = parent.db
self._object_id: uuid.UUID = object_id
@staticmethod
def get_impl_id(note: Note) -> uuid.UUID:
if not isinstance(note.object_id, uuid.UUID):
raise TypeError("Note not compatible with NoteImpl")
return note.object_id
2026-03-16 22:45:27 -05:00
# 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
@property
def readonly(self) -> bool:
return False
@property
def mtime(self) -> dt.datetime:
"""Return modification time as UTC datetime."""
mtime = self._db.get_mtime(self._object_id)
return mtime if mtime is not None else dt.datetime.now(tz=dt.timezone.utc)
@property
def mime(self) -> str:
"""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-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)
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""
def write_content(self, data: bytes) -> None:
self._db.write_content(self._object_id, data)
2026-03-16 22:45:27 -05:00
def children(self) -> Iterator[TreeEntry]:
"""Get all children of this note."""
for name, object_id in self._db.list_tree(self._object_id).items():
yield TreeEntry(name, object_id)
def new_child(self, name: str, mime: str, content: bytes | None, executable: bool, hidden: bool) -> Note:
"""Create a new child note."""
content = content if content is not None else b""
object_id = self._db.write_blob(data=content, object_id=None, dtype=mime, executable=executable, hidden=hidden)
# TODO fix this
if mime == 'inode/directory':
return TreeNoteImpl(self._parent, object_id)
return NoteImpl(self._parent, object_id)
def child(self, name: str) -> Note:
"""Retrieve a child note by name."""
entries = self._db.list_tree(self._object_id)
if name not in entries:
raise tr.ErrorNotFound(name)
child_id = entries[name]
value = self._parent.get_raw_note(child_id)
if value is None:
raise tr.ErrorNotFound("dangling child link") # FIXME: better errors
return value
def rm_child(self, name: str, recurse: bool) -> None:
"""Remove a child note."""
note = self.child(name)
if note.has_children() and not recurse:
raise tr.ErrorNotEmpty(name)
self._db.unlink(self._object_id, name)
2026-03-16 22:45:27 -05:00
class TreeNoteImpl(NoteImpl, TreeNote):
"""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:
"""Link name to an existing note."""
self._db.link(self._object_id, name, NoteImpl.get_impl_id(note))
2026-03-16 22:45:27 -05:00
def unlink(self, name: str) -> None:
"""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."""
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 entries(self):
"""Return all entries as an iterable of TreeEntry."""
for name, object_id in self._db.list_tree(self._object_id).items():
yield TreeEntry(name, object_id)
2026-03-19 22:43:11 -05:00
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)
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."""
if not isinstance(note_id, uuid.UUID):
note_id = uuid.UUID(str(note_id))
info = self._db.get_info(note_id)
if info is None:
raise NoteNotFound(note_id)
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)
return NoteImpl(self, note_id)
2026-03-16 22:45:27 -05:00
def create_blob(self, data: bytes | None = None,
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."""
obj_id = self._db.write_blob(data or b"", dtype=dtype)
return NoteImpl(self, obj_id)
2026-03-16 22:45:27 -05:00
def get_root(self) -> TreeNote:
"""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-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)