Coverage for src/blob_dict/dict/path.py: 0%
82 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-29 08:37 -0700
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-29 08:37 -0700
1import shutil
2from collections.abc import Iterator
3from pathlib import Path
4from typing import Any, override
6from cloudpathlib import CloudPath
8from ..blob import BytesBlob, StrBlob
9from ..blob.json import JsonDictBlob
10from . import BlobDictBase
13class LocalPath(Path):
14 def rmtree(self) -> None:
15 shutil.rmtree(self)
18class PathBlobDict(BlobDictBase):
19 def __init__(
20 self,
21 path: LocalPath | CloudPath,
22 *,
23 compression: bool = False,
24 blob_class: type[BytesBlob] = BytesBlob,
25 blob_class_args: dict[str, Any] | None = None,
26 ) -> None:
27 super().__init__()
29 self.__path: LocalPath | CloudPath = path
31 self.__compression: bool = compression
33 self.__blob_class: type[BytesBlob] = blob_class
34 self.__blob_class_args: dict[str, Any] = blob_class_args or {}
36 def create(self) -> None:
37 self.__path.mkdir(
38 parents=True,
39 exist_ok=True,
40 )
42 def delete(self) -> None:
43 self.__path.rmtree()
45 @override
46 def __contains__(self, key: str) -> bool:
47 return (self.__path / key).is_file()
49 def __get_blob_class(self, key: str) -> type[BytesBlob]:
50 match (self.__path / key).suffix.lower():
51 case ".json":
52 return JsonDictBlob
53 case ".png":
54 # Import here as it has optional dependency
55 from ..blob.image import ImageBlob # noqa: PLC0415
57 return ImageBlob
58 # Common text file extensions
59 # https://en.wikipedia.org/wiki/List_of_file_formats
60 case (
61 ".asc"
62 | ".bib"
63 | ".cfg"
64 | ".cnf"
65 | ".conf"
66 | ".csv"
67 | ".diff"
68 | ".htm"
69 | ".html"
70 | ".ini"
71 | ".log"
72 | ".markdown"
73 | ".md"
74 | ".tex"
75 | ".text"
76 | ".toml"
77 | ".tsv"
78 | ".txt"
79 | ".xhtml"
80 | ".xht"
81 | ".xml"
82 | ".yaml"
83 | ".yml"
84 ):
85 return StrBlob
86 case _:
87 return self.__blob_class
89 @override
90 def get(self, key: str, default: BytesBlob | None = None) -> BytesBlob | None:
91 if key not in self:
92 return default
94 blob_bytes: bytes = (self.__path / key).read_bytes()
96 blob: BytesBlob = BytesBlob.from_bytes(blob_bytes, compression=self.__compression)
97 return blob.as_blob(
98 self.__get_blob_class(key),
99 self.__blob_class_args,
100 )
102 @override
103 def __iter__(self) -> Iterator[str]:
104 # The concept of relative path does not exist for `CloudPath`,
105 # and each walked path is always absolute for `CloudPath`.
106 # Therefore, we extract each key by removing the path prefix.
107 # In this way, the same logic works for both absolute and relative path.
108 prefix_len: int = (
109 len(str(self.__path))
110 # Extra 1 is for separator `/` between prefix and filename
111 + 1
112 )
114 for parent, _, files in self.__path.walk(top_down=False):
115 for filename in files:
116 yield str(parent / filename)[prefix_len:]
118 @override
119 def clear(self) -> None:
120 for parent, dirs, files in self.__path.walk(top_down=False):
121 for filename in files:
122 (parent / filename).unlink()
123 for dirname in dirs:
124 (parent / dirname).rmdir()
126 def __cleanup(self, key: str) -> None:
127 (self.__path / key).unlink()
129 for parent in (self.__path / key).parents:
130 if parent == self.__path:
131 return
133 if parent.is_dir() and next(parent.iterdir(), None) is None:
134 parent.rmdir()
136 @override
137 def pop(self, key: str, default: BytesBlob | None = None) -> BytesBlob | None:
138 blob: BytesBlob | None = self.get(key)
139 if blob:
140 self.__cleanup(key)
142 return blob or default
144 @override
145 def __delitem__(self, key: str) -> None:
146 if key not in self:
147 raise KeyError
149 self.__cleanup(key)
151 __BAD_BLOB_CLASS_ERROR_MESSAGE: str = "Must specify blob that is instance of {blob_class}"
153 @override
154 def __setitem__(self, key: str, blob: BytesBlob) -> None:
155 if not isinstance(blob, self.__blob_class):
156 raise TypeError(PathBlobDict.__BAD_BLOB_CLASS_ERROR_MESSAGE.format(
157 blob_class=self.__blob_class,
158 ))
160 (self.__path / key).parent.mkdir(
161 parents=True,
162 exist_ok=True,
163 )
165 blob_bytes: bytes = blob.as_bytes(compression=self.__compression)
166 (self.__path / key).write_bytes(blob_bytes)