"""
stores to operate on local files
"""
import os
from functools import wraps
from py2store.base import Store, Persister
from py2store.core import PrefixRelativizationMixin, PrefixRelativization
from py2store.paths import mk_relative_path_store
from py2store.serializers.pickled import mk_pickle_rw_funcs
from py2store.persisters.local_files import (
PathFormatPersister,
DirpathFormatKeys,
DirReader,
ensure_slash_suffix,
)
# from py2store.filesys import DirCollection
from py2store.mixins import SimpleJsonMixin
RelPathLocalFileStore = mk_relative_path_store(
PathFormatPersister, __name__='RelPathLocalFileStore'
)
RelPathLocalFileStore.__doc__ = (
'''Local file store using templated relative paths.'''
)
RelPathLocalFileStoreEnforcingFormat = mk_relative_path_store(
PathFormatPersister, __name__='RelPathLocalFileStoreEnforcingFormat'
)
RelPathLocalFileStoreEnforcingFormat.__doc__ = '''A RelativePathFormatStore, but that won't allow one to use a key that is not valid
(according to the self.store.is_valid_key boolean method)'''
# aliases for back compatibility
RelativePathFormatStore = RelPathLocalFileStore
RelativePathFormatStoreEnforcingFormat = RelPathLocalFileStoreEnforcingFormat
# Old version it replaces
# class RelativePathFormatStore(PrefixRelativizationMixin, Store):
# """Local file store using templated relative paths.
# """
#
# @wraps(PathFormatStore.__init__)
# def __init__(self, *args, **kwargs):
# super().__init__(store=PathFormatStore(*args, **kwargs))
# self._prefix = self.store._prefix
#
#
# class RelativePathFormatStoreEnforcingFormat(RelativePathFormatStore):
# """A RelativePathFormatStore, but that won't allow one to use a key that is not valid
# (according to the self.store.is_valid_key boolean method).
# """
#
# def _id_of_key(self, k):
# _id = super()._id_of_key(k)
# if self.store.is_valid_key(_id):
# return _id
# else:
# raise KeyError(f"Key not valid: {k}")
# Would like to replace the above pattern with what's below, but
# from py2store.trans import store_wrap
# PathFormatStoreWithPrefix = store_wrap(PathFormatStore, 'PathFormatStoreWithPrefix')
[docs]class LocalTextStore(RelativePathFormatStore):
"""Local files store for text data"""
def __init__(self, path_format, max_levels=None):
super().__init__(path_format, max_levels=max_levels, mode='t')
[docs]class LocalBinaryStore(RelativePathFormatStore):
"""Local files store for binary data"""
def __init__(self, path_format, max_levels=None):
super().__init__(path_format, max_levels=max_levels, mode='b')
[docs]class LocalPickleStore(RelativePathFormatStore):
"""Local files store with pickle serialization"""
def __init__(
self,
path_format,
max_levels=None,
fix_imports=True,
protocol=None,
pickle_encoding='ASCII',
pickle_errors='strict',
**open_kwargs,
):
super().__init__(
path_format, max_levels=max_levels, mode='b', **open_kwargs
)
self._loads, self._dumps = mk_pickle_rw_funcs(
fix_imports, protocol, pickle_encoding, pickle_errors
)
@classmethod
def for_dill(
cls, path_format, max_levels=None, open_kwargs=None, *args, **kwargs
):
from py2store.serializers.pickled import mk_dill_rw_funcs
open_kwargs = open_kwargs or {}
self = cls(path_format, max_levels=max_levels, **open_kwargs)
self._loads, self._dumps = mk_dill_rw_funcs(*args, **kwargs)
return self
def __getitem__(self, k):
try:
return self._loads(super().__getitem__(k))
except (ModuleNotFoundError, AttributeError) as e:
if isinstance(e, AttributeError) and 'module' not in str(e):
raise
else:
raise type(e)(f'Some modules are missing to unpickle {k}: {e}')
def __setitem__(self, k, v):
return super().__setitem__(k, self._dumps(v))
# TODO: hack to take care of problem with head not playing well with wrappers. Find better solution.
def head(self):
for k, v in self.items():
return k, v
[docs]class LocalJsonStore(SimpleJsonMixin, LocalTextStore):
__doc__ = str(LocalTextStore.__doc__) + SimpleJsonMixin._docsuffix
PickleStore = LocalPickleStore # alias
def mk_tmp_quick_store_dirpath(dirname=''):
from tempfile import gettempdir
temp_root = gettempdir()
return os.path.join(temp_root, dirname)
def mk_absolute_path(path_format):
if path_format.startswith('~'):
path_format = os.path.expanduser(path_format)
elif path_format.startswith('.'):
path_format = os.path.abspath(path_format)
return path_format
[docs]class AutoMkDirsOnSetitemMixin:
"""A mixin that will automatically create directories on setitem, when missing."""
def __setitem__(self, k, v):
dirname = os.path.dirname(os.path.join(self._prefix, k))
os.makedirs(dirname, exist_ok=True)
return super().__setitem__(k, v)
[docs]class QuickLocalStoreMixin(AutoMkPathformatMixin, AutoMkDirsOnSetitemMixin):
"""A mixin that will choose a path_format if none given,
and will automatically create directories on setitem, when missing.
"""
# _tmp_dirname = "quick_store"
# _docsuffix = " with default temp root and auto dir generation on write."
#
# @classmethod
# def mk_tmp_quick_store_path_format(cls, subpath=""):
# return mk_tmp_quick_store_dirpath(
# os.path.join(cls._tmp_dirname, subpath)
# )
#
# def __init__(self, path_format=None, max_levels=None):
# if path_format is None:
# path_format = self.mk_tmp_quick_store_path_format()
# print(
# f"No path_format was given, so taking one from a tmp dir. Namely:\n\t{path_format}"
# )
# else:
# path_format = mk_absolute_path(path_format)
# super().__init__(path_format, max_levels=max_levels)
#
# def __setitem__(self, k, v):
# dirname = os.path.dirname(os.path.join(self._prefix, k))
# os.makedirs(dirname, exist_ok=True)
# return super().__setitem__(k, v)
[docs]class QuickTextStore(QuickLocalStoreMixin, LocalTextStore):
__doc__ = str(LocalTextStore.__doc__) + QuickLocalStoreMixin._docsuffix
[docs]class QuickBinaryStore(QuickLocalStoreMixin, LocalBinaryStore):
__doc__ = str(LocalBinaryStore.__doc__) + QuickLocalStoreMixin._docsuffix
[docs]class QuickJsonStore(SimpleJsonMixin, QuickTextStore):
__doc__ = str(QuickTextStore.__doc__) + SimpleJsonMixin._docsuffix
[docs]class QuickPickleStore(QuickLocalStoreMixin, PickleStore):
__doc__ = str(PickleStore.__doc__) + QuickLocalStoreMixin._docsuffix
QuickStore = QuickPickleStore # alias
LocalStore = QuickStore # alias
[docs]class DirStore(Store):
"""A store for local directories.
Keys are directory names and values are subdirectory DirStores.
>>> from py2store import __file__
>>> import os
>>> root = os.path.dirname(__file__)
>>> s = DirStore(root)
>>> assert set(s).issuperset({'stores', 'persisters', 'serializers', 'key_mappers'})
"""
def __init__(self, rootdir):
rootdir = ensure_slash_suffix(rootdir)
super().__init__(store=DirReader(rootdir))
self._prefix = rootdir
key_wrap = PrefixRelativization(_prefix=rootdir)
os_sep = os.sep
self._id_of_key = lambda k: key_wrap._id_of_key(k) + os_sep
self._key_of_id = lambda k: key_wrap._key_of_id(k)[:-1]
# TODO: Look into alternatives for the raison d'etre of _new_node and _class_name
# (They are there, because using self.__class__ directly goes to super)
self.store._new_node = self.__class__
self.store._class_name = self.__class__.__name__