Source code for labapi.tree.page

"""Notebook Page Module.

This module defines the :class:`~labapi.tree.page.NotebookPage` class,
representing a page within a LabArchives notebook. It extends
:class:`~labapi.tree.mixins.AbstractTreeNode` and provides access to the
entries contained within the page.
"""

from __future__ import annotations

import warnings
from typing import TYPE_CHECKING, Any, Literal, cast, override, Self

from labapi.entry import Attachment, Entries, Entry, UnknownEntry
from labapi.util import ALL_PART_TYPES, InsertBehavior, extract_etree

from .mixins import AbstractTreeContainer, AbstractTreeNode

if TYPE_CHECKING:
    from labapi.user import User


[docs] class NotebookPage(AbstractTreeNode): """Represents a single page within a LabArchives notebook. A `NotebookPage` is a leaf node in the tree structure and contains a collection of :class:`~labapi.entry.Entry` objects. It provides functionalities to access and manage these entries. """
[docs] def __init__( self, tree_id: str, name: str, root: AbstractTreeContainer, parent: AbstractTreeContainer, user: User, ): """Initialize a notebook page. :param tree_id: The unique ID of the page. :param name: The name of the page. :param root: The root node of the tree (the Notebook). :param parent: The parent node of this page (a Directory or Notebook). :param user: The authenticated user. """ super().__init__(tree_id, name, root, parent, user) self._entries: Entries | None = None
@property @override def id(self) -> str: """Return the page identifier. :returns: The page's ID. """ return super().id @property def entries(self) -> Entries: """Return this page's entries, loading them on first access. This property lazily loads the entries from the LabArchives API if they have not been loaded yet. .. note:: Slicing on the returned collection provides snapshots. Iterators over the collection are also snapshots and are therefore insulated from later collection mutations. :returns: An :class:`~labapi.entry.Entries` object managing the page's entries. """ if self._entries is None: entries: list[Entry[Any]] = [] entries_tree = self._user.api_get( "tree_tools/get_entries_for_page", page_tree_id=self.id, nbid=self.root.id, entry_data=True, ) for entry in entries_tree.iterfind(".//entry"): entry_data = extract_etree( entry, { "eid": str, "part-type": str, "attach-file-name": str, "attach-content-type": str, "entry-data": str, }, ) part_type = entry_data["part-type"] if part_type in ALL_PART_TYPES: if Entry.is_registered(part_type): # Cast extracted string values to ensure type checker knows they're not None entries.append( Entry.from_part_type( part_type, cast(str, entry_data["eid"]), cast(str, entry_data["entry-data"]), self._user, ) ) else: warnings.warn( f"Entry type '{part_type}' (ID: {entry_data['eid']}) is recognized but not " f"implemented in labapi. Wrapping as UnknownEntry.", UserWarning, stacklevel=2, ) entries.append( UnknownEntry( cast(str, entry_data["eid"]), cast(str, entry_data["entry-data"]), self._user, part_type=part_type, ) ) else: warnings.warn( f"Unknown entry type '{part_type}' (ID: {entry_data['eid']}) encountered. " f"Wrapping as UnknownEntry.", RuntimeWarning, stacklevel=2, ) entries.append( UnknownEntry( cast(str, entry_data["eid"]), cast(str, entry_data["entry-data"]), self._user, part_type=part_type, ) ) self._entries = Entries(entries, self._user, self) return self._entries
[docs] @override def copy_to(self, destination: AbstractTreeContainer) -> NotebookPage: """Copy this page and its entries into ``destination``. .. warning:: This method has known limitations: - LabArchives may rename attachment files during copy operations - Only certain entry types are fully supported (text, plain text, headers, attachments) - Some entry types may fail to copy and will cause errors :param destination: The target container to copy the page to. :returns: A new instance of the copied page in the destination. Copy behavior for attachments is explicit: - attachment payloads are copied by reading and re-uploading the attachment content, - attachment resources opened during copy are always released, - any per-entry copy failure is reported via warning and that entry is skipped. .. note:: This method is best-effort and may produce partial copies if one or more entries fail while others succeed. :raises RuntimeWarning: Emitted when an individual entry fails to copy. """ new_page = destination.create( NotebookPage, self.name, if_exists=InsertBehavior.Ignore ) for entry in self.entries: entry_content: Any | None = None try: entry_content = entry.content # Re-upload behavior is intentional: copy_to creates a new entry on the # destination page using the source entry's runtime class and content. # For attachments, Entries.create uploads the payload and returns a # distinct destination attachment entry; it does not mutate the source # entry or preserve source attachment IDs. assert entry_content is not None new_page.entries.create(cast(Any, entry.__class__), entry_content) except Exception as exc: warnings.warn( f"Failed to copy entry {entry.id!r} ({entry.content_type!r}) from page " f"{self.id!r} to page {new_page.id!r}: {exc}. This entry was skipped.", RuntimeWarning, stacklevel=2, ) finally: if isinstance(entry_content, Attachment): entry_content.close() return new_page
[docs] @override def is_dir(self) -> Literal[False]: """Return ``False`` because pages are leaf nodes. :returns: Always False. """ return False
[docs] @override def refresh(self) -> Self: """Refresh this page by clearing its cached entries. This method clears the internal entries cache, forcing the page to re-fetch its entries from the LabArchives API on the next access. .. note:: Currently only clears the entries cache. Future implementation should properly invalidate all entry objects before clearing. """ # TODO: Properly invalidate all entry objects before clearing self._entries = None return self