dotted-notation 0.44.2__tar.gz → 0.44.3__tar.gz

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.
Files changed (38) hide show
  1. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/CHANGELOG.md +8 -0
  2. {dotted_notation-0.44.2/dotted_notation.egg-info → dotted_notation-0.44.3}/PKG-INFO +82 -5
  3. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/README.md +81 -4
  4. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/base.py +2 -2
  5. {dotted_notation-0.44.2 → dotted_notation-0.44.3/dotted_notation.egg-info}/PKG-INFO +82 -5
  6. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/pyproject.toml +1 -1
  7. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/LICENSE +0 -0
  8. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/MANIFEST.in +0 -0
  9. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/__init__.py +0 -0
  10. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/__main__.py +0 -0
  11. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/access.py +0 -0
  12. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/api.py +0 -0
  13. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/cli/__init__.py +0 -0
  14. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/cli/_compat.py +0 -0
  15. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/cli/formats.py +0 -0
  16. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/cli/main.py +0 -0
  17. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/containers.py +0 -0
  18. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/engine.py +0 -0
  19. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/filters.py +0 -0
  20. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/grammar.py +0 -0
  21. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/groups.py +0 -0
  22. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/matchers.py +0 -0
  23. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/predicates.py +0 -0
  24. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/recursive.py +0 -0
  25. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/results.py +0 -0
  26. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/sql/__init__.py +0 -0
  27. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/sql/core.py +0 -0
  28. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/sql/pg.py +0 -0
  29. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/transforms.py +0 -0
  30. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/utils.py +0 -0
  31. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/utypes.py +0 -0
  32. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/wrappers.py +0 -0
  33. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/SOURCES.txt +0 -0
  34. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/dependency_links.txt +0 -0
  35. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/entry_points.txt +0 -0
  36. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/requires.txt +0 -0
  37. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/top_level.txt +0 -0
  38. {dotted_notation-0.44.2 → dotted_notation-0.44.3}/setup.cfg +0 -0
@@ -3,6 +3,14 @@
3
3
  All notable changes to `dotted` are recorded here. Versions prior to
4
4
  the ones listed are omitted — browse git history for earlier entries.
5
5
 
6
+ ## [0.44.3]
7
+
8
+ ### Fixed
9
+ - Transforms with a dict (or other nested-container) argument, e.g.
10
+ `code|lookup:{"a": 1}`, no longer raise `TypeError: unhashable type: 'dict'`
11
+ when the path is parsed. `Transform.__hash__` now freezes container params
12
+ recursively.
13
+
6
14
  ## [0.44.2]
7
15
 
8
16
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.2
3
+ Version: 0.44.3
4
4
  Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
5
5
  Author-email: Frey Waid <logophage1@gmail.com>
6
6
  License: MIT
@@ -176,6 +176,7 @@ Or pick only what you need:
176
176
  - [Projection](#projection)
177
177
  - [Unpack](#unpack)
178
178
  - [Pack](#pack)
179
+ - [Recipes](#recipes)
179
180
  - [FAQ](#faq)
180
181
  - [Why do I get a tuple for my get?](#why-do-i-get-a-tuple-for-my-get)
181
182
  - [How do I craft an efficient path?](#how-do-i-craft-an-efficient-path)
@@ -801,10 +802,13 @@ normal form. All three call `unpack()` internally.
801
802
  >>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
802
803
  {'b'}
803
804
 
804
- All three accept `attrs=` (same as `unpack`):
805
+ All three accept the same `attrs=`, `project=`, and `partial=` arguments as
806
+ `unpack`:
805
807
 
806
808
  >>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
807
809
  dict_keys(['point@x', 'point@y'])
810
+ >>> dotted.items({'a': {'b': 1, 'c': 2}, 'x': 3}, project='a')
811
+ dict_items([('a.b', 1), ('a.c', 2)])
808
812
 
809
813
  <a id="build"></a>
810
814
  ### Build
@@ -2956,7 +2960,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
2956
2960
  >>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
2957
2961
  >>> combined = "WHERE " + r.where
2958
2962
  >>> r.build(combined, min_age=30)
2959
- ('WHERE age >= $1::bigint', [30])
2963
+ ('WHERE age >= $1', [30])
2960
2964
 
2961
2965
  ### Hoisted params
2962
2966
 
@@ -3120,7 +3124,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
3120
3124
  >>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
3121
3125
  >>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
3122
3126
  >>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
3123
- ('(status = $1) AND (age = $2)', ['active', 30])
3127
+ ('(status = $1) AND (age >= $2)', ['active', 30])
3124
3128
 
3125
3129
  Substitutions by the same original name dedup across Resolvers sharing
3126
3130
  a pool — one slot, one value, back-referenced in the rendered SQL:
@@ -3177,7 +3181,7 @@ segment before building a `Raw`:
3177
3181
  Col('matched.customer')
3178
3182
  >>> Col('schema', 'table', 'col')
3179
3183
  Col('schema.table.col')
3180
- >>> Col('bad; DROP TABLE')
3184
+ >>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
3181
3185
  Traceback (most recent call last):
3182
3186
  ...
3183
3187
  dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
@@ -3423,6 +3427,79 @@ example, removing an entire group without listing every key:
3423
3427
  echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
3424
3428
  # {"app.debug": true}
3425
3429
 
3430
+ <a id="recipes"></a>
3431
+ ## Recipes
3432
+
3433
+ A grab-bag of one-liners that show off what the notation can do. Every example
3434
+ below runs as-is.
3435
+
3436
+ **Flatten a nested list** (leaves only, any depth) — recurse through every slot
3437
+ with `*([*])`, then keep only the deepest match on each branch with `:-1`:
3438
+
3439
+ >>> import dotted
3440
+ >>> dotted.get([1, 2, 3, [4, 5, [6, 7]]], '*([*]):-1')
3441
+ (1, 2, 3, 4, 5, 6, 7)
3442
+
3443
+ Because `:-1` is deepest-*per-branch*, shallow leaves survive alongside deep
3444
+ ones — a ragged list still flattens completely.
3445
+
3446
+ **Collect every leaf value** of a nested dict — same idea with key recursion:
3447
+
3448
+ >>> dotted.get({'a': {'b': 1}, 'c': 2}, '**:-1')
3449
+ (1, 2)
3450
+
3451
+ **Find a key at any depth** — `**` recurses through dict keys, then continue
3452
+ with the key you want:
3453
+
3454
+ >>> dotted.get({'a': {'b': {'name': 'x'}}, 'name': 'y'}, '**.name')
3455
+ ('x',)
3456
+
3457
+ If the tree mixes lists and dicts, recurse through both with `*(*#, [*])`:
3458
+
3459
+ >>> dotted.get({'kids': [{'name': 'a'}, {'name': 'b'}]}, '*(*#, [*]).name')
3460
+ ('a', 'b')
3461
+
3462
+ **Find values matching a condition anywhere** — attach a value guard to the
3463
+ recursive walk:
3464
+
3465
+ >>> dotted.get({'a': {'b': 7, 'c': 3}, 'd': {'e': 9}}, '**>5')
3466
+ (7, 9)
3467
+
3468
+ **Bulk-update everything that matches** — the same pattern drives `update` and
3469
+ `remove`:
3470
+
3471
+ >>> dotted.update({'a': {'b': 7, 'c': 3}, 'd': 7}, '**=7', 99)
3472
+ {'a': {'b': 99, 'c': 3}, 'd': 99}
3473
+
3474
+ **Filter a list of dicts, then project a field** — combine a key-value filter
3475
+ with a continuation:
3476
+
3477
+ >>> users = [{'name': 'x', 'active': True},
3478
+ ... {'name': 'y', 'active': False},
3479
+ ... {'name': 'z', 'active': True}]
3480
+ >>> dotted.get(users, '[*&active=true].name')
3481
+ ('x', 'z')
3482
+
3483
+ **Upsert** — update a list entry if it exists, else append, using cut (`#`) in a
3484
+ disjunction so the first matching branch wins:
3485
+
3486
+ >>> dotted.update({'emails': [{'email': 'a@x'}]},
3487
+ ... 'emails[(*&email="a@x"#, +)].email', 'NEW')
3488
+ {'emails': [{'email': 'NEW'}]}
3489
+ >>> dotted.update({'emails': [{'email': 'a@x'}]},
3490
+ ... 'emails[(*&email="z@x"#, +)]', {'email': 'z@x'})
3491
+ {'emails': [{'email': 'a@x'}, {'email': 'z@x'}]}
3492
+
3493
+ **Coerce on the way out** — pipe a value through transforms:
3494
+
3495
+ >>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'name|uppercase')
3496
+ 'BOB'
3497
+ >>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'tags|len')
3498
+ 3
3499
+
3500
+ See [Recursive Traversal](#recursive-traversal), [Filters](#filters), and
3501
+ [Transforms](#transforms) for the full story behind each of these.
3502
+
3426
3503
  <a id="faq"></a>
3427
3504
  ## FAQ
3428
3505
 
@@ -136,6 +136,7 @@ Or pick only what you need:
136
136
  - [Projection](#projection)
137
137
  - [Unpack](#unpack)
138
138
  - [Pack](#pack)
139
+ - [Recipes](#recipes)
139
140
  - [FAQ](#faq)
140
141
  - [Why do I get a tuple for my get?](#why-do-i-get-a-tuple-for-my-get)
141
142
  - [How do I craft an efficient path?](#how-do-i-craft-an-efficient-path)
@@ -761,10 +762,13 @@ normal form. All three call `unpack()` internally.
761
762
  >>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
762
763
  {'b'}
763
764
 
764
- All three accept `attrs=` (same as `unpack`):
765
+ All three accept the same `attrs=`, `project=`, and `partial=` arguments as
766
+ `unpack`:
765
767
 
766
768
  >>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
767
769
  dict_keys(['point@x', 'point@y'])
770
+ >>> dotted.items({'a': {'b': 1, 'c': 2}, 'x': 3}, project='a')
771
+ dict_items([('a.b', 1), ('a.c', 2)])
768
772
 
769
773
  <a id="build"></a>
770
774
  ### Build
@@ -2916,7 +2920,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
2916
2920
  >>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
2917
2921
  >>> combined = "WHERE " + r.where
2918
2922
  >>> r.build(combined, min_age=30)
2919
- ('WHERE age >= $1::bigint', [30])
2923
+ ('WHERE age >= $1', [30])
2920
2924
 
2921
2925
  ### Hoisted params
2922
2926
 
@@ -3080,7 +3084,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
3080
3084
  >>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
3081
3085
  >>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
3082
3086
  >>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
3083
- ('(status = $1) AND (age = $2)', ['active', 30])
3087
+ ('(status = $1) AND (age >= $2)', ['active', 30])
3084
3088
 
3085
3089
  Substitutions by the same original name dedup across Resolvers sharing
3086
3090
  a pool — one slot, one value, back-referenced in the rendered SQL:
@@ -3137,7 +3141,7 @@ segment before building a `Raw`:
3137
3141
  Col('matched.customer')
3138
3142
  >>> Col('schema', 'table', 'col')
3139
3143
  Col('schema.table.col')
3140
- >>> Col('bad; DROP TABLE')
3144
+ >>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
3141
3145
  Traceback (most recent call last):
3142
3146
  ...
3143
3147
  dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
@@ -3383,6 +3387,79 @@ example, removing an entire group without listing every key:
3383
3387
  echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
3384
3388
  # {"app.debug": true}
3385
3389
 
3390
+ <a id="recipes"></a>
3391
+ ## Recipes
3392
+
3393
+ A grab-bag of one-liners that show off what the notation can do. Every example
3394
+ below runs as-is.
3395
+
3396
+ **Flatten a nested list** (leaves only, any depth) — recurse through every slot
3397
+ with `*([*])`, then keep only the deepest match on each branch with `:-1`:
3398
+
3399
+ >>> import dotted
3400
+ >>> dotted.get([1, 2, 3, [4, 5, [6, 7]]], '*([*]):-1')
3401
+ (1, 2, 3, 4, 5, 6, 7)
3402
+
3403
+ Because `:-1` is deepest-*per-branch*, shallow leaves survive alongside deep
3404
+ ones — a ragged list still flattens completely.
3405
+
3406
+ **Collect every leaf value** of a nested dict — same idea with key recursion:
3407
+
3408
+ >>> dotted.get({'a': {'b': 1}, 'c': 2}, '**:-1')
3409
+ (1, 2)
3410
+
3411
+ **Find a key at any depth** — `**` recurses through dict keys, then continue
3412
+ with the key you want:
3413
+
3414
+ >>> dotted.get({'a': {'b': {'name': 'x'}}, 'name': 'y'}, '**.name')
3415
+ ('x',)
3416
+
3417
+ If the tree mixes lists and dicts, recurse through both with `*(*#, [*])`:
3418
+
3419
+ >>> dotted.get({'kids': [{'name': 'a'}, {'name': 'b'}]}, '*(*#, [*]).name')
3420
+ ('a', 'b')
3421
+
3422
+ **Find values matching a condition anywhere** — attach a value guard to the
3423
+ recursive walk:
3424
+
3425
+ >>> dotted.get({'a': {'b': 7, 'c': 3}, 'd': {'e': 9}}, '**>5')
3426
+ (7, 9)
3427
+
3428
+ **Bulk-update everything that matches** — the same pattern drives `update` and
3429
+ `remove`:
3430
+
3431
+ >>> dotted.update({'a': {'b': 7, 'c': 3}, 'd': 7}, '**=7', 99)
3432
+ {'a': {'b': 99, 'c': 3}, 'd': 99}
3433
+
3434
+ **Filter a list of dicts, then project a field** — combine a key-value filter
3435
+ with a continuation:
3436
+
3437
+ >>> users = [{'name': 'x', 'active': True},
3438
+ ... {'name': 'y', 'active': False},
3439
+ ... {'name': 'z', 'active': True}]
3440
+ >>> dotted.get(users, '[*&active=true].name')
3441
+ ('x', 'z')
3442
+
3443
+ **Upsert** — update a list entry if it exists, else append, using cut (`#`) in a
3444
+ disjunction so the first matching branch wins:
3445
+
3446
+ >>> dotted.update({'emails': [{'email': 'a@x'}]},
3447
+ ... 'emails[(*&email="a@x"#, +)].email', 'NEW')
3448
+ {'emails': [{'email': 'NEW'}]}
3449
+ >>> dotted.update({'emails': [{'email': 'a@x'}]},
3450
+ ... 'emails[(*&email="z@x"#, +)]', {'email': 'z@x'})
3451
+ {'emails': [{'email': 'a@x'}, {'email': 'z@x'}]}
3452
+
3453
+ **Coerce on the way out** — pipe a value through transforms:
3454
+
3455
+ >>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'name|uppercase')
3456
+ 'BOB'
3457
+ >>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'tags|len')
3458
+ 3
3459
+
3460
+ See [Recursive Traversal](#recursive-traversal), [Filters](#filters), and
3461
+ [Transforms](#transforms) for the full story behind each of these.
3462
+
3386
3463
  <a id="faq"></a>
3387
3464
  ## FAQ
3388
3465
 
@@ -272,8 +272,8 @@ class Transform(Op):
272
272
  try:
273
273
  return hash(('transform', self.name, self.params))
274
274
  except TypeError:
275
- return hash(('transform', self.name,
276
- tuple(tuple(p) if isinstance(p, list) else p for p in self.params)))
275
+ from .results import Dotted
276
+ return hash(('transform', self.name, Dotted._hashable(self.params)))
277
277
 
278
278
  def __eq__(self, other):
279
279
  return (isinstance(other, Transform)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotted_notation
3
- Version: 0.44.2
3
+ Version: 0.44.3
4
4
  Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
5
5
  Author-email: Frey Waid <logophage1@gmail.com>
6
6
  License: MIT
@@ -176,6 +176,7 @@ Or pick only what you need:
176
176
  - [Projection](#projection)
177
177
  - [Unpack](#unpack)
178
178
  - [Pack](#pack)
179
+ - [Recipes](#recipes)
179
180
  - [FAQ](#faq)
180
181
  - [Why do I get a tuple for my get?](#why-do-i-get-a-tuple-for-my-get)
181
182
  - [How do I craft an efficient path?](#how-do-i-craft-an-efficient-path)
@@ -801,10 +802,13 @@ normal form. All three call `unpack()` internally.
801
802
  >>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
802
803
  {'b'}
803
804
 
804
- All three accept `attrs=` (same as `unpack`):
805
+ All three accept the same `attrs=`, `project=`, and `partial=` arguments as
806
+ `unpack`:
805
807
 
806
808
  >>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
807
809
  dict_keys(['point@x', 'point@y'])
810
+ >>> dotted.items({'a': {'b': 1, 'c': 2}, 'x': 3}, project='a')
811
+ dict_items([('a.b', 1), ('a.c', 2)])
808
812
 
809
813
  <a id="build"></a>
810
814
  ### Build
@@ -2956,7 +2960,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
2956
2960
  >>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
2957
2961
  >>> combined = "WHERE " + r.where
2958
2962
  >>> r.build(combined, min_age=30)
2959
- ('WHERE age >= $1::bigint', [30])
2963
+ ('WHERE age >= $1', [30])
2960
2964
 
2961
2965
  ### Hoisted params
2962
2966
 
@@ -3120,7 +3124,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
3120
3124
  >>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
3121
3125
  >>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
3122
3126
  >>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
3123
- ('(status = $1) AND (age = $2)', ['active', 30])
3127
+ ('(status = $1) AND (age >= $2)', ['active', 30])
3124
3128
 
3125
3129
  Substitutions by the same original name dedup across Resolvers sharing
3126
3130
  a pool — one slot, one value, back-referenced in the rendered SQL:
@@ -3177,7 +3181,7 @@ segment before building a `Raw`:
3177
3181
  Col('matched.customer')
3178
3182
  >>> Col('schema', 'table', 'col')
3179
3183
  Col('schema.table.col')
3180
- >>> Col('bad; DROP TABLE')
3184
+ >>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
3181
3185
  Traceback (most recent call last):
3182
3186
  ...
3183
3187
  dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
@@ -3423,6 +3427,79 @@ example, removing an entire group without listing every key:
3423
3427
  echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
3424
3428
  # {"app.debug": true}
3425
3429
 
3430
+ <a id="recipes"></a>
3431
+ ## Recipes
3432
+
3433
+ A grab-bag of one-liners that show off what the notation can do. Every example
3434
+ below runs as-is.
3435
+
3436
+ **Flatten a nested list** (leaves only, any depth) — recurse through every slot
3437
+ with `*([*])`, then keep only the deepest match on each branch with `:-1`:
3438
+
3439
+ >>> import dotted
3440
+ >>> dotted.get([1, 2, 3, [4, 5, [6, 7]]], '*([*]):-1')
3441
+ (1, 2, 3, 4, 5, 6, 7)
3442
+
3443
+ Because `:-1` is deepest-*per-branch*, shallow leaves survive alongside deep
3444
+ ones — a ragged list still flattens completely.
3445
+
3446
+ **Collect every leaf value** of a nested dict — same idea with key recursion:
3447
+
3448
+ >>> dotted.get({'a': {'b': 1}, 'c': 2}, '**:-1')
3449
+ (1, 2)
3450
+
3451
+ **Find a key at any depth** — `**` recurses through dict keys, then continue
3452
+ with the key you want:
3453
+
3454
+ >>> dotted.get({'a': {'b': {'name': 'x'}}, 'name': 'y'}, '**.name')
3455
+ ('x',)
3456
+
3457
+ If the tree mixes lists and dicts, recurse through both with `*(*#, [*])`:
3458
+
3459
+ >>> dotted.get({'kids': [{'name': 'a'}, {'name': 'b'}]}, '*(*#, [*]).name')
3460
+ ('a', 'b')
3461
+
3462
+ **Find values matching a condition anywhere** — attach a value guard to the
3463
+ recursive walk:
3464
+
3465
+ >>> dotted.get({'a': {'b': 7, 'c': 3}, 'd': {'e': 9}}, '**>5')
3466
+ (7, 9)
3467
+
3468
+ **Bulk-update everything that matches** — the same pattern drives `update` and
3469
+ `remove`:
3470
+
3471
+ >>> dotted.update({'a': {'b': 7, 'c': 3}, 'd': 7}, '**=7', 99)
3472
+ {'a': {'b': 99, 'c': 3}, 'd': 99}
3473
+
3474
+ **Filter a list of dicts, then project a field** — combine a key-value filter
3475
+ with a continuation:
3476
+
3477
+ >>> users = [{'name': 'x', 'active': True},
3478
+ ... {'name': 'y', 'active': False},
3479
+ ... {'name': 'z', 'active': True}]
3480
+ >>> dotted.get(users, '[*&active=true].name')
3481
+ ('x', 'z')
3482
+
3483
+ **Upsert** — update a list entry if it exists, else append, using cut (`#`) in a
3484
+ disjunction so the first matching branch wins:
3485
+
3486
+ >>> dotted.update({'emails': [{'email': 'a@x'}]},
3487
+ ... 'emails[(*&email="a@x"#, +)].email', 'NEW')
3488
+ {'emails': [{'email': 'NEW'}]}
3489
+ >>> dotted.update({'emails': [{'email': 'a@x'}]},
3490
+ ... 'emails[(*&email="z@x"#, +)]', {'email': 'z@x'})
3491
+ {'emails': [{'email': 'a@x'}, {'email': 'z@x'}]}
3492
+
3493
+ **Coerce on the way out** — pipe a value through transforms:
3494
+
3495
+ >>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'name|uppercase')
3496
+ 'BOB'
3497
+ >>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'tags|len')
3498
+ 3
3499
+
3500
+ See [Recursive Traversal](#recursive-traversal), [Filters](#filters), and
3501
+ [Transforms](#transforms) for the full story behind each of these.
3502
+
3426
3503
  <a id="faq"></a>
3427
3504
  ## FAQ
3428
3505
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dotted_notation"
7
- version = "0.44.2"
7
+ version = "0.44.3"
8
8
  description = "Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.6"