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.
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/CHANGELOG.md +8 -0
- {dotted_notation-0.44.2/dotted_notation.egg-info → dotted_notation-0.44.3}/PKG-INFO +82 -5
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/README.md +81 -4
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/base.py +2 -2
- {dotted_notation-0.44.2 → dotted_notation-0.44.3/dotted_notation.egg-info}/PKG-INFO +82 -5
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/pyproject.toml +1 -1
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/LICENSE +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/MANIFEST.in +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/__init__.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/__main__.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/access.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/api.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/cli/main.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/containers.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/engine.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/filters.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/grammar.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/groups.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/matchers.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/predicates.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/recursive.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/results.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/sql/core.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/transforms.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/utils.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/utypes.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted/wrappers.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/top_level.txt +0 -0
- {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.
|
|
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=`
|
|
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
|
|
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
|
|
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=`
|
|
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
|
|
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
|
|
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
|
-
|
|
276
|
-
|
|
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.
|
|
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=`
|
|
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
|
|
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
|
|
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.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotted_notation-0.44.2 → dotted_notation-0.44.3}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|