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
« 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."""
3from pathlib import Path
4from typing import Any
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
19class FileTemplateRepository(TemplateRepositoryPort):
20 """File-based implementation of template repository."""
22 def __init__(self, templates_root: Path | str) -> None:
23 """Initialize repository with templates root directory.
25 Args:
26 templates_root: Path to the root templates directory
28 Raises:
29 TemplateDirectoryNotFoundError: If root directory doesn't exist
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)
40 def get_template(self, template_name: str) -> Template:
41 """Load a single template by name.
43 Args:
44 template_name: Name of the template (without .md extension)
46 Returns:
47 Template object
49 Raises:
50 TemplateNotFoundError: If template file doesn't exist
52 """
53 # Look for template file directly in templates root
54 template_file = self._templates_root / f'{template_name}.md'
56 if not template_file.exists():
57 raise TemplateNotFoundError(template_name=template_name, search_path=str(self._templates_root))
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
67 def get_template_directory(self, directory_name: str) -> TemplateDirectory:
68 """Load a template directory by name.
70 Args:
71 directory_name: Name of the template directory
73 Returns:
74 TemplateDirectory object
76 Raises:
77 TemplateDirectoryNotFoundError: If directory doesn't exist
79 """
80 directory_path = self._templates_root / directory_name
82 if not directory_path.exists():
83 msg = f"Template directory '{directory_name}' not found in {self._templates_root}"
84 raise TemplateDirectoryNotFoundError(msg)
86 if not directory_path.is_dir():
87 msg = f"Template directory path '{directory_path}' is not a directory"
88 raise TemplateDirectoryNotFoundError(msg)
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
97 @classmethod
98 def list_templates(cls, search_path: Path) -> list[Template]:
99 """List all individual templates in the search path.
101 Args:
102 search_path: Directory path to search for templates
104 Returns:
105 List of Template instances (excludes directory templates)
107 Raises:
108 TemplateDirectoryNotFoundError: If search_path does not exist
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)
115 if not search_path.is_dir():
116 msg = f'Search path is not a directory: {search_path}'
117 raise TemplateDirectoryNotFoundError(msg)
119 templates = []
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
132 logging.getLogger(__name__).warning('Skipping invalid template file %s: %s', template_file, e)
133 continue
135 return sorted(templates, key=lambda t: t.name)
137 def list_template_directories(self, search_path: Path) -> list[TemplateDirectory]:
138 """List all template directories in the search path.
140 Args:
141 search_path: Directory path to search for template directories
143 Returns:
144 List of TemplateDirectory instances
146 Raises:
147 TemplateDirectoryNotFoundError: If search_path does not exist
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)
154 if not search_path.is_dir():
155 msg = f'Search path is not a directory: {search_path}'
156 raise TemplateDirectoryNotFoundError(msg)
158 directories = []
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
170 logging.getLogger(__name__).warning('Skipping invalid template directory %s: %s', item, e)
171 continue
173 return sorted(directories, key=lambda d: d.name)
175 def list_all_template_names(self) -> list[str]:
176 """List all single template names available in the templates root.
178 Returns:
179 List of template names (without .md extension)
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 ]
187 return sorted(template_names)
189 def list_all_template_directory_names(self) -> list[str]:
190 """List all template directory names available in the templates root.
192 Returns:
193 List of template directory names
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 ]
203 return sorted(directory_names)
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.
209 Args:
210 name: Template name (without .md extension)
211 search_path: Directory path to search for templates
213 Returns:
214 Template instance if found, None otherwise
216 Raises:
217 TemplateDirectoryNotFoundError: If search_path does not exist
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)
224 if not search_path.is_dir():
225 msg = f'Search path is not a directory: {search_path}'
226 raise TemplateDirectoryNotFoundError(msg)
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
237 def find_template_directory(self, name: str, search_path: Path) -> TemplateDirectory | None:
238 """Find a template directory by name.
240 Args:
241 name: Directory name
242 search_path: Parent directory to search in
244 Returns:
245 TemplateDirectory instance if found, None otherwise
247 Raises:
248 TemplateDirectoryNotFoundError: If search_path does not exist
250 """
251 if not search_path.exists():
252 msg = f'Search path does not exist: {search_path}'
253 raise TemplateDirectoryNotFoundError(msg)
255 if not search_path.is_dir():
256 msg = f'Search path is not a directory: {search_path}'
257 raise TemplateDirectoryNotFoundError(msg)
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
267 @classmethod
268 def load_template_content(cls, template_path: Path) -> str:
269 """Load raw content from a template file.
271 Args:
272 template_path: Absolute path to template file
274 Returns:
275 Raw template content as string
277 Raises:
278 TemplateNotFoundError: If template file does not exist
279 PermissionError: If template file is not readable
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))
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
291 @classmethod
292 def validate_template_path(cls, path: Path) -> bool:
293 """Validate that a path points to a valid template location.
295 Args:
296 path: Path to validate
298 Returns:
299 True if path is valid for template operations
301 """
302 return path.exists() and path.is_file() and path.suffix == '.md'
304 def template_exists(self, template_name: str) -> bool:
305 """Check if a single template exists.
307 Args:
308 template_name: Name of the template
310 Returns:
311 True if template exists, False otherwise
313 """
314 template_file = self._templates_root / f'{template_name}.md'
315 return template_file.exists() and template_file.is_file()
317 def template_directory_exists(self, directory_name: str) -> bool:
318 """Check if a template directory exists.
320 Args:
321 directory_name: Name of the template directory
323 Returns:
324 True if directory exists and contains templates, False otherwise
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 )
332 def get_templates_root(self) -> Path:
333 """Get the root directory for templates.
335 Returns:
336 Path to templates root directory
338 """
339 return self._templates_root
341 @staticmethod
342 def _directory_contains_templates(directory_path: Path) -> bool:
343 """Check if a directory contains any template files.
345 Args:
346 directory_path: Path to directory to check
348 Returns:
349 True if directory contains .md files, False otherwise
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
358 def get_template_info(self, template_name: str) -> dict[str, Any]:
359 """Get metadata about a template without fully loading it.
361 Args:
362 template_name: Name of the template
364 Returns:
365 Dictionary with template metadata
367 Raises:
368 TemplateNotFoundError: If template doesn't exist
370 """
371 template_file = self._templates_root / f'{template_name}.md'
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))
376 try:
377 # Get file stats
378 stat = template_file.stat()
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
390 def get_template_directory_info(self, directory_name: str) -> dict[str, Any]:
391 """Get metadata about a template directory without fully loading it.
393 Args:
394 directory_name: Name of the template directory
396 Returns:
397 Dictionary with directory metadata
399 Raises:
400 TemplateDirectoryNotFoundError: If directory doesn't exist
402 """
403 directory_path = self._templates_root / directory_name
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)
409 try:
410 # Count template files
411 template_count = sum(1 for f in directory_path.rglob('*.md') if f.is_file())
413 # Get directory stats
414 stat = directory_path.stat()
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