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
« 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
5from extratools_git.repo import Repo
7from ..blob import BytesBlob
8from .path import LocalPath, PathBlobDict
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(".")
25 self.__repo_path: LocalPath = path.expanduser()
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
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
39 super().__init__(self.__repo_path, **kwargs)
41 @override
42 def create(self) -> None:
43 super().create()
45 Repo.init(
46 self.__repo_path,
47 user_name=self.__user_name,
48 user_email=self.__user_email,
49 )
51 @staticmethod
52 def is_forbidden_key(key: str) -> bool:
53 return key in {".git", ".gitignore"} or key.startswith(".git/")
55 __FORBIDDEN_KEY_ERROR_MESSAGE: str = "Cannot use any Git reserved file name as key"
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)
62 return super().__contains__(key)
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 )
70 @override
71 def __getitem__(self, key: str | tuple[str, Any], /) -> BytesBlob:
72 if self.__can_use_remote():
73 self.__repo.pull(background=True)
75 if isinstance(key, str):
76 return super().__getitem__(key)
78 try:
79 key, version = key
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
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
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))
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)
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)
122 @override
123 def __delitem__(self, key: str, /) -> None:
124 if self.is_forbidden_key(key):
125 raise ValueError(self.__FORBIDDEN_KEY_ERROR_MESSAGE)
127 super().__delitem__(key)
129 self.__repo.stage(key)
130 self.__repo.commit(f"Delete {key}")
132 if self.__can_use_remote():
133 self.__repo.push(background=True)
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)
140 existing_blob: BytesBlob | None = self.get(key)
141 if existing_blob == blob:
142 return
144 super().__setitem__(key, blob)
146 self.__repo.stage(key)
147 self.__repo.commit(f"{"Update" if existing_blob else "Add"} {key}")
149 if self.__can_use_remote():
150 self.__repo.push(background=True)