Coverage for src/blob_dict/dict/git.py: 0%

83 statements  

« prev     ^ index     » next       coverage.py v7.8.1, created at 2025-05-29 23:07 -0700

1from collections.abc import Iterator, MutableMapping 

2from datetime import UTC, datetime, timedelta 

3from typing import Any, Literal, override 

4 

5from extratools_git.repo import Repo 

6 

7from ..blob import BytesBlob 

8from .path import LocalPath, PathBlobDict 

9 

10 

11class GitBlobDict(PathBlobDict): 

12 def __init__( 

13 self, 

14 path: LocalPath | None = None, 

15 *, 

16 user_name: str, 

17 user_email: str, 

18 use_remote: bool = False, 

19 use_remote_frequence: timedelta = timedelta(minutes=1), 

20 **kwargs: Any, 

21 ) -> None: 

22 if path is None: 

23 path = LocalPath(".") 

24 

25 self.__repo_path: LocalPath = path.expanduser() 

26 

27 self.__repo: Repo = Repo.init( 

28 path, 

29 user_name=user_name, 

30 user_email=user_email, 

31 ) 

32 self.__user_name: str = user_name 

33 self.__user_email: str = user_email 

34 

35 self.__use_remote: bool = use_remote 

36 self.__use_remote_frequence: timedelta = use_remote_frequence 

37 self.__last_use_remote_time: datetime = datetime.now(UTC) - use_remote_frequence 

38 

39 super().__init__(self.__repo_path, **kwargs) 

40 

41 @override 

42 def create(self) -> None: 

43 super().create() 

44 

45 Repo.init( 

46 self.__repo_path, 

47 user_name=self.__user_name, 

48 user_email=self.__user_email, 

49 ) 

50 

51 @staticmethod 

52 def is_forbidden_key(key: str) -> bool: 

53 return key in {".git", ".gitignore"} or key.startswith(".git/") 

54 

55 __FORBIDDEN_KEY_ERROR_MESSAGE: str = "Cannot use any Git reserved file name as key" 

56 

57 @override 

58 def __contains__(self, key: object) -> bool: 

59 if self.is_forbidden_key(str(key)): 

60 raise ValueError(self.__FORBIDDEN_KEY_ERROR_MESSAGE) 

61 

62 return super().__contains__(key) 

63 

64 def __can_use_remote(self) -> bool: 

65 return ( 

66 self.__use_remote 

67 and datetime.now(UTC) - self.__last_use_remote_time >= self.__use_remote_frequence 

68 ) 

69 

70 @override 

71 def __getitem__(self, key: str | tuple[str, Any], /) -> BytesBlob: 

72 if self.__can_use_remote(): 

73 self.__repo.pull(background=True) 

74 

75 if isinstance(key, str): 

76 return super().__getitem__(key) 

77 

78 try: 

79 key, version = key 

80 

81 return self._get( 

82 key, 

83 self.__repo.get_blob(key, version=version), 

84 ) 

85 except FileNotFoundError as e: 

86 raise KeyError from e 

87 

88 @override 

89 def __iter__(self) -> Iterator[str]: 

90 for child_path in self.__repo_path.iterdir(): 

91 if self.is_forbidden_key(child_path.name): 

92 continue 

93 

94 if child_path.is_dir(): 

95 for parent, _, files in child_path.walk(): 

96 for filename in files: 

97 yield str((parent / filename).relative_to(self.__repo_path)) 

98 else: 

99 yield str(child_path.relative_to(self.__repo_path)) 

100 

101 @override 

102 def clear(self) -> None: 

103 # We want to depend on `MutableMapping`'s default implementation based on `__delitem__` here 

104 # Otherwise, it will depend on `PathBlobDict`'s own implementation and will skip Git update 

105 # 

106 # Note that each deleted item will create one commit 

107 MutableMapping.clear(self) 

108 

109 @override 

110 def pop[T]( 

111 self, 

112 key: str, 

113 /, 

114 default: BytesBlob | T | Literal['__DEFAULT'] = "__DEFAULT", 

115 ) -> BytesBlob | T: 

116 # We want to depend on `MutableMapping`'s default implementation based on `__delitem__` here 

117 # Otherwise, it will depend on `PathBlobDict`'s own implementation and will skip Git update 

118 if default == "__DEFAULT": 

119 return MutableMapping.pop(self, key) 

120 return MutableMapping.pop(self, key, default) 

121 

122 @override 

123 def __delitem__(self, key: str, /) -> None: 

124 if self.is_forbidden_key(key): 

125 raise ValueError(self.__FORBIDDEN_KEY_ERROR_MESSAGE) 

126 

127 super().__delitem__(key) 

128 

129 self.__repo.stage(key) 

130 self.__repo.commit(f"Delete {key}") 

131 

132 if self.__can_use_remote(): 

133 self.__repo.push(background=True) 

134 

135 @override 

136 def __setitem__(self, key: str, blob: BytesBlob, /) -> None: 

137 if self.is_forbidden_key(key): 

138 raise ValueError(self.__FORBIDDEN_KEY_ERROR_MESSAGE) 

139 

140 existing_blob: BytesBlob | None = self.get(key) 

141 if existing_blob == blob: 

142 return 

143 

144 super().__setitem__(key, blob) 

145 

146 self.__repo.stage(key) 

147 self.__repo.commit(f"{"Update" if existing_blob else "Add"} {key}") 

148 

149 if self.__can_use_remote(): 

150 self.__repo.push(background=True)