Skip to content

Commit e397ba5

Browse files
committed
transitioned existing code into vpd.legacy and added vpd.next code for in-memory relationship graphs and kubernetes state management
1 parent 0116425 commit e397ba5

26 files changed

Lines changed: 2006 additions & 631 deletions

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,48 @@
11
# python-vpd
2+
23
VirtualPathDictChains - Hierarchical Settings Management using YaML
34

4-
This is an amalgamation of existing code that has been separated into its own package. Documentation and examples coming soon.
5+
This is an amalgamation of existing code that has been separated into its own package.
56

67
Hosted on GitHub: https://github.com/dbotwinick/python-vpd
8+
9+
As of version 0.9.5+, python 2.7 support is being phased out and targeting 3.9+ for python support. The legacy code
10+
is actually still python 2.7 compatible; however, the vpd.next code is not guaranteed to support python versions
11+
less than 3.9.
12+
13+
The base legacy code for VirtualPathDictChains is still useful and provides a mechanism to find data in "chains" of
14+
yaml. For example, if you had a dict: "{"test": {"v1": "v1", "v2":"v2"}}", using the VPD/VirtualPathDictChain approach,
15+
you could query for "test/v1" and get the result "v1". This was originally designed for complex settings or
16+
preferences management in python applications.
17+
18+
The "chain" part is that if a value is not found, the next source/VirtualPathDict would be searched. By having a chain,
19+
settings could be "merged" into a single queryable view. String values in the dicts also supported default arg
20+
substitution such that if a query result contained a text value of "{test/v2}", the bracketed expression
21+
would be used as a lookup to find that value--allowing references--which is also really useful for managing complex
22+
settings or preferences for an application etc.
23+
24+
The legacy code is maintained at vpd.legacy and backwards-compatible stubs are provided in the package root. Therefore,
25+
the following packages still work:
26+
27+
- vpd.arguments
28+
- vpd.cid
29+
- vpd.cmp
30+
- vpd.iterable
31+
- vpd.yaml_dict
32+
33+
The newer generation code expands on this base concept to allow modeling arbitrary relationships among data "types"
34+
(expected to be yaml or yaml-like) to be able to create novel use-cases. So the next generation mechanism can also
35+
be used to model settings and use references to share settings/preferences. It's basically something like a simple,
36+
non-indexed, in-memory graph database for modeling relationships that do not require much complexity such that a
37+
real graph solution is warranted; however, the problem at hand benefits from describing relationships first and
38+
then lazily calculating some result following along the data relationships.
39+
40+
This newer code is in "vpd.next.graph".
41+
42+
As an additional utility basis, vpd.next.k8s provides utility mechanisms for Kubernetes. These are meant to ease
43+
tasks managing state (via ConfigMaps) and secrets for python applications. These can be combined with the legacy
44+
VirtualPathDict mechanisms as well as the vpd.next.graph mechanisms as part of maintaining complex applications
45+
intended to operate in a Kubernetes context. Note that the official Kubernetes python client is required for Kubernetes
46+
functionality.
47+
48+
More documentation to come...

setup.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setuptools.setup(
99
name="vpd",
10-
version="0.9.4",
10+
version="0.9.10",
1111
author="Drew Botwinick",
1212
author_email="foss@drewbotwinick.com",
1313
description="VirtualPathDictChains. Hierarchical, Addressable Dicts, potentially using YaML",
@@ -19,10 +19,9 @@
1919
classifiers=[
2020
'Development Status :: 4 - Beta',
2121
'Intended Audience :: Developers',
22-
'Programming Language :: Python :: 2',
2322
'Programming Language :: Python :: 3',
2423
'License :: OSI Approved :: BSD License',
2524
"Operating System :: OS Independent",
2625
],
27-
python_requires='>=2.7, >=3.6',
26+
python_requires='>=3.9',
2827
)

vpd/__init__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# author: Drew Botwinick, Botwinick Innovations
22
# license: 3-clause BSD
33

4-
from .arguments import arg_substitute
5-
from .cid import CaseInsensitiveDict
6-
from .iterable import flatten, is_iterable
7-
from .yaml_dict import VirtualPathDictChain, get_data, read_yaml, vpd_chain, vpd_data, vpd_get
4+
# this is a stub to provide backwards compatibility while transitioning code
5+
6+
from .legacy.arguments import arg_substitute
7+
from .legacy.cid import CaseInsensitiveDict
8+
from .legacy.iterable import flatten, is_iterable
9+
from .legacy.yaml_dict import VirtualPathDictChain, get_data, vpd_chain, vpd_data, vpd_get
10+
11+
from .next.util import read_yaml

vpd/arguments.py

Lines changed: 3 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,7 @@
11
# author: Drew Botwinick, Botwinick Innovations
22
# license: 3-clause BSD
3-
import operator
4-
import re
5-
from functools import reduce
63

7-
from six import iteritems, string_types
4+
# this is a stub to provide backwards compatibility while transitioning code
85

9-
_find_arg_re = re.compile(r"\{(.*?)\}")
10-
11-
12-
# region Argument Interpretation
13-
14-
def arg_substitute(arg, data):
15-
if isinstance(arg, list):
16-
return [arg_substitute(a, data) for a in arg]
17-
elif not isinstance(arg, string_types):
18-
return None
19-
20-
def _replace_match(m):
21-
r = data.get(m.group().strip("}{"))
22-
return str(r) if r is not None else m.group() # put back the original if we don't have a match
23-
24-
return _find_arg_re.sub(_replace_match, arg)
25-
26-
27-
def variable_substitute(arg, data, flatten=False):
28-
if isinstance(arg, list) and flatten:
29-
# TODO: switch to chain? maybe just remove flatten altogether?
30-
return reduce(operator.add, (variable_substitute(a, data, flatten) for a in arg))
31-
elif isinstance(arg, list):
32-
return [variable_substitute(a, data, flatten) for a in arg]
33-
elif not isinstance(arg, string_types):
34-
return None
35-
36-
matches = _find_arg_re.findall(arg)
37-
subs = [data.get(m) for m in matches] if matches else None
38-
if matches and subs:
39-
ref_type = type(subs[0])
40-
types_match = all(type(s) == ref_type for s in subs)
41-
if types_match and isinstance(subs[0], (list, string_types)):
42-
if len(subs) > 1:
43-
return reduce(operator.add, subs)
44-
else:
45-
return subs[0]
46-
return None
47-
48-
49-
def interpret_args_yaml(args, data, default_arg_sep=' ', sub_fn=arg_substitute):
50-
if args:
51-
result = []
52-
for s in args:
53-
if isinstance(s, dict):
54-
arg_sep = s.pop('SEPARATOR', default_arg_sep) # separator between arguments
55-
key_join = s.pop('KEY_JOIN', default_arg_sep) # separator between complex arg key and resolved value
56-
for k, v in iteritems(s):
57-
if isinstance(v, list):
58-
t = interpret_args_yaml(v, data, default_arg_sep)
59-
if t and arg_sep != ' ':
60-
t = arg_sep.join(t)
61-
if key_join != ' ':
62-
result.append(k + key_join + t)
63-
else: # if the separator is ' ' then we're going to pop them on as separate tokens
64-
if k:
65-
result.append(k)
66-
result.append(t)
67-
elif t:
68-
result += t # t is definitely a list at this point...
69-
else:
70-
t = sub_fn(v, data)
71-
if v != t and len(t) > 0:
72-
if arg_sep != ' ':
73-
result.append(k + arg_sep + t)
74-
else: # if the separator is ' ' then we're going to pop them on as separate tokens
75-
if k:
76-
result.append(k)
77-
result.append(t)
78-
else:
79-
a = sub_fn(s, data)
80-
if isinstance(a, list):
81-
result.extend(a)
82-
else:
83-
result.append(a)
84-
return result
85-
return []
86-
87-
# endregion
6+
# noinspection PyUnresolvedReferences
7+
from .legacy.arguments import *

vpd/cid.py

Lines changed: 5 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,7 @@
1-
"""
2-
This implementation of CaseInsensitiveDict is taken from the python requests library.
1+
# author: Drew Botwinick, Botwinick Innovations
2+
# license: 3-clause BSD
33

4-
Although some modifications were made, they were generally trivial. Given the ubiquitous
5-
nature of requests, it would've been reasonable to just depend on the upstream code, but
6-
it seemed inappropriate to risk upstream changes breaking code with changes across versions.
7-
This seemed extremely relevant because the previous version of this code that was used in
8-
a few places was from an older implementation that used upper-cased keys instead of
9-
lower-cased keys. This type of change could break augmenting code. Therefore, it made
10-
sense to reproduce it here to prevent version-related breaking changes.
4+
# this is a stub to provide backwards compatibility while transitioning code
115

12-
The original license and copyright are:
13-
14-
(c) 2017 by Kenneth Reitz.
15-
License: Apache 2.0
16-
17-
"""
18-
19-
from collections import OrderedDict
20-
21-
try:
22-
from collections import Mapping, MutableMapping
23-
except ImportError:
24-
from collections.abc import Mapping, MutableMapping
25-
26-
27-
class CaseInsensitiveDict(MutableMapping):
28-
"""A case-insensitive ``dict``-like object.
29-
30-
Implements all methods and operations of
31-
``MutableMapping`` as well as dict's ``copy``. Also
32-
provides ``lower_items``.
33-
34-
All keys are expected to be strings. The structure remembers the
35-
case of the last key to be set, and ``iter(instance)``,
36-
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
37-
will contain case-sensitive keys. However, querying and contains
38-
testing is case insensitive::
39-
40-
cid = CaseInsensitiveDict()
41-
cid['Accept'] = 'application/json'
42-
cid['aCCEPT'] == 'application/json' # True
43-
list(cid) == ['Accept'] # True
44-
45-
For example, ``headers['content-encoding']`` will return the
46-
value of a ``'Content-Encoding'`` response header, regardless
47-
of how the header name was originally stored.
48-
49-
If the constructor, ``.update``, or equality comparison
50-
operations are given keys that have equal ``.lower()``s, the
51-
behavior is undefined.
52-
"""
53-
54-
def __init__(self, data=None, **kwargs):
55-
self._store = OrderedDict()
56-
if data is None:
57-
data = {}
58-
self.update(data, **kwargs)
59-
60-
def __setitem__(self, key, value):
61-
# Use the lowercased key for lookups, but store the actual key alongside the value.
62-
self._store[key.lower()] = (key, value)
63-
64-
def __getitem__(self, key):
65-
return self._store[key.lower()][1]
66-
67-
def __delitem__(self, key):
68-
del self._store[key.lower()]
69-
70-
def __iter__(self):
71-
return (cased_key for cased_key, mapped_value in self._store.values())
72-
73-
def __len__(self):
74-
return len(self._store)
75-
76-
def lower_items(self):
77-
"""Like iteritems(), but with all lowercase keys."""
78-
return ((lower_key, key_val[1]) for (lower_key, key_val) in self._store.items())
79-
80-
def get_item(self, key, default_key=None, default_value=None):
81-
"""
82-
Alternative version of get(key, default) for a case-insensitive dict. This always return a (key, value) tuple.
83-
The key will be returned in its original cased form. This can be useful when filtering user input to lookup
84-
data. The user provided key may not match the case of the backing dict data. This allows retrieval of the
85-
"authoritative" key and associated value.
86-
87-
:param key: key of item to retrieve
88-
:param default_key: default key to respond with if key not in dict
89-
:param default_value: default value to respond with if key not in dict
90-
:return: (key, value) tuple
91-
"""
92-
kl = key.lower()
93-
return self._store[kl] if kl in self._store else (default_key, default_value)
94-
95-
def __eq__(self, other):
96-
if isinstance(other, Mapping):
97-
other = CaseInsensitiveDict(other)
98-
else:
99-
return NotImplemented
100-
# Compare insensitively
101-
return dict(self.lower_items()) == dict(other.lower_items())
102-
103-
# Copy is required
104-
def copy(self):
105-
return CaseInsensitiveDict(self._store.values())
106-
107-
def __repr__(self):
108-
return str(dict(self.items()))
6+
# noinspection PyUnresolvedReferences
7+
from .legacy.cid import CaseInsensitiveDict

vpd/cmp.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
11
# author: Drew Botwinick, Botwinick Innovations
22
# license: 3-clause BSD
33

4-
try:
5-
from collections import Mapping
6-
except ImportError:
7-
from collections.abc import Mapping
4+
# this is a stub to provide backwards compatibility while transitioning code
85

9-
from .iterable import is_iterable
10-
11-
12-
def tuplize(obj, iterable_exclusions=None):
13-
result = []
14-
if obj is not None:
15-
if isinstance(obj, Mapping):
16-
result.append(tuplize(obj.items(), iterable_exclusions))
17-
elif not is_iterable(obj, excluded_types=iterable_exclusions):
18-
result.append(((), obj))
19-
else:
20-
for o in obj:
21-
result.extend(tuplize(o, iterable_exclusions))
22-
return tuple(result)
6+
# noinspection PyUnresolvedReferences
7+
from .legacy.cmp import tuplize

0 commit comments

Comments
 (0)