dotted-notation 0.43.4__tar.gz → 0.43.6__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.43.4/dotted_notation.egg-info → dotted_notation-0.43.6}/PKG-INFO +65 -10
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/README.md +64 -9
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/__init__.py +6 -2
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/access.py +8 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/api.py +88 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/base.py +6 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/filters.py +45 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/groups.py +10 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/sqlize.py +418 -63
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/wrappers.py +25 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6/dotted_notation.egg-info}/PKG-INFO +65 -10
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/setup.py +1 -1
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_api.py +109 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_sqlize.py +126 -38
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/LICENSE +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/__main__.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/cli/main.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/containers.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/engine.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/grammar.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/matchers.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/predicates.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/recursive.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/results.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/transforms.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/utils.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted/utypes.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/setup.cfg +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/__init__.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_appender.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_assemble.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_attrs.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_bindings.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_cli.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_concat.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_container_filter.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_cut.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_empty.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_filter_keyvalue.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_get.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_guard_transforms.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_invert.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_json_sentinels.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_keys_values.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_match.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_matchable.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_named_subst.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_negation.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_nop.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_numeric.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_opgroup.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_pluck.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_predicates.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_quote_idempotent.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_recursive.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_reference.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_replace.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_slice.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_softcut.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_strict.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_string_glob.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_subst_escape.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_subst_transforms.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_threading.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_transforms.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_translate.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_type_restriction.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_unpack.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_update.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_update_if.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.6}/tests/test_value_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.43.
|
|
3
|
+
Version: 0.43.6
|
|
4
4
|
Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
|
|
5
5
|
Home-page: https://github.com/freywaid/dotted
|
|
6
6
|
Author: Frey Waid
|
|
@@ -2801,9 +2801,11 @@ View all registered transforms with `dotted.registry()`.
|
|
|
2801
2801
|
## SQL Translation (`sqlize`)
|
|
2802
2802
|
|
|
2803
2803
|
> **Under development.** The API shape, output format, and supported subset
|
|
2804
|
-
> of dotted features may still change. Postgres-only, SQLAlchemy-style
|
|
2805
|
-
> placeholders
|
|
2806
|
-
>
|
|
2804
|
+
> of dotted features may still change. Postgres-only, SQLAlchemy-style
|
|
2805
|
+
> named placeholders. Pattern paths (`*`, `[*]`, `**`, filter brackets)
|
|
2806
|
+
> are supported only as WHERE-side existence checks via
|
|
2807
|
+
> `jsonb_path_exists`; pattern paths that yield sets as SELECT (needing
|
|
2808
|
+
> `LATERAL jsonb_path_query`) are not supported yet.
|
|
2807
2809
|
|
|
2808
2810
|
`dotted.sqlize` translates a dotted path into SQL clause components — a
|
|
2809
2811
|
`select` expression, a `where` predicate, and a `params` dict with hoisted
|
|
@@ -2871,18 +2873,36 @@ time:
|
|
|
2871
2873
|
|
|
2872
2874
|
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2873
2875
|
>>> r
|
|
2874
|
-
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2876
|
+
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2877
|
+
'unbound': {'min_age': 'min_age'}}
|
|
2875
2878
|
|
|
2876
|
-
`
|
|
2877
|
-
`
|
|
2879
|
+
`unbound` is a dict keyed by the **SQL bind parameter name** — the name
|
|
2880
|
+
that appears in the SQL `:placeholder` and as a key in `params`. Each
|
|
2881
|
+
value is the **original substitution name** (what was inside `$(...)`),
|
|
2882
|
+
kept as provenance so callers can trace a bind back to its source.
|
|
2883
|
+
|
|
2884
|
+
`unbound.keys()` is the list of bind slots the caller needs to fill.
|
|
2885
|
+
Most often you just fill `params` by bind name directly:
|
|
2878
2886
|
|
|
2879
2887
|
>>> r['params']['min_age'] = 30
|
|
2880
2888
|
>>> # hand (r['where'], r['params']) to cursor.execute / session.execute / etc.
|
|
2881
2889
|
|
|
2882
|
-
|
|
2890
|
+
Non-identifier names (dotted paths, quoted keys, spaces, etc.) hash to a
|
|
2891
|
+
deterministic `_s_<hex>` bind name — the bind-side name is different
|
|
2892
|
+
from what appeared in source, but the original is preserved in the
|
|
2893
|
+
`unbound` value:
|
|
2894
|
+
|
|
2895
|
+
>>> r = dotted.sqlize('age >= $(user.min_age)')
|
|
2896
|
+
>>> r['where'] # doctest: +ELLIPSIS
|
|
2897
|
+
'age >= :_s_...'
|
|
2898
|
+
>>> list(r['unbound'].values())
|
|
2899
|
+
['user.min_age']
|
|
2900
|
+
|
|
2901
|
+
Repeated uses of the same name share one entry automatically:
|
|
2883
2902
|
|
|
2884
2903
|
>>> dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2885
|
-
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2904
|
+
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2905
|
+
'unbound': {'x': 'x'}}
|
|
2886
2906
|
|
|
2887
2907
|
If you pass `bindings=`, substitutions are resolved at sqlize time and hoisted
|
|
2888
2908
|
like any other literal:
|
|
@@ -2901,6 +2921,41 @@ Postgres's `->` / `#>` accept runtime-computed keys:
|
|
|
2901
2921
|
Relative references (`^`) and parent references (`^^+`) are not supported
|
|
2902
2922
|
yet.
|
|
2903
2923
|
|
|
2924
|
+
### Pattern paths
|
|
2925
|
+
|
|
2926
|
+
Paths that contain wildcards (`*`, `[*]`), recursive descent (`**`), or
|
|
2927
|
+
bracket filters (`[pred]`, `[*&pred]`) translate to a Postgres
|
|
2928
|
+
`jsonb_path_exists(…)` WHERE predicate. The dotted path becomes a JSONPath
|
|
2929
|
+
expression; guard values embed inline when safely literal (numbers,
|
|
2930
|
+
booleans, null) or flow through a `jsonb_build_object` vars argument
|
|
2931
|
+
(strings, substitutions).
|
|
2932
|
+
|
|
2933
|
+
>>> dotted.sqlize("data.users[*].age >= 30")['where']
|
|
2934
|
+
"jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')"
|
|
2935
|
+
|
|
2936
|
+
>>> dotted.sqlize("data.**.active = True")['where']
|
|
2937
|
+
"jsonb_path_exists(data, '$.**.active ? (@ == true)')"
|
|
2938
|
+
|
|
2939
|
+
>>> dotted.sqlize("data.users[age>=30]")['where']
|
|
2940
|
+
"jsonb_path_exists(data, '$.users[*] ? (@.age >= 30)')"
|
|
2941
|
+
|
|
2942
|
+
>>> dotted.sqlize('data.users[*&age>=30].name = "alice"')['where']
|
|
2943
|
+
"jsonb_path_exists(data, '$.users[*] ? (@.age >= 30).name ? (@ == :_p1)', jsonb_build_object('_p1', :_p1))"
|
|
2944
|
+
|
|
2945
|
+
Pattern predicates compose with scalar ones via dotted's `&` / `,` / `!`:
|
|
2946
|
+
|
|
2947
|
+
>>> dotted.sqlize('(data.users[*].age >= 30 & status = "active")')['where']
|
|
2948
|
+
"(jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')) AND (status = :_p1)"
|
|
2949
|
+
|
|
2950
|
+
`select` is set to the column itself — pattern paths don't have a single
|
|
2951
|
+
extractable value. Use the WHERE predicate to filter rows; write your
|
|
2952
|
+
own SELECT for the columns you want.
|
|
2953
|
+
|
|
2954
|
+
Not yet supported:
|
|
2955
|
+
- Pattern paths as SELECT (needing `LATERAL jsonb_path_query`)
|
|
2956
|
+
- Regex-in-access like `data./prefix_\d+/.field`
|
|
2957
|
+
- Pattern refs
|
|
2958
|
+
|
|
2904
2959
|
### Output keys
|
|
2905
2960
|
|
|
2906
2961
|
Returned dict may contain any subset of:
|
|
@@ -2910,7 +2965,7 @@ Returned dict may contain any subset of:
|
|
|
2910
2965
|
| `select` | `str` | Expression yielding the value dotted would return |
|
|
2911
2966
|
| `where` | `str` | Predicate from guards / filters / boolean groups |
|
|
2912
2967
|
| `params` | `dict` | Hoisted values keyed by name |
|
|
2913
|
-
| `
|
|
2968
|
+
| `unbound` | `dict` | Deferred substitutions: `{bind_param_name: original_name}` |
|
|
2914
2969
|
|
|
2915
2970
|
`select` is omitted when combining predicates across different columns
|
|
2916
2971
|
(e.g. top-level groups), since there's no single meaningful expression.
|
|
@@ -2764,9 +2764,11 @@ View all registered transforms with `dotted.registry()`.
|
|
|
2764
2764
|
## SQL Translation (`sqlize`)
|
|
2765
2765
|
|
|
2766
2766
|
> **Under development.** The API shape, output format, and supported subset
|
|
2767
|
-
> of dotted features may still change. Postgres-only, SQLAlchemy-style
|
|
2768
|
-
> placeholders
|
|
2769
|
-
>
|
|
2767
|
+
> of dotted features may still change. Postgres-only, SQLAlchemy-style
|
|
2768
|
+
> named placeholders. Pattern paths (`*`, `[*]`, `**`, filter brackets)
|
|
2769
|
+
> are supported only as WHERE-side existence checks via
|
|
2770
|
+
> `jsonb_path_exists`; pattern paths that yield sets as SELECT (needing
|
|
2771
|
+
> `LATERAL jsonb_path_query`) are not supported yet.
|
|
2770
2772
|
|
|
2771
2773
|
`dotted.sqlize` translates a dotted path into SQL clause components — a
|
|
2772
2774
|
`select` expression, a `where` predicate, and a `params` dict with hoisted
|
|
@@ -2834,18 +2836,36 @@ time:
|
|
|
2834
2836
|
|
|
2835
2837
|
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2836
2838
|
>>> r
|
|
2837
|
-
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2839
|
+
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2840
|
+
'unbound': {'min_age': 'min_age'}}
|
|
2838
2841
|
|
|
2839
|
-
`
|
|
2840
|
-
`
|
|
2842
|
+
`unbound` is a dict keyed by the **SQL bind parameter name** — the name
|
|
2843
|
+
that appears in the SQL `:placeholder` and as a key in `params`. Each
|
|
2844
|
+
value is the **original substitution name** (what was inside `$(...)`),
|
|
2845
|
+
kept as provenance so callers can trace a bind back to its source.
|
|
2846
|
+
|
|
2847
|
+
`unbound.keys()` is the list of bind slots the caller needs to fill.
|
|
2848
|
+
Most often you just fill `params` by bind name directly:
|
|
2841
2849
|
|
|
2842
2850
|
>>> r['params']['min_age'] = 30
|
|
2843
2851
|
>>> # hand (r['where'], r['params']) to cursor.execute / session.execute / etc.
|
|
2844
2852
|
|
|
2845
|
-
|
|
2853
|
+
Non-identifier names (dotted paths, quoted keys, spaces, etc.) hash to a
|
|
2854
|
+
deterministic `_s_<hex>` bind name — the bind-side name is different
|
|
2855
|
+
from what appeared in source, but the original is preserved in the
|
|
2856
|
+
`unbound` value:
|
|
2857
|
+
|
|
2858
|
+
>>> r = dotted.sqlize('age >= $(user.min_age)')
|
|
2859
|
+
>>> r['where'] # doctest: +ELLIPSIS
|
|
2860
|
+
'age >= :_s_...'
|
|
2861
|
+
>>> list(r['unbound'].values())
|
|
2862
|
+
['user.min_age']
|
|
2863
|
+
|
|
2864
|
+
Repeated uses of the same name share one entry automatically:
|
|
2846
2865
|
|
|
2847
2866
|
>>> dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2848
|
-
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2867
|
+
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2868
|
+
'unbound': {'x': 'x'}}
|
|
2849
2869
|
|
|
2850
2870
|
If you pass `bindings=`, substitutions are resolved at sqlize time and hoisted
|
|
2851
2871
|
like any other literal:
|
|
@@ -2864,6 +2884,41 @@ Postgres's `->` / `#>` accept runtime-computed keys:
|
|
|
2864
2884
|
Relative references (`^`) and parent references (`^^+`) are not supported
|
|
2865
2885
|
yet.
|
|
2866
2886
|
|
|
2887
|
+
### Pattern paths
|
|
2888
|
+
|
|
2889
|
+
Paths that contain wildcards (`*`, `[*]`), recursive descent (`**`), or
|
|
2890
|
+
bracket filters (`[pred]`, `[*&pred]`) translate to a Postgres
|
|
2891
|
+
`jsonb_path_exists(…)` WHERE predicate. The dotted path becomes a JSONPath
|
|
2892
|
+
expression; guard values embed inline when safely literal (numbers,
|
|
2893
|
+
booleans, null) or flow through a `jsonb_build_object` vars argument
|
|
2894
|
+
(strings, substitutions).
|
|
2895
|
+
|
|
2896
|
+
>>> dotted.sqlize("data.users[*].age >= 30")['where']
|
|
2897
|
+
"jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')"
|
|
2898
|
+
|
|
2899
|
+
>>> dotted.sqlize("data.**.active = True")['where']
|
|
2900
|
+
"jsonb_path_exists(data, '$.**.active ? (@ == true)')"
|
|
2901
|
+
|
|
2902
|
+
>>> dotted.sqlize("data.users[age>=30]")['where']
|
|
2903
|
+
"jsonb_path_exists(data, '$.users[*] ? (@.age >= 30)')"
|
|
2904
|
+
|
|
2905
|
+
>>> dotted.sqlize('data.users[*&age>=30].name = "alice"')['where']
|
|
2906
|
+
"jsonb_path_exists(data, '$.users[*] ? (@.age >= 30).name ? (@ == :_p1)', jsonb_build_object('_p1', :_p1))"
|
|
2907
|
+
|
|
2908
|
+
Pattern predicates compose with scalar ones via dotted's `&` / `,` / `!`:
|
|
2909
|
+
|
|
2910
|
+
>>> dotted.sqlize('(data.users[*].age >= 30 & status = "active")')['where']
|
|
2911
|
+
"(jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')) AND (status = :_p1)"
|
|
2912
|
+
|
|
2913
|
+
`select` is set to the column itself — pattern paths don't have a single
|
|
2914
|
+
extractable value. Use the WHERE predicate to filter rows; write your
|
|
2915
|
+
own SELECT for the columns you want.
|
|
2916
|
+
|
|
2917
|
+
Not yet supported:
|
|
2918
|
+
- Pattern paths as SELECT (needing `LATERAL jsonb_path_query`)
|
|
2919
|
+
- Regex-in-access like `data./prefix_\d+/.field`
|
|
2920
|
+
- Pattern refs
|
|
2921
|
+
|
|
2867
2922
|
### Output keys
|
|
2868
2923
|
|
|
2869
2924
|
Returned dict may contain any subset of:
|
|
@@ -2873,7 +2928,7 @@ Returned dict may contain any subset of:
|
|
|
2873
2928
|
| `select` | `str` | Expression yielding the value dotted would return |
|
|
2874
2929
|
| `where` | `str` | Predicate from guards / filters / boolean groups |
|
|
2875
2930
|
| `params` | `dict` | Hoisted values keyed by name |
|
|
2876
|
-
| `
|
|
2931
|
+
| `unbound` | `dict` | Deferred substitutions: `{bind_param_name: original_name}` |
|
|
2877
2932
|
|
|
2878
2933
|
`select` is omitted when combining predicates across different columns
|
|
2879
2934
|
(e.g. top-level groups), since there's no single meaningful expression.
|
|
@@ -56,7 +56,8 @@ For full documentation including all options and flags:
|
|
|
56
56
|
or see README.md
|
|
57
57
|
"""
|
|
58
58
|
from .api import \
|
|
59
|
-
parse, is_pattern, is_template,
|
|
59
|
+
parse, is_pattern, is_template, is_reference, is_indeterminate, is_simple, \
|
|
60
|
+
is_inverted, mutable, quote, ANY, AUTO, Attrs, GroupMode, \
|
|
60
61
|
register, transform, \
|
|
61
62
|
assemble, assemble_multi, \
|
|
62
63
|
build, build_multi, \
|
|
@@ -82,7 +83,10 @@ __all__ = [
|
|
|
82
83
|
# Transform
|
|
83
84
|
'apply', 'apply_multi', 'register', 'transform',
|
|
84
85
|
# Utility
|
|
85
|
-
'parse', 'assemble', 'assemble_multi', 'quote',
|
|
86
|
+
'parse', 'assemble', 'assemble_multi', 'quote',
|
|
87
|
+
'is_pattern', 'is_template', 'is_reference',
|
|
88
|
+
'is_indeterminate', 'is_simple',
|
|
89
|
+
'is_inverted', 'mutable',
|
|
86
90
|
# SQL
|
|
87
91
|
'sqlize', 'TranslationError',
|
|
88
92
|
# Constants
|
|
@@ -870,6 +870,14 @@ class SliceFilter(BaseOp):
|
|
|
870
870
|
hasattr(f, 'is_template') and f.is_template()
|
|
871
871
|
for f in self.filters)
|
|
872
872
|
|
|
873
|
+
def is_reference(self):
|
|
874
|
+
"""
|
|
875
|
+
True if any attached filter contains an internal reference.
|
|
876
|
+
"""
|
|
877
|
+
return any(
|
|
878
|
+
hasattr(f, 'is_reference') and f.is_reference()
|
|
879
|
+
for f in self.filters)
|
|
880
|
+
|
|
873
881
|
def is_empty(self, node):
|
|
874
882
|
return not node
|
|
875
883
|
|
|
@@ -188,6 +188,94 @@ def is_template(key):
|
|
|
188
188
|
return _is_template(parse(key))
|
|
189
189
|
|
|
190
190
|
|
|
191
|
+
@functools.lru_cache(CACHE_SIZE)
|
|
192
|
+
def _is_reference(ops):
|
|
193
|
+
if not ops:
|
|
194
|
+
return False
|
|
195
|
+
if ops[0].is_reference():
|
|
196
|
+
return True
|
|
197
|
+
return _is_reference(ops[1:])
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def is_reference(key):
|
|
201
|
+
"""
|
|
202
|
+
True if dotted path contains internal references ($$(...)).
|
|
203
|
+
Walks into wraps, filters, and group branches.
|
|
204
|
+
|
|
205
|
+
>>> is_reference('a.$$(b)')
|
|
206
|
+
True
|
|
207
|
+
>>> is_reference('a.b')
|
|
208
|
+
False
|
|
209
|
+
>>> is_reference('a.$(x)')
|
|
210
|
+
False
|
|
211
|
+
"""
|
|
212
|
+
if isinstance(key, results.Dotted):
|
|
213
|
+
return _is_reference(key)
|
|
214
|
+
return _is_reference(parse(key))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def is_indeterminate(key):
|
|
218
|
+
"""
|
|
219
|
+
True if the path contains substitutions or references — anything
|
|
220
|
+
whose access target isn't pinned down by the source alone and
|
|
221
|
+
requires bindings or data to resolve. Patterns are not indeterminate:
|
|
222
|
+
they describe a set inherent to the query.
|
|
223
|
+
|
|
224
|
+
>>> is_indeterminate('a.$(x)')
|
|
225
|
+
True
|
|
226
|
+
>>> is_indeterminate('a.$$(b)')
|
|
227
|
+
True
|
|
228
|
+
>>> is_indeterminate('a.b')
|
|
229
|
+
False
|
|
230
|
+
>>> is_indeterminate('a.*.b')
|
|
231
|
+
False
|
|
232
|
+
"""
|
|
233
|
+
parsed = key if isinstance(key, results.Dotted) else parse(key)
|
|
234
|
+
return _is_template(parsed) or _is_reference(parsed)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@functools.lru_cache(CACHE_SIZE)
|
|
238
|
+
def _is_simple(ops):
|
|
239
|
+
for op in ops:
|
|
240
|
+
if not isinstance(op, (access.Key, access.Attr, access.Slot)):
|
|
241
|
+
return False
|
|
242
|
+
if not isinstance(op.op, matchers.Const):
|
|
243
|
+
return False
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def is_simple(key):
|
|
248
|
+
"""
|
|
249
|
+
True if the path is a plain chain of access ops (Key, Attr, Slot)
|
|
250
|
+
with literal matchers — no patterns, substitutions, references,
|
|
251
|
+
guards, transforms, filters, groups, or other decorations.
|
|
252
|
+
|
|
253
|
+
This is the strictest classification: paths that spell out one
|
|
254
|
+
specific access target character-for-character.
|
|
255
|
+
|
|
256
|
+
>>> is_simple('a.b.c')
|
|
257
|
+
True
|
|
258
|
+
>>> is_simple('a[0].b')
|
|
259
|
+
True
|
|
260
|
+
>>> is_simple('a@attr')
|
|
261
|
+
True
|
|
262
|
+
>>> is_simple('a.*.b')
|
|
263
|
+
False
|
|
264
|
+
>>> is_simple('a=30')
|
|
265
|
+
False
|
|
266
|
+
>>> is_simple('a|int')
|
|
267
|
+
False
|
|
268
|
+
>>> is_simple('a.$(x)')
|
|
269
|
+
False
|
|
270
|
+
>>> is_simple('a.$$(b)')
|
|
271
|
+
False
|
|
272
|
+
"""
|
|
273
|
+
parsed = key if isinstance(key, results.Dotted) else parse(key)
|
|
274
|
+
if parsed.transforms:
|
|
275
|
+
return False
|
|
276
|
+
return _is_simple(parsed)
|
|
277
|
+
|
|
278
|
+
|
|
191
279
|
def is_inverted(key):
|
|
192
280
|
"""
|
|
193
281
|
True if an inverted style pattern
|
|
@@ -13,6 +13,13 @@ def _any_template(items):
|
|
|
13
13
|
return any(hasattr(x, 'is_template') and x.is_template() for x in items)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _any_reference(items):
|
|
17
|
+
"""
|
|
18
|
+
True if any item in an iterable has is_reference() returning True.
|
|
19
|
+
"""
|
|
20
|
+
return any(hasattr(x, 'is_reference') and x.is_reference() for x in items)
|
|
21
|
+
|
|
22
|
+
|
|
16
23
|
class FilterOp(base.MatchOp):
|
|
17
24
|
def is_pattern(self):
|
|
18
25
|
return False
|
|
@@ -63,6 +70,12 @@ class FilterKey(base.MatchOp):
|
|
|
63
70
|
"""
|
|
64
71
|
return _any_template(self.parts)
|
|
65
72
|
|
|
73
|
+
def is_reference(self):
|
|
74
|
+
"""
|
|
75
|
+
True if any part of the filter key contains an internal reference.
|
|
76
|
+
"""
|
|
77
|
+
return _any_reference(self.parts)
|
|
78
|
+
|
|
66
79
|
def get_values(self, node):
|
|
67
80
|
"""
|
|
68
81
|
Get all values from node matching this key, traversing dotted path if needed.
|
|
@@ -211,6 +224,17 @@ class FilterKeyValue(FilterOp):
|
|
|
211
224
|
return True
|
|
212
225
|
return _any_template(self.transforms)
|
|
213
226
|
|
|
227
|
+
def is_reference(self):
|
|
228
|
+
"""
|
|
229
|
+
True if the key, value, or any transform contains an internal
|
|
230
|
+
reference.
|
|
231
|
+
"""
|
|
232
|
+
if hasattr(self.key, 'is_reference') and self.key.is_reference():
|
|
233
|
+
return True
|
|
234
|
+
if hasattr(self.val, 'is_reference') and self.val.is_reference():
|
|
235
|
+
return True
|
|
236
|
+
return _any_reference(self.transforms)
|
|
237
|
+
|
|
214
238
|
def _eq_match(self, node):
|
|
215
239
|
"""
|
|
216
240
|
True if any value from self.key in node matches self.val (after transforms).
|
|
@@ -379,6 +403,11 @@ class FilterGroup(FilterOp):
|
|
|
379
403
|
and hasattr(self.inner, 'is_template')
|
|
380
404
|
and self.inner.is_template())
|
|
381
405
|
|
|
406
|
+
def is_reference(self):
|
|
407
|
+
return (self.inner is not None
|
|
408
|
+
and hasattr(self.inner, 'is_reference')
|
|
409
|
+
and self.inner.is_reference())
|
|
410
|
+
|
|
382
411
|
def resolve(self, bindings, partial=False):
|
|
383
412
|
"""
|
|
384
413
|
Resolve $N in inner filter.
|
|
@@ -434,6 +463,9 @@ class FilterAnd(FilterOp):
|
|
|
434
463
|
def is_template(self):
|
|
435
464
|
return _any_template(self.filters)
|
|
436
465
|
|
|
466
|
+
def is_reference(self):
|
|
467
|
+
return _any_reference(self.filters)
|
|
468
|
+
|
|
437
469
|
def resolve(self, bindings, partial=False):
|
|
438
470
|
"""
|
|
439
471
|
Resolve $N in each filter.
|
|
@@ -477,6 +509,9 @@ class FilterOr(FilterOp):
|
|
|
477
509
|
def is_template(self):
|
|
478
510
|
return _any_template(self.filters)
|
|
479
511
|
|
|
512
|
+
def is_reference(self):
|
|
513
|
+
return _any_reference(self.filters)
|
|
514
|
+
|
|
480
515
|
def resolve(self, bindings, partial=False):
|
|
481
516
|
"""
|
|
482
517
|
Resolve $N in each filter.
|
|
@@ -527,6 +562,11 @@ class FilterKeyValueFirst(FilterOp):
|
|
|
527
562
|
and hasattr(self.inner, 'is_template')
|
|
528
563
|
and self.inner.is_template())
|
|
529
564
|
|
|
565
|
+
def is_reference(self):
|
|
566
|
+
return (self.inner is not None
|
|
567
|
+
and hasattr(self.inner, 'is_reference')
|
|
568
|
+
and self.inner.is_reference())
|
|
569
|
+
|
|
530
570
|
def resolve(self, bindings, partial=False):
|
|
531
571
|
"""
|
|
532
572
|
Resolve $N in inner filter.
|
|
@@ -585,6 +625,11 @@ class FilterNot(FilterOp):
|
|
|
585
625
|
and hasattr(self.inner, 'is_template')
|
|
586
626
|
and self.inner.is_template())
|
|
587
627
|
|
|
628
|
+
def is_reference(self):
|
|
629
|
+
return (self.inner is not None
|
|
630
|
+
and hasattr(self.inner, 'is_reference')
|
|
631
|
+
and self.inner.is_reference())
|
|
632
|
+
|
|
588
633
|
def resolve(self, bindings, partial=False):
|
|
589
634
|
"""
|
|
590
635
|
Resolve $N in inner filter.
|
|
@@ -42,6 +42,16 @@ class OpGroup(base.TraversalOp):
|
|
|
42
42
|
return True
|
|
43
43
|
return False
|
|
44
44
|
|
|
45
|
+
def is_reference(self):
|
|
46
|
+
"""
|
|
47
|
+
True if any branch contains an internal reference.
|
|
48
|
+
"""
|
|
49
|
+
for branch in base.branches_only(self.branches):
|
|
50
|
+
for op in branch:
|
|
51
|
+
if hasattr(op, 'is_reference') and op.is_reference():
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
45
55
|
def default(self):
|
|
46
56
|
"""
|
|
47
57
|
Derive default from the first branch's first op, so auto-creation works
|