blob: 3e8bbab0d02999d93ff422508b98063fe9cdbcdf [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 kdanilov108ac362017-01-19 20:17:16 +02006import re
koder aka kdanilov22d134e2016-11-08 11:33:19 +02007import abc
koder aka kdanilov39e449e2016-12-17 15:15:26 +02008import array
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +02009import shutil
koder aka kdanilov7f59d562016-12-26 01:34:23 +020010import sqlite3
koder aka kdanilov108ac362017-01-19 20:17:16 +020011import logging
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020012from typing import Any, TypeVar, Type, IO, Tuple, cast, List, Dict, Iterable, Iterator
koder aka kdanilov39e449e2016-12-17 15:15:26 +020013
14import yaml
15try:
16 from yaml import CLoader as Loader, CDumper as Dumper # type: ignore
17except ImportError:
18 from yaml import Loader, Dumper # type: ignore
koder aka kdanilov22d134e2016-11-08 11:33:19 +020019
20
koder aka kdanilov108ac362017-01-19 20:17:16 +020021from .common_types import IStorable
22
23
24logger = logging.getLogger("wally")
koder aka kdanilov22d134e2016-11-08 11:33:19 +020025
26
koder aka kdanilov22d134e2016-11-08 11:33:19 +020027class ISimpleStorage(metaclass=abc.ABCMeta):
28 """interface for low-level storage, which doesn't support serialization
29 and can operate only on bytes"""
30
31 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020032 def put(self, value: bytes, path: str) -> None:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020033 pass
34
35 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020036 def get(self, path: str) -> bytes:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020037 pass
38
39 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020040 def rm(self, path: str) -> None:
41 pass
42
43 @abc.abstractmethod
44 def sync(self) -> None:
koder aka kdanilov73084622016-11-16 21:51:08 +020045 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 kdanilov7f59d562016-12-26 01:34:23 +020052 def get_fd(self, path: str, mode: str = "rb+") -> IO:
koder aka kdanilov39e449e2016-12-17 15:15:26 +020053 pass
54
55 @abc.abstractmethod
56 def sub_storage(self, path: str) -> 'ISimpleStorage':
koder aka kdanilov22d134e2016-11-08 11:33:19 +020057 pass
58
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020059 @abc.abstractmethod
60 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
61 pass
62
koder aka kdanilov22d134e2016-11-08 11:33:19 +020063
64class ISerializer(metaclass=abc.ABCMeta):
65 """Interface for serialization class"""
66 @abc.abstractmethod
koder aka kdanilovf2865172016-12-30 03:35:11 +020067 def pack(self, value: IStorable) -> bytes:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020068 pass
69
70 @abc.abstractmethod
koder aka kdanilov7f59d562016-12-26 01:34:23 +020071 def unpack(self, data: bytes) -> Any:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020072 pass
73
74
koder aka kdanilov7f59d562016-12-26 01:34:23 +020075class DBStorage(ISimpleStorage):
76
77 create_tb_sql = "CREATE TABLE IF NOT EXISTS wally_storage (key text, data blob, type text)"
78 insert_sql = "INSERT INTO wally_storage VALUES (?, ?, ?)"
79 update_sql = "UPDATE wally_storage SET data=?, type=? WHERE key=?"
80 select_sql = "SELECT data, type FROM wally_storage WHERE key=?"
81 contains_sql = "SELECT 1 FROM wally_storage WHERE key=?"
82 rm_sql = "DELETE FROM wally_storage WHERE key LIKE '{}%'"
83 list2_sql = "SELECT key, length(data), type FROM wally_storage"
koder aka kdanilovf2865172016-12-30 03:35:11 +020084 SQLITE3_THREADSAFE = 1
koder aka kdanilov7f59d562016-12-26 01:34:23 +020085
86 def __init__(self, db_path: str = None, existing: bool = False,
87 prefix: str = None, db: sqlite3.Connection = None) -> None:
88
89 assert not prefix or "'" not in prefix, "Broken sql prefix {!r}".format(prefix)
90
91 if db_path:
92 self.existing = existing
93 if existing:
94 if not os.path.isfile(db_path):
95 raise IOError("No storage found at {!r}".format(db_path))
96
97 os.makedirs(os.path.dirname(db_path), exist_ok=True)
koder aka kdanilovf2865172016-12-30 03:35:11 +020098 if sqlite3.threadsafety != self.SQLITE3_THREADSAFE:
99 raise RuntimeError("Sqlite3 compiled without threadsafe support, can't use DB storage on it")
100
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200101 try:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200102 self.db = sqlite3.connect(db_path, check_same_thread=False)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200103 except sqlite3.OperationalError as exc:
104 raise IOError("Can't open database at {!r}".format(db_path)) from exc
105
106 self.db.execute(self.create_tb_sql)
107 else:
108 if db is None:
109 raise ValueError("Either db or db_path parameter must be passed")
110 self.db = db
111
112 if prefix is None:
113 self.prefix = ""
114 elif not prefix.endswith('/'):
115 self.prefix = prefix + '/'
116 else:
117 self.prefix = prefix
118
119 def put(self, value: bytes, path: str) -> None:
120 c = self.db.cursor()
121 fpath = self.prefix + path
122 c.execute(self.contains_sql, (fpath,))
123 if len(c.fetchall()) == 0:
124 c.execute(self.insert_sql, (fpath, value, 'yaml'))
125 else:
126 c.execute(self.update_sql, (value, 'yaml', fpath))
127
128 def get(self, path: str) -> bytes:
129 c = self.db.cursor()
130 c.execute(self.select_sql, (self.prefix + path,))
131 res = cast(List[Tuple[bytes, str]], c.fetchall()) # type: List[Tuple[bytes, str]]
132 if not res:
133 raise KeyError(path)
134 assert len(res) == 1
135 val, tp = res[0]
136 assert tp == 'yaml'
137 return val
138
139 def rm(self, path: str) -> None:
140 c = self.db.cursor()
141 path = self.prefix + path
142 assert "'" not in path, "Broken sql path {!r}".format(path)
143 c.execute(self.rm_sql.format(path))
144
145 def __contains__(self, path: str) -> bool:
146 c = self.db.cursor()
147 path = self.prefix + path
148 c.execute(self.contains_sql, (self.prefix + path,))
149 return len(c.fetchall()) != 0
150
151 def print_tree(self):
152 c = self.db.cursor()
153 c.execute(self.list2_sql)
154 data = list(c.fetchall())
155 data.sort()
156 print("------------------ DB ---------------------")
157 for key, data_ln, type in data:
158 print(key, data_ln, type)
159 print("------------------ END --------------------")
160
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200161 def sub_storage(self, path: str) -> 'DBStorage':
162 return self.__class__(prefix=self.prefix + path, db=self.db)
163
164 def sync(self):
165 self.db.commit()
166
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200167 def get_fd(self, path: str, mode: str = "rb+") -> IO[bytes]:
168 raise NotImplementedError("SQLITE3 doesn't provide fd-like interface")
169
170 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
171 raise NotImplementedError("SQLITE3 doesn't provide list method")
172
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200173
174DB_REL_PATH = "__db__.db"
175
176
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200177class FSStorage(ISimpleStorage):
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200178 """Store all data in files on FS"""
179
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200180 def __init__(self, root_path: str, existing: bool) -> None:
181 self.root_path = root_path
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200182 self.existing = existing
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200183 self.ignored = {self.j(DB_REL_PATH), '.', '..'}
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200184
185 def j(self, path: str) -> str:
186 return os.path.join(self.root_path, path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200187
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200188 def put(self, value: bytes, path: str) -> None:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200189 jpath = self.j(path)
190 os.makedirs(os.path.dirname(jpath), exist_ok=True)
191 with open(jpath, "wb") as fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200192 fd.write(value)
193
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200194 def get(self, path: str) -> bytes:
koder aka kdanilov73084622016-11-16 21:51:08 +0200195 try:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200196 with open(self.j(path), "rb") as fd:
197 return fd.read()
198 except FileNotFoundError as exc:
199 raise KeyError(path) from exc
koder aka kdanilov73084622016-11-16 21:51:08 +0200200
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200201 def rm(self, path: str) -> None:
202 if os.path.isdir(path):
203 shutil.rmtree(path, ignore_errors=True)
204 elif os.path.exists(path):
205 os.unlink(path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200206
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200207 def __contains__(self, path: str) -> bool:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200208 return os.path.exists(self.j(path))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200209
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200210 def get_fd(self, path: str, mode: str = "rb+") -> IO[bytes]:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200211 jpath = self.j(path)
212
213 if "cb" == mode:
214 create_on_fail = True
215 mode = "rb+"
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200216 os.makedirs(os.path.dirname(jpath), exist_ok=True)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200217 else:
218 create_on_fail = False
219
220 try:
221 fd = open(jpath, mode)
222 except IOError:
223 if not create_on_fail:
224 raise
225 fd = open(jpath, "wb")
226
227 return cast(IO[bytes], fd)
228
229 def sub_storage(self, path: str) -> 'FSStorage':
230 return self.__class__(self.j(path), self.existing)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200231
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200232 def sync(self):
233 pass
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200234
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200235 def list(self, path: str) -> Iterator[Tuple[bool, str]]:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200236 path = self.j(path)
237
238 if not os.path.exists(path):
239 return
240
241 if not os.path.isdir(path):
242 raise OSError("{!r} is not a directory".format(path))
243
244 for fobj in os.scandir(path):
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200245 if fobj.path not in self.ignored:
246 if fobj.is_dir():
247 yield False, fobj.name
248 else:
249 yield True, fobj.name
250
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200251
252class YAMLSerializer(ISerializer):
253 """Serialize data to yaml"""
koder aka kdanilovf2865172016-12-30 03:35:11 +0200254 def pack(self, value: IStorable) -> bytes:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200255 try:
256 return yaml.dump(value, Dumper=Dumper, encoding="utf8")
257 except Exception as exc:
258 raise ValueError("Can't pickle object {!r} to yaml".format(type(value))) from exc
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200259
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200260 def unpack(self, data: bytes) -> Any:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200261 return yaml.load(data, Loader=Loader)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200262
263
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200264class SAFEYAMLSerializer(ISerializer):
265 """Serialize data to yaml"""
koder aka kdanilovf2865172016-12-30 03:35:11 +0200266 def pack(self, value: IStorable) -> bytes:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200267 try:
268 return yaml.safe_dump(value, encoding="utf8")
269 except Exception as exc:
270 raise ValueError("Can't pickle object {!r} to yaml".format(type(value))) from exc
271
272 def unpack(self, data: bytes) -> Any:
273 return yaml.safe_load(data)
274
275
276ObjClass = TypeVar('ObjClass', bound=IStorable)
277
278
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200279class _Raise:
280 pass
281
282
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200283class Storage:
284 """interface for storage"""
koder aka kdanilov108ac362017-01-19 20:17:16 +0200285
286 typechar_pad_size = 16
287 typepad = bytes(0 for i in range(typechar_pad_size - 1))
288
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200289 def __init__(self, fs_storage: ISimpleStorage, db_storage: ISimpleStorage, serializer: ISerializer) -> None:
290 self.fs = fs_storage
291 self.db = db_storage
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200292 self.serializer = serializer
293
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200294 def sub_storage(self, *path: str) -> 'Storage':
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200295 fpath = "/".join(path)
296 return self.__class__(self.fs.sub_storage(fpath), self.db.sub_storage(fpath), self.serializer)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200297
koder aka kdanilov108ac362017-01-19 20:17:16 +0200298 def put(self, value: Any, *path: str) -> None:
299 dct_value = cast(IStorable, value).raw() if isinstance(value, IStorable) else value
300 serialized = self.serializer.pack(dct_value) # type: ignore
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200301 fpath = "/".join(path)
302 self.db.put(serialized, fpath)
303 self.fs.put(serialized, fpath)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200304
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200305 def put_list(self, value: Iterable[IStorable], *path: str) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200306 serialized = self.serializer.pack([obj.raw() for obj in value]) # type: ignore
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200307 fpath = "/".join(path)
308 self.db.put(serialized, fpath)
309 self.fs.put(serialized, fpath)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200310
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200311 def get(self, path: str, default: Any = _Raise) -> Any:
312 try:
313 vl = self.db.get(path)
314 except:
315 if default is _Raise:
316 raise
317 return default
318
319 return self.serializer.unpack(vl)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200320
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200321 def rm(self, *path: str) -> None:
322 fpath = "/".join(path)
323 self.fs.rm(fpath)
324 self.db.rm(fpath)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200325
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200326 def __contains__(self, path: str) -> bool:
327 return path in self.fs or path in self.db
koder aka kdanilov73084622016-11-16 21:51:08 +0200328
koder aka kdanilov108ac362017-01-19 20:17:16 +0200329 def put_raw(self, val: bytes, *path: str) -> str:
330 fpath = "/".join(path)
331 self.fs.put(val, fpath)
332 # TODO: dirty hack
333 return self.resolve_raw(fpath)
334
335 def resolve_raw(self, fpath) -> str:
336 return cast(FSStorage, self.fs).j(fpath)
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200337
338 def get_raw(self, *path: str) -> bytes:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200339 return self.fs.get("/".join(path))
koder aka kdanilov3af3c332016-12-19 17:12:34 +0200340
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200341 def append_raw(self, value: bytes, *path: str) -> None:
342 with self.fs.get_fd("/".join(path), "rb+") as fd:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200343 fd.seek(0, os.SEEK_END)
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200344 fd.write(value)
345
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200346 def get_fd(self, path: str, mode: str = "r") -> IO:
347 return self.fs.get_fd(path, mode)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200348
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200349 def put_array(self, value: array.array, *path: str) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200350 typechar = value.typecode.encode('ascii')
351 assert len(typechar) == 1
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200352 with self.get_fd("/".join(path), "wb") as fd:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200353 fd.write(typechar + self.typepad)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200354 value.tofile(fd) # type: ignore
355
koder aka kdanilov108ac362017-01-19 20:17:16 +0200356 def get_array(self, *path: str) -> array.array:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200357 path_s = "/".join(path)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200358 with self.get_fd(path_s, "rb") as fd:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200359 fd.seek(0, os.SEEK_END)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200360 size = fd.tell() - self.typechar_pad_size
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200361 fd.seek(0, os.SEEK_SET)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200362 typecode = chr(fd.read(self.typechar_pad_size)[0])
363 res = array.array(typecode)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200364 assert size % res.itemsize == 0, "Storage object at path {} contains no array of {} or corrupted."\
365 .format(path_s, typecode)
366 res.fromfile(fd, size // res.itemsize) # type: ignore
367 return res
368
369 def append(self, value: array.array, *path: str) -> None:
koder aka kdanilov108ac362017-01-19 20:17:16 +0200370 typechar = value.typecode.encode('ascii')
371 assert len(typechar) == 1
372 expected_typeheader = typechar + self.typepad
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200373 with self.get_fd("/".join(path), "cb") as fd:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200374 fd.seek(0, os.SEEK_END)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200375 if fd.tell() != 0:
376 fd.seek(0, os.SEEK_SET)
377 real_typecode = fd.read(self.typechar_pad_size)
378 if real_typecode[0] != expected_typeheader[0]:
379 logger.error("Try to append array with typechar %r to array with typechar %r at path %r",
380 value.typecode, typechar, "/".join(path))
381 raise StopIteration()
382 fd.seek(0, os.SEEK_END)
383 else:
384 fd.write(expected_typeheader)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200385 value.tofile(fd) # type: ignore
386
koder aka kdanilov70227062016-11-26 23:23:21 +0200387 def load_list(self, obj_class: Type[ObjClass], *path: str) -> List[ObjClass]:
388 path_s = "/".join(path)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200389 raw_val = cast(List[Dict[str, Any]], self.get(path_s))
koder aka kdanilov73084622016-11-16 21:51:08 +0200390 assert isinstance(raw_val, list)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200391 return [cast(ObjClass, obj_class.fromraw(val)) for val in raw_val]
koder aka kdanilov73084622016-11-16 21:51:08 +0200392
koder aka kdanilov70227062016-11-26 23:23:21 +0200393 def load(self, obj_class: Type[ObjClass], *path: str) -> ObjClass:
394 path_s = "/".join(path)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200395 return cast(ObjClass, obj_class.fromraw(self.get(path_s)))
koder aka kdanilov73084622016-11-16 21:51:08 +0200396
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200397 def sync(self) -> None:
398 self.db.sync()
399 self.fs.sync()
koder aka kdanilov73084622016-11-16 21:51:08 +0200400
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200401 def __enter__(self) -> 'Storage':
402 return self
403
404 def __exit__(self, x: Any, y: Any, z: Any) -> None:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200405 self.sync()
koder aka kdanilov70227062016-11-26 23:23:21 +0200406
koder aka kdanilovffaf48d2016-12-27 02:25:29 +0200407 def list(self, *path: str) -> Iterator[Tuple[bool, str]]:
408 return self.fs.list("/".join(path))
409
koder aka kdanilov108ac362017-01-19 20:17:16 +0200410 def _iter_paths(self,
411 root: str,
412 path_parts: List[str],
413 groups: Dict[str, str]) -> Iterator[Tuple[bool, str, Dict[str, str]]]:
414
415 curr = path_parts[0]
416 rest = path_parts[1:]
417
418 for is_file, name in self.list(root):
419 if rest and is_file:
420 continue
421
422 rr = re.match(pattern=curr + "$", string=name)
423 if rr:
424 if root:
425 path = root + "/" + name
426 else:
427 path = name
428
429 new_groups = rr.groupdict().copy()
430 new_groups.update(groups)
431
432 if rest:
433 yield from self._iter_paths(path, rest, new_groups)
434 else:
435 yield is_file, path, new_groups
436
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200437
438def make_storage(url: str, existing: bool = False) -> Storage:
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200439 return Storage(FSStorage(url, existing),
440 DBStorage(os.path.join(url, DB_REL_PATH)),
441 SAFEYAMLSerializer())
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200442