qs-codec 0.2.0__tar.gz → 0.2.2__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.
- {qs_codec-0.2.0 → qs_codec-0.2.2}/CHANGELOG.md +9 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/PKG-INFO +8 -8
- {qs_codec-0.2.0 → qs_codec-0.2.2}/README.rst +5 -5
- {qs_codec-0.2.0 → qs_codec-0.2.2}/docs/README.rst +5 -5
- {qs_codec-0.2.0 → qs_codec-0.2.2}/pyproject.toml +2 -2
- {qs_codec-0.2.0 → qs_codec-0.2.2}/requirements_dev.txt +2 -2
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/__init__.py +1 -1
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/decode.py +16 -10
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/encode.py +1 -1
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/models/encode_options.py +1 -1
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/utils/utils.py +10 -10
- qs_codec-0.2.2/tests/comparison/test_cases.json +306 -0
- qs_codec-0.2.2/tests/e2e/e2e_test.py +204 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/unit/decode_test.py +27 -27
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/unit/encode_test.py +12 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/unit/example_test.py +4 -4
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/unit/utils_test.py +4 -4
- qs_codec-0.2.0/tests/comparison/test_cases.json +0 -79
- qs_codec-0.2.0/tests/e2e/e2e_test.py +0 -49
- {qs_codec-0.2.0 → qs_codec-0.2.2}/.gitignore +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/CODE-OF-CONDUCT.md +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/LICENSE +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/enums/__init__.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/enums/charset.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/enums/duplicates.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/enums/format.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/enums/list_format.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/enums/sentinel.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/models/__init__.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/models/decode_options.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/models/undefined.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/models/weak_wrapper.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/py.typed +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/utils/__init__.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/utils/decode_utils.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/utils/encode_utils.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/src/qs_codec/utils/str_utils.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/comparison/.gitignore +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/comparison/__init__.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/comparison/compare_outputs.sh +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/comparison/package.json +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/comparison/qs.js +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/comparison/qs.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/e2e/__init__.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/unit/__init__.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/unit/fixed_qs_issues_test.py +0 -0
- {qs_codec-0.2.0 → qs_codec-0.2.2}/tests/unit/weakref_test.py +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.2.2
|
|
2
|
+
|
|
3
|
+
* [FEAT] `decode` returns `dict[str, Any]` instead of `dict` ([#4](https://github.com/techouse/qs_codec/pull/4))
|
|
4
|
+
* [FIX] fix decoding encoded square brackets in key names
|
|
5
|
+
|
|
6
|
+
## 0.2.1
|
|
7
|
+
|
|
8
|
+
* [CHORE] update dependencies
|
|
9
|
+
|
|
1
10
|
## 0.2.0
|
|
2
11
|
|
|
3
12
|
* [CHORE] update dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: qs-codec
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A query string encoding and decoding library for Python. Ported from qs for JavaScript.
|
|
5
5
|
Project-URL: Homepage, https://techouse.github.io/qs_codec/
|
|
6
6
|
Project-URL: Documentation, https://techouse.github.io/qs_codec/
|
|
@@ -29,8 +29,8 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
29
29
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
30
30
|
Classifier: Topic :: Software Development :: Libraries
|
|
31
31
|
Requires-Python: >=3.8
|
|
32
|
-
Requires-Dist: regex
|
|
33
|
-
Requires-Dist: types-regex
|
|
32
|
+
Requires-Dist: regex>=2024.4.28
|
|
33
|
+
Requires-Dist: types-regex>=2024.4.28.20240506
|
|
34
34
|
Description-Content-Type: text/x-rst
|
|
35
35
|
|
|
36
36
|
qs-codec
|
|
@@ -72,7 +72,7 @@ dictionaries
|
|
|
72
72
|
def decode(
|
|
73
73
|
value: t.Optional[t.Union[str, t.Mapping]],
|
|
74
74
|
options: qs_codec.DecodeOptions = qs_codec.DecodeOptions(),
|
|
75
|
-
) ->
|
|
75
|
+
) -> t.Dict[str, t.Any]:
|
|
76
76
|
"""Decodes a str or Mapping into a Dict.
|
|
77
77
|
|
|
78
78
|
Providing custom DecodeOptions will override the default behavior."""
|
|
@@ -363,7 +363,7 @@ over this huge ``list``.
|
|
|
363
363
|
|
|
364
364
|
import qs_codec
|
|
365
365
|
|
|
366
|
-
assert qs_codec.decode('a[100]=b') == {'a': {100: 'b'}}
|
|
366
|
+
assert qs_codec.decode('a[100]=b') == {'a': {'100': 'b'}}
|
|
367
367
|
|
|
368
368
|
This limit can be overridden by passing an `list_limit <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.list_limit>`__
|
|
369
369
|
option:
|
|
@@ -375,7 +375,7 @@ option:
|
|
|
375
375
|
assert qs_codec.decode(
|
|
376
376
|
'a[1]=b',
|
|
377
377
|
qs_codec.DecodeOptions(list_limit=0),
|
|
378
|
-
) == {'a': {1: 'b'}}
|
|
378
|
+
) == {'a': {'1': 'b'}}
|
|
379
379
|
|
|
380
380
|
To disable ``list`` parsing entirely, set `parse_lists <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.parse_lists>`__
|
|
381
381
|
to ``False``.
|
|
@@ -387,7 +387,7 @@ to ``False``.
|
|
|
387
387
|
assert qs_codec.decode(
|
|
388
388
|
'a[]=b',
|
|
389
389
|
qs_codec.DecodeOptions(parse_lists=False),
|
|
390
|
-
) == {'a': {0: 'b'}}
|
|
390
|
+
) == {'a': {'0': 'b'}}
|
|
391
391
|
|
|
392
392
|
If you mix notations, `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will merge the two items into a ``dict``:
|
|
393
393
|
|
|
@@ -395,7 +395,7 @@ If you mix notations, `decode <https://techouse.github.io/qs_codec/qs_codec.mode
|
|
|
395
395
|
|
|
396
396
|
import qs_codec
|
|
397
397
|
|
|
398
|
-
assert qs_codec.decode('a[0]=b&a[b]=c') == {'a': {0: 'b', 'b': 'c'}}
|
|
398
|
+
assert qs_codec.decode('a[0]=b&a[b]=c') == {'a': {'0': 'b', 'b': 'c'}}
|
|
399
399
|
|
|
400
400
|
You can also create ``list``\ s of ``dict``\ s:
|
|
401
401
|
|
|
@@ -37,7 +37,7 @@ dictionaries
|
|
|
37
37
|
def decode(
|
|
38
38
|
value: t.Optional[t.Union[str, t.Mapping]],
|
|
39
39
|
options: qs_codec.DecodeOptions = qs_codec.DecodeOptions(),
|
|
40
|
-
) ->
|
|
40
|
+
) -> t.Dict[str, t.Any]:
|
|
41
41
|
"""Decodes a str or Mapping into a Dict.
|
|
42
42
|
|
|
43
43
|
Providing custom DecodeOptions will override the default behavior."""
|
|
@@ -328,7 +328,7 @@ over this huge ``list``.
|
|
|
328
328
|
|
|
329
329
|
import qs_codec
|
|
330
330
|
|
|
331
|
-
assert qs_codec.decode('a[100]=b') == {'a': {100: 'b'}}
|
|
331
|
+
assert qs_codec.decode('a[100]=b') == {'a': {'100': 'b'}}
|
|
332
332
|
|
|
333
333
|
This limit can be overridden by passing an `list_limit <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.list_limit>`__
|
|
334
334
|
option:
|
|
@@ -340,7 +340,7 @@ option:
|
|
|
340
340
|
assert qs_codec.decode(
|
|
341
341
|
'a[1]=b',
|
|
342
342
|
qs_codec.DecodeOptions(list_limit=0),
|
|
343
|
-
) == {'a': {1: 'b'}}
|
|
343
|
+
) == {'a': {'1': 'b'}}
|
|
344
344
|
|
|
345
345
|
To disable ``list`` parsing entirely, set `parse_lists <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.parse_lists>`__
|
|
346
346
|
to ``False``.
|
|
@@ -352,7 +352,7 @@ to ``False``.
|
|
|
352
352
|
assert qs_codec.decode(
|
|
353
353
|
'a[]=b',
|
|
354
354
|
qs_codec.DecodeOptions(parse_lists=False),
|
|
355
|
-
) == {'a': {0: 'b'}}
|
|
355
|
+
) == {'a': {'0': 'b'}}
|
|
356
356
|
|
|
357
357
|
If you mix notations, `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will merge the two items into a ``dict``:
|
|
358
358
|
|
|
@@ -360,7 +360,7 @@ If you mix notations, `decode <https://techouse.github.io/qs_codec/qs_codec.mode
|
|
|
360
360
|
|
|
361
361
|
import qs_codec
|
|
362
362
|
|
|
363
|
-
assert qs_codec.decode('a[0]=b&a[b]=c') == {'a': {0: 'b', 'b': 'c'}}
|
|
363
|
+
assert qs_codec.decode('a[0]=b&a[b]=c') == {'a': {'0': 'b', 'b': 'c'}}
|
|
364
364
|
|
|
365
365
|
You can also create ``list``\ s of ``dict``\ s:
|
|
366
366
|
|
|
@@ -11,7 +11,7 @@ dictionaries
|
|
|
11
11
|
def decode(
|
|
12
12
|
value: t.Optional[t.Union[str, t.Mapping]],
|
|
13
13
|
options: qs_codec.DecodeOptions = qs_codec.DecodeOptions(),
|
|
14
|
-
) ->
|
|
14
|
+
) -> t.Dict[str, t.Any]:
|
|
15
15
|
"""Decodes a str or Mapping into a Dict.
|
|
16
16
|
|
|
17
17
|
Providing custom DecodeOptions will override the default behavior."""
|
|
@@ -303,7 +303,7 @@ over this huge ``list``.
|
|
|
303
303
|
|
|
304
304
|
import qs_codec
|
|
305
305
|
|
|
306
|
-
assert qs_codec.decode('a[100]=b') == {'a': {100: 'b'}}
|
|
306
|
+
assert qs_codec.decode('a[100]=b') == {'a': {'100': 'b'}}
|
|
307
307
|
|
|
308
308
|
This limit can be overridden by passing an :py:attr:`list_limit <qs_codec.models.decode_options.DecodeOptions.list_limit>`
|
|
309
309
|
option:
|
|
@@ -315,7 +315,7 @@ option:
|
|
|
315
315
|
assert qs_codec.decode(
|
|
316
316
|
'a[1]=b',
|
|
317
317
|
qs_codec.DecodeOptions(list_limit=0),
|
|
318
|
-
) == {'a': {1: 'b'}}
|
|
318
|
+
) == {'a': {'1': 'b'}}
|
|
319
319
|
|
|
320
320
|
To disable ``list`` parsing entirely, set :py:attr:`parse_lists <qs_codec.models.decode_options.DecodeOptions.parse_lists>`
|
|
321
321
|
to ``False``.
|
|
@@ -327,7 +327,7 @@ to ``False``.
|
|
|
327
327
|
assert qs_codec.decode(
|
|
328
328
|
'a[]=b',
|
|
329
329
|
qs_codec.DecodeOptions(parse_lists=False),
|
|
330
|
-
) == {'a': {0: 'b'}}
|
|
330
|
+
) == {'a': {'0': 'b'}}
|
|
331
331
|
|
|
332
332
|
If you mix notations, :py:attr:`decode <qs_codec.decode>` will merge the two items into a ``dict``:
|
|
333
333
|
|
|
@@ -335,7 +335,7 @@ If you mix notations, :py:attr:`decode <qs_codec.decode>` will merge the two ite
|
|
|
335
335
|
|
|
336
336
|
import qs_codec
|
|
337
337
|
|
|
338
|
-
assert qs_codec.decode('a[0]=b&a[b]=c') == {'a': {0: 'b', 'b': 'c'}}
|
|
338
|
+
assert qs_codec.decode('a[0]=b&a[b]=c') == {'a': {'0': 'b', 'b': 'c'}}
|
|
339
339
|
|
|
340
340
|
You can also create ``list``\ s of ``dict``\ s:
|
|
341
341
|
|
|
@@ -14,20 +14,25 @@ from .models.undefined import Undefined
|
|
|
14
14
|
from .utils.utils import Utils
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def decode(
|
|
17
|
+
def decode(
|
|
18
|
+
value: t.Optional[t.Union[str, t.Dict[str, t.Any]]], options: DecodeOptions = DecodeOptions()
|
|
19
|
+
) -> t.Dict[str, t.Any]:
|
|
18
20
|
"""
|
|
19
21
|
Decodes a ``str`` or ``Mapping`` into a ``dict``.
|
|
20
22
|
|
|
21
23
|
Providing custom ``DecodeOptions`` will override the default behavior.
|
|
22
24
|
"""
|
|
25
|
+
obj: t.Dict[str, t.Any] = {}
|
|
26
|
+
|
|
23
27
|
if not value:
|
|
24
|
-
return
|
|
28
|
+
return obj
|
|
25
29
|
|
|
26
|
-
if not isinstance(value, (str,
|
|
30
|
+
if not isinstance(value, (str, dict)):
|
|
27
31
|
raise ValueError("The input must be a String or a Dict")
|
|
28
32
|
|
|
29
|
-
temp_obj: t.Optional[t.
|
|
30
|
-
|
|
33
|
+
temp_obj: t.Optional[t.Dict[str, t.Any]] = (
|
|
34
|
+
_parse_query_string_values(value, options) if isinstance(value, str) else value
|
|
35
|
+
)
|
|
31
36
|
|
|
32
37
|
# Iterate over the keys and setup the new object
|
|
33
38
|
if temp_obj:
|
|
@@ -49,10 +54,11 @@ def _parse_array_value(value: t.Any, options: DecodeOptions) -> t.Any:
|
|
|
49
54
|
return value
|
|
50
55
|
|
|
51
56
|
|
|
52
|
-
def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict:
|
|
53
|
-
obj: t.Dict =
|
|
57
|
+
def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict[str, t.Any]:
|
|
58
|
+
obj: t.Dict[str, t.Any] = {}
|
|
54
59
|
|
|
55
60
|
clean_str: str = value.replace("?", "", 1) if options.ignore_query_prefix else value
|
|
61
|
+
clean_str = clean_str.replace("%5B", "[").replace("%5b", "[").replace("%5D", "]").replace("%5d", "]")
|
|
56
62
|
limit: t.Optional[int] = None if isinf(options.parameter_limit) else options.parameter_limit # type: ignore [assignment]
|
|
57
63
|
parts: t.List[str]
|
|
58
64
|
if isinstance(options.delimiter, re.Pattern):
|
|
@@ -118,7 +124,7 @@ def _parse_object(
|
|
|
118
124
|
|
|
119
125
|
i: int
|
|
120
126
|
for i in reversed(range(len(chain))):
|
|
121
|
-
obj: t.Optional[t.Any]
|
|
127
|
+
obj: t.Optional[t.Union[t.Dict[str, t.Any], t.List[t.Any]]]
|
|
122
128
|
root: str = chain[i]
|
|
123
129
|
|
|
124
130
|
if root == "[]" and options.parse_lists:
|
|
@@ -140,7 +146,7 @@ def _parse_object(
|
|
|
140
146
|
index = None
|
|
141
147
|
|
|
142
148
|
if not options.parse_lists and decoded_root == "":
|
|
143
|
-
obj = {0: leaf}
|
|
149
|
+
obj = {"0": leaf}
|
|
144
150
|
elif (
|
|
145
151
|
index is not None
|
|
146
152
|
and index >= 0
|
|
@@ -152,7 +158,7 @@ def _parse_object(
|
|
|
152
158
|
obj = [Undefined() for _ in range(index + 1)]
|
|
153
159
|
obj[index] = leaf
|
|
154
160
|
else:
|
|
155
|
-
obj[index if index is not None else decoded_root] = leaf
|
|
161
|
+
obj[str(index) if index is not None else decoded_root] = leaf
|
|
156
162
|
|
|
157
163
|
leaf = obj
|
|
158
164
|
|
|
@@ -114,7 +114,7 @@ def _encode(
|
|
|
114
114
|
prefix: t.Optional[str],
|
|
115
115
|
comma_round_trip: t.Optional[bool],
|
|
116
116
|
encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]],
|
|
117
|
-
serialize_date: t.Callable[[datetime], str],
|
|
117
|
+
serialize_date: t.Callable[[datetime], t.Optional[str]],
|
|
118
118
|
sort: t.Optional[t.Callable[[t.Any, t.Any], int]],
|
|
119
119
|
filter: t.Optional[t.Union[t.Callable, t.List[t.Union[str, int]]]],
|
|
120
120
|
formatter: t.Optional[t.Callable[[str], str]],
|
|
@@ -63,7 +63,7 @@ class EncodeOptions:
|
|
|
63
63
|
skip_nulls: bool = False
|
|
64
64
|
"""Set to ``True`` to completely skip encoding keys with ``None`` values."""
|
|
65
65
|
|
|
66
|
-
serialize_date: t.Callable[[datetime], str] = EncodeUtils.serialize_date
|
|
66
|
+
serialize_date: t.Callable[[datetime], t.Optional[str]] = EncodeUtils.serialize_date
|
|
67
67
|
"""If you only want to override the serialization of ``datetime`` objects, you can provide a ``Callable``."""
|
|
68
68
|
|
|
69
69
|
encoder: t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str] = field( # type: ignore [assignment]
|
|
@@ -15,10 +15,10 @@ class Utils:
|
|
|
15
15
|
|
|
16
16
|
@staticmethod
|
|
17
17
|
def merge(
|
|
18
|
-
target: t.Optional[t.Union[t.Mapping, t.List, t.Tuple]],
|
|
19
|
-
source: t.Optional[t.Union[t.Mapping, t.List, t.Tuple, t.Any]],
|
|
18
|
+
target: t.Optional[t.Union[t.Mapping[str, t.Any], t.List[t.Any], t.Tuple]],
|
|
19
|
+
source: t.Optional[t.Union[t.Mapping[str, t.Any], t.List[t.Any], t.Tuple, t.Any]],
|
|
20
20
|
options: DecodeOptions = DecodeOptions(),
|
|
21
|
-
) -> t.Union[t.Dict, t.List, t.Tuple, t.Any]:
|
|
21
|
+
) -> t.Union[t.Dict[str, t.Any], t.List, t.Tuple, t.Any]:
|
|
22
22
|
"""Merge two objects together."""
|
|
23
23
|
if source is None:
|
|
24
24
|
return target
|
|
@@ -60,7 +60,7 @@ class Utils:
|
|
|
60
60
|
if isinstance(source, (list, tuple)):
|
|
61
61
|
target = {
|
|
62
62
|
**target,
|
|
63
|
-
**{i: item for i, item in enumerate(source) if not isinstance(item, Undefined)},
|
|
63
|
+
**{str(i): item for i, item in enumerate(source) if not isinstance(item, Undefined)},
|
|
64
64
|
}
|
|
65
65
|
elif source is not None:
|
|
66
66
|
if not isinstance(target, (list, tuple)) and isinstance(source, (list, tuple)):
|
|
@@ -72,7 +72,7 @@ class Utils:
|
|
|
72
72
|
if target is None or not isinstance(target, t.Mapping):
|
|
73
73
|
if isinstance(target, (list, tuple)):
|
|
74
74
|
return {
|
|
75
|
-
**{i: item for i, item in enumerate(target) if not isinstance(item, Undefined)},
|
|
75
|
+
**{str(i): item for i, item in enumerate(target) if not isinstance(item, Undefined)},
|
|
76
76
|
**source,
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -86,8 +86,8 @@ class Utils:
|
|
|
86
86
|
if not isinstance(el, Undefined)
|
|
87
87
|
]
|
|
88
88
|
|
|
89
|
-
merge_target: t.Dict = (
|
|
90
|
-
|
|
89
|
+
merge_target: t.Dict[str, t.Any] = (
|
|
90
|
+
{str(i): el for i, el in enumerate(source) if not isinstance(el, Undefined)}
|
|
91
91
|
if isinstance(target, (list, tuple)) and not isinstance(source, (list, tuple))
|
|
92
92
|
else copy.deepcopy(dict(target) if not isinstance(target, dict) else target)
|
|
93
93
|
)
|
|
@@ -95,15 +95,15 @@ class Utils:
|
|
|
95
95
|
return {
|
|
96
96
|
**merge_target,
|
|
97
97
|
**{
|
|
98
|
-
key: Utils.merge(merge_target[key], value, options) if key in merge_target else value
|
|
98
|
+
str(key): Utils.merge(merge_target[key], value, options) if key in merge_target else value
|
|
99
99
|
for key, value in source.items()
|
|
100
100
|
},
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
@staticmethod
|
|
104
|
-
def compact(value: t.Dict) -> t.Dict:
|
|
104
|
+
def compact(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
|
|
105
105
|
"""Remove all `Undefined` values from a dictionary."""
|
|
106
|
-
queue: t.List[t.Dict] = [{"obj": {"o": value}, "prop": "o"}]
|
|
106
|
+
queue: t.List[t.Dict[str, t.Any]] = [{"obj": {"o": value}, "prop": "o"}]
|
|
107
107
|
refs: t.List = []
|
|
108
108
|
|
|
109
109
|
for i in range(len(queue)): # pylint: disable=C0200
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"data": {},
|
|
4
|
+
"encoded": ""
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"data": {
|
|
8
|
+
"a": "b"
|
|
9
|
+
},
|
|
10
|
+
"encoded": "a=b"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"data": {
|
|
14
|
+
"a": "b",
|
|
15
|
+
"c": "d"
|
|
16
|
+
},
|
|
17
|
+
"encoded": "a=b&c=d"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"data": {
|
|
21
|
+
"a": "b",
|
|
22
|
+
"c": "d",
|
|
23
|
+
"e": "f"
|
|
24
|
+
},
|
|
25
|
+
"encoded": "a=b&c=d&e=f"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"data": {
|
|
29
|
+
"a": "b",
|
|
30
|
+
"c": "d",
|
|
31
|
+
"e": [
|
|
32
|
+
"f",
|
|
33
|
+
"g",
|
|
34
|
+
"h"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"encoded": "a=b&c=d&e[0]=f&e[1]=g&e[2]=h"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"data": {
|
|
41
|
+
"a": "b",
|
|
42
|
+
"c": "d",
|
|
43
|
+
"e": [
|
|
44
|
+
"f",
|
|
45
|
+
"g",
|
|
46
|
+
"h"
|
|
47
|
+
],
|
|
48
|
+
"i": {
|
|
49
|
+
"j": "k",
|
|
50
|
+
"l": "m"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"encoded": "a=b&c=d&e[0]=f&e[1]=g&e[2]=h&i[j]=k&i[l]=m"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"data": {
|
|
57
|
+
"filters": {
|
|
58
|
+
"$or": [
|
|
59
|
+
{
|
|
60
|
+
"date": {
|
|
61
|
+
"$eq": "2020-01-01"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"date": {
|
|
66
|
+
"$eq": "2020-01-02"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"author": {
|
|
71
|
+
"name": {
|
|
72
|
+
"$eq": "John Doe"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"encoded": "filters[$or][0][date][$eq]=2020-01-01&filters[$or][1][date][$eq]=2020-01-02&filters[author][name][$eq]=John Doe"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"data": {
|
|
81
|
+
"commentsEmbedResponse": [
|
|
82
|
+
{
|
|
83
|
+
"id": "1",
|
|
84
|
+
"post_id": "1",
|
|
85
|
+
"someId": "ma018-9ha12",
|
|
86
|
+
"text": "Hello",
|
|
87
|
+
"replies": [
|
|
88
|
+
{
|
|
89
|
+
"id": "3",
|
|
90
|
+
"comment_id": "1",
|
|
91
|
+
"someId": "ma020-9ha15",
|
|
92
|
+
"text": "Hello"
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "2",
|
|
98
|
+
"post_id": "1",
|
|
99
|
+
"someId": "mw012-7ha19",
|
|
100
|
+
"text": "How are you?",
|
|
101
|
+
"replies": [
|
|
102
|
+
{
|
|
103
|
+
"id": "4",
|
|
104
|
+
"comment_id": "2",
|
|
105
|
+
"someId": "mw023-9ha18",
|
|
106
|
+
"text": "Hello"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "5",
|
|
110
|
+
"comment_id": "2",
|
|
111
|
+
"someId": "mw035-0ha22",
|
|
112
|
+
"text": "Hello"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
"encoded": "commentsEmbedResponse[0][id]=1&commentsEmbedResponse[0][post_id]=1&commentsEmbedResponse[0][someId]=ma018-9ha12&commentsEmbedResponse[0][text]=Hello&commentsEmbedResponse[0][replies][0][id]=3&commentsEmbedResponse[0][replies][0][comment_id]=1&commentsEmbedResponse[0][replies][0][someId]=ma020-9ha15&commentsEmbedResponse[0][replies][0][text]=Hello&commentsEmbedResponse[1][id]=2&commentsEmbedResponse[1][post_id]=1&commentsEmbedResponse[1][someId]=mw012-7ha19&commentsEmbedResponse[1][text]=How are you?&commentsEmbedResponse[1][replies][0][id]=4&commentsEmbedResponse[1][replies][0][comment_id]=2&commentsEmbedResponse[1][replies][0][someId]=mw023-9ha18&commentsEmbedResponse[1][replies][0][text]=Hello&commentsEmbedResponse[1][replies][1][id]=5&commentsEmbedResponse[1][replies][1][comment_id]=2&commentsEmbedResponse[1][replies][1][someId]=mw035-0ha22&commentsEmbedResponse[1][replies][1][text]=Hello"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"data": {
|
|
122
|
+
"commentsResponse": [
|
|
123
|
+
{
|
|
124
|
+
"id": "1",
|
|
125
|
+
"post_id": "1",
|
|
126
|
+
"someId": "ma018-9ha12",
|
|
127
|
+
"text": "Hello",
|
|
128
|
+
"replies": [
|
|
129
|
+
{
|
|
130
|
+
"id": "3",
|
|
131
|
+
"comment_id": "1",
|
|
132
|
+
"someId": "ma020-9ha15",
|
|
133
|
+
"text": "Hello"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "2",
|
|
139
|
+
"post_id": "1",
|
|
140
|
+
"someId": "mw012-7ha19",
|
|
141
|
+
"text": "How are you?",
|
|
142
|
+
"replies": [
|
|
143
|
+
{
|
|
144
|
+
"id": "4",
|
|
145
|
+
"comment_id": "2",
|
|
146
|
+
"someId": "mw023-9ha18",
|
|
147
|
+
"text": "Hello"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "5",
|
|
151
|
+
"comment_id": "2",
|
|
152
|
+
"someId": "mw035-0ha22",
|
|
153
|
+
"text": "Hello"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
"encoded": "commentsResponse[0][id]=1&commentsResponse[0][post_id]=1&commentsResponse[0][someId]=ma018-9ha12&commentsResponse[0][text]=Hello&commentsResponse[0][replies][0][id]=3&commentsResponse[0][replies][0][comment_id]=1&commentsResponse[0][replies][0][someId]=ma020-9ha15&commentsResponse[0][replies][0][text]=Hello&commentsResponse[1][id]=2&commentsResponse[1][post_id]=1&commentsResponse[1][someId]=mw012-7ha19&commentsResponse[1][text]=How are you?&commentsResponse[1][replies][0][id]=4&commentsResponse[1][replies][0][comment_id]=2&commentsResponse[1][replies][0][someId]=mw023-9ha18&commentsResponse[1][replies][0][text]=Hello&commentsResponse[1][replies][1][id]=5&commentsResponse[1][replies][1][comment_id]=2&commentsResponse[1][replies][1][someId]=mw035-0ha22&commentsResponse[1][replies][1][text]=Hello"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"data": {
|
|
163
|
+
"data": {
|
|
164
|
+
"id": "1",
|
|
165
|
+
"someId": "af621-4aa41",
|
|
166
|
+
"text": "Lorem Ipsum Dolor",
|
|
167
|
+
"user": {
|
|
168
|
+
"firstname": "John",
|
|
169
|
+
"lastname": "Doe",
|
|
170
|
+
"age": "25"
|
|
171
|
+
},
|
|
172
|
+
"relationships": {
|
|
173
|
+
"tags": {
|
|
174
|
+
"data": [
|
|
175
|
+
{
|
|
176
|
+
"name": "super"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "awesome"
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"encoded": "data[id]=1&data[someId]=af621-4aa41&data[text]=Lorem Ipsum Dolor&data[user][firstname]=John&data[user][lastname]=Doe&data[user][age]=25&data[relationships][tags][data][0][name]=super&data[relationships][tags][data][1][name]=awesome"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"data": {
|
|
190
|
+
"id": "1",
|
|
191
|
+
"someId": "af621-4aa41",
|
|
192
|
+
"text": "Lorem Ipsum Dolor",
|
|
193
|
+
"user": {
|
|
194
|
+
"firstname": "John",
|
|
195
|
+
"lastname": "Doe",
|
|
196
|
+
"age": "25"
|
|
197
|
+
},
|
|
198
|
+
"relationships": {
|
|
199
|
+
"tags": [
|
|
200
|
+
{
|
|
201
|
+
"name": "super"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"name": "awesome"
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"encoded": "id=1&someId=af621-4aa41&text=Lorem Ipsum Dolor&user[firstname]=John&user[lastname]=Doe&user[age]=25&relationships[tags][0][name]=super&relationships[tags][1][name]=awesome"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"data": {
|
|
213
|
+
"postsResponse": [
|
|
214
|
+
{
|
|
215
|
+
"id": "1",
|
|
216
|
+
"someId": "du761-8bc98",
|
|
217
|
+
"text": "Lorem Ipsum Dolor",
|
|
218
|
+
"user": {
|
|
219
|
+
"firstname": "John",
|
|
220
|
+
"lastname": "Doe",
|
|
221
|
+
"age": "25"
|
|
222
|
+
},
|
|
223
|
+
"relationships": {
|
|
224
|
+
"tags": [
|
|
225
|
+
{
|
|
226
|
+
"name": "super"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"name": "awesome"
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"id": "1",
|
|
236
|
+
"someId": "pa813-7jx02",
|
|
237
|
+
"text": "Lorem Ipsum Dolor",
|
|
238
|
+
"user": {
|
|
239
|
+
"firstname": "Mary",
|
|
240
|
+
"lastname": "Doe",
|
|
241
|
+
"age": "25"
|
|
242
|
+
},
|
|
243
|
+
"relationships": {
|
|
244
|
+
"tags": [
|
|
245
|
+
{
|
|
246
|
+
"name": "super"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"name": "awesome"
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
},
|
|
256
|
+
"encoded": "postsResponse[0][id]=1&postsResponse[0][someId]=du761-8bc98&postsResponse[0][text]=Lorem Ipsum Dolor&postsResponse[0][user][firstname]=John&postsResponse[0][user][lastname]=Doe&postsResponse[0][user][age]=25&postsResponse[0][relationships][tags][0][name]=super&postsResponse[0][relationships][tags][1][name]=awesome&postsResponse[1][id]=1&postsResponse[1][someId]=pa813-7jx02&postsResponse[1][text]=Lorem Ipsum Dolor&postsResponse[1][user][firstname]=Mary&postsResponse[1][user][lastname]=Doe&postsResponse[1][user][age]=25&postsResponse[1][relationships][tags][0][name]=super&postsResponse[1][relationships][tags][1][name]=awesome"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"data": {
|
|
260
|
+
"posts": [
|
|
261
|
+
{
|
|
262
|
+
"id": "1",
|
|
263
|
+
"someId": "du761-8bc98",
|
|
264
|
+
"text": "Lorem Ipsum Dolor",
|
|
265
|
+
"user": {
|
|
266
|
+
"firstname": "John",
|
|
267
|
+
"lastname": "Doe",
|
|
268
|
+
"age": "25"
|
|
269
|
+
},
|
|
270
|
+
"relationships": {
|
|
271
|
+
"tags": [
|
|
272
|
+
{
|
|
273
|
+
"name": "super"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"name": "awesome"
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"id": "1",
|
|
283
|
+
"someId": "pa813-7jx02",
|
|
284
|
+
"text": "Lorem Ipsum Dolor",
|
|
285
|
+
"user": {
|
|
286
|
+
"firstname": "Mary",
|
|
287
|
+
"lastname": "Doe",
|
|
288
|
+
"age": "25"
|
|
289
|
+
},
|
|
290
|
+
"relationships": {
|
|
291
|
+
"tags": [
|
|
292
|
+
{
|
|
293
|
+
"name": "super"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "awesome"
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
"total": "2"
|
|
303
|
+
},
|
|
304
|
+
"encoded": "posts[0][id]=1&posts[0][someId]=du761-8bc98&posts[0][text]=Lorem Ipsum Dolor&posts[0][user][firstname]=John&posts[0][user][lastname]=Doe&posts[0][user][age]=25&posts[0][relationships][tags][0][name]=super&posts[0][relationships][tags][1][name]=awesome&posts[1][id]=1&posts[1][someId]=pa813-7jx02&posts[1][text]=Lorem Ipsum Dolor&posts[1][user][firstname]=Mary&posts[1][user][lastname]=Doe&posts[1][user][age]=25&posts[1][relationships][tags][0][name]=super&posts[1][relationships][tags][1][name]=awesome&total=2"
|
|
305
|
+
}
|
|
306
|
+
]
|