rich-object 0.1.0__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.
rich_object/__init__.py
ADDED
rich_object/object.py
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
class ObjectList(list):
|
|
6
|
+
"""A list subclass that respects lock state and auto-wraps dicts into Objects."""
|
|
7
|
+
def __init__(self, iterable=(), lock=False):
|
|
8
|
+
self._lock = False
|
|
9
|
+
super().__init__()
|
|
10
|
+
for item in iterable:
|
|
11
|
+
self.append(item)
|
|
12
|
+
self._lock = lock
|
|
13
|
+
|
|
14
|
+
def _check_lock(self):
|
|
15
|
+
if getattr(self, '_lock', False):
|
|
16
|
+
raise TypeError(f"'{self.__class__.__name__}' is locked and cannot be modified")
|
|
17
|
+
|
|
18
|
+
def _wrap(self, item):
|
|
19
|
+
if isinstance(item, dict) and not isinstance(item, Object):
|
|
20
|
+
return Object(item, lock=self._lock)
|
|
21
|
+
elif isinstance(item, list) and not isinstance(item, ObjectList):
|
|
22
|
+
return ObjectList(item, lock=self._lock)
|
|
23
|
+
return item
|
|
24
|
+
|
|
25
|
+
def append(self, item):
|
|
26
|
+
self._check_lock()
|
|
27
|
+
super().append(self._wrap(item))
|
|
28
|
+
|
|
29
|
+
def extend(self, iterable):
|
|
30
|
+
self._check_lock()
|
|
31
|
+
super().extend(self._wrap(item) for item in iterable)
|
|
32
|
+
|
|
33
|
+
def insert(self, index, item):
|
|
34
|
+
self._check_lock()
|
|
35
|
+
super().insert(index, self._wrap(item))
|
|
36
|
+
|
|
37
|
+
def remove(self, item):
|
|
38
|
+
self._check_lock()
|
|
39
|
+
super().remove(item)
|
|
40
|
+
|
|
41
|
+
def pop(self, index=-1):
|
|
42
|
+
self._check_lock()
|
|
43
|
+
return super().pop(index)
|
|
44
|
+
|
|
45
|
+
def clear(self):
|
|
46
|
+
self._check_lock()
|
|
47
|
+
super().clear()
|
|
48
|
+
|
|
49
|
+
def __setitem__(self, index, item):
|
|
50
|
+
self._check_lock()
|
|
51
|
+
super().__setitem__(index, self._wrap(item))
|
|
52
|
+
|
|
53
|
+
def __delitem__(self, index):
|
|
54
|
+
self._check_lock()
|
|
55
|
+
super().__delitem__(index)
|
|
56
|
+
|
|
57
|
+
def __iadd__(self, other):
|
|
58
|
+
self._check_lock()
|
|
59
|
+
super().extend(self._wrap(item) for item in other)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __copy__(self):
|
|
63
|
+
new_list = ObjectList(lock=False)
|
|
64
|
+
for item in self:
|
|
65
|
+
super(ObjectList, new_list).append(item)
|
|
66
|
+
new_list._lock = self._lock
|
|
67
|
+
return new_list
|
|
68
|
+
|
|
69
|
+
def __deepcopy__(self, memo):
|
|
70
|
+
new_list = ObjectList(lock=False)
|
|
71
|
+
memo[id(self)] = new_list
|
|
72
|
+
for item in self:
|
|
73
|
+
super(ObjectList, new_list).append(copy.deepcopy(item, memo))
|
|
74
|
+
new_list._lock = self._lock
|
|
75
|
+
return new_list
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Object(dict):
|
|
79
|
+
"""A dictionary subclass that allows dot notation attribute access
|
|
80
|
+
and automatically creates missing paths (autovivification).
|
|
81
|
+
|
|
82
|
+
Supports lock (disables create/update/delete).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, *args, lock=False, **kwargs):
|
|
86
|
+
"""Initializes a new Object instance.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
*args: Initial dictionary or iterable of key-value pairs to populate the Object.
|
|
90
|
+
lock (bool): If True, disables any create, update, or delete operations on this Object
|
|
91
|
+
and all of its descendants/elements recursively.
|
|
92
|
+
**kwargs: Additional key-value pairs to populate the Object.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> obj = Object(name="John", address={"city": "New York"})
|
|
96
|
+
>>> obj.name
|
|
97
|
+
'John'
|
|
98
|
+
>>> obj.address.city
|
|
99
|
+
'New York'
|
|
100
|
+
>>> locked_obj = Object(x=1, lock=True)
|
|
101
|
+
>>> locked_obj.x = 2
|
|
102
|
+
TypeError: 'Object' is locked and cannot be modified
|
|
103
|
+
"""
|
|
104
|
+
object.__setattr__(self, '_lock', lock)
|
|
105
|
+
object.__setattr__(self, '_initialized', False)
|
|
106
|
+
|
|
107
|
+
super().__init__()
|
|
108
|
+
self.update(dict(*args, **kwargs))
|
|
109
|
+
|
|
110
|
+
object.__setattr__(self, '_initialized', True)
|
|
111
|
+
|
|
112
|
+
def __getattr__(self, key: str):
|
|
113
|
+
if key.startswith('__') and key.endswith('__'):
|
|
114
|
+
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{key}'")
|
|
115
|
+
|
|
116
|
+
if key not in self:
|
|
117
|
+
if object.__getattribute__(self, '_lock') and object.__getattribute__(self, '_initialized'):
|
|
118
|
+
raise TypeError(f"'{self.__class__.__name__}' is locked and cannot be modified")
|
|
119
|
+
|
|
120
|
+
lock_val = object.__getattribute__(self, '_lock')
|
|
121
|
+
self[key] = Object(lock=lock_val)
|
|
122
|
+
return self[key]
|
|
123
|
+
|
|
124
|
+
def __setattr__(self, key: str, value):
|
|
125
|
+
if key == '_lock':
|
|
126
|
+
if getattr(self, '_initialized', False):
|
|
127
|
+
raise TypeError("Cannot modify '_lock' after initialization")
|
|
128
|
+
object.__setattr__(self, key, value)
|
|
129
|
+
elif key == '_initialized':
|
|
130
|
+
object.__setattr__(self, key, value)
|
|
131
|
+
else:
|
|
132
|
+
self[key] = value
|
|
133
|
+
|
|
134
|
+
def __delattr__(self, key: str):
|
|
135
|
+
try:
|
|
136
|
+
del self[key]
|
|
137
|
+
except KeyError:
|
|
138
|
+
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{key}'")
|
|
139
|
+
|
|
140
|
+
def __getitem__(self, key):
|
|
141
|
+
if key not in self:
|
|
142
|
+
if object.__getattribute__(self, '_lock') and object.__getattribute__(self, '_initialized'):
|
|
143
|
+
raise TypeError(f"'{self.__class__.__name__}' is locked and cannot be modified")
|
|
144
|
+
|
|
145
|
+
lock_val = object.__getattribute__(self, '_lock')
|
|
146
|
+
self[key] = Object(lock=lock_val)
|
|
147
|
+
return super().__getitem__(key)
|
|
148
|
+
|
|
149
|
+
def __setitem__(self, key, value):
|
|
150
|
+
if object.__getattribute__(self, '_lock') and getattr(self, '_initialized', True):
|
|
151
|
+
raise TypeError(f"'{self.__class__.__name__}' is locked and cannot be modified")
|
|
152
|
+
|
|
153
|
+
lock_val = object.__getattribute__(self, '_lock')
|
|
154
|
+
|
|
155
|
+
if isinstance(value, dict) and not isinstance(value, Object):
|
|
156
|
+
value = Object(value, lock=lock_val)
|
|
157
|
+
elif isinstance(value, list) and not isinstance(value, ObjectList):
|
|
158
|
+
value = ObjectList(value, lock=lock_val)
|
|
159
|
+
super().__setitem__(key, value)
|
|
160
|
+
|
|
161
|
+
def __delitem__(self, key):
|
|
162
|
+
if object.__getattribute__(self, '_lock') and getattr(self, '_initialized', True):
|
|
163
|
+
raise TypeError(f"'{self.__class__.__name__}' is locked and cannot be modified")
|
|
164
|
+
super().__delitem__(key)
|
|
165
|
+
|
|
166
|
+
def update(self, *args, **kwargs):
|
|
167
|
+
for k, v in dict(*args, **kwargs).items():
|
|
168
|
+
self[k] = v
|
|
169
|
+
|
|
170
|
+
def set(self, path: str, value):
|
|
171
|
+
"""Sets a value at the specified path, creating nested Objects or ObjectLists as needed.
|
|
172
|
+
|
|
173
|
+
Supports dot notation for dict keys and bracket notation with indices for list elements.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
path (str): The dot-and-bracket path (e.g., 'a.b.list[0].c').
|
|
177
|
+
value: The value to set at the path.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
TypeError: If the Object is locked or if the path encounters a non-container type.
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
>>> obj = Object()
|
|
184
|
+
>>> obj.set("store.books[1].title", "Moby Dick")
|
|
185
|
+
>>> obj.store.books[1].title
|
|
186
|
+
'Moby Dick'
|
|
187
|
+
>>> obj.store.books[0] is None
|
|
188
|
+
True
|
|
189
|
+
"""
|
|
190
|
+
tokens = re.findall(r"[^.\[\]]+|\[\d+\]", path)
|
|
191
|
+
parsed_tokens = []
|
|
192
|
+
for t in tokens:
|
|
193
|
+
if t.startswith("[") and t.endswith("]"):
|
|
194
|
+
parsed_tokens.append(int(t[1:-1]))
|
|
195
|
+
else:
|
|
196
|
+
parsed_tokens.append(t)
|
|
197
|
+
|
|
198
|
+
if not parsed_tokens:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
curr = self
|
|
202
|
+
for i in range(len(parsed_tokens) - 1):
|
|
203
|
+
token = parsed_tokens[i]
|
|
204
|
+
next_token = parsed_tokens[i+1]
|
|
205
|
+
|
|
206
|
+
node_type = ObjectList if isinstance(next_token, int) else Object
|
|
207
|
+
|
|
208
|
+
if isinstance(curr, dict):
|
|
209
|
+
if token not in curr:
|
|
210
|
+
curr[token] = node_type()
|
|
211
|
+
curr = curr[token]
|
|
212
|
+
elif isinstance(curr, list):
|
|
213
|
+
while len(curr) <= token:
|
|
214
|
+
curr.append(Object() if isinstance(next_token, str) else ObjectList())
|
|
215
|
+
if curr[token] is None:
|
|
216
|
+
curr[token] = node_type()
|
|
217
|
+
curr = curr[token]
|
|
218
|
+
else:
|
|
219
|
+
raise TypeError(f"Cannot set property '{next_token}' on non-object '{token}'")
|
|
220
|
+
|
|
221
|
+
last_token = parsed_tokens[-1]
|
|
222
|
+
if isinstance(curr, dict):
|
|
223
|
+
curr[last_token] = value
|
|
224
|
+
elif isinstance(curr, list):
|
|
225
|
+
while len(curr) <= last_token:
|
|
226
|
+
curr.append(None)
|
|
227
|
+
curr[last_token] = value
|
|
228
|
+
|
|
229
|
+
def get(self, key, default=None):
|
|
230
|
+
"""Retrieves a value by key. Supports JSONPath expressions if the key starts with '$'.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
key (str): The key or JSONPath expression (e.g., '$.store.book[*].price').
|
|
234
|
+
default: The fallback value to return if the key/path is not found.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
The matched value, a list of matched values (for JSONPath), or the default value.
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
>>> obj = Object({"store": {"book": [{"price": 10}, {"price": 15}]}})
|
|
241
|
+
>>> obj.get("$.store.book[*].price")
|
|
242
|
+
[10, 15]
|
|
243
|
+
>>> obj.get("nonexistent_key", "default_val")
|
|
244
|
+
'default_val'
|
|
245
|
+
"""
|
|
246
|
+
if isinstance(key, str) and key.startswith('$'):
|
|
247
|
+
try:
|
|
248
|
+
from jsonpath_ng import parse
|
|
249
|
+
jsonpath_expr = parse(key)
|
|
250
|
+
matches = [match.value for match in jsonpath_expr.find(self)]
|
|
251
|
+
|
|
252
|
+
if not matches:
|
|
253
|
+
return default
|
|
254
|
+
if len(matches) == 1:
|
|
255
|
+
return matches[0]
|
|
256
|
+
return matches
|
|
257
|
+
except Exception:
|
|
258
|
+
return default
|
|
259
|
+
return super().get(key, default)
|
|
260
|
+
|
|
261
|
+
def __add__(self, other):
|
|
262
|
+
"""Merges this Object with another dictionary or Object and returns a new Object.
|
|
263
|
+
|
|
264
|
+
Performs a deep merge, extending list items and recursively merging nested mappings.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
other (dict): The dictionary or Object to merge with.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Object: A new merged Object.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
TypeError: If this Object is locked, or other is not a dictionary.
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
>>> obj1 = Object({"a": {"x": 1}, "b": [1, 2]})
|
|
277
|
+
>>> obj2 = {"a": {"y": 2}, "b": [3, 4]}
|
|
278
|
+
>>> merged = obj1 + obj2
|
|
279
|
+
>>> merged.to_dict()
|
|
280
|
+
{'a': {'x': 1, 'y': 2}, 'b': [1, 2, 3, 4]}
|
|
281
|
+
"""
|
|
282
|
+
if object.__getattribute__(self, '_lock'):
|
|
283
|
+
raise TypeError(f"'{self.__class__.__name__}' is locked and cannot be merged")
|
|
284
|
+
if not isinstance(other, dict):
|
|
285
|
+
return NotImplemented
|
|
286
|
+
|
|
287
|
+
result = copy.deepcopy(self)
|
|
288
|
+
|
|
289
|
+
def merge(target, source):
|
|
290
|
+
for k, v in source.items():
|
|
291
|
+
if k in target:
|
|
292
|
+
if isinstance(target[k], dict) and isinstance(v, dict):
|
|
293
|
+
merge(target[k], v)
|
|
294
|
+
elif isinstance(target[k], list) and isinstance(v, list):
|
|
295
|
+
# Use target[k].extend so ObjectList wraps new dicts
|
|
296
|
+
target[k].extend(copy.deepcopy(v))
|
|
297
|
+
else:
|
|
298
|
+
target[k] = copy.deepcopy(v)
|
|
299
|
+
else:
|
|
300
|
+
target[k] = copy.deepcopy(v)
|
|
301
|
+
|
|
302
|
+
merge(result, other)
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
def to_dict(self):
|
|
306
|
+
"""Recursively converts this Object and all nested Objects/ObjectLists to standard Python dicts and lists.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
dict: A standard Python dict representation of this Object.
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
>>> obj = Object(a=1, b={"c": 2})
|
|
313
|
+
>>> type(obj.b)
|
|
314
|
+
<class 'rich_object.object.Object'>
|
|
315
|
+
>>> d = obj.to_dict()
|
|
316
|
+
>>> type(d['b'])
|
|
317
|
+
<class 'dict'>
|
|
318
|
+
"""
|
|
319
|
+
def _convert(obj):
|
|
320
|
+
if isinstance(obj, Object):
|
|
321
|
+
return {k: _convert(v) for k, v in obj.items()}
|
|
322
|
+
elif isinstance(obj, list):
|
|
323
|
+
return [_convert(v) for v in obj]
|
|
324
|
+
return obj
|
|
325
|
+
return {k: _convert(v) for k, v in self.items()}
|
|
326
|
+
|
|
327
|
+
def render(self, **kwargs):
|
|
328
|
+
"""Renders Jinja2 templates in all string values recursively across the entire structure.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
**kwargs: Additional context variables passed to the Jinja template rendering environment.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Object: A copy of this Object with all template variables rendered.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
TypeError: If this Object is locked.
|
|
338
|
+
ImportError: If the 'jinja2' package is not installed.
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> obj = Object({"greeting": "Hello {{ name }}!"})
|
|
342
|
+
>>> rendered = obj.render(name="World")
|
|
343
|
+
>>> rendered.greeting
|
|
344
|
+
'Hello World!'
|
|
345
|
+
|
|
346
|
+
>>> # Using filters like 'int' and 'str'
|
|
347
|
+
>>> obj = Object({
|
|
348
|
+
... "number": "{{ '42' | int }}",
|
|
349
|
+
... "serialized_list": "{{ [1, 2, 3] | str }}",
|
|
350
|
+
... "serialized_dict": "{{ {'a': 1} | str }}"
|
|
351
|
+
... })
|
|
352
|
+
>>> res = obj.render()
|
|
353
|
+
>>> res.number
|
|
354
|
+
42
|
|
355
|
+
>>> res.serialized_list
|
|
356
|
+
'[1, 2, 3]'
|
|
357
|
+
|
|
358
|
+
>>> # Passing helper objects like a faker instance or datetime
|
|
359
|
+
>>> from datetime import datetime
|
|
360
|
+
>>> class MockFaker:
|
|
361
|
+
... def name(self): return "Alice Smith"
|
|
362
|
+
...
|
|
363
|
+
>>> obj = Object({
|
|
364
|
+
... "username": "{{ fake.name() }}",
|
|
365
|
+
... "created_year": "{{ now.strftime('%Y') }}"
|
|
366
|
+
... })
|
|
367
|
+
>>> res = obj.render(fake=MockFaker(), now=datetime(2026, 7, 6))
|
|
368
|
+
>>> res.username
|
|
369
|
+
'Alice Smith'
|
|
370
|
+
>>> res.created_year
|
|
371
|
+
'2026'
|
|
372
|
+
|
|
373
|
+
>>> # Passing entire modules
|
|
374
|
+
>>> import datetime
|
|
375
|
+
>>> obj = Object({
|
|
376
|
+
... "year": "{{ dt.datetime(2026, 7, 6).year }}",
|
|
377
|
+
... "future_date": "{{ (dt.datetime(2026, 7, 6) + dt.timedelta(days=5)).strftime('%Y-%m-%d') }}"
|
|
378
|
+
... })
|
|
379
|
+
>>> res = obj.render(dt=datetime)
|
|
380
|
+
>>> res.year
|
|
381
|
+
2026
|
|
382
|
+
>>> res.future_date
|
|
383
|
+
'2026-07-11'
|
|
384
|
+
"""
|
|
385
|
+
if object.__getattribute__(self, '_lock'):
|
|
386
|
+
raise TypeError(f"'{self.__class__.__name__}' is locked and cannot be rendered")
|
|
387
|
+
try:
|
|
388
|
+
from jinja2.nativetypes import NativeEnvironment
|
|
389
|
+
from jinja2 import DebugUndefined
|
|
390
|
+
except ImportError:
|
|
391
|
+
raise ImportError("jinja2 is required to use the render() method. Install it with 'pip install jinja2'")
|
|
392
|
+
|
|
393
|
+
env = NativeEnvironment(undefined=DebugUndefined)
|
|
394
|
+
|
|
395
|
+
def parse_bool(val):
|
|
396
|
+
if isinstance(val, str):
|
|
397
|
+
return val.lower() in ('true', '1', 't', 'yes', 'y')
|
|
398
|
+
return bool(val)
|
|
399
|
+
|
|
400
|
+
env.filters['bool'] = parse_bool
|
|
401
|
+
env.filters['Object'] = Object
|
|
402
|
+
|
|
403
|
+
env.filters['str'] = lambda val: json.dumps(val) if isinstance(val, (dict, list)) else str(val)
|
|
404
|
+
|
|
405
|
+
env.globals['true'] = True
|
|
406
|
+
env.globals['false'] = False
|
|
407
|
+
env.globals['null'] = None
|
|
408
|
+
env.globals['Object'] = Object
|
|
409
|
+
|
|
410
|
+
context = self.to_dict()
|
|
411
|
+
context.update(kwargs)
|
|
412
|
+
|
|
413
|
+
result = copy.deepcopy(self)
|
|
414
|
+
|
|
415
|
+
def traverse_item(item):
|
|
416
|
+
if isinstance(item, str):
|
|
417
|
+
try:
|
|
418
|
+
template = env.from_string(item)
|
|
419
|
+
return template.render(**context)
|
|
420
|
+
except Exception:
|
|
421
|
+
return item
|
|
422
|
+
elif isinstance(item, dict):
|
|
423
|
+
for k, v in item.items():
|
|
424
|
+
item[k] = traverse_item(v)
|
|
425
|
+
return item
|
|
426
|
+
elif isinstance(item, list):
|
|
427
|
+
for i in range(len(item)):
|
|
428
|
+
item[i] = traverse_item(item[i])
|
|
429
|
+
return item
|
|
430
|
+
return item
|
|
431
|
+
|
|
432
|
+
for k, v in result.items():
|
|
433
|
+
result[k] = traverse_item(v)
|
|
434
|
+
|
|
435
|
+
return result
|
|
436
|
+
|
|
437
|
+
def diff(
|
|
438
|
+
self,
|
|
439
|
+
other,
|
|
440
|
+
ignore_order=False,
|
|
441
|
+
ignore_string_case=False,
|
|
442
|
+
exclude_paths=None,
|
|
443
|
+
exclude_regex_paths=None,
|
|
444
|
+
exclude_types=None,
|
|
445
|
+
include_paths=None,
|
|
446
|
+
significant_digits=None,
|
|
447
|
+
math_epsilon=None,
|
|
448
|
+
ignore_numeric_type_changes=False,
|
|
449
|
+
ignore_type_in_groups=None,
|
|
450
|
+
ignore_type_subclasses=False,
|
|
451
|
+
ignore_string_type_changes=False,
|
|
452
|
+
ignore_nan_inequality=False,
|
|
453
|
+
ignore_encoding_errors=False,
|
|
454
|
+
ignore_private_variables=True,
|
|
455
|
+
truncate_datetime=None,
|
|
456
|
+
cutoff_distance_for_pairs=0.3,
|
|
457
|
+
cutoff_intersection_for_pairs=0.7,
|
|
458
|
+
cache_size=0,
|
|
459
|
+
cache_purge_level=1,
|
|
460
|
+
log_frequency_in_sec=0,
|
|
461
|
+
max_passes=10000000,
|
|
462
|
+
max_diffs=None,
|
|
463
|
+
verbose_level=1,
|
|
464
|
+
view="text",
|
|
465
|
+
**kwargs
|
|
466
|
+
):
|
|
467
|
+
"""Computes the difference between this Object and another object using DeepDiff.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
other: The other object or dictionary to compare against.
|
|
471
|
+
ignore_order (bool): See DeepDiff documentation.
|
|
472
|
+
ignore_string_case (bool): See DeepDiff documentation.
|
|
473
|
+
exclude_paths (list/set): See DeepDiff documentation.
|
|
474
|
+
exclude_regex_paths (list/set): See DeepDiff documentation.
|
|
475
|
+
exclude_types (list/set): See DeepDiff documentation.
|
|
476
|
+
include_paths (list/set): See DeepDiff documentation.
|
|
477
|
+
significant_digits (int): See DeepDiff documentation.
|
|
478
|
+
math_epsilon (float): See DeepDiff documentation.
|
|
479
|
+
ignore_numeric_type_changes (bool): See DeepDiff documentation.
|
|
480
|
+
ignore_type_in_groups: See DeepDiff documentation.
|
|
481
|
+
ignore_type_subclasses (bool): See DeepDiff documentation.
|
|
482
|
+
ignore_string_type_changes (bool): See DeepDiff documentation.
|
|
483
|
+
ignore_nan_inequality (bool): See DeepDiff documentation.
|
|
484
|
+
ignore_encoding_errors (bool): See DeepDiff documentation.
|
|
485
|
+
ignore_private_variables (bool): See DeepDiff documentation.
|
|
486
|
+
truncate_datetime: See DeepDiff documentation.
|
|
487
|
+
cutoff_distance_for_pairs (float): See DeepDiff documentation.
|
|
488
|
+
cutoff_intersection_for_pairs (float): See DeepDiff documentation.
|
|
489
|
+
cache_size (int): See DeepDiff documentation.
|
|
490
|
+
cache_purge_level (int): See DeepDiff documentation.
|
|
491
|
+
log_frequency_in_sec (int): See DeepDiff documentation.
|
|
492
|
+
max_passes (int): See DeepDiff documentation.
|
|
493
|
+
max_diffs (int): See DeepDiff documentation.
|
|
494
|
+
verbose_level (int): See DeepDiff documentation.
|
|
495
|
+
view (str): See DeepDiff documentation.
|
|
496
|
+
**kwargs: Additional keyword arguments passed directly to DeepDiff.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
DeepDiff: The resulting comparison object.
|
|
500
|
+
|
|
501
|
+
Raises:
|
|
502
|
+
ImportError: If the 'deepdiff' package is not installed.
|
|
503
|
+
|
|
504
|
+
Example:
|
|
505
|
+
>>> obj1 = Object({"a": 1})
|
|
506
|
+
>>> obj2 = {"a": 2}
|
|
507
|
+
>>> diff = obj1.diff(obj2)
|
|
508
|
+
>>> 'values_changed' in diff
|
|
509
|
+
True
|
|
510
|
+
"""
|
|
511
|
+
try:
|
|
512
|
+
from deepdiff import DeepDiff
|
|
513
|
+
except ImportError:
|
|
514
|
+
raise ImportError("deepdiff is required to use the diff() method. Install it with pip install deepdiff")
|
|
515
|
+
|
|
516
|
+
kwargs.update({
|
|
517
|
+
"ignore_order": ignore_order,
|
|
518
|
+
"ignore_string_case": ignore_string_case,
|
|
519
|
+
"exclude_paths": exclude_paths,
|
|
520
|
+
"exclude_regex_paths": exclude_regex_paths,
|
|
521
|
+
"exclude_types": exclude_types,
|
|
522
|
+
"include_paths": include_paths,
|
|
523
|
+
"significant_digits": significant_digits,
|
|
524
|
+
"math_epsilon": math_epsilon,
|
|
525
|
+
"ignore_numeric_type_changes": ignore_numeric_type_changes,
|
|
526
|
+
"ignore_type_in_groups": ignore_type_in_groups,
|
|
527
|
+
"ignore_type_subclasses": ignore_type_subclasses,
|
|
528
|
+
"ignore_string_type_changes": ignore_string_type_changes,
|
|
529
|
+
"ignore_nan_inequality": ignore_nan_inequality,
|
|
530
|
+
"ignore_encoding_errors": ignore_encoding_errors,
|
|
531
|
+
"ignore_private_variables": ignore_private_variables,
|
|
532
|
+
"truncate_datetime": truncate_datetime,
|
|
533
|
+
"cutoff_distance_for_pairs": cutoff_distance_for_pairs,
|
|
534
|
+
"cutoff_intersection_for_pairs": cutoff_intersection_for_pairs,
|
|
535
|
+
"cache_size": cache_size,
|
|
536
|
+
"cache_purge_level": cache_purge_level,
|
|
537
|
+
"log_frequency_in_sec": log_frequency_in_sec,
|
|
538
|
+
"max_passes": max_passes,
|
|
539
|
+
"max_diffs": max_diffs,
|
|
540
|
+
"verbose_level": verbose_level,
|
|
541
|
+
"view": view,
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
545
|
+
|
|
546
|
+
return DeepDiff(
|
|
547
|
+
self.to_dict(),
|
|
548
|
+
other.to_dict() if hasattr(other, "to_dict") else other,
|
|
549
|
+
**kwargs
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def __str__(self) -> str:
|
|
555
|
+
return str(self.to_dict())
|
|
556
|
+
|
|
557
|
+
def __repr__(self) -> str:
|
|
558
|
+
return repr(self.to_dict())
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rich-object
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package containing rich-object utilities
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: jsonpath-ng~=1.7
|
|
11
|
+
Requires-Dist: jinja2~=3.1
|
|
12
|
+
Requires-Dist: deepdiff~=9.0
|
|
13
|
+
|
|
14
|
+
# rich-object
|
|
15
|
+
|
|
16
|
+
A powerful dictionary wrapper subclass that enables dot-notation attribute access, automatic nested path creation (autovivification), recursive locking, template rendering, JSONPath query resolution, and deep structure diffing.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install rich-object
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Features & Examples
|
|
25
|
+
|
|
26
|
+
### 1. Basic Usage & Dot Access
|
|
27
|
+
Create an `Object` from mappings or keyword arguments and access properties with standard dot notation.
|
|
28
|
+
```python
|
|
29
|
+
from rich_object import Object
|
|
30
|
+
|
|
31
|
+
# Initialize using keywords or raw dictionaries
|
|
32
|
+
obj = Object(name="John", address={"city": "New York"})
|
|
33
|
+
|
|
34
|
+
print(obj.name) # -> "John"
|
|
35
|
+
print(obj.address.city) # -> "New York"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Autovivification
|
|
39
|
+
Missing nested directories/attributes are automatically initialized when accessed, resolving nested paths on the fly.
|
|
40
|
+
```python
|
|
41
|
+
obj = Object()
|
|
42
|
+
obj.user.profile.settings.theme = "dark"
|
|
43
|
+
|
|
44
|
+
print(obj.to_dict())
|
|
45
|
+
# -> {'user': {'profile': {'settings': {'theme': 'dark'}}}}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. Structural Locks
|
|
49
|
+
Prevent mutations (creation, updates, deletes) recursively across all nested dictionaries and lists by passing `lock=True`.
|
|
50
|
+
```python
|
|
51
|
+
frozen = Object({"items": [1, 2], "user": {"id": 42}}, lock=True)
|
|
52
|
+
|
|
53
|
+
# Any mutation throws a TypeError
|
|
54
|
+
try:
|
|
55
|
+
frozen.user.id = 99
|
|
56
|
+
except TypeError as e:
|
|
57
|
+
print(e) # -> 'Object' is locked and cannot be modified
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
frozen.items.append(3)
|
|
61
|
+
except TypeError as e:
|
|
62
|
+
print(e) # -> 'ObjectList' is locked and cannot be modified
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 4. Advanced Property Setting (`set`)
|
|
66
|
+
Assign values safely to deeply nested paths using dot notation and bracket indices. Intermediary dictionary keys and list slots expand automatically.
|
|
67
|
+
```python
|
|
68
|
+
obj = Object()
|
|
69
|
+
obj.set("store.books[1].title", "Moby Dick")
|
|
70
|
+
|
|
71
|
+
print(obj.store.books[1].title) # -> "Moby Dick"
|
|
72
|
+
print(obj.store.books[0]) # -> None (automatically padded slot)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 5. JSONPath Querying (`get`)
|
|
76
|
+
Query the data structure dynamically by passing JSONPath queries starting with `$`.
|
|
77
|
+
```python
|
|
78
|
+
data = Object({
|
|
79
|
+
"store": {
|
|
80
|
+
"book": [
|
|
81
|
+
{"category": "fiction", "price": 8.95},
|
|
82
|
+
{"category": "reference", "price": 12.00}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
# Retrieve a single value
|
|
88
|
+
print(data.get("$.store.book[0].category")) # -> "fiction"
|
|
89
|
+
|
|
90
|
+
# Retrieve matching node value list
|
|
91
|
+
print(data.get("$..price")) # -> [8.95, 12.0]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 6. Deep Merging (`+`)
|
|
95
|
+
Perform clean, recursive merges of two structures using the `+` operator. Nested lists are automatically concatenated, and conflicting keys default to the right-hand value.
|
|
96
|
+
```python
|
|
97
|
+
obj1 = Object({"a": {"x": 1}, "b": [1, 2]})
|
|
98
|
+
obj2 = Object({"a": {"y": 2}, "b": [3, 4]})
|
|
99
|
+
|
|
100
|
+
res = obj1 + obj2
|
|
101
|
+
print(res.to_dict())
|
|
102
|
+
# -> {'a': {'x': 1, 'y': 2}, 'b': [1, 2, 3, 4]}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 7. Template Rendering (`render`)
|
|
106
|
+
Render Jinja2 templates in all string values recursively across the entire structure (including nested dictionaries and lists).
|
|
107
|
+
```python
|
|
108
|
+
obj = Object({
|
|
109
|
+
"first_name": "Jane",
|
|
110
|
+
"greeting": "Hello {{ first_name }}!",
|
|
111
|
+
"matrix": [["Welcome {{ first_name }}"]]
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
rendered = obj.render()
|
|
115
|
+
print(rendered.greeting) # -> "Hello Jane!"
|
|
116
|
+
print(rendered.matrix) # -> [["Welcome Jane"]]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
You can also pass entire modules or objects to the `render` method to use them inside your templates:
|
|
120
|
+
```python
|
|
121
|
+
import datetime
|
|
122
|
+
|
|
123
|
+
obj = Object({
|
|
124
|
+
"year": "{{ dt.datetime(2026, 7, 6).year }}",
|
|
125
|
+
"future_date": "{{ (dt.datetime(2026, 7, 6) + dt.timedelta(days=5)).strftime('%Y-%m-%d') }}"
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
res = obj.render(dt=datetime)
|
|
129
|
+
print(res.year) # -> 2026
|
|
130
|
+
print(res.future_date) # -> "2026-07-11"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 8. Structural Diffing (`diff`)
|
|
134
|
+
Identify differences between two objects using the built-in DeepDiff interface.
|
|
135
|
+
```python
|
|
136
|
+
obj1 = Object({"a": 1, "b": 2})
|
|
137
|
+
obj2 = Object({"a": 1, "b": 3})
|
|
138
|
+
|
|
139
|
+
difference = obj1.diff(obj2)
|
|
140
|
+
print(difference)
|
|
141
|
+
# -> {'values_changed': {"root['b']": {'new_value': 3, 'old_value': 2}}}
|
|
142
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
rich_object/__init__.py,sha256=WHDyu-jkBrraMHkPhJVJ-x-ziM9-3rqlhinnrnY2Q20,71
|
|
2
|
+
rich_object/object.py,sha256=5hZOuugzPD2jr471QFa1d7dW3x8PWjRGqL-mLDx0gZs,20889
|
|
3
|
+
rich_object-0.1.0.dist-info/METADATA,sha256=5blIiqVaI2DwKibHPhcZEeMaybOy19wTeF1185berpw,4316
|
|
4
|
+
rich_object-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
rich_object-0.1.0.dist-info/top_level.txt,sha256=CG_kf4Ih8st1axr8bMHR1-A8nKaGct0l7prrLcvQJJc,12
|
|
6
|
+
rich_object-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rich_object
|