python-jsonstore 1.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jsonstore.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- encoding: UTF-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Provides a Python class that maps values to/from a JSON file
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import absolute_import
|
|
7
|
+
import json
|
|
8
|
+
import os.path
|
|
9
|
+
import sys
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
|
|
13
|
+
__all__ = ["JsonStore"]
|
|
14
|
+
|
|
15
|
+
STRING_TYPES = (str,)
|
|
16
|
+
INT_TYPES = (int,)
|
|
17
|
+
if sys.version_info < (3,):
|
|
18
|
+
STRING_TYPES += (unicode,)
|
|
19
|
+
INT_TYPES += (long,)
|
|
20
|
+
VALUE_TYPES = (bool, int, float, type(None)) + INT_TYPES
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JsonStore(object):
|
|
24
|
+
"""A class to provide object based access to a JSON file"""
|
|
25
|
+
|
|
26
|
+
def __enter__(self):
|
|
27
|
+
current_state = self.__dict__["_data"]
|
|
28
|
+
self.__dict__["_states"].append(current_state)
|
|
29
|
+
self.__dict__["_data"] = deepcopy(current_state)
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def __exit__(self, *args):
|
|
33
|
+
previous_state = self.__dict__["_states"].pop()
|
|
34
|
+
if any(args):
|
|
35
|
+
self.__dict__["_data"] = previous_state
|
|
36
|
+
elif not self.__dict__["_states"]:
|
|
37
|
+
self._save()
|
|
38
|
+
|
|
39
|
+
def _do_auto_commit(self):
|
|
40
|
+
if self._auto_commit and not self.__dict__["_states"]:
|
|
41
|
+
self._save()
|
|
42
|
+
|
|
43
|
+
def _load(self):
|
|
44
|
+
if not os.path.exists(self._path):
|
|
45
|
+
with open(self._path, "w+b") as store:
|
|
46
|
+
store.write("{}".encode("utf-8"))
|
|
47
|
+
with open(self._path, "r+b") as store:
|
|
48
|
+
raw_data = store.read().decode("utf-8")
|
|
49
|
+
if not raw_data:
|
|
50
|
+
data = OrderedDict()
|
|
51
|
+
else:
|
|
52
|
+
data = json.loads(raw_data, object_pairs_hook=OrderedDict)
|
|
53
|
+
|
|
54
|
+
if not isinstance(data, dict):
|
|
55
|
+
raise ValueError("Root element is not an object")
|
|
56
|
+
self.__dict__["_data"] = data
|
|
57
|
+
|
|
58
|
+
def _save(self):
|
|
59
|
+
temp = self._path + "~"
|
|
60
|
+
with open(temp, "wb") as store:
|
|
61
|
+
output = json.dumps(self._data, indent=self._indent)
|
|
62
|
+
store.write(output.encode("utf-8"))
|
|
63
|
+
|
|
64
|
+
if sys.version_info >= (3, 3):
|
|
65
|
+
os.replace(temp, self._path)
|
|
66
|
+
elif os.name == "windows":
|
|
67
|
+
os.remove(self._path)
|
|
68
|
+
os.rename(temp, self._path)
|
|
69
|
+
else:
|
|
70
|
+
os.rename(temp, self._path)
|
|
71
|
+
|
|
72
|
+
def __init__(self, path, indent=2, auto_commit=True):
|
|
73
|
+
self.__dict__.update(
|
|
74
|
+
{
|
|
75
|
+
"_auto_commit": auto_commit,
|
|
76
|
+
"_data": None,
|
|
77
|
+
"_path": path,
|
|
78
|
+
"_indent": indent,
|
|
79
|
+
"_states": [],
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
self._load()
|
|
83
|
+
|
|
84
|
+
def __getattr__(self, key):
|
|
85
|
+
if key in self._data:
|
|
86
|
+
return deepcopy(self._data[key])
|
|
87
|
+
else:
|
|
88
|
+
raise AttributeError(key)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def _verify_object(cls, obj, parents=None):
|
|
92
|
+
"""
|
|
93
|
+
Raise an exception if the object is not suitable for assignment.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
# pylint: disable=unicode-builtin,long-builtin
|
|
97
|
+
if isinstance(obj, (dict, list)):
|
|
98
|
+
if parents is None:
|
|
99
|
+
parents = []
|
|
100
|
+
elif any(o is obj for o in parents):
|
|
101
|
+
raise ValueError("Cycle detected in list/dictionary")
|
|
102
|
+
parents.append(obj)
|
|
103
|
+
|
|
104
|
+
if isinstance(obj, dict):
|
|
105
|
+
for k, v in obj.items():
|
|
106
|
+
if not cls._valid_string(k):
|
|
107
|
+
# this is necessary because of the JSON serialisation
|
|
108
|
+
raise TypeError("a dict has non-string keys")
|
|
109
|
+
cls._verify_object(v, parents)
|
|
110
|
+
elif isinstance(obj, (list, tuple)):
|
|
111
|
+
for o in obj:
|
|
112
|
+
cls._verify_object(o, parents)
|
|
113
|
+
else:
|
|
114
|
+
return cls._valid_value(obj)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def _valid_value(cls, value):
|
|
118
|
+
if isinstance(value, VALUE_TYPES):
|
|
119
|
+
return True
|
|
120
|
+
else:
|
|
121
|
+
return cls._valid_string(value)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def _valid_string(cls, value):
|
|
125
|
+
if isinstance(value, STRING_TYPES):
|
|
126
|
+
return True
|
|
127
|
+
else:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def _canonical_key(cls, key):
|
|
132
|
+
"""Convert a set/get/del key into the canonical form."""
|
|
133
|
+
if cls._valid_string(key):
|
|
134
|
+
return tuple(key.split("."))
|
|
135
|
+
|
|
136
|
+
if isinstance(key, (tuple, list)):
|
|
137
|
+
key = tuple(key)
|
|
138
|
+
if not key:
|
|
139
|
+
raise TypeError("key must be a string or non-empty tuple/list")
|
|
140
|
+
return key
|
|
141
|
+
|
|
142
|
+
raise TypeError("key must be a string or non-empty tuple/list")
|
|
143
|
+
|
|
144
|
+
def __setattr__(self, attr, value):
|
|
145
|
+
self._verify_object(value)
|
|
146
|
+
self._data[attr] = deepcopy(value)
|
|
147
|
+
self._do_auto_commit()
|
|
148
|
+
|
|
149
|
+
def __delattr__(self, attr):
|
|
150
|
+
del self._data[attr]
|
|
151
|
+
|
|
152
|
+
def __get_obj(self, steps):
|
|
153
|
+
"""Returns the object which is under the given path."""
|
|
154
|
+
path = []
|
|
155
|
+
obj = self._data
|
|
156
|
+
for step in steps:
|
|
157
|
+
if isinstance(obj, dict) and not self._valid_string(step):
|
|
158
|
+
# this is necessary because of the JSON serialisation
|
|
159
|
+
raise TypeError("%s is a dict and %s is not a string" % (path, step))
|
|
160
|
+
try:
|
|
161
|
+
obj = obj[step]
|
|
162
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
163
|
+
raise type(e)("unable to get %s from %s: %s" % (step, path, e))
|
|
164
|
+
path.append(step)
|
|
165
|
+
return obj
|
|
166
|
+
|
|
167
|
+
def __setitem__(self, key, value):
|
|
168
|
+
steps = self._canonical_key(key)
|
|
169
|
+
path, step = steps[:-1], steps[-1]
|
|
170
|
+
self._verify_object(value)
|
|
171
|
+
container = self.__get_obj(path)
|
|
172
|
+
if isinstance(container, dict) and not self._valid_string(step):
|
|
173
|
+
raise TypeError("%s is a dict and %s is not a string" % (path, step))
|
|
174
|
+
try:
|
|
175
|
+
container[step] = deepcopy(value)
|
|
176
|
+
except (IndexError, TypeError) as e:
|
|
177
|
+
raise type(e)("unable to set %s from %s: %s" % (step, path, e))
|
|
178
|
+
self._do_auto_commit()
|
|
179
|
+
|
|
180
|
+
def __getitem__(self, key):
|
|
181
|
+
steps = self._canonical_key(key)
|
|
182
|
+
obj = self.__get_obj(steps)
|
|
183
|
+
return deepcopy(obj)
|
|
184
|
+
|
|
185
|
+
def __delitem__(self, key):
|
|
186
|
+
steps = self._canonical_key(key)
|
|
187
|
+
path, step = steps[:-1], steps[-1]
|
|
188
|
+
obj = self.__get_obj(path)
|
|
189
|
+
try:
|
|
190
|
+
del obj[step]
|
|
191
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
192
|
+
raise type(e)("unable to delete %s from %s: %s" % (step, path, e))
|
|
193
|
+
|
|
194
|
+
def __contains__(self, key):
|
|
195
|
+
steps = self._canonical_key(key)
|
|
196
|
+
try:
|
|
197
|
+
self.__get_obj(steps)
|
|
198
|
+
return True
|
|
199
|
+
except (KeyError, IndexError, TypeError):
|
|
200
|
+
# this is rather permissive as the types are dynamic
|
|
201
|
+
return False
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-jsonstore
|
|
3
|
+
Version: 1.3.3
|
|
4
|
+
Summary: Map JSON files onto Python attributes for simple configuration storage
|
|
5
|
+
Author-email: Oliver Bristow <github+pypi@oliverbristow.co.uk>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Code0x58/python-jsonstore/
|
|
8
|
+
Keywords: json,key,value,store
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 2
|
|
14
|
+
Classifier: Programming Language :: Python :: 2.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.4
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.5
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
29
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
30
|
+
Classifier: Topic :: Database
|
|
31
|
+
Classifier: Topic :: Software Development
|
|
32
|
+
Requires-Python: >=2.7
|
|
33
|
+
Description-Content-Type: text/x-rst
|
|
34
|
+
License-File: LICENCE.md
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
|PyPI| |Build Status| |Coverage Report|
|
|
38
|
+
|
|
39
|
+
jsonstore
|
|
40
|
+
=========
|
|
41
|
+
|
|
42
|
+
This module provides a class that maps keys and values from a JSON file
|
|
43
|
+
onto its attributes.
|
|
44
|
+
|
|
45
|
+
The goal was to provide a convenient way of loading and saving
|
|
46
|
+
configuration in a familiar human readable format. This is a bit more
|
|
47
|
+
flexible than the
|
|
48
|
+
`configparser <https://docs.python.org/3/library/configparser.html>`__
|
|
49
|
+
module which is included with Python.
|
|
50
|
+
|
|
51
|
+
This works is tested and working on Python 2.7 and Python 3.6+. It will
|
|
52
|
+
not work on 2.6 or lower. The tests are also executed on PyPy.
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
|
|
57
|
+
Basics
|
|
58
|
+
~~~~~~
|
|
59
|
+
|
|
60
|
+
.. code:: python
|
|
61
|
+
|
|
62
|
+
# by default JsonStore commits on every change unless in a transaction
|
|
63
|
+
store = JsonStore('config.json')
|
|
64
|
+
store.a_string = "something"
|
|
65
|
+
store.a_list = [1, 2, 3]
|
|
66
|
+
store.a_dictionary = {
|
|
67
|
+
'dict-list': [{}],
|
|
68
|
+
'ln(2)': 0.69314718056,
|
|
69
|
+
'for-you': u"💐",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# you can use […] to set/get/delete string keys
|
|
73
|
+
store['some_key'] = "a value"
|
|
74
|
+
# the key is split on '.'s and works on dictionaries
|
|
75
|
+
del store['a_dictionary.dict-list']
|
|
76
|
+
store['a_dictionary.new_value'] = "old value"
|
|
77
|
+
# you can also use the syntactic sugar for tuple keys (explicit lists work too)
|
|
78
|
+
assert store['a_dictionary', 'new_value'] == "old value"
|
|
79
|
+
# you can traverse lists too
|
|
80
|
+
assert store['a_list', -1] == 3
|
|
81
|
+
# you can use slices in lists
|
|
82
|
+
assert len(store['a_list', 1:]) == 2
|
|
83
|
+
del store['a_list', :2]
|
|
84
|
+
assert store.a_list == [3]
|
|
85
|
+
|
|
86
|
+
# deep copies are made when assigning values
|
|
87
|
+
my_list = ['fun']
|
|
88
|
+
store.a_list = my_list
|
|
89
|
+
assert store.a_list is not my_list
|
|
90
|
+
assert 'a_list' in store
|
|
91
|
+
|
|
92
|
+
# deep copies are also returned to avoid unsanitary changes being made
|
|
93
|
+
store.a_dictionary['new_value'] = "new value" # won't update the store!
|
|
94
|
+
assert store.a_dictionary['new_value'] == "old value"
|
|
95
|
+
assert store.a_dictionary is not store.a_dictionary
|
|
96
|
+
|
|
97
|
+
# Appending to, extending a list
|
|
98
|
+
>>> store.list = [1, 2, 3]
|
|
99
|
+
|
|
100
|
+
# Because of the fact that .append won't modify the list in the actual file,
|
|
101
|
+
# but only a copy...
|
|
102
|
+
>>> store.list.append(4)
|
|
103
|
+
>>> store.list
|
|
104
|
+
[1, 2, 3]
|
|
105
|
+
|
|
106
|
+
# ... we need to rather use the += operator to append to a list.
|
|
107
|
+
>>> store.list += [4]
|
|
108
|
+
>>> store.list
|
|
109
|
+
[1, 2, 3, 4]
|
|
110
|
+
|
|
111
|
+
# Similarly, we can extend the list
|
|
112
|
+
>>> store.list += [5, 6]
|
|
113
|
+
>>> store.list
|
|
114
|
+
[1, 2, 3, 4, 5, 6]
|
|
115
|
+
|
|
116
|
+
Transactions
|
|
117
|
+
~~~~~~~~~~~~
|
|
118
|
+
|
|
119
|
+
``JsonStore`` objects can be used as `context
|
|
120
|
+
managers <https://www.python.org/dev/peps/pep-0343/>`__ to provide
|
|
121
|
+
transactions which are rolled back in the event of an exception. The
|
|
122
|
+
transaction model is primitive; you can only nest transactions.
|
|
123
|
+
|
|
124
|
+
While a store is put into a transaction, it will not save changes to
|
|
125
|
+
file until all of the transactions have been closed.
|
|
126
|
+
|
|
127
|
+
.. code:: python
|
|
128
|
+
|
|
129
|
+
from jsonstore import JsonStore
|
|
130
|
+
|
|
131
|
+
# even with auto_commit=True, the file won't be saved until the last contexts has been closed
|
|
132
|
+
with JsonStore('config.json', indent=None, auto_commit=False) as store:
|
|
133
|
+
self.value = 1
|
|
134
|
+
|
|
135
|
+
# the context manager will roll back changes made if an exception is raised
|
|
136
|
+
store = JsonStore('config.json', indent=None)
|
|
137
|
+
try:
|
|
138
|
+
with store:
|
|
139
|
+
store.value = "new"
|
|
140
|
+
raise Exception
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
# here we see the value that was saved previously
|
|
144
|
+
assert store.value == 1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
.. |Build Status| image:: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml/badge.svg
|
|
148
|
+
:target: https://github.com/Code0x58/python-jsonstore/actions/workflows/ci.yml
|
|
149
|
+
.. |PyPI| image:: https://img.shields.io/pypi/v/python-jsonstore.svg
|
|
150
|
+
:target: https://pypi.org/project/python-jsonstore/
|
|
151
|
+
.. |Coverage Report| image:: https://codecov.io/gh/Code0x58/python-jsonstore/branch/master/graph/badge.svg
|
|
152
|
+
:target: https://codecov.io/gh/Code0x58/python-jsonstore
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
jsonstore.py,sha256=YGCPCsjTnklMHR_neqgbcV6ltQh5A5QKW4rbOQzzXJk,6511
|
|
2
|
+
python_jsonstore-1.3.3.dist-info/licenses/LICENCE.md,sha256=CYFsxv7F0RTimJ83xSWkMtR8audySdDV_z9XzTr2EP0,1071
|
|
3
|
+
python_jsonstore-1.3.3.dist-info/METADATA,sha256=yxS-sej7Mi05NMnx9Zrz3gkLVtIlA8uXpPVBD1Dpbpc,5434
|
|
4
|
+
python_jsonstore-1.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
python_jsonstore-1.3.3.dist-info/top_level.txt,sha256=5Lr-ZZ_07h17JRcT8M4FAt5aEkwAMHNGZuVHNC7yZ5w,10
|
|
6
|
+
python_jsonstore-1.3.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 Oliver Bristow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jsonstore
|