blob: a17e3c0bab2adaa861dc8516249194a6236142a6 [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)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200108 if not os.path.exists(jpath):
109 return
110
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200111 for entry in os.scandir(jpath):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200112 if not entry.name in ('..', '.'):
113 yield entry.is_file(), entry.name
114
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200115 def get_stream(self, path: str, mode: str = "rb+") -> IO[bytes]:
116 jpath = self.j(path)
117
118 if "cb" == mode:
119 create_on_fail = True
120 mode = "rb+"
121 else:
122 create_on_fail = False
123
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200124 os.makedirs(os.path.dirname(jpath), exist_ok=True)
125
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200126 try:
127 fd = open(jpath, mode)
128 except IOError:
129 if not create_on_fail:
130 raise
131 fd = open(jpath, "wb")
132
133 return cast(IO[bytes], fd)
134
135 def sub_storage(self, path: str) -> 'FSStorage':
136 return self.__class__(self.j(path), self.existing)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200137
138
139class YAMLSerializer(ISerializer):
140 """Serialize data to yaml"""
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200141 def pack(self, value: Any) -> bytes:
142 if type(value) not in basic_types:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200143 # for name, val in value.__dict__.items():
144 # if type(val) not in basic_types:
145 # raise ValueError(("Can't pack {!r}. Attribute {} has value {!r} (type: {}), but only" +
146 # " basic types accepted as attributes").format(value, name, val, type(val)))
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200147 value = value.__dict__
148 return yaml.dump(value, Dumper=Dumper, encoding="utf8")
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200149
koder aka kdanilov73084622016-11-16 21:51:08 +0200150 def unpack(self, data: bytes) -> IStorable:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200151 return yaml.load(data, Loader=Loader)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200152
153
154class Storage:
155 """interface for storage"""
koder aka kdanilov73084622016-11-16 21:51:08 +0200156 def __init__(self, storage: ISimpleStorage, serializer: ISerializer) -> None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200157 self.storage = storage
158 self.serializer = serializer
159
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200160 def sub_storage(self, *path: str) -> 'Storage':
161 return self.__class__(self.storage.sub_storage("/".join(path)), self.serializer)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200162
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200163 def __setitem__(self, path: Union[str, Iterable[str]], value: Any) -> None:
164 if not isinstance(path, str):
165 path = "/".join(path)
166
167 self.storage[path] = self.serializer.pack(cast(IStorable, value))
168
169 def __getitem__(self, path: Union[str, Iterable[str]]) -> IStorable:
170 if not isinstance(path, str):
171 path = "/".join(path)
172
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200173 return self.serializer.unpack(self.storage[path])
174
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200175 def __delitem__(self, path: Union[str, Iterable[str]]) -> None:
176 if not isinstance(path, str):
177 path = "/".join(path)
178
koder aka kdanilov73084622016-11-16 21:51:08 +0200179 del self.storage[path]
180
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200181 def __contains__(self, path: Union[str, Iterable[str]]) -> bool:
182 if not isinstance(path, str):
183 path = "/".join(path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200184 return path in self.storage
185
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200186 def store_raw(self, val: bytes, *path: str) -> None:
187 if not isinstance(path, str):
188 path = "/".join(path)
189 self.storage[path] = val
190
191 def get_raw(self, *path: str) -> bytes:
192 if not isinstance(path, str):
193 path = "/".join(path)
194 return self.storage[path]
195
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200196 def list(self, *path: str) -> Iterator[Tuple[bool, str]]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200197 return self.storage.list("/".join(path))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200198
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200199 def set_array(self, value: array.array, *path: str) -> None:
200 with self.get_stream("/".join(path), "wb") as fd:
201 value.tofile(fd) # type: ignore
202
203 def get_array(self, typecode: str, *path: str) -> array.array:
204 res = array.array(typecode)
205 path_s = "/".join(path)
206 with self.get_stream(path_s, "rb") as fd:
207 fd.seek(0, os.SEEK_END)
208 size = fd.tell()
209 fd.seek(0, os.SEEK_SET)
210 assert size % res.itemsize == 0, "Storage object at path {} contains no array of {} or corrupted."\
211 .format(path_s, typecode)
212 res.fromfile(fd, size // res.itemsize) # type: ignore
213 return res
214
215 def append(self, value: array.array, *path: str) -> None:
216 with self.get_stream("/".join(path), "cb") as fd:
217 fd.seek(0, os.SEEK_END)
218 value.tofile(fd) # type: ignore
219
220 def construct(self, path: str, raw_val: Dict, obj_class: Type[ObjClass]) -> ObjClass:
221 "Internal function, used to construct user type from raw unpacked value"
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200222 if obj_class in (int, str, dict, list, None):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200223 raise ValueError("Can't load into build-in value - {!r} into type {}")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200224
225 if not isinstance(raw_val, dict):
226 raise ValueError("Can't load path {!r} into python type. Raw value not dict".format(path))
227
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200228 if not all(isinstance(key, str) for key in raw_val.keys()):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200229 raise ValueError("Can't load path {!r} into python type.".format(path) +
230 "Raw not all keys in raw value is strings")
231
koder aka kdanilov73084622016-11-16 21:51:08 +0200232 obj = obj_class.__new__(obj_class) # type: ObjClass
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200233 obj.__dict__.update(raw_val)
234 return obj
235
koder aka kdanilov70227062016-11-26 23:23:21 +0200236 def load_list(self, obj_class: Type[ObjClass], *path: str) -> List[ObjClass]:
237 path_s = "/".join(path)
238 raw_val = self[path_s]
koder aka kdanilov73084622016-11-16 21:51:08 +0200239 assert isinstance(raw_val, list)
koder aka kdanilov70227062016-11-26 23:23:21 +0200240 return [self.construct(path_s, val, obj_class) for val in cast(list, raw_val)]
koder aka kdanilov73084622016-11-16 21:51:08 +0200241
koder aka kdanilov70227062016-11-26 23:23:21 +0200242 def load(self, obj_class: Type[ObjClass], *path: str) -> ObjClass:
243 path_s = "/".join(path)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200244 return self.construct(path_s, cast(Dict, self[path_s]), obj_class)
koder aka kdanilov73084622016-11-16 21:51:08 +0200245
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200246 def get_stream(self, path: str, mode: str = "r") -> IO:
247 return self.storage.get_stream(path, mode)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200248
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200249 def get(self, path: Union[str, Iterable[str]], default: Any = None) -> Any:
250 if not isinstance(path, str):
251 path = "/".join(path)
252
koder aka kdanilov73084622016-11-16 21:51:08 +0200253 try:
254 return self[path]
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200255 except Exception:
koder aka kdanilov73084622016-11-16 21:51:08 +0200256 return default
257
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200258 def __enter__(self) -> 'Storage':
259 return self
260
261 def __exit__(self, x: Any, y: Any, z: Any) -> None:
262 return
koder aka kdanilov70227062016-11-26 23:23:21 +0200263
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200264
265def make_storage(url: str, existing: bool = False) -> Storage:
266 return Storage(FSStorage(url, existing), YAMLSerializer())
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200267