Coverage for src/prosemark/ports/node_repo.py: 100%
17 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
1"""NodeRepo abstract base class for node file operations."""
3from abc import ABC, abstractmethod
4from typing import TYPE_CHECKING
6if TYPE_CHECKING: # pragma: no cover
7 from prosemark.domain.models import NodeId
10class NodeRepo(ABC):
11 """Abstract base class for node file operations.
13 The NodeRepo defines the contract for managing node files ({id}.md and {id}.notes.md).
14 It handles frontmatter operations, file lifecycle, and editor integration.
16 All implementations must handle:
17 - Node file creation with proper frontmatter structure
18 - Reading and writing frontmatter metadata
19 - Editor integration for different node parts
20 - Node deletion with configurable file removal
22 File Structure:
23 - {id}.md: Node draft content with frontmatter
24 - {id}.notes.md: Node notes content (may or may not have frontmatter)
26 Frontmatter Format:
27 The frontmatter should be in YAML format with these fields:
28 - id: NodeId as string (required)
29 - title: Optional title string
30 - synopsis: Optional synopsis string
31 - created: ISO 8601 timestamp string (required)
32 - updated: ISO 8601 timestamp string (required, auto-updated)
33 """
35 @abstractmethod
36 def create(self, node_id: 'NodeId', title: str | None, synopsis: str | None) -> None:
37 """Create new node files with initial frontmatter.
39 Creates both {id}.md and {id}.notes.md files with appropriate
40 frontmatter and initial content structure.
42 Args:
43 node_id: Unique identifier for the node
44 title: Optional title for the node
45 synopsis: Optional synopsis/summary for the node
47 Raises:
48 FilesystemError: If files cannot be created
49 NodeIdentityError: If node with this ID already exists
51 """
53 @abstractmethod
54 def read_frontmatter(self, node_id: 'NodeId') -> dict[str, str | None]:
55 """Read frontmatter from node draft file.
57 Parses the YAML frontmatter from the {id}.md file and returns
58 it as a dictionary.
60 Args:
61 node_id: NodeId to read frontmatter for
63 Returns:
64 Dictionary containing frontmatter fields:
65 - id: NodeId as string
66 - title: Title string or None
67 - synopsis: Synopsis string or None
68 - created: ISO 8601 timestamp string
69 - updated: ISO 8601 timestamp string
71 Raises:
72 NodeNotFoundError: If node file doesn't exist
73 FilesystemError: If file cannot be read
74 ValueError: If frontmatter format is invalid
76 """
78 @abstractmethod
79 def write_frontmatter(self, node_id: 'NodeId', fm: dict[str, str | None]) -> None:
80 """Update frontmatter in node draft file.
82 Updates the YAML frontmatter in the {id}.md file. The 'updated'
83 timestamp should be automatically set to the current time.
85 Args:
86 node_id: NodeId to update frontmatter for
87 fm: Dictionary containing frontmatter fields to write
89 Raises:
90 NodeNotFoundError: If node file doesn't exist
91 FilesystemError: If file cannot be written
92 ValueError: If frontmatter data is invalid
94 """
96 @abstractmethod
97 def open_in_editor(self, node_id: 'NodeId', part: str) -> None:
98 """Open specified node part in editor.
100 Launches the configured editor to edit the specified part of the node.
102 Args:
103 node_id: NodeId to open in editor
104 part: Which part to open - must be one of:
105 - 'draft': Open {id}.md file
106 - 'notes': Open {id}.notes.md file
107 - 'synopsis': Open {id}.md file focused on synopsis
109 Raises:
110 NodeNotFoundError: If node file doesn't exist
111 ValueError: If part is not a valid option
112 FilesystemError: If editor cannot be launched
114 """
116 @abstractmethod
117 def delete(self, node_id: 'NodeId', *, delete_files: bool) -> None:
118 """Remove node from system.
120 Removes the node from the system, optionally deleting the actual
121 files from the filesystem.
123 Args:
124 node_id: NodeId to delete
125 delete_files: If True, delete actual {id}.md and {id}.notes.md files.
126 If False, just remove from any internal tracking.
128 Raises:
129 NodeNotFoundError: If node doesn't exist
130 FilesystemError: If files cannot be deleted (when delete_files=True)
132 """
134 @abstractmethod
135 def get_existing_files(self) -> set['NodeId']:
136 """Get all existing node files from the filesystem.
138 Scans the project directory for node files ({id}.md) and returns
139 the set of NodeIds that have existing files.
141 Returns:
142 Set of NodeIds for files that exist on disk
144 Raises:
145 FileSystemError: If directory cannot be scanned
147 """
149 @abstractmethod
150 def file_exists(self, node_id: 'NodeId', file_type: str) -> bool:
151 """Check if a specific node file exists.
153 Args:
154 node_id: NodeId to check
155 file_type: Type of file to check ('draft' for {id}.md, 'notes' for {id}.notes.md)
157 Returns:
158 True if the file exists, False otherwise
160 Raises:
161 ValueError: If file_type is not valid
163 """