blob: d33f8e5ea9e4e76e7078b92474506926dd94ae11 [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 kdanilov7f59d562016-12-26 01:34:23 +02009import sqlite3
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020010from typing import Any, TypeVar, Type, IO, Tuple, cast, List, Dict, Iterable, Iterator
koder aka kdanilov39e449e2016-12-17 15:15:26 +020011
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
koder aka kdanilov7f59d562016-12-26 01:34:23 +020019from .result_classes import Storable, IStorable
koder aka kdanilov22d134e2016-11-08 11:33:19 +020020
21
koder aka kdanilov22d134e2016-11-08 11:33:19 +020022class ISimpleStorage(metaclass=abc.ABCMeta):
23 """interface for low-level storage, which doesn't support serialization
24 and can operate only on bytes"""
25
26 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020027 def put(self, value: bytes, path: str) -> None:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020028 pass
29
30 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020031 def get(self, path: str) -> bytes:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020032 pass
33
34 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020035 def rm(self, path: str) -> None:
36 pass
37
38 @abc.abstractmethod
39 def sync(self) -> None:
koder aka kdanilov73084622016-11-16 21:51:08 +020040 pass
41
42 @abc.abstractmethod
koder aka kdanilov22d134e2016-11-08 11:33:19 +020043 def __contains__(self, path: str) -> bool:
44 pass
45
46 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020047 def get_fd(self, path: str, mode: str = "rb+") -> IO:
koder aka kdanilov39e449e2016-12-17 15:15:26 +020048 pass
49
50 @abc.abstractmethod
51 def sub_storage(self, path: str) -> 'ISimpleStorage':
koder aka kdanilov22d134e2016-11-08 11:33:19 +020052 pass
53
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020054 @abc.abstractmethod
55 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
56 pass
57
koder aka kdanilov22d134e2016-11-08 11:33:19 +020058
59class ISerializer(metaclass=abc.ABCMeta):
60 """Interface for serialization class"""
61 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020062 def pack(self, value: Storable) -> bytes:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020063 pass
64
65 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020066 def unpack(self, data: bytes) -> Any:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020067 pass
68
69
koder aka kdanilov7f59d562016-12-26 01:34:23 +020070class DBStorage(ISimpleStorage):
71
72 create_tb_sql = "CREATE TABLE IF NOT EXISTS wally_storage (key text, data blob, type text)"
73 insert_sql = "INSERT INTO wally_storage VALUES (?, ?, ?)"
74 update_sql = "UPDATE wally_storage SET data=?, type=? WHERE key=?"
75 select_sql = "SELECT data, type FROM wally_storage WHERE key=?"
76 contains_sql = "SELECT 1 FROM wally_storage WHERE key=?"
77 rm_sql = "DELETE FROM wally_storage WHERE key LIKE '{}%'"
78 list2_sql = "SELECT key, length(data), type FROM wally_storage"
79
80 def __init__(self, db_path: str = None, existing: bool = False,
81 prefix: str = None, db: sqlite3.Connection = None) -> None:
82
83 assert not prefix or "'" not in prefix, "Broken sql prefix {!r}".format(prefix)
84
85 if db_path:
86 self.existing = existing
87 if existing:
88 if not os.path.isfile(db_path):
89 raise IOError("No storage found at {!r}".format(db_path))
90
91 os.makedirs(os.path.dirname(db_path), exist_ok=True)
92 try:
93 self.db = sqlite3.connect(db_path)
94 except sqlite3.OperationalError as exc:
95 raise IOError("Can't open database at {!r}".format(db_path)) from exc
96
97 self.db.execute(self.create_tb_sql)
98 else:
99 if db is None:
100 raise ValueError("Either db or db_path parameter must be passed")
101 self.db = db
102
103 if prefix is None:
104 self.prefix = ""
105 elif not prefix.endswith('/'):
106 self.prefix = prefix + '/'
107 else:
108 self.prefix = prefix
109
110 def put(self, value: bytes, path: str) -> None:
111 c = self.db.cursor()
112 fpath = self.prefix + path
113 c.execute(self.contains_sql, (fpath,))
114 if len(c.fetchall()) == 0:
115 c.execute(self.insert_sql, (fpath, value, 'yaml'))
116 else:
117 c.execute(self.update_sql, (value, 'yaml', fpath))
118
119 def get(self, path: str) -> bytes:
120 c = self.db.cursor()
121 c.execute(self.select_sql, (self.prefix + path,))
122 res = cast(List[Tuple[bytes, str]], c.fetchall()) # type: List[Tuple[bytes, str]]
123 if not res:
124 raise KeyError(path)
125 assert len(res) == 1
126 val, tp = res[0]
127 assert tp == 'yaml'
128 return val
129
130 def rm(self, path: str) -> None:
131 c = self.db.cursor()
132 path = self.prefix + path
133 assert "'" not in path, "Broken sql path {!r}".format(path)
134 c.execute(self.rm_sql.format(path))
135
136 def __contains__(self, path: str) -> bool:
137 c = self.db.cursor()
138 path = self.prefix + path
139 c.execute(self.contains_sql, (self.prefix + path,))
140 return len(c.fetchall()) != 0
141
142 def print_tree(self):
143 c = self.db.cursor()
144 c.execute(self.list2_sql)
145 data = list(c.fetchall())
146 data.sort()
147 print("------------------ DB ---------------------")
148 for key, data_ln, type in data:
149 print(key, data_ln, type)
150 print("------------------ END --------------------")
151
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200152 def sub_storage(self, path: str) -> 'DBStorage':
153 return self.__class__(prefix=self.prefix + path, db=self.db)
154
155 def sync(self):
156 self.db.commit()
157
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200158 def get_fd(self, path: str, mode: str = "rb+") -> IO[bytes]:
159 raise NotImplementedError("SQLITE3 doesn't provide fd-like interface")
160
161 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
162 raise NotImplementedError("SQLITE3 doesn't provide list method")
163
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200164
165DB_REL_PATH = "__db__.db"
166
167
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200168class FSStorage(ISimpleStorage):
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200169 """Store all data in files on FS"""
170
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200171 def __init__(self, root_path: str, existing: bool) -> None:
172 self.root_path = root_path
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200173 self.existing = existing
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200174 self.ignored = {self.j(DB_REL_PATH), '.', '..'}
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200175
176 def j(self, path: str) -> str:
177 return os.path.join(self.root_path, path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200178
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200179 def put(self, value: bytes, path: str) -> None:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200180 jpath = self.j(path)
181 os.makedirs(os.path.dirname(jpath), exist_ok=True)
182 with open(jpath, "wb") as fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200183 fd.write(value)
184
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200185 def get(self, path: str) -> bytes:
koder aka kdanilov73084622016-11-16 21:51:08 +0200186 try:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200187 with open(self.j(path), "rb") as fd:
188 return fd.read()
189 except FileNotFoundError as exc:
190 raise KeyError(path) from exc
koder aka kdanilov73084622016-11-16 21:51:08 +0200191
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200192 def rm(self, path: str) -> None:
193 if os.path.isdir(path):
194 shutil.rmtree(path, ignore_errors=True)
195 elif os.path.exists(path):
196 os.unlink(path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200197
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200198 def __contains__(self, path: str) -> bool:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200199 return os.path.exists(self.j(path))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200200
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200201 def get_fd(self, path: str, mode: str = "rb+") -> IO[bytes]:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200202 jpath = self.j(path)
203
204 if "cb" == mode:
205 create_on_fail = True
206 mode = "rb+"
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200207 os.makedirs(os.path.dirname(jpath), exist_ok=True)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200208 else:
209 create_on_fail = False
210
211 try:
212 fd = open(jpath, mode)
213 except IOError:
214 if not create_on_fail:
215 raise
216 fd = open(jpath, "wb")
217
218 return cast(IO[bytes], fd)
219
220 def sub_storage(self, path: str) -> 'FSStorage':
221 return self.__class__(self.j(path), self.existing)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200222
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200223 def sync(self):
224 pass
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200225
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200226 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
227 for fobj in os.scandir(self.j(path)):
228 if fobj.path not in self.ignored:
229 if fobj.is_dir():
230 yield False, fobj.name
231 else:
232 yield True, fobj.name
233
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200234
235class YAMLSerializer(ISerializer):
236 """Serialize data to yaml"""
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200237 def pack(self, value: Storable) -> bytes:
238 try:
239 return yaml.dump(value, Dumper=Dumper, encoding="utf8")
240 except Exception as exc:
241 raise ValueError("Can't pickle object {!r} to yaml".format(type(value))) from exc
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200242
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200243 def unpack(self, data: bytes) -> Any:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200244 return yaml.load(data, Loader=Loader)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200245
246
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200247class SAFEYAMLSerializer(ISerializer):
248 """Serialize data to yaml"""
249 def pack(self, value: Storable) -> bytes:
250 try:
251 return yaml.safe_dump(value, encoding="utf8")
252 except Exception as exc:
253 raise ValueError("Can't pickle object {!r} to yaml".format(type(value))) from exc
254
255 def unpack(self, data: bytes) -> Any:
256 return yaml.safe_load(data)
257
258
259ObjClass = TypeVar('ObjClass', bound=IStorable)
260
261
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200262class _Raise:
263 pass
264
265
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200266class Storage:
267 """interface for storage"""
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200268 def __init__(self, fs_storage: ISimpleStorage, db_storage: ISimpleStorage, serializer: ISerializer) -> None:
269 self.fs = fs_storage
270 self.db = db_storage
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200271 self.serializer = serializer
272
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200273 def sub_storage(self, *path: str) -> 'Storage':
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200274 fpath = "/".join(path)
275 return self.__class__(self.fs.sub_storage(fpath), self.db.sub_storage(fpath), self.serializer)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200276
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200277 def put(self, value: Storable, *path: str) -> None:
278 dct_value = value.raw() if isinstance(value, IStorable) else value
279 serialized = self.serializer.pack(dct_value)
280 fpath = "/".join(path)
281 self.db.put(serialized, fpath)
282 self.fs.put(serialized, fpath)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200283
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200284 def put_list(self, value: Iterable[IStorable], *path: str) -> None:
285 serialized = self.serializer.pack([obj.raw() for obj in value])
286 fpath = "/".join(path)
287 self.db.put(serialized, fpath)
288 self.fs.put(serialized, fpath)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200289
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200290 def get(self, path: str, default: Any = _Raise) -> Any:
291 try:
292 vl = self.db.get(path)
293 except:
294 if default is _Raise:
295 raise
296 return default
297
298 return self.serializer.unpack(vl)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200299
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200300 def rm(self, *path: str) -> None:
301 fpath = "/".join(path)
302 self.fs.rm(fpath)
303 self.db.rm(fpath)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200304
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200305 def __contains__(self, path: str) -> bool:
306 return path in self.fs or path in self.db
koder aka kdanilov73084622016-11-16 21:51:08 +0200307
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200308 def put_raw(self, val: bytes, *path: str) -> None:
309 self.fs.put(val, "/".join(path))
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200310
311 def get_raw(self, *path: str) -> bytes:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200312 return self.fs.get("/".join(path))
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200313
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200314 def append_raw(self, value: bytes, *path: str) -> None:
315 with self.fs.get_fd("/".join(path), "rb+") as fd:
316 fd.seek(offset=0, whence=os.SEEK_END)
317 fd.write(value)
318
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200319 def get_fd(self, path: str, mode: str = "r") -> IO:
320 return self.fs.get_fd(path, mode)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200321
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200322 def put_array(self, value: array.array, *path: str) -> None:
323 with self.get_fd("/".join(path), "wb") as fd:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200324 value.tofile(fd) # type: ignore
325
326 def get_array(self, typecode: str, *path: str) -> array.array:
327 res = array.array(typecode)
328 path_s = "/".join(path)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200329 with self.get_fd(path_s, "rb") as fd:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200330 fd.seek(0, os.SEEK_END)
331 size = fd.tell()
332 fd.seek(0, os.SEEK_SET)
333 assert size % res.itemsize == 0, "Storage object at path {} contains no array of {} or corrupted."\
334 .format(path_s, typecode)
335 res.fromfile(fd, size // res.itemsize) # type: ignore
336 return res
337
338 def append(self, value: array.array, *path: str) -> None:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200339 with self.get_fd("/".join(path), "cb") as fd:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200340 fd.seek(0, os.SEEK_END)
341 value.tofile(fd) # type: ignore
342
koder aka kdanilov70227062016-11-26 23:23:21 +0200343 def load_list(self, obj_class: Type[ObjClass], *path: str) -> List[ObjClass]:
344 path_s = "/".join(path)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200345 raw_val = cast(List[Dict[str, Any]], self.get(path_s))
koder aka kdanilov73084622016-11-16 21:51:08 +0200346 assert isinstance(raw_val, list)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200347 return [obj_class.fromraw(val) for val in raw_val]
koder aka kdanilov73084622016-11-16 21:51:08 +0200348
koder aka kdanilov70227062016-11-26 23:23:21 +0200349 def load(self, obj_class: Type[ObjClass], *path: str) -> ObjClass:
350 path_s = "/".join(path)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200351 return obj_class.fromraw(self.get(path_s))
koder aka kdanilov73084622016-11-16 21:51:08 +0200352
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200353 def sync(self) -> None:
354 self.db.sync()
355 self.fs.sync()
koder aka kdanilov73084622016-11-16 21:51:08 +0200356
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200357 def __enter__(self) -> 'Storage':
358 return self
359
360 def __exit__(self, x: Any, y: Any, z: Any) -> None:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200361 self.sync()
koder aka kdanilov70227062016-11-26 23:23:21 +0200362
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200363 def list(self, *path: str) -> Iterator[Tuple[bool, str]]:
364 return self.fs.list("/".join(path))
365
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200366
367def make_storage(url: str, existing: bool = False) -> Storage:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200368 return Storage(FSStorage(url, existing),
369 DBStorage(os.path.join(url, DB_REL_PATH)),
370 SAFEYAMLSerializer())
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200371