Skip to content

Commit d20371a

Browse files
authored
Merge pull request #63 from i2mint/dev
Dev
2 parents 0ae918e + f4bdeaa commit d20371a

34 files changed

+5176
-542
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.DS_Store
2+
13
# Byte-compiled / optimized / DLL files
24
__pycache__/
35
*.py[cod]

py2store/__init__.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
file_sep = os.path.sep
55

6+
from py2store.util import lazyprop, groupby, regroupby
7+
68
# Imports to be able to easily get started...
79
from py2store.base import Collection, KvReader, KvPersister, Reader, Persister
810

@@ -11,11 +13,20 @@
1113

1214
from py2store.misc import MiscGetter, MiscGetterAndSetter, misc_objs, misc_objs_get, get_obj, set_obj
1315
from py2store.base import Store
14-
from py2store.trans import wrap_kvs, disable_delitem, disable_setitem, mk_read_only, kv_wrap, cache_iter, filtered_iter
15-
from py2store.access import user_configs_dict, user_configs, user_defaults_dict, user_defaults, mystores
16+
from py2store.trans import wrap_kvs, disable_delitem, disable_setitem, mk_read_only, kv_wrap, \
17+
cached_keys, filtered_iter, add_path_get, insert_aliases
18+
from py2store.trans import cache_iter # being deprecated
19+
from py2store.access import user_configs_dict, user_configs, user_defaults_dict, user_defaults
20+
from py2store.caching import mk_cached_store, store_cached, store_cached_with_single_key, \
21+
ensure_clear_to_kv_store, flush_on_exit, mk_write_cached_store
1622

1723
from py2store.stores.local_store import PickleStore # consider deprecating and use LocalPickleStore instead?
18-
from py2store.persisters.local_files import ZipReader, FilesOfZip, ZipFilesReader
24+
from py2store.slib.s_zipfile import ZipReader, ZipFilesReader, FilesOfZip, FlatZipFilesReader
25+
26+
with ModuleNotFoundIgnore():
27+
from py2store.access import myconfigs
28+
with ModuleNotFoundIgnore():
29+
from py2store.access import mystores
1930

2031
with ModuleNotFoundIgnore():
2132
from py2store.stores.s3_store import S3BinaryStore, S3TextStore, S3PickleStore
@@ -31,5 +42,5 @@ def kvhead(store):
3142

3243
def ihead(store):
3344
"""Get the first item of an iterable"""
34-
for item in store:
45+
for item in iter(store):
3546
return item

py2store/access.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,11 @@ def fakit(fak, func_loader=dflt_func_loader):
149149
import json
150150

151151
user_configs_dirpath = os.path.expanduser(getenv('PY2STORE_CONFIGS_DIR', '~/.py2store_configs'))
152+
my_configs_dirname = os.path.expanduser(getenv('MY_PY2STORE_DIR_NAME', 'my'))
153+
myconfigs_dirpath = os.path.join(user_configs_dirpath, my_configs_dirname)
154+
152155
if os.path.isdir(user_configs_dirpath):
156+
153157
def directory_json_items():
154158
for f in filter(lambda x: x.endswith('.json'), os.listdir(user_configs_dirpath)):
155159
filepath = os.path.join(user_configs_dirpath, f)
@@ -166,8 +170,56 @@ def directory_json_items():
166170
user_configs = DictAttr(**{k: v for k, v in directory_json_items()})
167171

168172
from py2store.base import KvStore
169-
from py2store.stores.local_store import LocalJsonStore
173+
from py2store.stores.local_store import LocalJsonStore, LocalBinaryStore
170174
from py2store.trans import wrap_kvs
175+
from py2store.misc import MiscStoreMixin
176+
from functools import wraps
177+
178+
if os.path.isdir(myconfigs_dirpath):
179+
class MyConfigs(MiscStoreMixin, LocalBinaryStore):
180+
@wraps(LocalBinaryStore)
181+
def __init__(self, *args, **kwargs):
182+
LocalBinaryStore.__init__(self, *args, **kwargs)
183+
self._init_args_kwargs = (args, kwargs)
184+
if len(args) > 0:
185+
self.dirpath = args[0]
186+
elif len(kwargs) > 0:
187+
self.dirpath = kwargs[next(iter(kwargs))]
188+
189+
def refresh(self):
190+
"""Sometimes you add a file, and you want to make sure your myconfigs sees it.
191+
refresh() does that for you. It reinitializes the reader."""
192+
args, kwargs = self._init_args_kwargs
193+
self.__init__(*args, **kwargs)
194+
195+
def get(self, k):
196+
v = super().get(k, None)
197+
if v is None:
198+
from warnings import warn
199+
warn(f"""You don't have the config file I'm expecting, so you'll have to enter it manually.
200+
I'm expecting this filepath:\n\t {os.path.join(self.dirpath, k)}
201+
""")
202+
return v
203+
204+
def get_config_value(self, k, path=None):
205+
v = self.get(k)
206+
if path is None:
207+
return v
208+
else:
209+
if path in v:
210+
return v[path]
211+
else:
212+
raise KeyError(f"I don't see any {path} in {k}")
213+
214+
215+
myconfigs = MyConfigs(myconfigs_dirpath)
216+
myconfigs.dirpath = myconfigs_dirpath
217+
else:
218+
warn(f"""The py2store-myconfigs directory wasn't found: {myconfigs_dirpath}
219+
If you want to have all the cool functionality of `myconfigs`, you should make this directory,
220+
and put stuff in it. Here's to make it easy for you to do it. Go to a terminal and run this:
221+
mkdir {myconfigs_dirpath}
222+
""")
171223

172224

173225
class MyStores(KvStore):
@@ -202,10 +254,10 @@ def add_json_ext(k):
202254

203255
stores_json_path_format = os.path.join(user_configs_dirpath, 'stores', 'json', '{}.json')
204256
mystores = MyStores(ExtLessJsonStore(stores_json_path_format))
257+
mystores.dirpath = user_configs_dirpath
205258

206259
else:
207260
warn(f"The configs directory wasn't found (please make it): {user_configs_dirpath}")
208-
warn("Configs in a single json is being deprecated")
209261
user_configs_filepath = os.path.expanduser(getenv('PY2STORE_CONFIGS_JSON_FILEPATH', '~/.py2store_configs.json'))
210262
if os.path.isfile(user_configs_filepath):
211263
user_configs_dict = json.load(open(user_configs_filepath))

py2store/base.py

Lines changed: 134 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,20 @@
3838
ItemIter = Iterable[Item]
3939

4040

41+
class AttrNames:
42+
CollectionABC = {'__len__', '__iter__', '__contains__'}
43+
Mapping = CollectionABC | {'keys', 'get', 'items', '__reversed__', 'values', '__getitem__'}
44+
MutableMapping = Mapping | {'setdefault', 'pop', 'popitem', 'clear', 'update', '__delitem__', '__setitem__'}
45+
46+
Collection = CollectionABC | {'head'}
47+
KvReader = (Mapping | {'head'}) - {'__reversed__'}
48+
KvPersister = (MutableMapping | {'head'}) - {'__reversed__'} - {'clear'}
49+
50+
4151
class Collection(CollectionABC):
52+
"""The same as collections.abc.Collection, with some modifications:
53+
- Addition of a ``head``
54+
"""
4255

4356
def __contains__(self, x) -> bool:
4457
"""
@@ -96,28 +109,86 @@ class KvReader(Collection, Mapping):
96109

97110
def head(self):
98111
for k, v in self.items():
99-
yield k, v
112+
return k, v
113+
114+
def __reversed__(self):
115+
"""The __reversed__ is disabled at the base, but can be re-defined in subclasses.
116+
Rationale: KvReader is meant to wrap a variety of storage backends or key-value perspectives thereof.
117+
Not all of these would have a natural or intuitive order nor do we want to maintain one systematically.
118+
119+
If you need a reversed list, here's one way to do it, but note that it
120+
depends on how self iterates, which is not even assured to be consistent at every call:
121+
```
122+
reversed = list(self)[::-1]
123+
```
124+
125+
If the keys are comparable, therefore sortable, another natural option would be:
126+
```
127+
reversed = sorted(self)[::-1]
128+
```
129+
"""
130+
raise NotImplementedError(__doc__)
100131

101132

102133
Reader = KvReader # alias
103134

104135

136+
# TODO: Should we really be using MutableMapping if we're disabling so many of it's methods?
105137
# TODO: Wishful thinking: Define store type so the type is defined by it's methods, not by subclassing.
106-
class Persister(Reader, MutableMapping):
107-
""" Acts as a MutableMapping abc, but disabling the clear method, and computing __len__ by counting keys"""
138+
class KvPersister(KvReader, MutableMapping):
139+
""" Acts as a MutableMapping abc, but disabling the clear and __reversed__ method,
140+
and computing __len__ by iterating over all keys, and counting them.
141+
142+
Note that KvPersister is a MutableMapping, and as such, is dict-like.
143+
But that doesn't mean it's a dict.
144+
145+
For instance, consider the following code:
146+
```
147+
s = SomeKvPersister()
148+
s['a']['b'] = 3
149+
```
150+
If `s` is a dict, this would have the effect of adding a ('b', 3) item under 'a'.
151+
But in the general case, this might
152+
- fail, because the `s['a']` doesn't support sub-scripting (doesn't have a `__getitem__`)
153+
- or, worse, will pass silently but not actually persist the write as expected (e.g. LocalFileStore)
154+
155+
Another example: `s.popitem()` will pop a `(k, v)` pair off of the `s` store.
156+
That is, retrieve the `v` for `k`, delete the entry for `k`, and return a `(k, v)`.
157+
Note that unlike modern dicts which will return the last item that was stored
158+
-- that is, LIFO (last-in, first-out) order -- for KvPersisters,
159+
there's no assurance as to what item will be, since it will depend on the backend storage system
160+
and/or how the persister was implemented.
161+
162+
"""
108163

109164
def clear(self):
110-
raise NotImplementedError('''
111-
The clear method was overridden to make dangerous difficult.
112-
If you really want to delete all your data, you can do so by doing:
113-
try:
114-
while True:
115-
self.popitem()
116-
except KeyError:
117-
pass''')
165+
"""The clear method is disabled to make dangerous difficult.
166+
You don't want to delete your whole DB
167+
If you really want to delete all your data, you can do so by doing something like this:
168+
```
169+
for k in self:
170+
try:
171+
del self[k]
172+
except KeyError:
173+
pass
174+
```
175+
"""
176+
raise NotImplementedError(__doc__)
118177

178+
# # TODO: Tests and documentation demos needed.
179+
# def popitem(self):
180+
# """pop a (k, v) pair off of the store.
181+
# That is, retrieve the v for k, delete the entry for k, and return a (k, v)
182+
# Note that unlike modern dicts which will return the last item that was stored
183+
# -- that is, LIFO (last-in, first-out) order -- for KvPersisters,
184+
# there's no assurance as to what item will be, since it will depend on the backend storage system
185+
# and/or how the persister was implemented.
186+
# :return:
187+
# """
188+
# return super(KvPersister, self).popitem()
119189

120-
KvPersister = Persister # alias with explict name
190+
191+
Persister = KvPersister # alias for back-compatibility
121192

122193

123194
# TODO: Make identity_func "identifiable". If we use the following one, we can use == to detect it's use,
@@ -136,7 +207,7 @@ class NoSuchItem():
136207
no_such_item = NoSuchItem()
137208

138209

139-
class Store(Persister):
210+
class Store(KvPersister):
140211
"""
141212
By store we mean key-value store. This could be files in a filesystem, objects in s3, or a database. Where and
142213
how the content is stored should be specified, but StoreInterface offers a dict-like interface to this.
@@ -245,9 +316,12 @@ def __init__(self, store=dict):
245316
_data_of_obj = static_identity_method
246317
_obj_of_data = static_identity_method
247318

319+
_max_repr_size = None
320+
248321
# Read ####################################################################
249322
def __getitem__(self, k: Key) -> Val:
250-
return self._obj_of_data(self.store.__getitem__(self._id_of_key(k)))
323+
return self._obj_of_data(self.store[self._id_of_key(k)])
324+
# return self._obj_of_data(self.store.__getitem__(self._id_of_key(k)))
251325

252326
def get(self, k: Key, default=None) -> Val:
253327
if hasattr(self.store, 'get'): # if store has a get method, use it
@@ -262,9 +336,20 @@ def get(self, k: Key, default=None) -> Val:
262336
else:
263337
return default
264338

339+
# def update(self, other=(), /, **kwds):
340+
# """
341+
# update(self, other=(), /, **kwds)
342+
# D.update([E, ]**F) -> None. Update D from mapping/iterable E and F.
343+
# If E present and has a .keys() method, does: for k in E: D[k] = E[k]
344+
# If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v
345+
# In either case, this is followed by: for k, v in F.items(): D[k] = v
346+
# :return:
347+
# """
348+
265349
# Explore ####################################################################
266350
def __iter__(self) -> KeyIter:
267-
return map(self._key_of_id, self.store.__iter__())
351+
yield from (self._key_of_id(k) for k in self.store)
352+
# return map(self._key_of_id, self.store.__iter__())
268353

269354
# def items(self) -> ItemIter:
270355
# if hasattr(self.store, 'items'):
@@ -273,27 +358,33 @@ def __iter__(self) -> KeyIter:
273358
# yield from ((self._key_of_id(k), self._obj_of_data(self.store[k])) for k in self.store.__iter__())
274359

275360
def __len__(self) -> int:
276-
return self.store.__len__()
361+
return len(self.store)
362+
# return self.store.__len__()
277363

278364
def __contains__(self, k) -> bool:
279-
return self.store.__contains__(self._id_of_key(k))
365+
return self._id_of_key(k) in self.store
366+
# return self.store.__contains__(self._id_of_key(k))
280367

281368
def head(self) -> Item:
369+
k = None
282370
try:
283371
for k in self:
284372
return k, self[k]
285373
except Exception as e:
286374

287375
from warnings import warn
288-
msg = f"Couldn't get data for the key {k}. This could be be...\n"
289-
msg += "... because it's not a store (just a collection, that doesn't have a __getitem__)\n"
290-
msg += "... because there's a layer transforming outcoming keys that are not the ones the store actually " \
291-
"uses? If you didn't wrap the store with the inverse ingoing keys transformation, " \
292-
"that would happen.\n"
293-
msg += "I'll ask the inner-layer what it's head is, but IT MAY NOT REFLECT the reality of your store " \
294-
"if you have some filtering, caching etc."
295-
msg += f"The error messages was: \n{e}"
296-
warn(msg)
376+
if k is None:
377+
raise
378+
else:
379+
msg = f"Couldn't get data for the key {k}. This could be be...\n"
380+
msg += "... because it's not a store (just a collection, that doesn't have a __getitem__)\n"
381+
msg += "... because there's a layer transforming outcoming keys that are not the ones the store actually " \
382+
"uses? If you didn't wrap the store with the inverse ingoing keys transformation, " \
383+
"that would happen.\n"
384+
msg += "I'll ask the inner-layer what it's head is, but IT MAY NOT REFLECT the reality of your store " \
385+
"if you have some filtering, caching etc."
386+
msg += f"The error messages was: \n{e}"
387+
warn(msg)
297388

298389
for _id in self.store:
299390
return self._key_of_id(_id), self._obj_of_data(self.store[_id])
@@ -312,21 +403,28 @@ def __setitem__(self, k: Key, v: Val):
312403
def __delitem__(self, k: Key):
313404
return self.store.__delitem__(self._id_of_key(k))
314405

315-
def clear(self):
316-
raise NotImplementedError('''
317-
The clear method was overridden to make dangerous difficult.
318-
If you really want to delete all your data, you can do so by doing:
319-
try:
320-
while True:
321-
self.popitem()
322-
except KeyError:
323-
pass''')
406+
# def clear(self):
407+
# raise NotImplementedError('''
408+
# The clear method was overridden to make dangerous difficult.
409+
# If you really want to delete all your data, you can do so by doing:
410+
# try:
411+
# while True:
412+
# self.popitem()
413+
# except KeyError:
414+
# pass''')
324415

325416
# Misc ####################################################################
326417
def __repr__(self):
327-
return self.store.__repr__()
418+
x = repr(self.store)
419+
if isinstance(self._max_repr_size, int):
420+
half = int(self._max_repr_size)
421+
if len(x) > self._max_repr_size:
422+
x = x[:half] + ' ... ' + x[-half:]
423+
return x
424+
# return self.store.__repr__()
328425

329426

427+
# Store.register(dict) # TODO: Would this be a good idea? To make isinstance({}, Store) be True (though missing head())
330428
KvStore = Store # alias with explict name
331429

332430

0 commit comments

Comments
 (0)