Coverage for src/blob_dict/dict/git.py: 0%
83 statements
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-21 20:53 -0700
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-21 20:53 -0700
1from collections.abc import Iterator
2from datetime import UTC, datetime, timedelta
3from typing import Any, 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,
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 self.__repo_path: LocalPath = path.expanduser()
24 self.__repo: Repo = Repo.init(
25 path,
26 user_name=user_name,
27 user_email=user_email,
28 )
29 self.__user_name: str = user_name
30 self.__user_email: str = user_email
32 self.__use_remote: bool = use_remote
33 self.__use_remote_frequence: timedelta = use_remote_frequence
34 self.__last_use_remote_time: datetime = datetime.now(UTC) - use_remote_frequence
36 super().__init__(self.__repo_path, **kwargs)
38 @override
39 def create(self) -> None:
40 super().create()
42 Repo.init(
43 self.__repo_path,
44 user_name=self.__user_name,
45 user_email=self.__user_email,
46 )
48 @staticmethod
49 def is_forbidden_key(key: str) -> bool:
50 return key in {".git", ".gitignore"} or key.startswith(".git/")
52 __FORBIDDEN_KEY_ERROR_MESSAGE: str = "Cannot use any Git reserved file name as key"
54 @override
55 def __contains__(self, key: str) -> bool:
56 if self.is_forbidden_key(key):
57 raise ValueError(self.__FORBIDDEN_KEY_ERROR_MESSAGE)
59 return super().__contains__(key)
61 def __can_use_remote(self) -> bool:
62 return (
63 self.__use_remote
64 and datetime.now(UTC) - self.__last_use_remote_time >= self.__use_remote_frequence
65 )
67 @override
68 def __getitem__(self, key: str | tuple[str, Any], /) -> BytesBlob:
69 if self.__can_use_remote():
70 self.__repo.pull(background=True)
72 if isinstance(key, str):
73 return super().__getitem__(key)
75 try:
76 key, version = key
78 return self._get(
79 key,
80 self.__repo.get_blob(key, version=version),
81 )
82 except FileNotFoundError as e:
83 raise KeyError from e
85 @override
86 def __iter__(self) -> Iterator[str]:
87 for child_path in self.__repo_path.iterdir():
88 if self.is_forbidden_key(child_path.name):
89 continue
91 if child_path.is_dir():
92 for parent, _, files in child_path.walk():
93 for filename in files:
94 yield str((parent / filename).relative_to(self.__repo_path))
95 else:
96 yield str(child_path.relative_to(self.__repo_path))
98 @override
99 def pop(self, key: str, /, default: BytesBlob | None = None) -> BytesBlob | None:
100 if self.is_forbidden_key(key):
101 raise ValueError(self.__FORBIDDEN_KEY_ERROR_MESSAGE)
103 result: BytesBlob | None = super().pop(key, default)
104 if result is None:
105 return None
107 self.__repo.stage(key)
108 self.__repo.commit(f"Delete {key}")
110 if self.__can_use_remote():
111 self.__repo.push(background=True)
113 return result
115 @override
116 def __delitem__(self, key: str, /) -> None:
117 if self.is_forbidden_key(key):
118 raise ValueError(self.__FORBIDDEN_KEY_ERROR_MESSAGE)
120 super().__delitem__(key)
122 self.__repo.stage(key)
123 self.__repo.commit(f"Delete {key}")
125 if self.__can_use_remote():
126 self.__repo.push(background=True)
128 @override
129 def __setitem__(self, key: str, blob: BytesBlob, /) -> None:
130 if self.is_forbidden_key(key):
131 raise ValueError(self.__FORBIDDEN_KEY_ERROR_MESSAGE)
133 exists: bool = key in self
135 super().__setitem__(key, blob)
137 self.__repo.stage(key)
138 self.__repo.commit(f"{"Update" if exists else "Add"} {key}")
140 if self.__can_use_remote():
141 self.__repo.push(background=True)