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

1"""NodeRepo abstract base class for node file operations.""" 

2 

3from abc import ABC, abstractmethod 

4from typing import TYPE_CHECKING 

5 

6if TYPE_CHECKING: # pragma: no cover 

7 from prosemark.domain.models import NodeId 

8 

9 

10class NodeRepo(ABC): 

11 """Abstract base class for node file operations. 

12 

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. 

15 

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 

21 

22 File Structure: 

23 - {id}.md: Node draft content with frontmatter 

24 - {id}.notes.md: Node notes content (may or may not have frontmatter) 

25 

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 """ 

34 

35 @abstractmethod 

36 def create(self, node_id: 'NodeId', title: str | None, synopsis: str | None) -> None: 

37 """Create new node files with initial frontmatter. 

38 

39 Creates both {id}.md and {id}.notes.md files with appropriate 

40 frontmatter and initial content structure. 

41 

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 

46 

47 Raises: 

48 FilesystemError: If files cannot be created 

49 NodeIdentityError: If node with this ID already exists 

50 

51 """ 

52 

53 @abstractmethod 

54 def read_frontmatter(self, node_id: 'NodeId') -> dict[str, str | None]: 

55 """Read frontmatter from node draft file. 

56 

57 Parses the YAML frontmatter from the {id}.md file and returns 

58 it as a dictionary. 

59 

60 Args: 

61 node_id: NodeId to read frontmatter for 

62 

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 

70 

71 Raises: 

72 NodeNotFoundError: If node file doesn't exist 

73 FilesystemError: If file cannot be read 

74 ValueError: If frontmatter format is invalid 

75 

76 """ 

77 

78 @abstractmethod 

79 def write_frontmatter(self, node_id: 'NodeId', fm: dict[str, str | None]) -> None: 

80 """Update frontmatter in node draft file. 

81 

82 Updates the YAML frontmatter in the {id}.md file. The 'updated' 

83 timestamp should be automatically set to the current time. 

84 

85 Args: 

86 node_id: NodeId to update frontmatter for 

87 fm: Dictionary containing frontmatter fields to write 

88 

89 Raises: 

90 NodeNotFoundError: If node file doesn't exist 

91 FilesystemError: If file cannot be written 

92 ValueError: If frontmatter data is invalid 

93 

94 """ 

95 

96 @abstractmethod 

97 def open_in_editor(self, node_id: 'NodeId', part: str) -> None: 

98 """Open specified node part in editor. 

99 

100 Launches the configured editor to edit the specified part of the node. 

101 

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 

108 

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 

113 

114 """ 

115 

116 @abstractmethod 

117 def delete(self, node_id: 'NodeId', *, delete_files: bool) -> None: 

118 """Remove node from system. 

119 

120 Removes the node from the system, optionally deleting the actual 

121 files from the filesystem. 

122 

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. 

127 

128 Raises: 

129 NodeNotFoundError: If node doesn't exist 

130 FilesystemError: If files cannot be deleted (when delete_files=True) 

131 

132 """ 

133 

134 @abstractmethod 

135 def get_existing_files(self) -> set['NodeId']: 

136 """Get all existing node files from the filesystem. 

137 

138 Scans the project directory for node files ({id}.md) and returns 

139 the set of NodeIds that have existing files. 

140 

141 Returns: 

142 Set of NodeIds for files that exist on disk 

143 

144 Raises: 

145 FileSystemError: If directory cannot be scanned 

146 

147 """ 

148 

149 @abstractmethod 

150 def file_exists(self, node_id: 'NodeId', file_type: str) -> bool: 

151 """Check if a specific node file exists. 

152 

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) 

156 

157 Returns: 

158 True if the file exists, False otherwise 

159 

160 Raises: 

161 ValueError: If file_type is not valid 

162 

163 """