Skip to content

Commit 30db189

Browse files
author
twhalen
committed
Merge branch 'dev'
# Conflicts: # README.md
2 parents 3b85db7 + 3a1d3de commit 30db189

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3713
-1351
lines changed

py2store/__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import os
2+
from py2store.util import ModuleNotFoundIgnore
3+
4+
file_sep = os.path.sep
25

36
# Imports to be able to easily get started...
4-
from py2store.stores.local_store import LocalStore
5-
from py2store.stores.local_store import QuickStore
7+
from py2store.base import KvCollection, KvReader, KvPersister, Reader, Persister
8+
from py2store.stores.local_store import LocalStore, LocalBinaryStore, LocalTextStore, PickleStore
9+
from py2store.stores.local_store import QuickStore, QuickBinaryStore, QuickTextStore, QuickJsonStore, QuickPickleStore
610
from py2store.base import Store
11+
from py2store.trans import wrap_kvs
12+
from py2store.access import user_configs_dict, user_configs, user_defaults_dict, user_defaults
713

8-
file_sep = os.path.sep
14+
with ModuleNotFoundIgnore():
15+
from py2store.stores.s3_store import S3BinaryStore, S3TextStore, S3PickleStore
16+
with ModuleNotFoundIgnore():
17+
from py2store.stores.mongo_store import MongoStore, MongoTupleKeyStore, MongoAnyKeyStore

py2store/access.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
The logic to allow configurations (and defaults) to be parametrized by external environmental variables and files.
3+
4+
There are two main key-value stores: One for configurations the user wants to reuse, and the other for the user's
5+
desired defaults. Both have the same structure:
6+
* first level key: Name of the resource (should be a valid python variable name)
7+
* The reminder is more or less free form (until the day we lay out some schemas for this)
8+
9+
The system will look for the specification of user_configs and user_defaults in a json file.
10+
The filepath to this json file can specified in environment variables
11+
PY2STORE_CONFIGS_JSON_FILEPATH and PY2STORE_DEFAULTS_JSON_FILEPATH
12+
respectively.
13+
By default, they are:
14+
~/.py2store_configs.json and ~/.py2store_defaults.json
15+
respectively.
16+
"""
17+
import os
18+
from warnings import warn
19+
from py2store.util import DictAttr, str_to_var_str
20+
21+
22+
def getenv(name, default=None):
23+
"""Like os.getenv, but removes a suffix \\r character if present (problem with some env var systems)"""
24+
v = os.getenv(name, default)
25+
if v.endswith('\r'):
26+
return v[:-1]
27+
else:
28+
return v
29+
30+
31+
user_configs_dict = {}
32+
user_defaults_dict = {}
33+
user_configs = None
34+
user_defaults = None
35+
36+
try:
37+
import json
38+
39+
user_configs_dirpath = os.path.expanduser(getenv('PY2STORE_CONFIGS_DIR', '~/.py2store_configs'))
40+
if os.path.isdir(user_configs_dirpath):
41+
def directory_json_items():
42+
for f in filter(lambda x: x.endswith('.json'), os.listdir(user_configs_dirpath)):
43+
filepath = os.path.join(user_configs_dirpath, f)
44+
name, _ = os.path.splitext(f)
45+
try:
46+
d = json.load(open(filepath))
47+
yield str_to_var_str(name), d
48+
except json.JSONDecodeError:
49+
warn(f"This json file couldn't be json-decoded: {filepath}")
50+
except Exception:
51+
warn(f"Unknown error when trying to json.load this file: {filepath}")
52+
53+
54+
user_configs = DictAttr(**{k: v for k, v in directory_json_items()})
55+
56+
else:
57+
warn(f"The configs directory wasn't found (please make it): {user_configs_dirpath}")
58+
warn("Configs in a single json is being deprecated")
59+
user_configs_filepath = os.path.expanduser(getenv('PY2STORE_CONFIGS_JSON_FILEPATH', '~/.py2store_configs.json'))
60+
if os.path.isfile(user_configs_filepath):
61+
user_configs_dict = json.load(open(user_configs_filepath))
62+
user_configs = DictAttr(**{str_to_var_str(k): v for k, v in user_configs_dict.items()})
63+
64+
user_defaults_filepath = os.path.expanduser(getenv('PY2STORE_DEFAULTS_JSON_FILEPATH', '~/.py2store_defaults.json'))
65+
if os.path.isfile(user_defaults_filepath):
66+
user_defaults_dict = json.load(open(user_defaults_filepath))
67+
user_defaults = DictAttr(**{str_to_var_str(k): v for k, v in user_defaults_dict.items()})
68+
69+
except Exception as e:
70+
warn(f"There was an exception when trying to get configs and defaults: {e}")

py2store/base.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
the storage methods themselves.
2525
"""
2626

27-
from collections.abc import MutableMapping
27+
from collections.abc import Collection, Mapping, MutableMapping
2828
from typing import Any, Iterable, Tuple
2929

3030
Key = Any
@@ -37,16 +37,52 @@
3737
ItemIter = Iterable[Item]
3838

3939

40-
# TODO: Wishful thinking: Define store type so the type is defined by it's methods, not by subclassing.
41-
class Persister(MutableMapping):
42-
""" Acts as a MutableMapping abc, but disabling the clear method, and computing __len__ by counting keys"""
40+
class KvCollection(Collection):
41+
42+
def __contains__(self, k: Key) -> bool:
43+
"""
44+
Check if collection of keys contains k.
45+
Note: This method actually fetches the contents for k, returning False if there's a key error trying to do so
46+
Therefore it may not be efficient, and in most cases, a method specific to the case should be used.
47+
:return: True if k is in the collection, and False if not
48+
"""
49+
try:
50+
self.__getitem__(k)
51+
return True
52+
except KeyError:
53+
return False
4354

44-
def __len__(self):
55+
def __len__(self) -> int:
56+
"""
57+
Number of elements in collection of keys.
58+
Note: This method iterates over all elements of the collection and counts them.
59+
Therefore it is not efficient, and in most cases should be overridden with a more efficient version.
60+
:return: The number (int) of elements in the collection of keys.
61+
"""
62+
# TODO: some other means to more quickly count files?
63+
# Note: Found that sum(1 for _ in self.__iter__()) was slower for small, slightly faster for big inputs.
4564
count = 0
4665
for _ in self.__iter__():
4766
count += 1
4867
return count
4968

69+
def head(self):
70+
return next(iter(self.items()))
71+
72+
73+
class KvReader(KvCollection, Mapping):
74+
"""Acts as a Mapping abc, but with default __len__ (implemented by counting keys)
75+
and head method to get the first (k, v) item of the store"""
76+
pass
77+
78+
79+
Reader = KvReader # alias
80+
81+
82+
# TODO: Wishful thinking: Define store type so the type is defined by it's methods, not by subclassing.
83+
class Persister(Reader, MutableMapping):
84+
""" Acts as a MutableMapping abc, but disabling the clear method, and computing __len__ by counting keys"""
85+
5086
def clear(self):
5187
raise NotImplementedError('''
5288
The clear method was overridden to make dangerous difficult.
@@ -58,6 +94,9 @@ def clear(self):
5894
pass''')
5995

6096

97+
KvPersister = Persister # alias with explict name
98+
99+
61100
# TODO: Make identity_func "identifiable". If we use the following one, we can use == to detect it's use,
62101
# TODO: ... but there may be a way to annotate, register, or type any identity function so it can be detected.
63102
def identity_func(x):
@@ -211,6 +250,9 @@ def head(self) -> Item:
211250
def __setitem__(self, k: Key, v: Val):
212251
return self.store.__setitem__(self._id_of_key(k), self._data_of_obj(v))
213252

253+
# def update(self, *args, **kwargs):
254+
# return self.store.update(*args, **kwargs)
255+
214256
# Delete ####################################################################
215257
def __delitem__(self, k: Key):
216258
return self.store.__delitem__(self._id_of_key(k))
@@ -230,6 +272,21 @@ def __repr__(self):
230272
return self.store.__repr__()
231273

232274

275+
KvStore = Store # alias with explict name
276+
277+
278+
def has_kv_store_interface(o):
279+
"""Check if object has the KvStore interface (that is, has the kv wrapper methods
280+
Args:
281+
o: object (class or instance)
282+
283+
Returns: True if kv has the four key (in/out) and value (in/out) transformation methods
284+
285+
"""
286+
return hasattr(o, '_id_of_key') and hasattr(o, '_key_of_id') \
287+
and hasattr(o, '_data_of_obj') and hasattr(o, '_obj_of_data')
288+
289+
233290
from abc import ABCMeta, abstractmethod
234291
from py2store.errors import KeyValidationError
235292

0 commit comments

Comments
 (0)