blob: 93a7cddb9b40bcf84c2c36e1b11a89a2db8a3cc4 [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
8from typing import Any, Iterator, TypeVar, Type, IO, Tuple, cast, List, Dict, Union, Iterable
9
10
11import yaml
12try:
13 from yaml import CLoader as Loader, CDumper as Dumper # type: ignore
14except ImportError:
15 from yaml import Loader, Dumper # type: ignore
koder aka kdanilov22d134e2016-11-08 11:33:19 +020016
17
18class IStorable(metaclass=abc.ABCMeta):
19 """Interface for type, which can be stored"""
koder aka kdanilov22d134e2016-11-08 11:33:19 +020020
koder aka kdanilov39e449e2016-12-17 15:15:26 +020021basic_types = {list, dict, tuple, set, type(None), int, str, bytes, bool, float}
22for btype in basic_types:
23 # pylint: disable=E1101
24 IStorable.register(btype) # type: ignore
koder aka kdanilov22d134e2016-11-08 11:33:19 +020025
26
27ObjClass = TypeVar('ObjClass')
28
29
koder aka kdanilov22d134e2016-11-08 11:33:19 +020030class ISimpleStorage(metaclass=abc.ABCMeta):
31 """interface for low-level storage, which doesn't support serialization
32 and can operate only on bytes"""
33
34 @abc.abstractmethod
koder aka kdanilov22d134e2016-11-08 11:33:19 +020035 def __setitem__(self, path: str, value: bytes) -> None:
36 pass
37
38 @abc.abstractmethod
39 def __getitem__(self, path: str) -> bytes:
40 pass
41
42 @abc.abstractmethod
koder aka kdanilov73084622016-11-16 21:51:08 +020043 def __delitem__(self, path: str) -> None:
44 pass
45
46 @abc.abstractmethod
koder aka kdanilov22d134e2016-11-08 11:33:19 +020047 def __contains__(self, path: str) -> bool:
48 pass
49
50 @abc.abstractmethod
koder aka kdanilov39e449e2016-12-17 15:15:26 +020051 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020052 pass
53
54 @abc.abstractmethod
koder aka kdanilov39e449e2016-12-17 15:15:26 +020055 def get_stream(self, path: str, mode: str = "rb+") -> IO:
56 pass
57
58 @abc.abstractmethod
59 def sub_storage(self, path: str) -> 'ISimpleStorage':
koder aka kdanilov22d134e2016-11-08 11:33:19 +020060 pass
61
62
63class ISerializer(metaclass=abc.ABCMeta):
64 """Interface for serialization class"""
65 @abc.abstractmethod
66 def pack(self, value: IStorable) -> bytes:
67 pass
68
69 @abc.abstractmethod
70 def unpack(self, data: bytes) -> IStorable:
71 pass
72
73
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020074class FSStorage(ISimpleStorage):
koder aka kdanilov22d134e2016-11-08 11:33:19 +020075 """Store all data in files on FS"""
76
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020077 def __init__(self, root_path: str, existing: bool) -> None:
78 self.root_path = root_path
koder aka kdanilov39e449e2016-12-17 15:15:26 +020079 self.existing = existing
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020080 if existing:
81 if not os.path.isdir(self.root_path):
koder aka kdanilov39e449e2016-12-17 15:15:26 +020082 raise IOError("No storage found at {!r}".format(root_path))
83
84 def j(self, path: str) -> str:
85 return os.path.join(self.root_path, path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020086
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020087 def __setitem__(self, path: str, value: bytes) -> None:
koder aka kdanilov39e449e2016-12-17 15:15:26 +020088 jpath = self.j(path)
89 os.makedirs(os.path.dirname(jpath), exist_ok=True)
90 with open(jpath, "wb") as fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020091 fd.write(value)
92
koder aka kdanilov73084622016-11-16 21:51:08 +020093 def __delitem__(self, path: str) -> None:
94 try:
95 os.unlink(path)
96 except FileNotFoundError:
97 pass
98
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020099 def __getitem__(self, path: str) -> bytes:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200100 with open(self.j(path), "rb") as fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200101 return fd.read()
102
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200103 def __contains__(self, path: str) -> bool:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200104 return os.path.exists(self.j(path))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200105
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200106 def list(self, path: str = "") -> Iterator[Tuple[bool, str]]:
107 jpath = self.j(path)
108 for entry in os.scandir(jpath):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200109 if not entry.name in ('..', '.'):
110 yield entry.is_file(), entry.name
111
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200112 def get_stream(self, path: str, mode: str = "rb+") -> IO[bytes]:
113 jpath = self.j(path)
114
115 if "cb" == mode:
116 create_on_fail = True
117 mode = "rb+"
118 else:
119 create_on_fail = False
120
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200121 os.makedirs(os.path.dirname(jpath), exist_ok=True)
122
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200123 try:
124 fd = open(jpath, mode)
125 except IOError:
126 if not create_on_fail:
127 raise
128 fd = open(jpath, "wb")
129
130 return cast(IO[bytes], fd)
131
132 def sub_storage(self, path: str) -> 'FSStorage':
133 return self.__class__(self.j(path), self.existing)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200134
135
136class YAMLSerializer(ISerializer):
137 """Serialize data to yaml"""
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200138 def pack(self, value: Any) -> bytes:
139 if type(value) not in basic_types:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200140 # for name, val in value.__dict__.items():
141 # if type(val) not in basic_types:
142 # raise ValueError(("Can't pack {!r}. Attribute {} has value {!r} (type: {}), but only" +
143 # " basic types accepted as attributes").format(value, name, val, type(val)))
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200144 value = value.__dict__
145 return yaml.dump(value, Dumper=Dumper, encoding="utf8")
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200146
koder aka kdanilov73084622016-11-16 21:51:08 +0200147 def unpack(self, data: bytes) -> IStorable:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200148 return yaml.load(data, Loader=Loader)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200149
150
151class Storage:
152 """interface for storage"""
koder aka kdanilov73084622016-11-16 21:51:08 +0200153 def __init__(self, storage: ISimpleStorage, serializer: ISerializer) -> None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200154 self.storage = storage
155 self.serializer = serializer
156
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200157 def sub_storage(self, *path: str) -> 'Storage':
158 return self.__class__(self.storage.sub_storage("/".join(path)), self.serializer)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200159
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200160 def __setitem__(self, path: Union[str, Iterable[str]], value: Any) -> None:
161 if not isinstance(path, str):
162 path = "/".join(path)
163
164 self.storage[path] = self.serializer.pack(cast(IStorable, value))
165
166 def __getitem__(self, path: Union[str, Iterable[str]]) -> IStorable:
167 if not isinstance(path, str):
168 path = "/".join(path)
169
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200170 return self.serializer.unpack(self.storage[path])
171
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200172 def __delitem__(self, path: Union[str, Iterable[str]]) -> None:
173 if not isinstance(path, str):
174 path = "/".join(path)
175
koder aka kdanilov73084622016-11-16 21:51:08 +0200176 del self.storage[path]
177
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200178 def __contains__(self, path: Union[str, Iterable[str]]) -> bool:
179 if not isinstance(path, str):
180 path = "/".join(path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200181 return path in self.storage
182
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200183 def store_raw(self, val: bytes, *path: str) -> None:
184 if not isinstance(path, str):
185 path = "/".join(path)
186 self.storage[path] = val
187
188 def get_raw(self, *path: str) -> bytes:
189 if not isinstance(path, str):
190 path = "/".join(path)
191 return self.storage[path]
192
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200193 def list(self, *path: str) -> Iterator[Tuple[bool, str]]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200194 return self.storage.list("/".join(path))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200195
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200196 def set_array(self, value: array.array, *path: str) -> None:
197 with self.get_stream("/".join(path), "wb") as fd:
198 value.tofile(fd) # type: ignore
199
200 def get_array(self, typecode: str, *path: str) -> array.array:
201 res = array.array(typecode)
202 path_s = "/".join(path)
203 with self.get_stream(path_s, "rb") as fd:
204 fd.seek(0, os.SEEK_END)
205 size = fd.tell()
206 fd.seek(0, os.SEEK_SET)
207 assert size % res.itemsize == 0, "Storage object at path {} contains no array of {} or corrupted."\
208 .format(path_s, typecode)
209 res.fromfile(fd, size // res.itemsize) # type: ignore
210 return res
211
212 def append(self, value: array.array, *path: str) -> None:
213 with self.get_stream("/".join(path), "cb") as fd:
214 fd.seek(0, os.SEEK_END)
215 value.tofile(fd) # type: ignore
216
217 def construct(self, path: str, raw_val: Dict, obj_class: Type[ObjClass]) -> ObjClass:
218 "Internal function, used to construct user type from raw unpacked value"
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200219 if obj_class in (int, str, dict, list, None):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200220 raise ValueError("Can't load into build-in value - {!r} into type {}")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200221
222 if not isinstance(raw_val, dict):
223 raise ValueError("Can't load path {!r} into python type. Raw value not dict".format(path))
224
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200225 if not all(isinstance(key, str) for key in raw_val.keys()):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200226 raise ValueError("Can't load path {!r} into python type.".format(path) +
227 "Raw not all keys in raw value is strings")
228
koder aka kdanilov73084622016-11-16 21:51:08 +0200229 obj = obj_class.__new__(obj_class) # type: ObjClass
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200230 obj.__dict__.update(raw_val)
231 return obj
232
koder aka kdanilov70227062016-11-26 23:23:21 +0200233 def load_list(self, obj_class: Type[ObjClass], *path: str) -> List[ObjClass]:
234 path_s = "/".join(path)
235 raw_val = self[path_s]
koder aka kdanilov73084622016-11-16 21:51:08 +0200236 assert isinstance(raw_val, list)
koder aka kdanilov70227062016-11-26 23:23:21 +0200237 return [self.construct(path_s, val, obj_class) for val in cast(list, raw_val)]
koder aka kdanilov73084622016-11-16 21:51:08 +0200238
koder aka kdanilov70227062016-11-26 23:23:21 +0200239 def load(self, obj_class: Type[ObjClass], *path: str) -> ObjClass:
240 path_s = "/".join(path)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200241 return self.construct(path_s, cast(Dict, self[path_s]), obj_class)
koder aka kdanilov73084622016-11-16 21:51:08 +0200242
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200243 def get_stream(self, path: str, mode: str = "r") -> IO:
244 return self.storage.get_stream(path, mode)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200245
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200246 def get(self, path: Union[str, Iterable[str]], default: Any = None) -> Any:
247 if not isinstance(path, str):
248 path = "/".join(path)
249
koder aka kdanilov73084622016-11-16 21:51:08 +0200250 try:
251 return self[path]
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200252 except Exception:
koder aka kdanilov73084622016-11-16 21:51:08 +0200253 return default
254
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200255 def __enter__(self) -> 'Storage':
256 return self
257
258 def __exit__(self, x: Any, y: Any, z: Any) -> None:
259 return
koder aka kdanilov70227062016-11-26 23:23:21 +0200260
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200261
262def make_storage(url: str, existing: bool = False) -> Storage:
263 return Storage(FSStorage(url, existing), YAMLSerializer())
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200264