blob: e4e010c2205af82a1a8efd9ae3a1f5183af5339c [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 kdanilovf2865172016-12-30 03:35:11 +020010import threading
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020011from typing import Any, TypeVar, Type, IO, Tuple, cast, List, Dict, Iterable, Iterator
koder aka kdanilov39e449e2016-12-17 15:15:26 +020012
13import yaml
14try:
15 from yaml import CLoader as Loader, CDumper as Dumper # type: ignore
16except ImportError:
17 from yaml import Loader, Dumper # type: ignore
koder aka kdanilov22d134e2016-11-08 11:33:19 +020018
19
koder aka kdanilovf2865172016-12-30 03:35:11 +020020from .result_classes import IStorable
koder aka kdanilov22d134e2016-11-08 11:33:19 +020021
22
koder aka kdanilov22d134e2016-11-08 11:33:19 +020023class ISimpleStorage(metaclass=abc.ABCMeta):
24 """interface for low-level storage, which doesn't support serialization
25 and can operate only on bytes"""
26
27 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020028 def put(self, value: bytes, path: str) -> None:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020029 pass
30
31 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020032 def get(self, path: str) -> bytes:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020033 pass
34
35 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020036 def rm(self, path: str) -> None:
37 pass
38
39 @abc.abstractmethod
40 def sync(self) -> None:
koder aka kdanilov73084622016-11-16 21:51:08 +020041 pass
42
43 @abc.abstractmethod
koder aka kdanilov22d134e2016-11-08 11:33:19 +020044 def __contains__(self, path: str) -> bool:
45 pass
46
47 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020048 def get_fd(self, path: str, mode: str = "rb+") -> IO:
koder aka kdanilov39e449e2016-12-17 15:15:26 +020049 pass
50
51 @abc.abstractmethod
52 def sub_storage(self, path: str) -> 'ISimpleStorage':
koder aka kdanilov22d134e2016-11-08 11:33:19 +020053 pass
54
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020055 @abc.abstractmethod
56 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
57 pass
58
koder aka kdanilov22d134e2016-11-08 11:33:19 +020059
60class ISerializer(metaclass=abc.ABCMeta):
61 """Interface for serialization class"""
62 @abc.abstractmethod
koder aka kdanilovf2865172016-12-30 03:35:11 +020063 def pack(self, value: IStorable) -> bytes:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020064 pass
65
66 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020067 def unpack(self, data: bytes) -> Any:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020068 pass
69
70
koder aka kdanilov7f59d562016-12-26 01:34:23 +020071class DBStorage(ISimpleStorage):
72
73 create_tb_sql = "CREATE TABLE IF NOT EXISTS wally_storage (key text, data blob, type text)"
74 insert_sql = "INSERT INTO wally_storage VALUES (?, ?, ?)"
75 update_sql = "UPDATE wally_storage SET data=?, type=? WHERE key=?"
76 select_sql = "SELECT data, type FROM wally_storage WHERE key=?"
77 contains_sql = "SELECT 1 FROM wally_storage WHERE key=?"
78 rm_sql = "DELETE FROM wally_storage WHERE key LIKE '{}%'"
79 list2_sql = "SELECT key, length(data), type FROM wally_storage"
koder aka kdanilovf2865172016-12-30 03:35:11 +020080 SQLITE3_THREADSAFE = 1
koder aka kdanilov7f59d562016-12-26 01:34:23 +020081
82 def __init__(self, db_path: str = None, existing: bool = False,
83 prefix: str = None, db: sqlite3.Connection = None) -> None:
84
85 assert not prefix or "'" not in prefix, "Broken sql prefix {!r}".format(prefix)
86
87 if db_path:
88 self.existing = existing
89 if existing:
90 if not os.path.isfile(db_path):
91 raise IOError("No storage found at {!r}".format(db_path))
92
93 os.makedirs(os.path.dirname(db_path), exist_ok=True)
koder aka kdanilovf2865172016-12-30 03:35:11 +020094 if sqlite3.threadsafety != self.SQLITE3_THREADSAFE:
95 raise RuntimeError("Sqlite3 compiled without threadsafe support, can't use DB storage on it")
96
koder aka kdanilov7f59d562016-12-26 01:34:23 +020097 try:
koder aka kdanilovf2865172016-12-30 03:35:11 +020098 self.db = sqlite3.connect(db_path, check_same_thread=False)
koder aka kdanilov7f59d562016-12-26 01:34:23 +020099 except sqlite3.OperationalError as exc:
100 raise IOError("Can't open database at {!r}".format(db_path)) from exc
101
102 self.db.execute(self.create_tb_sql)
103 else:
104 if db is None:
105 raise ValueError("Either db or db_path parameter must be passed")
106 self.db = db
107
108 if prefix is None:
109 self.prefix = ""
110 elif not prefix.endswith('/'):
111 self.prefix = prefix + '/'
112 else:
113 self.prefix = prefix
114
115 def put(self, value: bytes, path: str) -> None:
116 c = self.db.cursor()
117 fpath = self.prefix + path
118 c.execute(self.contains_sql, (fpath,))
119 if len(c.fetchall()) == 0:
120 c.execute(self.insert_sql, (fpath, value, 'yaml'))
121 else:
122 c.execute(self.update_sql, (value, 'yaml', fpath))
123
124 def get(self, path: str) -> bytes:
125 c = self.db.cursor()
126 c.execute(self.select_sql, (self.prefix + path,))
127 res = cast(List[Tuple[bytes, str]], c.fetchall()) # type: List[Tuple[bytes, str]]
128 if not res:
129 raise KeyError(path)
130 assert len(res) == 1
131 val, tp = res[0]
132 assert tp == 'yaml'
133 return val
134
135 def rm(self, path: str) -> None:
136 c = self.db.cursor()
137 path = self.prefix + path
138 assert "'" not in path, "Broken sql path {!r}".format(path)
139 c.execute(self.rm_sql.format(path))
140
141 def __contains__(self, path: str) -> bool:
142 c = self.db.cursor()
143 path = self.prefix + path
144 c.execute(self.contains_sql, (self.prefix + path,))
145 return len(c.fetchall()) != 0
146
147 def print_tree(self):
148 c = self.db.cursor()
149 c.execute(self.list2_sql)
150 data = list(c.fetchall())
151 data.sort()
152 print("------------------ DB ---------------------")
153 for key, data_ln, type in data:
154 print(key, data_ln, type)
155 print("------------------ END --------------------")
156
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200157 def sub_storage(self, path: str) -> 'DBStorage':
158 return self.__class__(prefix=self.prefix + path, db=self.db)
159
160 def sync(self):
161 self.db.commit()
162
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200163 def get_fd(self, path: str, mode: str = "rb+") -> IO[bytes]:
164 raise NotImplementedError("SQLITE3 doesn't provide fd-like interface")
165
166 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
167 raise NotImplementedError("SQLITE3 doesn't provide list method")
168
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200169
170DB_REL_PATH = "__db__.db"
171
172
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200173class FSStorage(ISimpleStorage):
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200174 """Store all data in files on FS"""
175
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200176 def __init__(self, root_path: str, existing: bool) -> None:
177 self.root_path = root_path
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200178 self.existing = existing
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200179 self.ignored = {self.j(DB_REL_PATH), '.', '..'}
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200180
181 def j(self, path: str) -> str:
182 return os.path.join(self.root_path, path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200183
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200184 def put(self, value: bytes, path: str) -> None:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200185 jpath = self.j(path)
186 os.makedirs(os.path.dirname(jpath), exist_ok=True)
187 with open(jpath, "wb") as fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200188 fd.write(value)
189
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200190 def get(self, path: str) -> bytes:
koder aka kdanilov73084622016-11-16 21:51:08 +0200191 try:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200192 with open(self.j(path), "rb") as fd:
193 return fd.read()
194 except FileNotFoundError as exc:
195 raise KeyError(path) from exc
koder aka kdanilov73084622016-11-16 21:51:08 +0200196
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200197 def rm(self, path: str) -> None:
198 if os.path.isdir(path):
199 shutil.rmtree(path, ignore_errors=True)
200 elif os.path.exists(path):
201 os.unlink(path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200202
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200203 def __contains__(self, path: str) -> bool:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200204 return os.path.exists(self.j(path))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200205
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200206 def get_fd(self, path: str, mode: str = "rb+") -> IO[bytes]:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200207 jpath = self.j(path)
208
209 if "cb" == mode:
210 create_on_fail = True
211 mode = "rb+"
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200212 os.makedirs(os.path.dirname(jpath), exist_ok=True)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200213 else:
214 create_on_fail = False
215
216 try:
217 fd = open(jpath, mode)
218 except IOError:
219 if not create_on_fail:
220 raise
221 fd = open(jpath, "wb")
222
223 return cast(IO[bytes], fd)
224
225 def sub_storage(self, path: str) -> 'FSStorage':
226 return self.__class__(self.j(path), self.existing)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200227
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200228 def sync(self):
229 pass
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200230
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200231 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200232 path = self.j(path)
233
234 if not os.path.exists(path):
235 return
236
237 if not os.path.isdir(path):
238 raise OSError("{!r} is not a directory".format(path))
239
240 for fobj in os.scandir(path):
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200241 if fobj.path not in self.ignored:
242 if fobj.is_dir():
243 yield False, fobj.name
244 else:
245 yield True, fobj.name
246
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200247
248class YAMLSerializer(ISerializer):
249 """Serialize data to yaml"""
koder aka kdanilovf2865172016-12-30 03:35:11 +0200250 def pack(self, value: IStorable) -> bytes:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200251 try:
252 return yaml.dump(value, Dumper=Dumper, encoding="utf8")
253 except Exception as exc:
254 raise ValueError("Can't pickle object {!r} to yaml".format(type(value))) from exc
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200255
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200256 def unpack(self, data: bytes) -> Any:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200257 return yaml.load(data, Loader=Loader)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200258
259
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200260class SAFEYAMLSerializer(ISerializer):
261 """Serialize data to yaml"""
koder aka kdanilovf2865172016-12-30 03:35:11 +0200262 def pack(self, value: IStorable) -> bytes:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200263 try:
264 return yaml.safe_dump(value, encoding="utf8")
265 except Exception as exc:
266 raise ValueError("Can't pickle object {!r} to yaml".format(type(value))) from exc
267
268 def unpack(self, data: bytes) -> Any:
269 return yaml.safe_load(data)
270
271
272ObjClass = TypeVar('ObjClass', bound=IStorable)
273
274
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200275class _Raise:
276 pass
277
278
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200279class Storage:
280 """interface for storage"""
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200281 def __init__(self, fs_storage: ISimpleStorage, db_storage: ISimpleStorage, serializer: ISerializer) -> None:
282 self.fs = fs_storage
283 self.db = db_storage
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200284 self.serializer = serializer
285
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200286 def sub_storage(self, *path: str) -> 'Storage':
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200287 fpath = "/".join(path)
288 return self.__class__(self.fs.sub_storage(fpath), self.db.sub_storage(fpath), self.serializer)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200289
koder aka kdanilovf2865172016-12-30 03:35:11 +0200290 def put(self, value: IStorable, *path: str) -> None:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200291 dct_value = value.raw() if isinstance(value, IStorable) else value
292 serialized = self.serializer.pack(dct_value)
293 fpath = "/".join(path)
294 self.db.put(serialized, fpath)
295 self.fs.put(serialized, fpath)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200296
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200297 def put_list(self, value: Iterable[IStorable], *path: str) -> None:
298 serialized = self.serializer.pack([obj.raw() for obj in value])
299 fpath = "/".join(path)
300 self.db.put(serialized, fpath)
301 self.fs.put(serialized, fpath)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200302
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200303 def get(self, path: str, default: Any = _Raise) -> Any:
304 try:
305 vl = self.db.get(path)
306 except:
307 if default is _Raise:
308 raise
309 return default
310
311 return self.serializer.unpack(vl)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200312
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200313 def rm(self, *path: str) -> None:
314 fpath = "/".join(path)
315 self.fs.rm(fpath)
316 self.db.rm(fpath)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200317
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200318 def __contains__(self, path: str) -> bool:
319 return path in self.fs or path in self.db
koder aka kdanilov73084622016-11-16 21:51:08 +0200320
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200321 def put_raw(self, val: bytes, *path: str) -> None:
322 self.fs.put(val, "/".join(path))
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200323
324 def get_raw(self, *path: str) -> bytes:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200325 return self.fs.get("/".join(path))
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200326
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200327 def append_raw(self, value: bytes, *path: str) -> None:
328 with self.fs.get_fd("/".join(path), "rb+") as fd:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200329 fd.seek(0, os.SEEK_END)
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200330 fd.write(value)
331
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200332 def get_fd(self, path: str, mode: str = "r") -> IO:
333 return self.fs.get_fd(path, mode)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200334
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200335 def put_array(self, value: array.array, *path: str) -> None:
336 with self.get_fd("/".join(path), "wb") as fd:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200337 value.tofile(fd) # type: ignore
338
339 def get_array(self, typecode: str, *path: str) -> array.array:
340 res = array.array(typecode)
341 path_s = "/".join(path)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200342 with self.get_fd(path_s, "rb") as fd:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200343 fd.seek(0, os.SEEK_END)
344 size = fd.tell()
345 fd.seek(0, os.SEEK_SET)
346 assert size % res.itemsize == 0, "Storage object at path {} contains no array of {} or corrupted."\
347 .format(path_s, typecode)
348 res.fromfile(fd, size // res.itemsize) # type: ignore
349 return res
350
351 def append(self, value: array.array, *path: str) -> None:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200352 with self.get_fd("/".join(path), "cb") as fd:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200353 fd.seek(0, os.SEEK_END)
354 value.tofile(fd) # type: ignore
355
koder aka kdanilov70227062016-11-26 23:23:21 +0200356 def load_list(self, obj_class: Type[ObjClass], *path: str) -> List[ObjClass]:
357 path_s = "/".join(path)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200358 raw_val = cast(List[Dict[str, Any]], self.get(path_s))
koder aka kdanilov73084622016-11-16 21:51:08 +0200359 assert isinstance(raw_val, list)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200360 return [obj_class.fromraw(val) for val in raw_val]
koder aka kdanilov73084622016-11-16 21:51:08 +0200361
koder aka kdanilov70227062016-11-26 23:23:21 +0200362 def load(self, obj_class: Type[ObjClass], *path: str) -> ObjClass:
363 path_s = "/".join(path)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200364 return obj_class.fromraw(self.get(path_s))
koder aka kdanilov73084622016-11-16 21:51:08 +0200365
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200366 def sync(self) -> None:
367 self.db.sync()
368 self.fs.sync()
koder aka kdanilov73084622016-11-16 21:51:08 +0200369
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200370 def __enter__(self) -> 'Storage':
371 return self
372
373 def __exit__(self, x: Any, y: Any, z: Any) -> None:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200374 self.sync()
koder aka kdanilov70227062016-11-26 23:23:21 +0200375
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200376 def list(self, *path: str) -> Iterator[Tuple[bool, str]]:
377 return self.fs.list("/".join(path))
378
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200379
380def make_storage(url: str, existing: bool = False) -> Storage:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200381 return Storage(FSStorage(url, existing),
382 DBStorage(os.path.join(url, DB_REL_PATH)),
383 SAFEYAMLSerializer())
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200384