dotted-notation 0.44.1__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.1 → dotted_notation-0.44.3}/CHANGELOG.md +19 -0
- {dotted_notation-0.44.1/dotted_notation.egg-info → dotted_notation-0.44.3}/PKG-INFO +112 -5
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/README.md +111 -4
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/api.py +47 -11
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/base.py +2 -2
- {dotted_notation-0.44.1 → dotted_notation-0.44.3/dotted_notation.egg-info}/PKG-INFO +112 -5
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/pyproject.toml +1 -1
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/LICENSE +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/MANIFEST.in +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/__init__.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/__main__.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/access.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/cli/main.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/containers.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/engine.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/filters.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/grammar.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/groups.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/matchers.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/predicates.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/recursive.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/results.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/sql/core.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/transforms.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/utils.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/utypes.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted/wrappers.py +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.44.1 → dotted_notation-0.44.3}/setup.cfg +0 -0
|
@@ -3,6 +3,25 @@
|
|
|
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
|
+
|
|
14
|
+
## [0.44.2]
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `unpack(obj, project=...)` keeps only the leaf paths selected by one or
|
|
18
|
+
more dotted patterns. Selection is directional (a leaf survives if it
|
|
19
|
+
`match`es a pattern), so projecting `a.b` never pulls in a shallower scalar
|
|
20
|
+
`a`. Matching defaults to `partial=True` (trailing segment is greedy); pass
|
|
21
|
+
`partial=False` for exact-depth matching, or override per field with a
|
|
22
|
+
`(pattern, partial)` tuple. `project=`/`partial=` also flow through `keys`,
|
|
23
|
+
`values`, and `items`.
|
|
24
|
+
|
|
6
25
|
## [0.44.1]
|
|
7
26
|
|
|
8
27
|
### 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)
|
|
@@ -729,6 +730,36 @@ For example:
|
|
|
729
730
|
|
|
730
731
|
Pass both to include all attributes: `attrs=[Attrs.standard, Attrs.special]`.
|
|
731
732
|
|
|
733
|
+
Pass `project=` to keep only the leaf paths selected by one or more dotted
|
|
734
|
+
patterns. Selection is *directional* — a leaf survives if it `match`es a
|
|
735
|
+
projection pattern — so projecting `a.b` never drags in a shallower scalar leaf
|
|
736
|
+
`a`:
|
|
737
|
+
|
|
738
|
+
>>> o = {'a': {'b': 1, 'c': 2}, 'x': {'y': {'z': 3}}, 'extra': 9}
|
|
739
|
+
>>> dotted.unpack(o, project='a')
|
|
740
|
+
{'a.b': 1, 'a.c': 2}
|
|
741
|
+
>>> dotted.unpack(o, project=['a', 'x.y.z'])
|
|
742
|
+
{'a.b': 1, 'a.c': 2, 'x.y.z': 3}
|
|
743
|
+
|
|
744
|
+
Matching uses `match`'s `partial=True` by default, so a trailing segment is
|
|
745
|
+
greedy (`a.*` also keeps `a.b.c`). Set `partial=False` for exact-depth matching,
|
|
746
|
+
where `a.*`, `a.*.*`, and `a.**` are all distinct:
|
|
747
|
+
|
|
748
|
+
>>> deep = {'a': {'b': {'c': 1}}, 'd': 9}
|
|
749
|
+
>>> dotted.unpack(deep, project='a.*', partial=False)
|
|
750
|
+
{}
|
|
751
|
+
>>> dotted.unpack(deep, project='a.**', partial=False)
|
|
752
|
+
{'a.b.c': 1}
|
|
753
|
+
|
|
754
|
+
Override `partial` per field with a `(pattern, partial)` tuple — bare patterns
|
|
755
|
+
inherit the global setting. This matters for mid-pattern matches that `**`
|
|
756
|
+
cannot express (e.g. `a.*.c` keeping both `a.x.c` and `a.x.c.d`):
|
|
757
|
+
|
|
758
|
+
>>> dotted.unpack(deep, project=[('a.*', True), 'd'], partial=False)
|
|
759
|
+
{'a.b.c': 1, 'd': 9}
|
|
760
|
+
|
|
761
|
+
`project=` and `partial=` also flow through `keys()`, `values()`, and `items()`.
|
|
762
|
+
|
|
732
763
|
<a id="pack"></a>
|
|
733
764
|
### Pack
|
|
734
765
|
|
|
@@ -771,10 +802,13 @@ normal form. All three call `unpack()` internally.
|
|
|
771
802
|
>>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
|
|
772
803
|
{'b'}
|
|
773
804
|
|
|
774
|
-
All three accept `attrs=`
|
|
805
|
+
All three accept the same `attrs=`, `project=`, and `partial=` arguments as
|
|
806
|
+
`unpack`:
|
|
775
807
|
|
|
776
808
|
>>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
|
|
777
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)])
|
|
778
812
|
|
|
779
813
|
<a id="build"></a>
|
|
780
814
|
### Build
|
|
@@ -2926,7 +2960,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
|
2926
2960
|
>>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
|
|
2927
2961
|
>>> combined = "WHERE " + r.where
|
|
2928
2962
|
>>> r.build(combined, min_age=30)
|
|
2929
|
-
('WHERE age >= $1
|
|
2963
|
+
('WHERE age >= $1', [30])
|
|
2930
2964
|
|
|
2931
2965
|
### Hoisted params
|
|
2932
2966
|
|
|
@@ -3090,7 +3124,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
|
|
|
3090
3124
|
>>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
|
|
3091
3125
|
>>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
|
|
3092
3126
|
>>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
|
|
3093
|
-
('(status = $1) AND (age
|
|
3127
|
+
('(status = $1) AND (age >= $2)', ['active', 30])
|
|
3094
3128
|
|
|
3095
3129
|
Substitutions by the same original name dedup across Resolvers sharing
|
|
3096
3130
|
a pool — one slot, one value, back-referenced in the rendered SQL:
|
|
@@ -3147,7 +3181,7 @@ segment before building a `Raw`:
|
|
|
3147
3181
|
Col('matched.customer')
|
|
3148
3182
|
>>> Col('schema', 'table', 'col')
|
|
3149
3183
|
Col('schema.table.col')
|
|
3150
|
-
>>> Col('bad; DROP TABLE')
|
|
3184
|
+
>>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
3151
3185
|
Traceback (most recent call last):
|
|
3152
3186
|
...
|
|
3153
3187
|
dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
|
|
@@ -3393,6 +3427,79 @@ example, removing an entire group without listing every key:
|
|
|
3393
3427
|
echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
|
|
3394
3428
|
# {"app.debug": true}
|
|
3395
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
|
+
|
|
3396
3503
|
<a id="faq"></a>
|
|
3397
3504
|
## FAQ
|
|
3398
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)
|
|
@@ -689,6 +690,36 @@ For example:
|
|
|
689
690
|
|
|
690
691
|
Pass both to include all attributes: `attrs=[Attrs.standard, Attrs.special]`.
|
|
691
692
|
|
|
693
|
+
Pass `project=` to keep only the leaf paths selected by one or more dotted
|
|
694
|
+
patterns. Selection is *directional* — a leaf survives if it `match`es a
|
|
695
|
+
projection pattern — so projecting `a.b` never drags in a shallower scalar leaf
|
|
696
|
+
`a`:
|
|
697
|
+
|
|
698
|
+
>>> o = {'a': {'b': 1, 'c': 2}, 'x': {'y': {'z': 3}}, 'extra': 9}
|
|
699
|
+
>>> dotted.unpack(o, project='a')
|
|
700
|
+
{'a.b': 1, 'a.c': 2}
|
|
701
|
+
>>> dotted.unpack(o, project=['a', 'x.y.z'])
|
|
702
|
+
{'a.b': 1, 'a.c': 2, 'x.y.z': 3}
|
|
703
|
+
|
|
704
|
+
Matching uses `match`'s `partial=True` by default, so a trailing segment is
|
|
705
|
+
greedy (`a.*` also keeps `a.b.c`). Set `partial=False` for exact-depth matching,
|
|
706
|
+
where `a.*`, `a.*.*`, and `a.**` are all distinct:
|
|
707
|
+
|
|
708
|
+
>>> deep = {'a': {'b': {'c': 1}}, 'd': 9}
|
|
709
|
+
>>> dotted.unpack(deep, project='a.*', partial=False)
|
|
710
|
+
{}
|
|
711
|
+
>>> dotted.unpack(deep, project='a.**', partial=False)
|
|
712
|
+
{'a.b.c': 1}
|
|
713
|
+
|
|
714
|
+
Override `partial` per field with a `(pattern, partial)` tuple — bare patterns
|
|
715
|
+
inherit the global setting. This matters for mid-pattern matches that `**`
|
|
716
|
+
cannot express (e.g. `a.*.c` keeping both `a.x.c` and `a.x.c.d`):
|
|
717
|
+
|
|
718
|
+
>>> dotted.unpack(deep, project=[('a.*', True), 'd'], partial=False)
|
|
719
|
+
{'a.b.c': 1, 'd': 9}
|
|
720
|
+
|
|
721
|
+
`project=` and `partial=` also flow through `keys()`, `values()`, and `items()`.
|
|
722
|
+
|
|
692
723
|
<a id="pack"></a>
|
|
693
724
|
### Pack
|
|
694
725
|
|
|
@@ -731,10 +762,13 @@ normal form. All three call `unpack()` internally.
|
|
|
731
762
|
>>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
|
|
732
763
|
{'b'}
|
|
733
764
|
|
|
734
|
-
All three accept `attrs=`
|
|
765
|
+
All three accept the same `attrs=`, `project=`, and `partial=` arguments as
|
|
766
|
+
`unpack`:
|
|
735
767
|
|
|
736
768
|
>>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
|
|
737
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)])
|
|
738
772
|
|
|
739
773
|
<a id="build"></a>
|
|
740
774
|
### Build
|
|
@@ -2886,7 +2920,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
|
2886
2920
|
>>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
|
|
2887
2921
|
>>> combined = "WHERE " + r.where
|
|
2888
2922
|
>>> r.build(combined, min_age=30)
|
|
2889
|
-
('WHERE age >= $1
|
|
2923
|
+
('WHERE age >= $1', [30])
|
|
2890
2924
|
|
|
2891
2925
|
### Hoisted params
|
|
2892
2926
|
|
|
@@ -3050,7 +3084,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
|
|
|
3050
3084
|
>>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
|
|
3051
3085
|
>>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
|
|
3052
3086
|
>>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
|
|
3053
|
-
('(status = $1) AND (age
|
|
3087
|
+
('(status = $1) AND (age >= $2)', ['active', 30])
|
|
3054
3088
|
|
|
3055
3089
|
Substitutions by the same original name dedup across Resolvers sharing
|
|
3056
3090
|
a pool — one slot, one value, back-referenced in the rendered SQL:
|
|
@@ -3107,7 +3141,7 @@ segment before building a `Raw`:
|
|
|
3107
3141
|
Col('matched.customer')
|
|
3108
3142
|
>>> Col('schema', 'table', 'col')
|
|
3109
3143
|
Col('schema.table.col')
|
|
3110
|
-
>>> Col('bad; DROP TABLE')
|
|
3144
|
+
>>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
3111
3145
|
Traceback (most recent call last):
|
|
3112
3146
|
...
|
|
3113
3147
|
dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
|
|
@@ -3353,6 +3387,79 @@ example, removing an entire group without listing every key:
|
|
|
3353
3387
|
echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
|
|
3354
3388
|
# {"app.debug": true}
|
|
3355
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
|
+
|
|
3356
3463
|
<a id="faq"></a>
|
|
3357
3464
|
## FAQ
|
|
3358
3465
|
|
|
@@ -1099,7 +1099,7 @@ def pack(pathvalues, apply_transforms=True, strict=False, bindings=None):
|
|
|
1099
1099
|
return update_multi(AUTO, pathvalues, apply_transforms=apply_transforms, strict=strict, bindings=bindings)
|
|
1100
1100
|
|
|
1101
1101
|
|
|
1102
|
-
def unpack(obj, attrs=None):
|
|
1102
|
+
def unpack(obj, attrs=None, project=None, partial=True):
|
|
1103
1103
|
"""
|
|
1104
1104
|
Convert obj to dotted normal form. A dict mapping dotted paths to leaf
|
|
1105
1105
|
values, which can be replayed to regenerate the obj (see `pack`).
|
|
@@ -1117,6 +1117,27 @@ def unpack(obj, attrs=None):
|
|
|
1117
1117
|
{'a.b': [1, 2, 3], 'x.y.z': [4, 5], 'extra': 'stuff'}
|
|
1118
1118
|
>>> pack(r) == d
|
|
1119
1119
|
True
|
|
1120
|
+
|
|
1121
|
+
Pass project= to keep only the leaf paths selected by one or more dotted
|
|
1122
|
+
patterns. A leaf survives if it `match`es any projection pattern, so
|
|
1123
|
+
selection is directional: projecting 'a.b' never pulls in a scalar leaf
|
|
1124
|
+
'a'. project= accepts a single pattern or an iterable of them:
|
|
1125
|
+
|
|
1126
|
+
>>> o = {'a': {'b': 1, 'c': 2}, 'x': {'y': {'z': 3}}, 'extra': 9}
|
|
1127
|
+
>>> unpack(o, project='a')
|
|
1128
|
+
{'a.b': 1, 'a.c': 2}
|
|
1129
|
+
>>> unpack(o, project=['a', 'x.y.z'])
|
|
1130
|
+
{'a.b': 1, 'a.c': 2, 'x.y.z': 3}
|
|
1131
|
+
|
|
1132
|
+
Matching uses match()'s `partial=True` by default, so a trailing segment
|
|
1133
|
+
is greedy ('a.*' also keeps 'a.b.c'). Set partial=False for exact-depth
|
|
1134
|
+
matching, or override it per-field with a (pattern, partial) tuple:
|
|
1135
|
+
|
|
1136
|
+
>>> deep = {'a': {'b': {'c': 1}}, 'd': 9}
|
|
1137
|
+
>>> unpack(deep, project='a.*', partial=False)
|
|
1138
|
+
{}
|
|
1139
|
+
>>> unpack(deep, project=[('a.*', True), 'd'])
|
|
1140
|
+
{'a.b.c': 1, 'd': 9}
|
|
1120
1141
|
"""
|
|
1121
1142
|
# Accept either `Attrs` enum members or their string values.
|
|
1122
1143
|
attr_values = {a.value if isinstance(a, Attrs) else a for a in (attrs or ())}
|
|
@@ -1128,43 +1149,58 @@ def unpack(obj, attrs=None):
|
|
|
1128
1149
|
extra = ', @/(?!__).*/'
|
|
1129
1150
|
else:
|
|
1130
1151
|
extra = ', @/__.*/'
|
|
1131
|
-
|
|
1152
|
+
result = dict(pluck(obj, f'*(*#, [*]:!(str, bytes){extra}):-2(.*, []{extra})##, (*, []{extra})'))
|
|
1153
|
+
if project is None:
|
|
1154
|
+
return result
|
|
1155
|
+
if isinstance(project, str):
|
|
1156
|
+
project = [project]
|
|
1157
|
+
# Normalize each entry to (pattern, partial); bare patterns inherit the
|
|
1158
|
+
# global `partial`, (pattern, partial) tuples override it per-field.
|
|
1159
|
+
specs = [(p, partial) if isinstance(p, str) else tuple(p) for p in project]
|
|
1160
|
+
return {k: v for k, v in result.items()
|
|
1161
|
+
if any(match(pat, k, partial=pp) for pat, pp in specs)}
|
|
1132
1162
|
|
|
1133
1163
|
|
|
1134
|
-
def items(obj, attrs=None):
|
|
1164
|
+
def items(obj, attrs=None, project=None, partial=True):
|
|
1135
1165
|
"""
|
|
1136
1166
|
Return (path, value) pairs of obj in normal form as a dict_items view.
|
|
1137
|
-
Internally calls unpack().
|
|
1167
|
+
Internally calls unpack(); accepts project=/partial= (see unpack).
|
|
1138
1168
|
|
|
1139
1169
|
>>> d = {'a': {'b': 1}, 'x': 2}
|
|
1140
1170
|
>>> sorted(items(d))
|
|
1141
1171
|
[('a.b', 1), ('x', 2)]
|
|
1172
|
+
>>> sorted(items(d, project='a'))
|
|
1173
|
+
[('a.b', 1)]
|
|
1142
1174
|
"""
|
|
1143
|
-
return unpack(obj, attrs=attrs).items()
|
|
1175
|
+
return unpack(obj, attrs=attrs, project=project, partial=partial).items()
|
|
1144
1176
|
|
|
1145
1177
|
|
|
1146
|
-
def keys(obj, attrs=None):
|
|
1178
|
+
def keys(obj, attrs=None, project=None, partial=True):
|
|
1147
1179
|
"""
|
|
1148
1180
|
Return the dotted paths of obj in normal form as dict_keys.
|
|
1149
|
-
Internally calls unpack().
|
|
1181
|
+
Internally calls unpack(); accepts project=/partial= (see unpack).
|
|
1150
1182
|
|
|
1151
1183
|
>>> d = {'a': {'b': 1}, 'x': 2}
|
|
1152
1184
|
>>> sorted(keys(d))
|
|
1153
1185
|
['a.b', 'x']
|
|
1186
|
+
>>> sorted(keys(d, project='a'))
|
|
1187
|
+
['a.b']
|
|
1154
1188
|
"""
|
|
1155
|
-
return unpack(obj, attrs=attrs).keys()
|
|
1189
|
+
return unpack(obj, attrs=attrs, project=project, partial=partial).keys()
|
|
1156
1190
|
|
|
1157
1191
|
|
|
1158
|
-
def values(obj, attrs=None):
|
|
1192
|
+
def values(obj, attrs=None, project=None, partial=True):
|
|
1159
1193
|
"""
|
|
1160
1194
|
Return the leaf values of obj in normal form.
|
|
1161
|
-
Internally calls unpack().
|
|
1195
|
+
Internally calls unpack(); accepts project=/partial= (see unpack).
|
|
1162
1196
|
|
|
1163
1197
|
>>> d = {'a': {'b': 1}, 'x': 2}
|
|
1164
1198
|
>>> sorted(values(d))
|
|
1165
1199
|
[1, 2]
|
|
1200
|
+
>>> sorted(values(d, project='a'))
|
|
1201
|
+
[1]
|
|
1166
1202
|
"""
|
|
1167
|
-
return unpack(obj, attrs=attrs).values()
|
|
1203
|
+
return unpack(obj, attrs=attrs, project=project, partial=partial).values()
|
|
1168
1204
|
|
|
1169
1205
|
|
|
1170
1206
|
#
|
|
@@ -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)
|
|
@@ -729,6 +730,36 @@ For example:
|
|
|
729
730
|
|
|
730
731
|
Pass both to include all attributes: `attrs=[Attrs.standard, Attrs.special]`.
|
|
731
732
|
|
|
733
|
+
Pass `project=` to keep only the leaf paths selected by one or more dotted
|
|
734
|
+
patterns. Selection is *directional* — a leaf survives if it `match`es a
|
|
735
|
+
projection pattern — so projecting `a.b` never drags in a shallower scalar leaf
|
|
736
|
+
`a`:
|
|
737
|
+
|
|
738
|
+
>>> o = {'a': {'b': 1, 'c': 2}, 'x': {'y': {'z': 3}}, 'extra': 9}
|
|
739
|
+
>>> dotted.unpack(o, project='a')
|
|
740
|
+
{'a.b': 1, 'a.c': 2}
|
|
741
|
+
>>> dotted.unpack(o, project=['a', 'x.y.z'])
|
|
742
|
+
{'a.b': 1, 'a.c': 2, 'x.y.z': 3}
|
|
743
|
+
|
|
744
|
+
Matching uses `match`'s `partial=True` by default, so a trailing segment is
|
|
745
|
+
greedy (`a.*` also keeps `a.b.c`). Set `partial=False` for exact-depth matching,
|
|
746
|
+
where `a.*`, `a.*.*`, and `a.**` are all distinct:
|
|
747
|
+
|
|
748
|
+
>>> deep = {'a': {'b': {'c': 1}}, 'd': 9}
|
|
749
|
+
>>> dotted.unpack(deep, project='a.*', partial=False)
|
|
750
|
+
{}
|
|
751
|
+
>>> dotted.unpack(deep, project='a.**', partial=False)
|
|
752
|
+
{'a.b.c': 1}
|
|
753
|
+
|
|
754
|
+
Override `partial` per field with a `(pattern, partial)` tuple — bare patterns
|
|
755
|
+
inherit the global setting. This matters for mid-pattern matches that `**`
|
|
756
|
+
cannot express (e.g. `a.*.c` keeping both `a.x.c` and `a.x.c.d`):
|
|
757
|
+
|
|
758
|
+
>>> dotted.unpack(deep, project=[('a.*', True), 'd'], partial=False)
|
|
759
|
+
{'a.b.c': 1, 'd': 9}
|
|
760
|
+
|
|
761
|
+
`project=` and `partial=` also flow through `keys()`, `values()`, and `items()`.
|
|
762
|
+
|
|
732
763
|
<a id="pack"></a>
|
|
733
764
|
### Pack
|
|
734
765
|
|
|
@@ -771,10 +802,13 @@ normal form. All three call `unpack()` internally.
|
|
|
771
802
|
>>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
|
|
772
803
|
{'b'}
|
|
773
804
|
|
|
774
|
-
All three accept `attrs=`
|
|
805
|
+
All three accept the same `attrs=`, `project=`, and `partial=` arguments as
|
|
806
|
+
`unpack`:
|
|
775
807
|
|
|
776
808
|
>>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
|
|
777
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)])
|
|
778
812
|
|
|
779
813
|
<a id="build"></a>
|
|
780
814
|
### Build
|
|
@@ -2926,7 +2960,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
|
2926
2960
|
>>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
|
|
2927
2961
|
>>> combined = "WHERE " + r.where
|
|
2928
2962
|
>>> r.build(combined, min_age=30)
|
|
2929
|
-
('WHERE age >= $1
|
|
2963
|
+
('WHERE age >= $1', [30])
|
|
2930
2964
|
|
|
2931
2965
|
### Hoisted params
|
|
2932
2966
|
|
|
@@ -3090,7 +3124,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
|
|
|
3090
3124
|
>>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
|
|
3091
3125
|
>>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
|
|
3092
3126
|
>>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
|
|
3093
|
-
('(status = $1) AND (age
|
|
3127
|
+
('(status = $1) AND (age >= $2)', ['active', 30])
|
|
3094
3128
|
|
|
3095
3129
|
Substitutions by the same original name dedup across Resolvers sharing
|
|
3096
3130
|
a pool — one slot, one value, back-referenced in the rendered SQL:
|
|
@@ -3147,7 +3181,7 @@ segment before building a `Raw`:
|
|
|
3147
3181
|
Col('matched.customer')
|
|
3148
3182
|
>>> Col('schema', 'table', 'col')
|
|
3149
3183
|
Col('schema.table.col')
|
|
3150
|
-
>>> Col('bad; DROP TABLE')
|
|
3184
|
+
>>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
3151
3185
|
Traceback (most recent call last):
|
|
3152
3186
|
...
|
|
3153
3187
|
dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
|
|
@@ -3393,6 +3427,79 @@ example, removing an entire group without listing every key:
|
|
|
3393
3427
|
echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
|
|
3394
3428
|
# {"app.debug": true}
|
|
3395
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
|
+
|
|
3396
3503
|
<a id="faq"></a>
|
|
3397
3504
|
## FAQ
|
|
3398
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
|
{dotted_notation-0.44.1 → 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
|