Coverage for src/prosemark/templates/adapters/file_template_repository.py: 87%

149 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-09-30 23:09 +0000

1"""File-based template repository adapter for template storage and retrieval.""" 

2 

3from pathlib import Path 

4from typing import Any 

5 

6from prosemark.templates.domain.entities.template import Template 

7from prosemark.templates.domain.entities.template_directory import TemplateDirectory 

8from prosemark.templates.domain.exceptions.template_exceptions import ( 

9 InvalidTemplateDirectoryError, 

10 TemplateDirectoryNotFoundError, 

11 TemplateNotFoundError, 

12 TemplateParseError, 

13 TemplateValidationError, 

14) 

15from prosemark.templates.domain.values.template_path import TemplatePath 

16from prosemark.templates.ports.template_repository_port import TemplateRepositoryPort 

17 

18 

19class FileTemplateRepository(TemplateRepositoryPort): 

20 """File-based implementation of template repository.""" 

21 

22 def __init__(self, templates_root: Path | str) -> None: 

23 """Initialize repository with templates root directory. 

24 

25 Args: 

26 templates_root: Path to the root templates directory 

27 

28 Raises: 

29 TemplateDirectoryNotFoundError: If root directory doesn't exist 

30 

31 """ 

32 self._templates_root = Path(templates_root) 

33 if not self._templates_root.exists(): 

34 msg = f'Templates root directory not found: {self._templates_root}' 

35 raise TemplateDirectoryNotFoundError(msg) 

36 if not self._templates_root.is_dir(): 

37 msg = f'Templates root path is not a directory: {self._templates_root}' 

38 raise TemplateDirectoryNotFoundError(msg) 

39 

40 def get_template(self, template_name: str) -> Template: 

41 """Load a single template by name. 

42 

43 Args: 

44 template_name: Name of the template (without .md extension) 

45 

46 Returns: 

47 Template object 

48 

49 Raises: 

50 TemplateNotFoundError: If template file doesn't exist 

51 

52 """ 

53 # Look for template file directly in templates root 

54 template_file = self._templates_root / f'{template_name}.md' 

55 

56 if not template_file.exists(): 

57 raise TemplateNotFoundError(template_name=template_name, search_path=str(self._templates_root)) 

58 

59 try: 

60 template_path = TemplatePath(template_file) 

61 return Template.from_file(template_path.value) 

62 except OSError as e: 

63 # File system errors should be treated as template not found 

64 raise TemplateNotFoundError(template_name=template_name, search_path=str(self._templates_root)) from e 

65 # Let validation errors bubble up as they are 

66 

67 def get_template_directory(self, directory_name: str) -> TemplateDirectory: 

68 """Load a template directory by name. 

69 

70 Args: 

71 directory_name: Name of the template directory 

72 

73 Returns: 

74 TemplateDirectory object 

75 

76 Raises: 

77 TemplateDirectoryNotFoundError: If directory doesn't exist 

78 

79 """ 

80 directory_path = self._templates_root / directory_name 

81 

82 if not directory_path.exists(): 

83 msg = f"Template directory '{directory_name}' not found in {self._templates_root}" 

84 raise TemplateDirectoryNotFoundError(msg) 

85 

86 if not directory_path.is_dir(): 

87 msg = f"Template directory path '{directory_path}' is not a directory" 

88 raise TemplateDirectoryNotFoundError(msg) 

89 

90 try: 

91 return TemplateDirectory.from_directory(directory_path) 

92 except Exception as e: 

93 # Re-raise as TemplateDirectoryNotFoundError for consistency 

94 msg = f"Failed to load template directory '{directory_name}': {e}" 

95 raise TemplateDirectoryNotFoundError(msg) from e 

96 

97 @classmethod 

98 def list_templates(cls, search_path: Path) -> list[Template]: 

99 """List all individual templates in the search path. 

100 

101 Args: 

102 search_path: Directory path to search for templates 

103 

104 Returns: 

105 List of Template instances (excludes directory templates) 

106 

107 Raises: 

108 TemplateDirectoryNotFoundError: If search_path does not exist 

109 

110 """ 

111 if not search_path.exists(): 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true

112 msg = f'Search path does not exist: {search_path}' 

113 raise TemplateDirectoryNotFoundError(msg) 

114 

115 if not search_path.is_dir(): 

116 msg = f'Search path is not a directory: {search_path}' 

117 raise TemplateDirectoryNotFoundError(msg) 

118 

119 templates = [] 

120 

121 # Find all .md files directly in the search path 

122 for template_file in search_path.glob('*.md'): 

123 if template_file.is_file(): 123 ↛ 122line 123 didn't jump to line 122 because the condition on line 123 was always true

124 try: 

125 template_path = TemplatePath(template_file) 

126 template = Template.from_file(template_path.value) 

127 templates.append(template) 

128 except (TemplateParseError, TemplateValidationError) as e: 

129 # Log the error but skip invalid template files 

130 import logging 

131 

132 logging.getLogger(__name__).warning('Skipping invalid template file %s: %s', template_file, e) 

133 continue 

134 

135 return sorted(templates, key=lambda t: t.name) 

136 

137 def list_template_directories(self, search_path: Path) -> list[TemplateDirectory]: 

138 """List all template directories in the search path. 

139 

140 Args: 

141 search_path: Directory path to search for template directories 

142 

143 Returns: 

144 List of TemplateDirectory instances 

145 

146 Raises: 

147 TemplateDirectoryNotFoundError: If search_path does not exist 

148 

149 """ 

150 if not search_path.exists(): 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true

151 msg = f'Search path does not exist: {search_path}' 

152 raise TemplateDirectoryNotFoundError(msg) 

153 

154 if not search_path.is_dir(): 

155 msg = f'Search path is not a directory: {search_path}' 

156 raise TemplateDirectoryNotFoundError(msg) 

157 

158 directories = [] 

159 

160 # Find all directories in search path that contain .md files 

161 for item in search_path.iterdir(): 

162 if item.is_dir() and self._directory_contains_templates(item): 

163 try: 

164 template_directory = TemplateDirectory.from_directory(item) 

165 directories.append(template_directory) 

166 except (TemplateParseError, TemplateValidationError, InvalidTemplateDirectoryError) as e: 

167 # Log the error but skip invalid template directories 

168 import logging 

169 

170 logging.getLogger(__name__).warning('Skipping invalid template directory %s: %s', item, e) 

171 continue 

172 

173 return sorted(directories, key=lambda d: d.name) 

174 

175 def list_all_template_names(self) -> list[str]: 

176 """List all single template names available in the templates root. 

177 

178 Returns: 

179 List of template names (without .md extension) 

180 

181 """ 

182 # Find all .md files directly in the templates root 

183 template_names = [ 

184 template_file.stem for template_file in self._templates_root.glob('*.md') if template_file.is_file() 

185 ] 

186 

187 return sorted(template_names) 

188 

189 def list_all_template_directory_names(self) -> list[str]: 

190 """List all template directory names available in the templates root. 

191 

192 Returns: 

193 List of template directory names 

194 

195 """ 

196 # Find all directories in templates root that contain .md files using list comprehension 

197 directory_names = [ 

198 item.name 

199 for item in self._templates_root.iterdir() 

200 if item.is_dir() and self._directory_contains_templates(item) 

201 ] 

202 

203 return sorted(directory_names) 

204 

205 @classmethod 

206 def find_template_by_name(cls, name: str, search_path: Path) -> Template | None: 

207 """Find a template by name in the given search path. 

208 

209 Args: 

210 name: Template name (without .md extension) 

211 search_path: Directory path to search for templates 

212 

213 Returns: 

214 Template instance if found, None otherwise 

215 

216 Raises: 

217 TemplateDirectoryNotFoundError: If search_path does not exist 

218 

219 """ 

220 if not search_path.exists(): 220 ↛ 221line 220 didn't jump to line 221 because the condition on line 220 was never true

221 msg = f'Search path does not exist: {search_path}' 

222 raise TemplateDirectoryNotFoundError(msg) 

223 

224 if not search_path.is_dir(): 

225 msg = f'Search path is not a directory: {search_path}' 

226 raise TemplateDirectoryNotFoundError(msg) 

227 

228 template_file = search_path / f'{name}.md' 

229 if template_file.exists() and template_file.is_file(): 229 ↛ 235line 229 didn't jump to line 235 because the condition on line 229 was always true

230 try: 

231 template_path = TemplatePath(template_file) 

232 return Template.from_file(template_path.value) 

233 except (TemplateParseError, TemplateValidationError): 

234 return None 

235 return None 

236 

237 def find_template_directory(self, name: str, search_path: Path) -> TemplateDirectory | None: 

238 """Find a template directory by name. 

239 

240 Args: 

241 name: Directory name 

242 search_path: Parent directory to search in 

243 

244 Returns: 

245 TemplateDirectory instance if found, None otherwise 

246 

247 Raises: 

248 TemplateDirectoryNotFoundError: If search_path does not exist 

249 

250 """ 

251 if not search_path.exists(): 

252 msg = f'Search path does not exist: {search_path}' 

253 raise TemplateDirectoryNotFoundError(msg) 

254 

255 if not search_path.is_dir(): 

256 msg = f'Search path is not a directory: {search_path}' 

257 raise TemplateDirectoryNotFoundError(msg) 

258 

259 directory_path = search_path / name 

260 if directory_path.exists() and directory_path.is_dir() and self._directory_contains_templates(directory_path): 260 ↛ 265line 260 didn't jump to line 265 because the condition on line 260 was always true

261 try: 

262 return TemplateDirectory.from_directory(directory_path) 

263 except (TemplateParseError, TemplateValidationError, InvalidTemplateDirectoryError): 

264 return None 

265 return None 

266 

267 @classmethod 

268 def load_template_content(cls, template_path: Path) -> str: 

269 """Load raw content from a template file. 

270 

271 Args: 

272 template_path: Absolute path to template file 

273 

274 Returns: 

275 Raw template content as string 

276 

277 Raises: 

278 TemplateNotFoundError: If template file does not exist 

279 PermissionError: If template file is not readable 

280 

281 """ 

282 if not template_path.exists(): 282 ↛ 285line 282 didn't jump to line 285 because the condition on line 282 was always true

283 raise TemplateNotFoundError(template_name=template_path.stem, search_path=str(template_path.parent)) 

284 

285 try: 

286 return template_path.read_text(encoding='utf-8') 

287 except PermissionError as e: # pragma: no cover 

288 msg = f'Cannot read template file: {template_path}' 

289 raise PermissionError(msg) from e 

290 

291 @classmethod 

292 def validate_template_path(cls, path: Path) -> bool: 

293 """Validate that a path points to a valid template location. 

294 

295 Args: 

296 path: Path to validate 

297 

298 Returns: 

299 True if path is valid for template operations 

300 

301 """ 

302 return path.exists() and path.is_file() and path.suffix == '.md' 

303 

304 def template_exists(self, template_name: str) -> bool: 

305 """Check if a single template exists. 

306 

307 Args: 

308 template_name: Name of the template 

309 

310 Returns: 

311 True if template exists, False otherwise 

312 

313 """ 

314 template_file = self._templates_root / f'{template_name}.md' 

315 return template_file.exists() and template_file.is_file() 

316 

317 def template_directory_exists(self, directory_name: str) -> bool: 

318 """Check if a template directory exists. 

319 

320 Args: 

321 directory_name: Name of the template directory 

322 

323 Returns: 

324 True if directory exists and contains templates, False otherwise 

325 

326 """ 

327 directory_path = self._templates_root / directory_name 

328 return ( 

329 directory_path.exists() and directory_path.is_dir() and self._directory_contains_templates(directory_path) 

330 ) 

331 

332 def get_templates_root(self) -> Path: 

333 """Get the root directory for templates. 

334 

335 Returns: 

336 Path to templates root directory 

337 

338 """ 

339 return self._templates_root 

340 

341 @staticmethod 

342 def _directory_contains_templates(directory_path: Path) -> bool: 

343 """Check if a directory contains any template files. 

344 

345 Args: 

346 directory_path: Path to directory to check 

347 

348 Returns: 

349 True if directory contains .md files, False otherwise 

350 

351 """ 

352 try: 

353 # Check for .md files recursively 

354 return any(template_file.is_file() for template_file in directory_path.rglob('*.md')) 

355 except (OSError, PermissionError): # pragma: no cover 

356 return False 

357 

358 def get_template_info(self, template_name: str) -> dict[str, Any]: 

359 """Get metadata about a template without fully loading it. 

360 

361 Args: 

362 template_name: Name of the template 

363 

364 Returns: 

365 Dictionary with template metadata 

366 

367 Raises: 

368 TemplateNotFoundError: If template doesn't exist 

369 

370 """ 

371 template_file = self._templates_root / f'{template_name}.md' 

372 

373 if not template_file.exists(): 373 ↛ 376line 373 didn't jump to line 376 because the condition on line 373 was always true

374 raise TemplateNotFoundError(template_name=template_name, search_path=str(self._templates_root)) 

375 

376 try: 

377 # Get file stats 

378 stat = template_file.stat() 

379 

380 return { 

381 'name': template_name, 

382 'file_path': str(template_file), 

383 'file_size': stat.st_size, 

384 'modified_time': stat.st_mtime, 

385 'is_directory_template': False, 

386 } 

387 except (OSError, PermissionError) as e: # pragma: no cover 

388 raise TemplateNotFoundError(template_name=template_name, search_path=str(self._templates_root)) from e 

389 

390 def get_template_directory_info(self, directory_name: str) -> dict[str, Any]: 

391 """Get metadata about a template directory without fully loading it. 

392 

393 Args: 

394 directory_name: Name of the template directory 

395 

396 Returns: 

397 Dictionary with directory metadata 

398 

399 Raises: 

400 TemplateDirectoryNotFoundError: If directory doesn't exist 

401 

402 """ 

403 directory_path = self._templates_root / directory_name 

404 

405 if not directory_path.exists() or not directory_path.is_dir(): 405 ↛ 409line 405 didn't jump to line 409 because the condition on line 405 was always true

406 msg = f"Template directory '{directory_name}' not found in {self._templates_root}" 

407 raise TemplateDirectoryNotFoundError(msg) 

408 

409 try: 

410 # Count template files 

411 template_count = sum(1 for f in directory_path.rglob('*.md') if f.is_file()) 

412 

413 # Get directory stats 

414 stat = directory_path.stat() 

415 

416 return { 

417 'name': directory_name, 

418 'directory_path': str(directory_path), 

419 'template_count': template_count, 

420 'modified_time': stat.st_mtime, 

421 'is_directory_template': True, 

422 } 

423 except (OSError, PermissionError) as e: # pragma: no cover 

424 msg = f"Failed to get info for template directory '{directory_name}': {e}" 

425 raise TemplateDirectoryNotFoundError(msg) from e