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.
@@ -0,0 +1,4 @@
1
+ # rich_object package
2
+ from .object import Object
3
+
4
+ __all__ = ["Object"]
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ rich_object