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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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