Coverage for src/blob_dict/dict/path.py: 0%
99 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-07 08:32 -0700
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-07 08:32 -0700
1import shutil
2from abc import abstractmethod
3from collections.abc import Iterator
4from mimetypes import guess_type
5from pathlib import Path
6from typing import Any, Protocol, override
8from extratools_core.typing import PathLike
10from ..blob import BytesBlob, StrBlob
11from ..blob.json import JsonDictBlob, YamlDictBlob
12from . import BlobDictBase
15class LocalPath(Path):
16 def rmtree(self) -> None:
17 shutil.rmtree(self)
20class ExtraPathLike(PathLike, Protocol):
21 @abstractmethod
22 def rmtree(self) -> None:
23 ...
26class PathBlobDict(BlobDictBase):
27 def __init__(
28 self,
29 path: ExtraPathLike,
30 *,
31 compression: bool = False,
32 blob_class: type[BytesBlob] = BytesBlob,
33 blob_class_args: dict[str, Any] | None = None,
34 ) -> None:
35 super().__init__()
37 self.__path: ExtraPathLike = path
39 self.__compression: bool = compression
41 self.__blob_class: type[BytesBlob] = blob_class
42 self.__blob_class_args: dict[str, Any] = blob_class_args or {}
44 def create(self) -> None:
45 self.__path.mkdir(
46 parents=True,
47 exist_ok=True,
48 )
50 def delete(self) -> None:
51 self.__path.rmtree()
53 @override
54 def __contains__(self, key: str) -> bool:
55 return (self.__path / key).is_file()
57 def __get_blob_class(self, key: str) -> type[BytesBlob]: # noqa: PLR0911
58 mime_type: str | None
59 mime_type, _ = guess_type(self.__path / key)
61 match mime_type:
62 case "application/json":
63 return JsonDictBlob
64 case "application/octet-stream":
65 return BytesBlob
66 case "application/yaml":
67 return YamlDictBlob
68 case "audo/mpeg":
69 # Import here as it has optional dependency
70 from ..blob.audio import AudioBlob # noqa: PLC0415
72 return AudioBlob
73 case "image/png":
74 # Import here as it has optional dependency
75 from ..blob.image import ImageBlob # noqa: PLC0415
77 return ImageBlob
78 case (
79 "text/css"
80 | "text/csv"
81 | "text/html"
82 | "text/javascript"
83 | "text/markdown"
84 | "text/plain"
85 | "text/xml"
86 ):
87 return StrBlob
88 case "video/mp4":
89 # Import here as it has optional dependency
90 from ..blob.video import VideoBlob # noqa: PLC0415
92 return VideoBlob
93 case _:
94 return self.__blob_class
96 @override
97 def get(self, key: str, default: BytesBlob | None = None) -> BytesBlob | None:
98 if key not in self:
99 return default
101 blob_bytes: bytes = (self.__path / key).read_bytes()
103 blob: BytesBlob = BytesBlob.from_bytes(blob_bytes, compression=self.__compression)
104 return blob.as_blob(
105 self.__get_blob_class(key),
106 self.__blob_class_args,
107 )
109 @override
110 def __iter__(self) -> Iterator[str]:
111 # The concept of relative path does not exist for `CloudPath`,
112 # and each walked path is always absolute for `CloudPath`.
113 # Therefore, we extract each key by removing the path prefix.
114 # In this way, the same logic works for both absolute and relative path.
115 prefix_len: int = (
116 len(str(self.__path))
117 # Extra 1 is for separator `/` between prefix and filename
118 + 1
119 )
121 for parent, _, files in self.__path.walk(top_down=False):
122 for filename in files:
123 yield str(parent / filename)[prefix_len:]
125 @override
126 def clear(self) -> None:
127 for parent, dirs, files in self.__path.walk(top_down=False):
128 for filename in files:
129 (parent / filename).unlink()
130 for dirname in dirs:
131 (parent / dirname).rmdir()
133 def __cleanup(self, key: str) -> None:
134 (self.__path / key).unlink()
136 for parent in (self.__path / key).parents:
137 if parent == self.__path:
138 return
140 if parent.is_dir() and next(iter(parent.iterdir()), None) is None:
141 parent.rmdir()
143 @override
144 def pop(self, key: str, default: BytesBlob | None = None) -> BytesBlob | None:
145 blob: BytesBlob | None = self.get(key)
146 if blob:
147 self.__cleanup(key)
149 return blob or default
151 @override
152 def __delitem__(self, key: str) -> None:
153 if key not in self:
154 raise KeyError
156 self.__cleanup(key)
158 __BAD_BLOB_CLASS_ERROR_MESSAGE: str = "Must specify blob that is instance of {blob_class}"
160 @override
161 def __setitem__(self, key: str, blob: BytesBlob) -> None:
162 if not isinstance(blob, self.__blob_class):
163 raise TypeError(PathBlobDict.__BAD_BLOB_CLASS_ERROR_MESSAGE.format(
164 blob_class=self.__blob_class,
165 ))
167 (self.__path / key).parent.mkdir(
168 parents=True,
169 exist_ok=True,
170 )
172 blob_bytes: bytes = blob.as_bytes(compression=self.__compression)
173 (self.__path / key).write_bytes(blob_bytes)