blob: 05e4259ad4d79b2b9df9928359aa94821188142d [file] [log] [blame]
"""
This module contains interfaces for storage classes
"""
import os
import abc
from typing import Any, Iterable, TypeVar, Type, IO, Tuple, cast, List
class IStorable(metaclass=abc.ABCMeta):
"""Interface for type, which can be stored"""
@abc.abstractmethod
def __getstate__(self) -> Any:
pass
@abc.abstractmethod
def __setstate__(self, Any):
pass
# all builtin types can be stored
IStorable.register(list) # type: ignore
IStorable.register(dict) # type: ignore
IStorable.register(tuple) # type: ignore
IStorable.register(set) # type: ignore
IStorable.register(None) # type: ignore
IStorable.register(int) # type: ignore
IStorable.register(str) # type: ignore
IStorable.register(bytes) # type: ignore
IStorable.register(bool) # type: ignore
ObjClass = TypeVar('ObjClass')
class ISimpleStorage(metaclass=abc.ABCMeta):
"""interface for low-level storage, which doesn't support serialization
and can operate only on bytes"""
@abc.abstractmethod
def __setitem__(self, path: str, value: bytes) -> None:
pass
@abc.abstractmethod
def __getitem__(self, path: str) -> bytes:
pass
@abc.abstractmethod
def __delitem__(self, path: str) -> None:
pass
@abc.abstractmethod
def __contains__(self, path: str) -> bool:
pass
@abc.abstractmethod
def list(self, path: str) -> Iterable[Tuple[bool, str]]:
pass
@abc.abstractmethod
def get_stream(self, path: str) -> IO:
pass
class ISerializer(metaclass=abc.ABCMeta):
"""Interface for serialization class"""
@abc.abstractmethod
def pack(self, value: IStorable) -> bytes:
pass
@abc.abstractmethod
def unpack(self, data: bytes) -> IStorable:
pass
class FSStorage(ISimpleStorage):
"""Store all data in files on FS"""
def __init__(self, root_path: str, existing: bool) -> None:
self.root_path = root_path
if existing:
if not os.path.isdir(self.root_path):
raise ValueError("No storage found at {!r}".format(root_path))
def __setitem__(self, path: str, value: bytes) -> None:
path = os.path.join(self.root_path, path)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as fd:
fd.write(value)
def __delitem__(self, path: str) -> None:
try:
os.unlink(path)
except FileNotFoundError:
pass
def __getitem__(self, path: str) -> bytes:
path = os.path.join(self.root_path, path)
with open(path, "rb") as fd:
return fd.read()
def __contains__(self, path: str) -> bool:
path = os.path.join(self.root_path, path)
return os.path.exists(path)
def list(self, path: str) -> Iterable[Tuple[bool, str]]:
path = os.path.join(self.root_path, path)
for entry in os.scandir(path):
if not entry.name in ('..', '.'):
yield entry.is_file(), entry.name
def get_stream(self, path: str, mode: str = "rb") -> IO:
path = os.path.join(self.root_path, path)
return open(path, mode)
class YAMLSerializer(ISerializer):
"""Serialize data to yaml"""
def pack(self, value: IStorable) -> bytes:
raise NotImplementedError()
def unpack(self, data: bytes) -> IStorable:
raise NotImplementedError()
class Storage:
"""interface for storage"""
def __init__(self, storage: ISimpleStorage, serializer: ISerializer) -> None:
self.storage = storage
self.serializer = serializer
def __setitem__(self, path: str, value: IStorable) -> None:
self.storage[path] = self.serializer.pack(value)
def __getitem__(self, path: str) -> IStorable:
return self.serializer.unpack(self.storage[path])
def __delitem__(self, path: str) -> None:
del self.storage[path]
def __contains__(self, path: str) -> bool:
return path in self.storage
def list(self, *path: str) -> Iterable[Tuple[bool, str]]:
return self.storage.list("/".join(path))
def construct(self, path: str, raw_val: IStorable, obj_class: Type[ObjClass]) -> ObjClass:
if obj_class in (int, str, dict, list, None):
if not isinstance(raw_val, obj_class):
raise ValueError("Can't load path {!r} into type {}. Real type is {}"
.format(path, obj_class, type(raw_val)))
return cast(ObjClass, raw_val)
if not isinstance(raw_val, dict):
raise ValueError("Can't load path {!r} into python type. Raw value not dict".format(path))
if not all(isinstance(str, key) for key in raw_val.keys):
raise ValueError("Can't load path {!r} into python type.".format(path) +
"Raw not all keys in raw value is strings")
obj = obj_class.__new__(obj_class) # type: ObjClass
obj.__dict__.update(raw_val)
return obj
def load_list(self, obj_class: Type[ObjClass], *path: str) -> List[ObjClass]:
path_s = "/".join(path)
raw_val = self[path_s]
assert isinstance(raw_val, list)
return [self.construct(path_s, val, obj_class) for val in cast(list, raw_val)]
def load(self, obj_class: Type[ObjClass], *path: str) -> ObjClass:
path_s = "/".join(path)
return self.construct(path_s, self[path_s], obj_class)
def get_stream(self, *path: str) -> IO:
return self.storage.get_stream("/".join(path))
def get(self, path: str, default: Any = None) -> Any:
try:
return self[path]
except KeyError:
return default
def append(self, path: str, data: List):
raise NotImplemented()
def make_storage(url: str, existing: bool = False) -> Storage:
return Storage(FSStorage(url, existing), YAMLSerializer())