Coverage for src/prosemark/adapters/daily_repo_fs.py: 100%

32 statements  

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

1# Copyright (c) 2024 Prosemark Contributors 

2# This software is licensed under the MIT License 

3 

4"""File system implementation of DailyRepo for freeform writing files.""" 

5 

6from datetime import datetime 

7from pathlib import Path 

8 

9from prosemark.adapters.frontmatter_codec import FrontmatterCodec 

10from prosemark.exceptions import FileSystemError 

11from prosemark.ports.clock import Clock 

12from prosemark.ports.daily_repo import DailyRepo 

13from prosemark.ports.id_generator import IdGenerator 

14 

15 

16class DailyRepoFs(DailyRepo): 

17 """File system implementation of DailyRepo for freeform writing files. 

18 

19 This adapter manages the creation of timestamped freeform writing files 

20 outside the binder structure with: 

21 - Filename format: YYYYMMDDTHHMM_<uuid7>.md 

22 - Optional title in YAML frontmatter 

23 - Standalone markdown files for quick writing 

24 - Configurable daily directory for organization 

25 

26 Example file structure: 

27 ``` 

28 daily/ 

29 ├── 20250920T0830_0192f0c1-2345-7123-8abc-def012345678.md 

30 ├── 20250920T1430_0192f0c1-2345-7456-8abc-def012345678.md 

31 └── ... 

32 ``` 

33 

34 File content format: 

35 ```yaml 

36 --- 

37 title: "Morning Thoughts" 

38 id: "0192f0c1-2345-7123-8abc-def012345678" 

39 created: "2025-09-20T08:30:00Z" 

40 --- 

41 # Freeform Writing 

42 

43 Content goes here... 

44 ``` 

45 

46 The adapter ensures: 

47 - Unique filenames with timestamp and UUID components 

48 - Consistent frontmatter structure 

49 - Directory creation as needed 

50 - Proper error handling for file system operations 

51 """ 

52 

53 def __init__( 

54 self, 

55 daily_path: Path, 

56 id_generator: IdGenerator, 

57 clock: Clock, 

58 ) -> None: 

59 """Initialize repository with daily directory and dependencies. 

60 

61 Args: 

62 daily_path: Directory for storing daily freeform files 

63 id_generator: ID generator for creating unique identifiers 

64 clock: Clock for timestamp generation 

65 

66 """ 

67 self.daily_path = daily_path 

68 self.id_generator = id_generator 

69 self.clock = clock 

70 self.frontmatter_codec = FrontmatterCodec() 

71 

72 def write_freeform(self, title: str | None = None) -> str: 

73 """Create a new timestamped freewrite file. 

74 

75 Args: 

76 title: Optional title to include in the file's frontmatter 

77 

78 Returns: 

79 The filename of the created freewrite file 

80 

81 Raises: 

82 FileSystemError: If the file cannot be created 

83 

84 """ 

85 try: 

86 # Generate unique ID and timestamp 

87 unique_id = self.id_generator.new() 

88 now_iso = self.clock.now_iso() 

89 

90 # Create timestamp prefix for filename 

91 # Parse ISO format to extract timestamp components 

92 timestamp = datetime.fromisoformat(now_iso) 

93 timestamp_prefix = timestamp.strftime('%Y%m%dT%H%M') 

94 

95 # Build filename 

96 filename = f'{timestamp_prefix}_{unique_id}.md' 

97 file_path = self.daily_path / filename 

98 

99 # Ensure directory exists 

100 self.daily_path.mkdir(parents=True, exist_ok=True) 

101 

102 # Prepare frontmatter 

103 frontmatter = { 

104 'id': str(unique_id), 

105 'created': now_iso, 

106 } 

107 

108 if title: 

109 frontmatter['title'] = title 

110 

111 # Create content with frontmatter 

112 content_body = '\n# Freeform Writing\n\n' 

113 full_content = self.frontmatter_codec.generate(frontmatter, content_body) 

114 

115 # Write file 

116 file_path.write_text(full_content, encoding='utf-8') 

117 except OSError as exc: 

118 msg = f'Cannot create freeform file: {exc}' 

119 raise FileSystemError(msg) from exc 

120 else: 

121 return filename