blob: 6879dcf583a9158c45982db2f4a74d2d0cf05d44 [file] [log] [blame]
koder aka kdanilov22d134e2016-11-08 11:33:19 +02001"""
2This module contains interfaces for storage classes
3"""
4
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +02005import os
koder aka kdanilov22d134e2016-11-08 11:33:19 +02006import abc
koder aka kdanilov39e449e2016-12-17 15:15:26 +02007import array
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +02008import shutil
koder aka kdanilov39e449e2016-12-17 15:15:26 +02009from typing import Any, Iterator, TypeVar, Type, IO, Tuple, cast, List, Dict, Union, Iterable
10
11
12import yaml
13try:
14 from yaml import CLoader as Loader, CDumper as Dumper # type: ignore
15except ImportError:
16 from yaml import Loader, Dumper # type: ignore
koder aka kdanilov22d134e2016-11-08 11:33:19 +020017
18
19class IStorable(metaclass=abc.ABCMeta):
20 """Interface for type, which can be stored"""
koder aka kdanilov22d134e2016-11-08 11:33:19 +020021
koder aka kdanilov39e449e2016-12-17 15:15:26 +020022basic_types = {list, dict, tuple, set, type(None), int, str, bytes, bool, float}
23for btype in basic_types:
24 # pylint: disable=E1101
25 IStorable.register(btype) # type: ignore
koder aka kdanilov22d134e2016-11-08 11:33:19 +020026
27
28ObjClass = TypeVar('ObjClass')
29
30
koder aka kdanilov22d134e2016-11-08 11:33:19 +020031class ISimpleStorage(metaclass=abc.ABCMeta):
32 """interface for low-level storage, which doesn't support serialization
33 and can operate only on bytes"""
34
35 @abc.abstractmethod
koder aka kdanilov22d134e2016-11-08 11:33:19 +020036 def __setitem__(self, path: str, value: bytes) -> None:
37 pass
38
39 @abc.abstractmethod
40 def __getitem__(self, path: str) -> bytes:
41 pass
42
43 @abc.abstractmethod
koder aka kdanilov73084622016-11-16 21:51:08 +020044 def __delitem__(self, path: str) -> None:
45 pass
46
47 @abc.abstractmethod
koder aka kdanilov22d134e2016-11-08 11:33:19 +020048 def __contains__(self, path: str) -> bool:
49 pass
50
51 @abc.abstractmethod
koder aka kdanilov39e449e2016-12-17 15:15:26 +020052 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020053 pass
54
55 @abc.abstractmethod
koder aka kdanilov39e449e2016-12-17 15:15:26 +020056 def get_stream(self, path: str, mode: str = "rb+") -> IO:
57 pass
58
59 @abc.abstractmethod
60 def sub_storage(self, path: str) -> 'ISimpleStorage':
koder aka kdanilov22d134e2016-11-08 11:33:19 +020061 pass
62
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020063 @abc.abstractmethod
64 def clear(self, path: str) -> None:
65 pass
66
koder aka kdanilov22d134e2016-11-08 11:33:19 +020067
68class ISerializer(metaclass=abc.ABCMeta):
69 """Interface for serialization class"""
70 @abc.abstractmethod
71 def pack(self, value: IStorable) -> bytes:
72 pass
73
74 @abc.abstractmethod
75 def unpack(self, data: bytes) -> IStorable:
76 pass
77
78
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020079class FSStorage(ISimpleStorage):
koder aka kdanilov22d134e2016-11-08 11:33:19 +020080 """Store all data in files on FS"""
81
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020082 def __init__(self, root_path: str, existing: bool) -> None:
83 self.root_path = root_path
koder aka kdanilov39e449e2016-12-17 15:15:26 +020084 self.existing = existing
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020085 if existing:
86 if not os.path.isdir(self.root_path):
koder aka kdanilov39e449e2016-12-17 15:15:26 +020087 raise IOError("No storage found at {!r}".format(root_path))
88
89 def j(self, path: str) -> str:
90 return os.path.join(self.root_path, path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020091
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020092 def __setitem__(self, path: str, value: bytes) -> None:
koder aka kdanilov39e449e2016-12-17 15:15:26 +020093 jpath = self.j(path)
94 os.makedirs(os.path.dirname(jpath), exist_ok=True)
95 with open(jpath, "wb") as fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020096 fd.write(value)
97
koder aka kdanilov73084622016-11-16 21:51:08 +020098 def __delitem__(self, path: str) -> None:
99 try:
100 os.unlink(path)
101 except FileNotFoundError:
102 pass
103
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200104 def __getitem__(self, path: str) -> bytes:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200105 with open(self.j(path), "rb") as fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200106 return fd.read()
107
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200108 def __contains__(self, path: str) -> bool:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200109 return os.path.exists(self.j(path))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200110
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200111 def list(self, path: str = "") -> Iterator[Tuple[bool, str]]:
112 jpath = self.j(path)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200113 if not os.path.exists(jpath):
114 return
115
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200116 for entry in os.scandir(jpath):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200117 if not entry.name in ('..', '.'):
118 yield entry.is_file(), entry.name
119
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200120 def get_stream(self, path: str, mode: str = "rb+") -> IO[bytes]:
121 jpath = self.j(path)
122
123 if "cb" == mode:
124 create_on_fail = True
125 mode = "rb+"
126 else:
127 create_on_fail = False
128
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200129 os.makedirs(os.path.dirname(jpath), exist_ok=True)
130
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200131 try:
132 fd = open(jpath, mode)
133 except IOError:
134 if not create_on_fail:
135 raise
136 fd = open(jpath, "wb")
137
138 return cast(IO[bytes], fd)
139
140 def sub_storage(self, path: str) -> 'FSStorage':
141 return self.__class__(self.j(path), self.existing)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200142
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200143 def clear(self, path: str) -> None:
144 if os.path.exists(path):
145 shutil.rmtree(self.j(path))
146
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200147
148class YAMLSerializer(ISerializer):
149 """Serialize data to yaml"""
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200150 def pack(self, value: Any) -> bytes:
151 if type(value) not in basic_types:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200152 # for name, val in value.__dict__.items():
153 # if type(val) not in basic_types:
154 # raise ValueError(("Can't pack {!r}. Attribute {} has value {!r} (type: {}), but only" +
155 # " basic types accepted as attributes").format(value, name, val, type(val)))
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200156 value = value.__dict__
157 return yaml.dump(value, Dumper=Dumper, encoding="utf8")
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200158
koder aka kdanilov73084622016-11-16 21:51:08 +0200159 def unpack(self, data: bytes) -> IStorable:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200160 return yaml.load(data, Loader=Loader)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200161
162
163class Storage:
164 """interface for storage"""
koder aka kdanilov73084622016-11-16 21:51:08 +0200165 def __init__(self, storage: ISimpleStorage, serializer: ISerializer) -> None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200166 self.storage = storage
167 self.serializer = serializer
168
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200169 def sub_storage(self, *path: str) -> 'Storage':
170 return self.__class__(self.storage.sub_storage("/".join(path)), self.serializer)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200171
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200172 def __setitem__(self, path: Union[str, Iterable[str]], value: Any) -> None:
173 if not isinstance(path, str):
174 path = "/".join(path)
175
176 self.storage[path] = self.serializer.pack(cast(IStorable, value))
177
178 def __getitem__(self, path: Union[str, Iterable[str]]) -> IStorable:
179 if not isinstance(path, str):
180 path = "/".join(path)
181
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200182 return self.serializer.unpack(self.storage[path])
183
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200184 def __delitem__(self, path: Union[str, Iterable[str]]) -> None:
185 if not isinstance(path, str):
186 path = "/".join(path)
koder aka kdanilov73084622016-11-16 21:51:08 +0200187 del self.storage[path]
188
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200189 def __contains__(self, path: Union[str, Iterable[str]]) -> bool:
190 if not isinstance(path, str):
191 path = "/".join(path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200192 return path in self.storage
193
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200194 def store_raw(self, val: bytes, *path: str) -> None:
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200195 self.storage["/".join(path)] = val
196
197 def clear(self, *path: str) -> None:
198 self.storage.clear("/".join(path))
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200199
200 def get_raw(self, *path: str) -> bytes:
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200201 return self.storage["/".join(path)]
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200202
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200203 def list(self, *path: str) -> Iterator[Tuple[bool, str]]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200204 return self.storage.list("/".join(path))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200205
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200206 def set_array(self, value: array.array, *path: str) -> None:
207 with self.get_stream("/".join(path), "wb") as fd:
208 value.tofile(fd) # type: ignore
209
210 def get_array(self, typecode: str, *path: str) -> array.array:
211 res = array.array(typecode)
212 path_s = "/".join(path)
213 with self.get_stream(path_s, "rb") as fd:
214 fd.seek(0, os.SEEK_END)
215 size = fd.tell()
216 fd.seek(0, os.SEEK_SET)
217 assert size % res.itemsize == 0, "Storage object at path {} contains no array of {} or corrupted."\
218 .format(path_s, typecode)
219 res.fromfile(fd, size // res.itemsize) # type: ignore
220 return res
221
222 def append(self, value: array.array, *path: str) -> None:
223 with self.get_stream("/".join(path), "cb") as fd:
224 fd.seek(0, os.SEEK_END)
225 value.tofile(fd) # type: ignore
226
227 def construct(self, path: str, raw_val: Dict, obj_class: Type[ObjClass]) -> ObjClass:
228 "Internal function, used to construct user type from raw unpacked value"
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200229 if obj_class in (int, str, dict, list, None):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200230 raise ValueError("Can't load into build-in value - {!r} into type {}")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200231
232 if not isinstance(raw_val, dict):
233 raise ValueError("Can't load path {!r} into python type. Raw value not dict".format(path))
234
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200235 if not all(isinstance(key, str) for key in raw_val.keys()):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200236 raise ValueError("Can't load path {!r} into python type.".format(path) +
237 "Raw not all keys in raw value is strings")
238
koder aka kdanilov73084622016-11-16 21:51:08 +0200239 obj = obj_class.__new__(obj_class) # type: ObjClass
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200240 obj.__dict__.update(raw_val)
241 return obj
242
koder aka kdanilov70227062016-11-26 23:23:21 +0200243 def load_list(self, obj_class: Type[ObjClass], *path: str) -> List[ObjClass]:
244 path_s = "/".join(path)
245 raw_val = self[path_s]
koder aka kdanilov73084622016-11-16 21:51:08 +0200246 assert isinstance(raw_val, list)
koder aka kdanilov70227062016-11-26 23:23:21 +0200247 return [self.construct(path_s, val, obj_class) for val in cast(list, raw_val)]
koder aka kdanilov73084622016-11-16 21:51:08 +0200248
koder aka kdanilov70227062016-11-26 23:23:21 +0200249 def load(self, obj_class: Type[ObjClass], *path: str) -> ObjClass:
250 path_s = "/".join(path)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200251 return self.construct(path_s, cast(Dict, self[path_s]), obj_class)
koder aka kdanilov73084622016-11-16 21:51:08 +0200252
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200253 def get_stream(self, path: str, mode: str = "r") -> IO:
254 return self.storage.get_stream(path, mode)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200255
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200256 def get(self, path: Union[str, Iterable[str]], default: Any = None) -> Any:
257 if not isinstance(path, str):
258 path = "/".join(path)
259
koder aka kdanilov73084622016-11-16 21:51:08 +0200260 try:
261 return self[path]
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200262 except Exception:
koder aka kdanilov73084622016-11-16 21:51:08 +0200263 return default
264
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200265 def __enter__(self) -> 'Storage':
266 return self
267
268 def __exit__(self, x: Any, y: Any, z: Any) -> None:
269 return
koder aka kdanilov70227062016-11-26 23:23:21 +0200270
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200271
272def make_storage(url: str, existing: bool = False) -> Storage:
273 return Storage(FSStorage(url, existing), YAMLSerializer())
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200274