11"""
2- The logic to allow configurations (and defaults) to be parametrized by external environmental variables and files.
2+ Utils to load stores from store specifications.
3+ Includes the logic to allow configurations (and defaults) to be parametrized by external environmental
4+ variables and files.
5+
6+ Every data-sourced problem has it's problem-relevant stores. Once you get your stores right, along with the
7+ right access credentials, indexing, serialization, caching, filtering etc. you'd like to be able to name, save
8+ and/or share this specification, and easily get access to it later on.
9+
10+ Here are tools to help you out.
311
412There are two main key-value stores: One for configurations the user wants to reuse, and the other for the user's
513desired defaults. Both have the same structure:
1523respectively.
1624"""
1725import os
26+ import importlib
1827from warnings import warn
28+ from functools import reduce
1929from py2store .util import DictAttr , str_to_var_str
2030
31+ FAK = '$fak'
32+
33+
34+ # TODO: Make a config_utils.py module to centralize config tools (configs for access is just one -- serializers another)
35+ # TODO: Integrate (external because not standard lib) other safer tools for secrets, such as:
36+ # https://github.com/SimpleLegal/pocket_protector
37+
2138
2239def getenv (name , default = None ):
2340 """Like os.getenv, but removes a suffix \\ r character if present (problem with some env var systems)"""
@@ -28,10 +45,105 @@ def getenv(name, default=None):
2845 return v
2946
3047
48+ def assert_callable (f : callable ) -> callable :
49+ assert callable (f ), f"Is not callable: { f } "
50+ return f
51+
52+
53+ def dotpath_to_obj (dotpath ):
54+ """Loads and returns the object referenced by the string DOTPATH_TO_MODULE.OBJ_NAME"""
55+ * module_path , obj_name = dotpath .split ('.' )
56+ return getattr (importlib .import_module ('.' .join (module_path )), obj_name )
57+
58+
59+ def dotpath_to_func (f : (str , callable )) -> callable :
60+ """Loads and returns the function referenced by f,
61+ which could be a callable or a DOTPATH_TO_MODULE.FUNC_NAME dotpath string to one.
62+ """
63+
64+ if isinstance (f , str ):
65+ if '.' in f :
66+ * module_path , func_name = f .split ('.' )
67+ f = getattr (importlib .import_module ('.' .join (module_path )), func_name )
68+ else :
69+ f = getattr (importlib .import_module ('py2store' ), f )
70+
71+ return assert_callable (f )
72+
73+
74+ def compose (* functions ):
75+ """Make a function that is the composition of the input functions"""
76+ return reduce (lambda f , g : lambda x : f (g (x )), functions , lambda x : x )
77+
78+
79+ def dflt_func_loader (f ) -> callable :
80+ """Loads and returns the function referenced by f,
81+ which could be a callable or a DOTPATH_TO_MODULE.FUNC_NAME dotpath string to one, or a pipeline of these
82+ """
83+ if isinstance (f , str ) or callable (f ):
84+ return dotpath_to_func (f )
85+ else :
86+ return compose (* map (dflt_func_loader , f ))
87+
88+
89+ def _fakit (f : callable , a : (tuple , list ), k : dict ):
90+ return f (* (a or ()), ** (k or {}))
91+
92+
93+ def fakit_from_dict (d , func_loader = assert_callable ):
94+ return _fakit (func_loader (d ['f' ]), a = d .get ('a' , ()), k = d .get ('k' , {}))
95+
96+
97+ def fakit_from_tuple (t : (tuple , list ), func_loader : callable = dflt_func_loader ):
98+ f = func_loader (t [0 ])
99+ a = ()
100+ k = {}
101+ assert len (t ) in {1 , 2 , 3 }, "A tuple fak must be of length 1, 2, or 3. No more, no less."
102+ if len (t ) > 1 :
103+ if isinstance (t [1 ], dict ):
104+ k = t [1 ]
105+ else :
106+ assert isinstance (t [1 ], (tuple , list )), "argument specs should be dict, tuple, or list"
107+ a = t [1 ]
108+ if len (t ) > 2 :
109+ if isinstance (t [2 ], dict ):
110+ assert not k , "can only have one kwargs"
111+ k = t [2 ]
112+ else :
113+ assert isinstance (t [2 ], (tuple , list )), "argument specs should be dict, tuple, or list"
114+ assert not a , "can only have one args"
115+ a = t [2 ]
116+ return _fakit (f , a , k )
117+
118+
119+ def fakit (fak , func_loader = dflt_func_loader ):
120+ """Execute a fak with given f, a, k and function loader.
121+
122+ Essentially returns func_loader(f)(*a, **k)
123+
124+ Args:
125+ fak: A (f, a, k) specification. Could be a tuple or a dict (with 'f', 'a', 'k' keys). All but f are optional.
126+ func_loader: A function returning a function. This is where you specify any validation of func specification f,
127+ and/or how to get a callable from it.
128+
129+ Returns: A python object.
130+ """
131+
132+ if isinstance (fak , dict ):
133+ return fakit_from_dict (fak , func_loader = func_loader )
134+ else :
135+ assert isinstance (fak , (tuple , list )), "fak should be dict, tuple, or list"
136+ return fakit_from_tuple (fak , func_loader = func_loader )
137+
138+
139+ fakit .from_dict = fakit_from_dict
140+ fakit .from_tuple = fakit_from_tuple
141+
31142user_configs_dict = {}
32143user_defaults_dict = {}
33144user_configs = None
34145user_defaults = None
146+ mystores = None
35147
36148try :
37149 import json
@@ -53,6 +165,44 @@ def directory_json_items():
53165
54166 user_configs = DictAttr (** {k : v for k , v in directory_json_items ()})
55167
168+ from py2store .base import KvStore
169+ from py2store .stores .local_store import LocalJsonStore
170+ from py2store .trans import wrap_kvs
171+
172+
173+ class MyStores (KvStore ):
174+ func_loader = staticmethod (dflt_func_loader )
175+
176+ def _obj_of_data (self , data ):
177+ if '$fak' in data :
178+ return fakit (data ['$fak' ], self .func_loader )
179+ else :
180+ msg = "Case not handled by MyStores"
181+ if isinstance (data , dict ):
182+ raise ValueError (f"{ msg } : keys: { list (data .keys ())} " )
183+ else :
184+ raise ValueError (f"{ msg } : type: { type (data )} " )
185+
186+ @property
187+ def configs (self ):
188+ return self .store
189+
190+
191+ def without_json_ext (_id ):
192+ assert _id .endswith ('.json' ), "Should end with .json"
193+ return _id [:- len ('.json' )]
194+
195+
196+ def add_json_ext (k ):
197+ return k + '.json'
198+
199+
200+ ExtLessJsonStore = wrap_kvs (LocalJsonStore , name = 'ExtLessJsonStore' ,
201+ key_of_id = without_json_ext , id_of_key = add_json_ext )
202+
203+ stores_json_path_format = os .path .join (user_configs_dirpath , 'stores' , 'json' , '{}.json' )
204+ mystores = MyStores (ExtLessJsonStore (stores_json_path_format ))
205+
56206 else :
57207 warn (f"The configs directory wasn't found (please make it): { user_configs_dirpath } " )
58208 warn ("Configs in a single json is being deprecated" )
0 commit comments