Name | qs-codec JSON |
Version |
1.0.5
JSON |
| download |
home_page | None |
Summary | A query string encoding and decoding library for Python. Ported from qs for JavaScript. |
upload_time | 2024-11-24 16:52:00 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.8 |
license | BSD-3-Clause |
keywords |
codec
qs
query
query-string
querystring
url
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
qs-codec
========
A query string encoding and decoding library for Python.
Ported from `qs <https://www.npmjs.com/package/qs>`__ for JavaScript.
|PyPI - Version| |PyPI - Downloads| |PyPI - Status| |PyPI - Python Version| |PyPI - Format| |Black|
|Test| |CodeQL| |Publish| |Docs| |codecov| |Codacy| |Black| |flake8| |mypy| |pylint| |isort| |bandit|
|License| |Contributor Covenant| |GitHub Sponsors| |GitHub Repo stars|
Usage
-----
A simple usage example:
.. code:: python
import qs_codec as qs
# Encoding
assert qs.encode({'a': 'b'}) == 'a=b'
# Decoding
assert qs.decode('a=b') == {'a': 'b'}
Decoding
~~~~~~~~
dictionaries
^^^^^^^^^^^^
.. code:: python
import qs_codec as qs
import typing as t
def decode(
value: t.Optional[t.Union[str, t.Dict[str, t.Any]]],
options: qs.DecodeOptions = qs.DecodeOptions(),
) -> t.Dict[str, t.Any]:
"""Decodes a query string into a Dict[str, Any].
Providing custom DecodeOptions will override the default behavior."""
pass
`decode <https://techouse.github.io/qs_codec/qs_codec.html#module-qs_codec.decode>`__ allows you to create nested ``dict``\ s within your query
strings, by surrounding the name of sub-keys with square brackets
``[]``. For example, the string ``'foo[bar]=baz'`` converts to:
.. code:: python
import qs_codec as qs
assert qs.decode('foo[bar]=baz') == {'foo': {'bar': 'baz'}}
URI encoded strings work too:
.. code:: python
import qs_codec as qs
assert qs.decode('a%5Bb%5D=c') == {'a': {'b': 'c'}}
You can also nest your ``dict``\ s, like ``'foo[bar][baz]=foobarbaz'``:
.. code:: python
import qs_codec as qs
assert qs.decode('foo[bar][baz]=foobarbaz') == {'foo': {'bar': {'baz': 'foobarbaz'}}}
By default, when nesting ``dict``\ s qs will only decode up to 5
children deep. This means if you attempt to decode a string like
``'a[b][c][d][e][f][g][h][i]=j'`` your resulting ``dict`` will be:
.. code:: python
import qs_codec as qs
assert qs.decode("a[b][c][d][e][f][g][h][i]=j") == {
"a": {"b": {"c": {"d": {"e": {"f": {"[g][h][i]": "j"}}}}}}
}
This depth can be overridden by setting the `depth <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.depth>`_:
.. code:: python
import qs_codec as qs
assert qs.decode(
'a[b][c][d][e][f][g][h][i]=j',
qs.DecodeOptions(depth=1),
) == {'a': {'b': {'[c][d][e][f][g][h][i]': 'j'}}}
You can configure `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ to throw an error
when parsing nested input beyond this depth using `strict_depth <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.strict_depth>`__ (defaults to ``False``):
.. code:: python
import qs_codec as qs
try:
qs.decode(
'a[b][c][d][e][f][g][h][i]=j',
qs.DecodeOptions(depth=1, strict_depth=True),
)
except IndexError as e:
assert str(e) == 'Input depth exceeded depth option of 1 and strict_depth is True'
The depth limit helps mitigate abuse when `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ is used to parse user
input, and it is recommended to keep it a reasonably small number. `strict_depth <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.strict_depth>`__
adds a layer of protection by throwing a ``IndexError`` when the limit is exceeded, allowing you to catch and handle such cases.
For similar reasons, by default `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will only parse up to 1000 parameters. This can be overridden by passing a
`parameter_limit <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.parameter_limit>`__ option:
.. code:: python
import qs_codec as qs
assert qs.decode(
'a=b&c=d',
qs.DecodeOptions(parameter_limit=1),
) == {'a': 'b'}
To bypass the leading question mark, use `ignore_query_prefix <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.ignore_query_prefix>`__:
.. code:: python
import qs_codec as qs
assert qs.decode(
'?a=b&c=d',
qs.DecodeOptions(ignore_query_prefix=True),
) == {'a': 'b', 'c': 'd'}
An optional `delimiter <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.delimiter>`__ can also be passed:
.. code:: python
import qs_codec as qs
assert qs.decode(
'a=b;c=d',
qs.DecodeOptions(delimiter=';'),
) == {'a': 'b', 'c': 'd'}
`delimiter <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.delimiter>`__ can be a regular expression too:
.. code:: python
import qs_codec as qs
import re
assert qs.decode(
'a=b;c=d',
qs.DecodeOptions(delimiter=re.compile(r'[;,]')),
) == {'a': 'b', 'c': 'd'}
Option `allow_dots <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.allow_dots>`__
can be used to enable dot notation:
.. code:: python
import qs_codec as qs
assert qs.decode(
'a.b=c',
qs.DecodeOptions(allow_dots=True),
) == {'a': {'b': 'c'}}
Option `decode_dot_in_keys <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.decode_dot_in_keys>`__
can be used to decode dots in keys.
**Note:** it implies `allow_dots <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.allow_dots>`__, so
`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will error if you set `decode_dot_in_keys <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.decode_dot_in_keys>`__
to ``True``, and `allow_dots <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.allow_dots>`__ to ``False``.
.. code:: python
import qs_codec as qs
assert qs.decode(
'name%252Eobj.first=John&name%252Eobj.last=Doe',
qs.DecodeOptions(decode_dot_in_keys=True),
) == {'name.obj': {'first': 'John', 'last': 'Doe'}}
Option `allow_empty_lists <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.allow_empty_lists>`__ can
be used to allowing empty ``list`` values in a ``dict``
.. code:: python
import qs_codec as qs
assert qs.decode(
'foo[]&bar=baz',
qs.DecodeOptions(allow_empty_lists=True),
) == {'foo': [], 'bar': 'baz'}
Option `duplicates <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.duplicates>`__ can be used to
change the behavior when duplicate keys are encountered
.. code:: python
import qs_codec as qs
assert qs.decode('foo=bar&foo=baz') == {'foo': ['bar', 'baz']}
assert qs.decode(
'foo=bar&foo=baz',
qs.DecodeOptions(duplicates=qs.Duplicates.COMBINE),
) == {'foo': ['bar', 'baz']}
assert qs.decode(
'foo=bar&foo=baz',
qs.DecodeOptions(duplicates=qs.Duplicates.FIRST),
) == {'foo': 'bar'}
assert qs.decode(
'foo=bar&foo=baz',
qs.DecodeOptions(duplicates=qs.Duplicates.LAST),
) == {'foo': 'baz'}
If you have to deal with legacy browsers or services, there’s also
support for decoding percent-encoded octets as `LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__:
.. code:: python
import qs_codec as qs
assert qs.decode(
'a=%A7',
qs.DecodeOptions(charset=qs.Charset.LATIN1),
) == {'a': '§'}
Some services add an initial ``utf8=✓`` value to forms so that old
Internet Explorer versions are more likely to submit the form as utf-8.
Additionally, the server can check the value against wrong encodings of
the checkmark character and detect that a query string or
``application/x-www-form-urlencoded`` body was *not* sent as ``utf-8``,
e.g. if the form had an ``accept-charset`` parameter or the containing
page had a different character set.
`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ supports this mechanism via the
`charset_sentinel <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset_sentinel>`__ option.
If specified, the ``utf8`` parameter will be omitted from the returned
``dict``. It will be used to switch to `LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__ or
`UTF8 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.UTF8>`__ mode depending on how the checkmark is encoded.
**Important**: When you specify both the `charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset>`__
option and the `charset_sentinel <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset_sentinel>`__ option, the
`charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset>`__ will be overridden when the request contains a
``utf8`` parameter from which the actual charset can be deduced. In that
sense the `charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset>`__ will behave as the default charset
rather than the authoritative charset.
.. code:: python
import qs_codec as qs
assert qs.decode(
'utf8=%E2%9C%93&a=%C3%B8',
qs.DecodeOptions(
charset=qs.Charset.LATIN1,
charset_sentinel=True,
),
) == {'a': 'ø'}
assert qs.decode(
'utf8=%26%2310003%3B&a=%F8',
qs.DecodeOptions(
charset=qs.Charset.UTF8,
charset_sentinel=True,
),
) == {'a': 'ø'}
If you want to decode the `&#...; <https://www.w3schools.com/html/html_entities.asp>`__ syntax to the actual character, you can specify the
`interpret_numeric_entities <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.interpret_numeric_entities>`__
option as well:
.. code:: python
import qs_codec qs qs
assert qs.decode(
'a=%26%239786%3B',
qs.DecodeOptions(
charset=qs.Charset.LATIN1,
interpret_numeric_entities=True,
),
) == {'a': '☺'}
It also works when the charset has been detected in
`charset_sentinel <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset_sentinel>`__ mode.
lists
^^^^^
`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ can also decode ``list``\ s using a similar ``[]`` notation:
.. code:: python
import qs_codec as qs
assert qs.decode('a[]=b&a[]=c') == {'a': ['b', 'c']}
You may specify an index as well:
.. code:: python
import qs_codec as qs
assert qs.decode('a[1]=c&a[0]=b') == {'a': ['b', 'c']}
Note that the only difference between an index in a ``list`` and a key
in a ``dict`` is that the value between the brackets must be a number to
create a ``list``. When creating ``list``\ s with specific indices,
`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will compact a sparse ``list`` to
only the existing values preserving their order:
.. code:: python
import qs_codec as qs
assert qs.decode('a[1]=b&a[15]=c') == {'a': ['b', 'c']}
Note that an empty ``str``\ing is also a value, and will be preserved:
.. code:: python
import qs_codec as qs
assert qs.decode('a[]=&a[]=b') == {'a': ['', 'b']}
assert qs.decode('a[0]=b&a[1]=&a[2]=c') == {'a': ['b', '', 'c']}
`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will also limit specifying indices
in a ``list`` to a maximum index of ``20``. Any ``list`` members with an
index of greater than ``20`` will instead be converted to a ``dict`` with
the index as the key. This is needed to handle cases when someone sent,
for example, ``a[999999999]`` and it will take significant time to iterate
over this huge ``list``.
.. code:: python
import qs_codec as qs
assert qs.decode('a[100]=b') == {'a': {'100': 'b'}}
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>`__
option:
.. code:: python
import qs_codec as qs
assert qs.decode(
'a[1]=b',
qs.DecodeOptions(list_limit=0),
) == {'a': {'1': 'b'}}
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>`__
to ``False``.
.. code:: python
import qs_codec as qs
assert qs.decode(
'a[]=b',
qs.DecodeOptions(parse_lists=False),
) == {'a': {'0': 'b'}}
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``:
.. code:: python
import qs_codec as qs
assert qs.decode('a[0]=b&a[b]=c') == {'a': {'0': 'b', 'b': 'c'}}
You can also create ``list``\ s of ``dict``\ s:
.. code:: python
import qs_codec as qs
assert qs.decode('a[][b]=c') == {'a': [{'b': 'c'}]}
(`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ *cannot convert nested ``dict``\ s, such as ``'a={b:1},{c:d}'``*)
primitive values (``int``, ``bool``, ``None``, etc.)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, all values are parsed as ``str``\ings.
.. code:: python
import qs_codec as qs
assert qs.decode(
'a=15&b=true&c=null',
) == {'a': '15', 'b': 'true', 'c': 'null'}
Encoding
~~~~~~~~
.. code:: python
import qs_codec as qs
import typing as t
def encode(
value: t.Any,
options: qs.EncodeOptions = qs.EncodeOptions()
) -> str:
"""Encodes an object into a query string.
Providing custom EncodeOptions will override the default behavior."""
pass
When encoding, `encode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.encode>`__ by default URI encodes output. ``dict``\ s are
encoded as you would expect:
.. code:: python
import qs_codec as qs
assert qs.encode({'a': 'b'}) == 'a=b'
assert qs.encode({'a': {'b': 'c'}}) == 'a%5Bb%5D=c'
This encoding can be disabled by setting the `encode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode>`__
option to ``False``:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': {'b': 'c'}},
qs.EncodeOptions(encode=False),
) == 'a[b]=c'
Encoding can be disabled for keys by setting the
`encode_values_only <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode_values_only>`__ option to ``True``:
.. code:: python
import qs_codec as qs
assert qs.encode(
{
'a': 'b',
'c': ['d', 'e=f'],
'f': [
['g'],
['h']
]
},
qs.EncodeOptions(encode_values_only=True)
) == 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'
This encoding can also be replaced by a custom ``Callable`` in the
`encoder <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encoder>`__ option:
.. code:: python
import qs_codec as qs
import typing as t
def custom_encoder(
value: str,
charset: t.Optional[qs.Charset],
format: t.Optional[qs.Format],
) -> str:
if value == 'č':
return 'c'
return value
assert qs.encode(
{'a': {'b': 'č'}},
qs.EncodeOptions(encoder=custom_encoder),
) == 'a[b]=c'
(Note: the `encoder <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encoder>`__ option does not apply if
`encode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode>`__ is ``False``).
Similar to `encoder <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encoder>`__ there is a
`decoder <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.decoder>`__ option for `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__
to override decoding of properties and values:
.. code:: python
import qs_codec as qs,
typing as t
def custom_decoder(
value: t.Any,
charset: t.Optional[qs.Charset],
) -> t.Union[int, str]:
try:
return int(value)
except ValueError:
return value
assert qs.decode(
'foo=123',
qs.DecodeOptions(decoder=custom_decoder),
) == {'foo': 123}
Examples beyond this point will be shown as though the output is not URI
encoded for clarity. Please note that the return values in these cases
*will* be URI encoded during real usage.
When ``list``\s are encoded, they follow the
`list_format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.list_format>`__ option, which defaults to
`INDICES <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.list_format.ListFormat.INDICES>`__:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': ['b', 'c', 'd']},
qs.EncodeOptions(encode=False)
) == 'a[0]=b&a[1]=c&a[2]=d'
You may override this by setting the `indices <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.indices>`__ option to
``False``, or to be more explicit, the `list_format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.list_format>`__
option to `REPEAT <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.list_format.ListFormat.REPEAT>`__:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': ['b', 'c', 'd']},
qs.EncodeOptions(
encode=False,
indices=False,
),
) == 'a=b&a=c&a=d'
You may use the `list_format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.list_format>`__ option to specify the
format of the output ``list``:
.. code:: python
import qs_codec as qs
# ListFormat.INDICES
assert qs.encode(
{'a': ['b', 'c']},
qs.EncodeOptions(
encode=False,
list_format=qs.ListFormat.INDICES,
),
) == 'a[0]=b&a[1]=c'
# ListFormat.BRACKETS
assert qs.encode(
{'a': ['b', 'c']},
qs.EncodeOptions(
encode=False,
list_format=qs.ListFormat.BRACKETS,
),
) == 'a[]=b&a[]=c'
# ListFormat.REPEAT
assert qs.encode(
{'a': ['b', 'c']},
qs.EncodeOptions(
encode=False,
list_format=qs.ListFormat.REPEAT,
),
) == 'a=b&a=c'
# ListFormat.COMMA
assert qs.encode(
{'a': ['b', 'c']},
qs.EncodeOptions(
encode=False,
list_format=qs.ListFormat.COMMA,
),
) == 'a=b,c'
**Note:** When using `list_format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.list_format>`__ set to
`COMMA <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.list_format.ListFormat.COMMA>`_, you can also pass the
`comma_round_trip <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.comma_round_trip>`__ option set to ``True`` or
``False``, to append ``[]`` on single-item ``list``\ s, so that they can round trip through a decoding.
`BRACKETS <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.list_format.ListFormat.BRACKETS>`__ notation is used for encoding ``dict``\s by default:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': {'b': {'c': 'd', 'e': 'f'}}},
qs.EncodeOptions(encode=False),
) == 'a[b][c]=d&a[b][e]=f'
You may override this to use dot notation by setting the
`allow_dots <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.allow_dots>`__ option to ``True``:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': {'b': {'c': 'd', 'e': 'f'}}},
qs.EncodeOptions(encode=False, allow_dots=True),
) == 'a.b.c=d&a.b.e=f'
You may encode dots in keys of ``dict``\s by setting
`encode_dot_in_keys <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode_dot_in_keys>`__ to ``True``:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'name.obj': {'first': 'John', 'last': 'Doe'}},
qs.EncodeOptions(
allow_dots=True,
encode_dot_in_keys=True,
),
) == 'name%252Eobj.first=John&name%252Eobj.last=Doe'
**Caveat:** When both `encode_values_only <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode_values_only>`__
and `encode_dot_in_keys <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode_dot_in_keys>`__ are set to
``True``, only dots in keys and nothing else will be encoded!
You may allow empty ``list`` values by setting the
`allow_empty_lists <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.allow_empty_lists>`__ option to ``True``:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'foo': [], 'bar': 'baz', },
qs.EncodeOptions(
encode=False,
allow_empty_lists=True,
),
) == 'foo[]&bar=baz'
Empty ``str``\ings and ``None`` values will be omitted, but the equals sign (``=``) remains in place:
.. code:: python
import qs_codec as qs
assert qs.encode({'a': ''}) == 'a='
Keys with no values (such as an empty ``dict`` or ``list``) will return nothing:
.. code:: python
import qs_codec as qs
assert qs.encode({'a': []}) == ''
assert qs.encode({'a': {}}) == ''
assert qs.encode({'a': [{}]}) == ''
assert qs.encode({'a': {'b': []}}) == ''
assert qs.encode({'a': {'b': {}}}) == ''
`Undefined <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.undefined.Undefined>`__ properties will be omitted entirely:
.. code:: python
import qs_codec as qs
assert qs.encode({'a': None, 'b': qs.Undefined()}) == 'a='
The query string may optionally be prepended with a question mark (``?``) by setting
`add_query_prefix <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.add_query_prefix>`__ to ``True``:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': 'b', 'c': 'd'},
qs.EncodeOptions(add_query_prefix=True),
) == '?a=b&c=d'
The `delimiter <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.delimiter>`__ may be overridden as well:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': 'b', 'c': 'd', },
qs.EncodeOptions(delimiter=';')
) == 'a=b;c=d'
If you only want to override the serialization of `datetime <https://docs.python.org/3/library/datetime.html#datetime-objects>`__
objects, you can provide a ``Callable`` in the
`serialize_date <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.serialize_date>`__ option:
.. code:: python
import qs_codec as qs
import datetime
import sys
# First case: encoding a datetime object to an ISO 8601 string
assert (
qs.encode(
{
"a": (
datetime.datetime.fromtimestamp(7, datetime.UTC)
if sys.version_info.major == 3 and sys.version_info.minor >= 11
else datetime.datetime.utcfromtimestamp(7)
)
},
qs.EncodeOptions(encode=False),
)
== "a=1970-01-01T00:00:07+00:00"
if sys.version_info.major == 3 and sys.version_info.minor >= 11
else "a=1970-01-01T00:00:07"
)
# Second case: encoding a datetime object to a timestamp string
assert (
qs.encode(
{
"a": (
datetime.datetime.fromtimestamp(7, datetime.UTC)
if sys.version_info.major == 3 and sys.version_info.minor >= 11
else datetime.datetime.utcfromtimestamp(7)
)
},
qs.EncodeOptions(encode=False, serialize_date=lambda date: str(int(date.timestamp()))),
)
== "a=7"
)
To affect the order of parameter keys, you can set a ``Callable`` in the
`sort <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.sort>`__ option:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': 'c', 'z': 'y', 'b': 'f'},
qs.EncodeOptions(
encode=False,
sort=lambda a, b: (a > b) - (a < b)
)
) == 'a=c&b=f&z=y'
Finally, you can use the `filter <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.filter>`__ option to restrict
which keys will be included in the encoded output. If you pass a ``Callable``, it will be called for each key to obtain
the replacement value. Otherwise, if you pass a ``list``, it will be used to select properties and ``list`` indices to
be encoded:
.. code:: python
import qs_codec as qs
import datetime
import sys
# First case: using a Callable as filter
assert (
qs.encode(
{
"a": "b",
"c": "d",
"e": {
"f": (
datetime.datetime.fromtimestamp(123, datetime.UTC)
if sys.version_info.major == 3 and sys.version_info.minor >= 11
else datetime.datetime.utcfromtimestamp(123)
),
"g": [2],
},
},
qs.EncodeOptions(
encode=False,
filter=lambda prefix, value: {
"b": None,
"e[f]": int(value.timestamp()) if isinstance(value, datetime.datetime) else value,
"e[g][0]": value * 2 if isinstance(value, int) else value,
}.get(prefix, value),
),
)
== "a=b&c=d&e[f]=123&e[g][0]=4"
)
# Second case: using a list as filter
assert qs.encode(
{'a': 'b', 'c': 'd', 'e': 'f'},
qs.EncodeOptions(
encode=False,
filter=['a', 'e']
)
) == 'a=b&e=f'
# Third case: using a list as filter with indices
assert qs.encode(
{
'a': ['b', 'c', 'd'],
'e': 'f',
},
qs.EncodeOptions(
encode=False,
filter=['a', 0, 2]
)
) == 'a[0]=b&a[2]=d'
Handling ``None`` values
~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, ``None`` values are treated like empty ``str``\ings:
.. code:: python
import qs_codec as qs
assert qs.encode({'a': None, 'b': ''}) == 'a=&b='
To distinguish between ``None`` values and empty ``str``\s use the
`strict_null_handling <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.strict_null_handling>`__ flag.
In the result string the ``None`` values have no ``=`` sign:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': None, 'b': ''},
qs.EncodeOptions(strict_null_handling=True),
) == 'a&b='
To decode values without ``=`` back to ``None`` use the
`strict_null_handling <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.strict_null_handling>`__ flag:
.. code:: python
import qs_codec as qs
assert qs.decode(
'a&b=',
qs.DecodeOptions(strict_null_handling=True),
) == {'a': None, 'b': ''}
To completely skip rendering keys with ``None`` values, use the
`skip_nulls <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.skip_nulls>`__ flag:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': 'b', 'c': None},
qs.EncodeOptions(skip_nulls=True),
) == 'a=b'
If you’re communicating with legacy systems, you can switch to
`LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__ using the
`charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.charset>`__ option:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'æ': 'æ'},
qs.EncodeOptions(charset=qs.Charset.LATIN1)
) == '%E6=%E6'
Characters that don’t exist in `LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__
will be converted to numeric entities, similar to what browsers do:
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': '☺'},
qs.EncodeOptions(charset=qs.Charset.LATIN1)
) == 'a=%26%239786%3B'
You can use the `charset_sentinel <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.charset_sentinel>`__
option to announce the character by including an ``utf8=✓`` parameter with the proper
encoding of the checkmark, similar to what Ruby on Rails and others do when submitting forms.
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': '☺'},
qs.EncodeOptions(charset_sentinel=True)
) == 'utf8=%E2%9C%93&a=%E2%98%BA'
assert qs.encode(
{'a': 'æ'},
qs.EncodeOptions(charset=qs.Charset.LATIN1, charset_sentinel=True)
) == 'utf8=%26%2310003%3B&a=%E6'
Dealing with special character sets
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, the encoding and decoding of characters is done in
`UTF8 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.UTF8>`__, and
`LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__ support is also built in via
the `charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.charset>`__
and `charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset>`__ parameter,
respectively.
If you wish to encode query strings to a different character set (i.e.
`Shift JIS <https://en.wikipedia.org/wiki/Shift_JIS>`__)
.. code:: python
import qs_codec as qs
import codecs
import typing as t
def custom_encoder(
string: str,
charset: t.Optional[qs.Charset],
format: t.Optional[qs.Format],
) -> str:
if string:
buf: bytes = codecs.encode(string, 'shift_jis')
result: t.List[str] = ['{:02x}'.format(b) for b in buf]
return '%' + '%'.join(result)
return ''
assert qs.encode(
{'a': 'こんにちは!'},
qs.EncodeOptions(encoder=custom_encoder)
) == '%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49'
This also works for decoding of query strings:
.. code:: python
import qs_codec as qs
import re
import codecs
import typing as t
def custom_decoder(
string: str,
charset: t.Optional[qs.Charset],
) -> t.Optional[str]:
if string:
result: t.List[int] = []
while string:
match: t.Optional[t.Match[str]] = re.search(r'%([0-9A-F]{2})', string, re.IGNORECASE)
if match:
result.append(int(match.group(1), 16))
string = string[match.end():]
else:
break
buf: bytes = bytes(result)
return codecs.decode(buf, 'shift_jis')
return None
assert qs.decode(
'%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49',
qs.DecodeOptions(decoder=custom_decoder)
) == {'a': 'こんにちは!'}
RFC 3986 and RFC 1738 space encoding
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The default `format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.format>`__ is
`RFC3986 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.format.Format.RFC3986>`__ which encodes
``' '`` to ``%20`` which is backward compatible. You can also set the
`format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.format>`__ to
`RFC1738 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.format.Format.RFC1738>`__ which encodes ``' '`` to ``+``.
.. code:: python
import qs_codec as qs
assert qs.encode(
{'a': 'b c'},
qs.EncodeOptions(format=qs.Format.RFC3986)
) == 'a=b%20c'
assert qs.encode(
{'a': 'b c'},
qs.EncodeOptions(format=qs.Format.RFC3986)
) == 'a=b%20c'
assert qs.encode(
{'a': 'b c'},
qs.EncodeOptions(format=qs.Format.RFC1738)
) == 'a=b+c'
--------------
Special thanks to the authors of
`qs <https://www.npmjs.com/package/qs>`__ for JavaScript: - `Jordan
Harband <https://github.com/ljharb>`__ - `TJ
Holowaychuk <https://github.com/visionmedia/node-querystring>`__
.. |PyPI - Version| image:: https://img.shields.io/pypi/v/qs_codec
:target: https://pypi.org/project/qs-codec/
.. |PyPI - Downloads| image:: https://img.shields.io/pypi/dm/qs_codec
:target: https://pypistats.org/packages/qs-codec
.. |PyPI - Status| image:: https://img.shields.io/pypi/status/qs_codec
.. |PyPI - Python Version| image:: https://img.shields.io/pypi/pyversions/qs_codec
.. |PyPI - Format| image:: https://img.shields.io/pypi/format/qs_codec
.. |Test| image:: https://github.com/techouse/qs_codec/actions/workflows/test.yml/badge.svg
:target: https://github.com/techouse/qs_codec/actions/workflows/test.yml
.. |CodeQL| image:: https://github.com/techouse/qs_codec/actions/workflows/github-code-scanning/codeql/badge.svg
:target: https://github.com/techouse/qs_codec/actions/workflows/github-code-scanning/codeql
.. |Publish| image:: https://github.com/techouse/qs_codec/actions/workflows/publish.yml/badge.svg
:target: https://github.com/techouse/qs_codec/actions/workflows/publish.yml
.. |Docs| image:: https://github.com/techouse/qs_codec/actions/workflows/docs.yml/badge.svg
:target: https://github.com/techouse/qs_codec/actions/workflows/docs.yml
.. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
.. |codecov| image:: https://codecov.io/gh/techouse/qs_codec/graph/badge.svg?token=Vp0z05yj2l
:target: https://codecov.io/gh/techouse/qs_codec
.. |Codacy| image:: https://app.codacy.com/project/badge/Grade/7ead208221ae4f6785631043064647e4
:target: https://app.codacy.com/gh/techouse/qs_codec/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade
.. |License| image:: https://img.shields.io/github/license/techouse/qs_codec
:target: LICENSE
.. |GitHub Sponsors| image:: https://img.shields.io/github/sponsors/techouse
:target: https://github.com/sponsors/techouse
.. |GitHub Repo stars| image:: https://img.shields.io/github/stars/techouse/qs_codec
:target: https://github.com/techouse/qs_codec/stargazers
.. |Contributor Covenant| image:: https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg
:target: CODE-OF-CONDUCT.md
.. |flake8| image:: https://img.shields.io/badge/flake8-checked-blueviolet.svg
:target: https://flake8.pycqa.org/en/latest/
.. |mypy| image:: https://img.shields.io/badge/mypy-checked-blue.svg
:target: https://mypy.readthedocs.io/en/stable/
.. |pylint| image:: https://img.shields.io/badge/linting-pylint-yellowgreen.svg
:target: https://github.com/pylint-dev/pylint
.. |isort| image:: https://img.shields.io/badge/imports-isort-blue.svg
:target: https://pycqa.github.io/isort/
.. |bandit| image:: https://img.shields.io/badge/security-bandit-blue.svg
:target: https://github.com/PyCQA/bandit
:alt: Security Status
Raw data
{
"_id": null,
"home_page": null,
"name": "qs-codec",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "codec, qs, query, query-string, querystring, url",
"author": null,
"author_email": "Klemen Tusar <techouse@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/c0/53/c81596e96927f4d8867664cc74b697883e43afee32385c5a8514a87a5478/qs_codec-1.0.5.tar.gz",
"platform": null,
"description": "qs-codec\n========\n\nA query string encoding and decoding library for Python.\n\nPorted from `qs <https://www.npmjs.com/package/qs>`__ for JavaScript.\n\n|PyPI - Version| |PyPI - Downloads| |PyPI - Status| |PyPI - Python Version| |PyPI - Format| |Black|\n|Test| |CodeQL| |Publish| |Docs| |codecov| |Codacy| |Black| |flake8| |mypy| |pylint| |isort| |bandit|\n|License| |Contributor Covenant| |GitHub Sponsors| |GitHub Repo stars|\n\nUsage\n-----\n\nA simple usage example:\n\n.. code:: python\n\n import qs_codec as qs\n\n # Encoding\n assert qs.encode({'a': 'b'}) == 'a=b'\n\n # Decoding\n assert qs.decode('a=b') == {'a': 'b'}\n\nDecoding\n~~~~~~~~\n\ndictionaries\n^^^^^^^^^^^^\n\n.. code:: python\n\n import qs_codec as qs\n import typing as t\n\n def decode(\n value: t.Optional[t.Union[str, t.Dict[str, t.Any]]],\n options: qs.DecodeOptions = qs.DecodeOptions(),\n ) -> t.Dict[str, t.Any]:\n \"\"\"Decodes a query string into a Dict[str, Any].\n \n Providing custom DecodeOptions will override the default behavior.\"\"\"\n pass\n\n`decode <https://techouse.github.io/qs_codec/qs_codec.html#module-qs_codec.decode>`__ allows you to create nested ``dict``\\ s within your query\nstrings, by surrounding the name of sub-keys with square brackets\n``[]``. For example, the string ``'foo[bar]=baz'`` converts to:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('foo[bar]=baz') == {'foo': {'bar': 'baz'}}\n\nURI encoded strings work too:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('a%5Bb%5D=c') == {'a': {'b': 'c'}}\n\nYou can also nest your ``dict``\\ s, like ``'foo[bar][baz]=foobarbaz'``:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('foo[bar][baz]=foobarbaz') == {'foo': {'bar': {'baz': 'foobarbaz'}}}\n\nBy default, when nesting ``dict``\\ s qs will only decode up to 5\nchildren deep. This means if you attempt to decode a string like\n``'a[b][c][d][e][f][g][h][i]=j'`` your resulting ``dict`` will be:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\"a[b][c][d][e][f][g][h][i]=j\") == {\n \"a\": {\"b\": {\"c\": {\"d\": {\"e\": {\"f\": {\"[g][h][i]\": \"j\"}}}}}}\n }\n\nThis depth can be overridden by setting the `depth <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.depth>`_:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a[b][c][d][e][f][g][h][i]=j',\n qs.DecodeOptions(depth=1),\n ) == {'a': {'b': {'[c][d][e][f][g][h][i]': 'j'}}}\n\nYou can configure `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ to throw an error\nwhen parsing nested input beyond this depth using `strict_depth <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.strict_depth>`__ (defaults to ``False``):\n\n.. code:: python\n\n import qs_codec as qs\n\n try:\n qs.decode(\n 'a[b][c][d][e][f][g][h][i]=j',\n qs.DecodeOptions(depth=1, strict_depth=True),\n )\n except IndexError as e:\n assert str(e) == 'Input depth exceeded depth option of 1 and strict_depth is True'\n\nThe depth limit helps mitigate abuse when `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ is used to parse user\ninput, and it is recommended to keep it a reasonably small number. `strict_depth <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.strict_depth>`__\nadds a layer of protection by throwing a ``IndexError`` when the limit is exceeded, allowing you to catch and handle such cases.\n\nFor similar reasons, by default `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will only parse up to 1000 parameters. This can be overridden by passing a\n`parameter_limit <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.parameter_limit>`__ option:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a=b&c=d',\n qs.DecodeOptions(parameter_limit=1),\n ) == {'a': 'b'}\n\nTo bypass the leading question mark, use `ignore_query_prefix <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.ignore_query_prefix>`__:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n '?a=b&c=d',\n qs.DecodeOptions(ignore_query_prefix=True),\n ) == {'a': 'b', 'c': 'd'}\n\nAn optional `delimiter <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.delimiter>`__ can also be passed:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a=b;c=d',\n qs.DecodeOptions(delimiter=';'),\n ) == {'a': 'b', 'c': 'd'}\n\n`delimiter <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.delimiter>`__ can be a regular expression too:\n\n.. code:: python\n\n import qs_codec as qs\n import re\n\n assert qs.decode(\n 'a=b;c=d',\n qs.DecodeOptions(delimiter=re.compile(r'[;,]')),\n ) == {'a': 'b', 'c': 'd'}\n\nOption `allow_dots <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.allow_dots>`__\ncan be used to enable dot notation:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a.b=c',\n qs.DecodeOptions(allow_dots=True),\n ) == {'a': {'b': 'c'}}\n\nOption `decode_dot_in_keys <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.decode_dot_in_keys>`__\ncan be used to decode dots in keys.\n\n**Note:** it implies `allow_dots <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.allow_dots>`__, so\n`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will error if you set `decode_dot_in_keys <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.decode_dot_in_keys>`__\nto ``True``, and `allow_dots <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.allow_dots>`__ to ``False``.\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'name%252Eobj.first=John&name%252Eobj.last=Doe',\n qs.DecodeOptions(decode_dot_in_keys=True),\n ) == {'name.obj': {'first': 'John', 'last': 'Doe'}}\n\nOption `allow_empty_lists <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.allow_empty_lists>`__ can\nbe used to allowing empty ``list`` values in a ``dict``\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'foo[]&bar=baz',\n qs.DecodeOptions(allow_empty_lists=True),\n ) == {'foo': [], 'bar': 'baz'}\n\nOption `duplicates <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.duplicates>`__ can be used to\nchange the behavior when duplicate keys are encountered\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('foo=bar&foo=baz') == {'foo': ['bar', 'baz']}\n\n assert qs.decode(\n 'foo=bar&foo=baz',\n qs.DecodeOptions(duplicates=qs.Duplicates.COMBINE),\n ) == {'foo': ['bar', 'baz']}\n\n assert qs.decode(\n 'foo=bar&foo=baz',\n qs.DecodeOptions(duplicates=qs.Duplicates.FIRST),\n ) == {'foo': 'bar'}\n\n assert qs.decode(\n 'foo=bar&foo=baz',\n qs.DecodeOptions(duplicates=qs.Duplicates.LAST),\n ) == {'foo': 'baz'}\n\nIf you have to deal with legacy browsers or services, there\u2019s also\nsupport for decoding percent-encoded octets as `LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a=%A7',\n qs.DecodeOptions(charset=qs.Charset.LATIN1),\n ) == {'a': '\u00a7'}\n\nSome services add an initial ``utf8=\u2713`` value to forms so that old\nInternet Explorer versions are more likely to submit the form as utf-8.\nAdditionally, the server can check the value against wrong encodings of\nthe checkmark character and detect that a query string or\n``application/x-www-form-urlencoded`` body was *not* sent as ``utf-8``,\ne.g. if the form had an ``accept-charset`` parameter or the containing\npage had a different character set.\n\n`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ supports this mechanism via the\n`charset_sentinel <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset_sentinel>`__ option.\nIf specified, the ``utf8`` parameter will be omitted from the returned\n``dict``. It will be used to switch to `LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__ or\n`UTF8 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.UTF8>`__ mode depending on how the checkmark is encoded.\n\n**Important**: When you specify both the `charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset>`__\noption and the `charset_sentinel <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset_sentinel>`__ option, the\n`charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset>`__ will be overridden when the request contains a\n``utf8`` parameter from which the actual charset can be deduced. In that\nsense the `charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset>`__ will behave as the default charset\nrather than the authoritative charset.\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'utf8=%E2%9C%93&a=%C3%B8',\n qs.DecodeOptions(\n charset=qs.Charset.LATIN1,\n charset_sentinel=True,\n ),\n ) == {'a': '\u00f8'}\n\n assert qs.decode(\n 'utf8=%26%2310003%3B&a=%F8',\n qs.DecodeOptions(\n charset=qs.Charset.UTF8,\n charset_sentinel=True,\n ),\n ) == {'a': '\u00f8'}\n\nIf you want to decode the `&#...; <https://www.w3schools.com/html/html_entities.asp>`__ syntax to the actual character, you can specify the\n`interpret_numeric_entities <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.interpret_numeric_entities>`__\noption as well:\n\n.. code:: python\n\n import qs_codec qs qs\n\n assert qs.decode(\n 'a=%26%239786%3B',\n qs.DecodeOptions(\n charset=qs.Charset.LATIN1,\n interpret_numeric_entities=True,\n ),\n ) == {'a': '\u263a'}\n\nIt also works when the charset has been detected in\n`charset_sentinel <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset_sentinel>`__ mode.\n\nlists\n^^^^^\n\n`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ can also decode ``list``\\ s using a similar ``[]`` notation:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('a[]=b&a[]=c') == {'a': ['b', 'c']}\n\nYou may specify an index as well:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('a[1]=c&a[0]=b') == {'a': ['b', 'c']}\n\nNote that the only difference between an index in a ``list`` and a key\nin a ``dict`` is that the value between the brackets must be a number to\ncreate a ``list``. When creating ``list``\\ s with specific indices,\n`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will compact a sparse ``list`` to\nonly the existing values preserving their order:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('a[1]=b&a[15]=c') == {'a': ['b', 'c']}\n\nNote that an empty ``str``\\ing is also a value, and will be preserved:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('a[]=&a[]=b') == {'a': ['', 'b']}\n\n assert qs.decode('a[0]=b&a[1]=&a[2]=c') == {'a': ['b', '', 'c']}\n\n`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will also limit specifying indices\nin a ``list`` to a maximum index of ``20``. Any ``list`` members with an\nindex of greater than ``20`` will instead be converted to a ``dict`` with\nthe index as the key. This is needed to handle cases when someone sent,\nfor example, ``a[999999999]`` and it will take significant time to iterate\nover this huge ``list``.\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('a[100]=b') == {'a': {'100': 'b'}}\n\nThis 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>`__\noption:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a[1]=b',\n qs.DecodeOptions(list_limit=0),\n ) == {'a': {'1': 'b'}}\n\nTo 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>`__\nto ``False``.\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a[]=b',\n qs.DecodeOptions(parse_lists=False),\n ) == {'a': {'0': 'b'}}\n\nIf 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``:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('a[0]=b&a[b]=c') == {'a': {'0': 'b', 'b': 'c'}}\n\nYou can also create ``list``\\ s of ``dict``\\ s:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode('a[][b]=c') == {'a': [{'b': 'c'}]}\n\n(`decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ *cannot convert nested ``dict``\\ s, such as ``'a={b:1},{c:d}'``*)\n\nprimitive values (``int``, ``bool``, ``None``, etc.)\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nBy default, all values are parsed as ``str``\\ings.\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a=15&b=true&c=null',\n ) == {'a': '15', 'b': 'true', 'c': 'null'}\n\nEncoding\n~~~~~~~~\n\n.. code:: python\n\n import qs_codec as qs\n import typing as t\n\n def encode(\n value: t.Any,\n options: qs.EncodeOptions = qs.EncodeOptions()\n ) -> str:\n \"\"\"Encodes an object into a query string.\n \n Providing custom EncodeOptions will override the default behavior.\"\"\"\n pass\n\nWhen encoding, `encode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.encode>`__ by default URI encodes output. ``dict``\\ s are\nencoded as you would expect:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode({'a': 'b'}) == 'a=b'\n assert qs.encode({'a': {'b': 'c'}}) == 'a%5Bb%5D=c'\n\nThis encoding can be disabled by setting the `encode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode>`__\noption to ``False``:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': {'b': 'c'}},\n qs.EncodeOptions(encode=False),\n ) == 'a[b]=c'\n\nEncoding can be disabled for keys by setting the\n`encode_values_only <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode_values_only>`__ option to ``True``:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {\n 'a': 'b',\n 'c': ['d', 'e=f'],\n 'f': [\n ['g'],\n ['h']\n ]\n },\n qs.EncodeOptions(encode_values_only=True)\n ) == 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'\n\nThis encoding can also be replaced by a custom ``Callable`` in the\n`encoder <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encoder>`__ option:\n\n.. code:: python\n\n import qs_codec as qs\n import typing as t\n\n\n def custom_encoder(\n value: str,\n charset: t.Optional[qs.Charset],\n format: t.Optional[qs.Format],\n ) -> str:\n if value == '\u010d':\n return 'c'\n return value\n\n\n assert qs.encode(\n {'a': {'b': '\u010d'}},\n qs.EncodeOptions(encoder=custom_encoder),\n ) == 'a[b]=c'\n\n(Note: the `encoder <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encoder>`__ option does not apply if\n`encode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode>`__ is ``False``).\n\nSimilar to `encoder <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encoder>`__ there is a\n`decoder <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.decoder>`__ option for `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__\nto override decoding of properties and values:\n\n.. code:: python\n\n import qs_codec as qs,\n typing as t\n\n def custom_decoder(\n value: t.Any,\n charset: t.Optional[qs.Charset],\n ) -> t.Union[int, str]:\n try:\n return int(value)\n except ValueError:\n return value\n\n assert qs.decode(\n 'foo=123',\n qs.DecodeOptions(decoder=custom_decoder),\n ) == {'foo': 123}\n\nExamples beyond this point will be shown as though the output is not URI\nencoded for clarity. Please note that the return values in these cases\n*will* be URI encoded during real usage.\n\nWhen ``list``\\s are encoded, they follow the\n`list_format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.list_format>`__ option, which defaults to\n`INDICES <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.list_format.ListFormat.INDICES>`__:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': ['b', 'c', 'd']},\n qs.EncodeOptions(encode=False)\n ) == 'a[0]=b&a[1]=c&a[2]=d'\n\nYou may override this by setting the `indices <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.indices>`__ option to\n``False``, or to be more explicit, the `list_format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.list_format>`__\noption to `REPEAT <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.list_format.ListFormat.REPEAT>`__:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': ['b', 'c', 'd']},\n qs.EncodeOptions(\n encode=False,\n indices=False,\n ),\n ) == 'a=b&a=c&a=d'\n\nYou may use the `list_format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.list_format>`__ option to specify the\nformat of the output ``list``:\n\n.. code:: python\n\n import qs_codec as qs\n\n # ListFormat.INDICES\n assert qs.encode(\n {'a': ['b', 'c']},\n qs.EncodeOptions(\n encode=False,\n list_format=qs.ListFormat.INDICES,\n ),\n ) == 'a[0]=b&a[1]=c'\n\n # ListFormat.BRACKETS\n assert qs.encode(\n {'a': ['b', 'c']},\n qs.EncodeOptions(\n encode=False,\n list_format=qs.ListFormat.BRACKETS,\n ),\n ) == 'a[]=b&a[]=c'\n\n # ListFormat.REPEAT\n assert qs.encode(\n {'a': ['b', 'c']},\n qs.EncodeOptions(\n encode=False,\n list_format=qs.ListFormat.REPEAT,\n ),\n ) == 'a=b&a=c'\n\n # ListFormat.COMMA\n assert qs.encode(\n {'a': ['b', 'c']},\n qs.EncodeOptions(\n encode=False,\n list_format=qs.ListFormat.COMMA,\n ),\n ) == 'a=b,c'\n\n**Note:** When using `list_format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.list_format>`__ set to\n`COMMA <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.list_format.ListFormat.COMMA>`_, you can also pass the\n`comma_round_trip <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.comma_round_trip>`__ option set to ``True`` or\n``False``, to append ``[]`` on single-item ``list``\\ s, so that they can round trip through a decoding.\n\n`BRACKETS <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.list_format.ListFormat.BRACKETS>`__ notation is used for encoding ``dict``\\s by default:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': {'b': {'c': 'd', 'e': 'f'}}},\n qs.EncodeOptions(encode=False),\n ) == 'a[b][c]=d&a[b][e]=f'\n\nYou may override this to use dot notation by setting the\n`allow_dots <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.allow_dots>`__ option to ``True``:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': {'b': {'c': 'd', 'e': 'f'}}},\n qs.EncodeOptions(encode=False, allow_dots=True),\n ) == 'a.b.c=d&a.b.e=f'\n\nYou may encode dots in keys of ``dict``\\s by setting\n`encode_dot_in_keys <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode_dot_in_keys>`__ to ``True``:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'name.obj': {'first': 'John', 'last': 'Doe'}},\n qs.EncodeOptions(\n allow_dots=True,\n encode_dot_in_keys=True,\n ),\n ) == 'name%252Eobj.first=John&name%252Eobj.last=Doe'\n\n**Caveat:** When both `encode_values_only <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode_values_only>`__\nand `encode_dot_in_keys <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.encode_dot_in_keys>`__ are set to\n``True``, only dots in keys and nothing else will be encoded!\n\nYou may allow empty ``list`` values by setting the\n`allow_empty_lists <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.allow_empty_lists>`__ option to ``True``:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'foo': [], 'bar': 'baz', },\n qs.EncodeOptions(\n encode=False,\n allow_empty_lists=True,\n ),\n ) == 'foo[]&bar=baz'\n\nEmpty ``str``\\ings and ``None`` values will be omitted, but the equals sign (``=``) remains in place:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode({'a': ''}) == 'a='\n\nKeys with no values (such as an empty ``dict`` or ``list``) will return nothing:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode({'a': []}) == ''\n\n assert qs.encode({'a': {}}) == ''\n\n assert qs.encode({'a': [{}]}) == ''\n\n assert qs.encode({'a': {'b': []}}) == ''\n\n assert qs.encode({'a': {'b': {}}}) == ''\n\n`Undefined <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.undefined.Undefined>`__ properties will be omitted entirely:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode({'a': None, 'b': qs.Undefined()}) == 'a='\n\nThe query string may optionally be prepended with a question mark (``?``) by setting\n`add_query_prefix <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.add_query_prefix>`__ to ``True``:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': 'b', 'c': 'd'},\n qs.EncodeOptions(add_query_prefix=True),\n ) == '?a=b&c=d'\n\nThe `delimiter <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.delimiter>`__ may be overridden as well:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': 'b', 'c': 'd', },\n qs.EncodeOptions(delimiter=';')\n ) == 'a=b;c=d'\n\nIf you only want to override the serialization of `datetime <https://docs.python.org/3/library/datetime.html#datetime-objects>`__\nobjects, you can provide a ``Callable`` in the\n`serialize_date <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.serialize_date>`__ option:\n\n.. code:: python\n\n import qs_codec as qs\n import datetime\n import sys\n\n # First case: encoding a datetime object to an ISO 8601 string\n assert (\n qs.encode(\n {\n \"a\": (\n datetime.datetime.fromtimestamp(7, datetime.UTC)\n if sys.version_info.major == 3 and sys.version_info.minor >= 11\n else datetime.datetime.utcfromtimestamp(7)\n )\n },\n qs.EncodeOptions(encode=False),\n )\n == \"a=1970-01-01T00:00:07+00:00\"\n if sys.version_info.major == 3 and sys.version_info.minor >= 11\n else \"a=1970-01-01T00:00:07\"\n )\n\n # Second case: encoding a datetime object to a timestamp string\n assert (\n qs.encode(\n {\n \"a\": (\n datetime.datetime.fromtimestamp(7, datetime.UTC)\n if sys.version_info.major == 3 and sys.version_info.minor >= 11\n else datetime.datetime.utcfromtimestamp(7)\n )\n },\n qs.EncodeOptions(encode=False, serialize_date=lambda date: str(int(date.timestamp()))),\n )\n == \"a=7\"\n )\n\nTo affect the order of parameter keys, you can set a ``Callable`` in the\n`sort <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.sort>`__ option:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': 'c', 'z': 'y', 'b': 'f'},\n qs.EncodeOptions(\n encode=False,\n sort=lambda a, b: (a > b) - (a < b)\n )\n ) == 'a=c&b=f&z=y'\n\nFinally, you can use the `filter <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.filter>`__ option to restrict\nwhich keys will be included in the encoded output. If you pass a ``Callable``, it will be called for each key to obtain\nthe replacement value. Otherwise, if you pass a ``list``, it will be used to select properties and ``list`` indices to\nbe encoded:\n\n.. code:: python\n\n import qs_codec as qs\n import datetime\n import sys\n\n # First case: using a Callable as filter\n assert (\n qs.encode(\n {\n \"a\": \"b\",\n \"c\": \"d\",\n \"e\": {\n \"f\": (\n datetime.datetime.fromtimestamp(123, datetime.UTC)\n if sys.version_info.major == 3 and sys.version_info.minor >= 11\n else datetime.datetime.utcfromtimestamp(123)\n ),\n \"g\": [2],\n },\n },\n qs.EncodeOptions(\n encode=False,\n filter=lambda prefix, value: {\n \"b\": None,\n \"e[f]\": int(value.timestamp()) if isinstance(value, datetime.datetime) else value,\n \"e[g][0]\": value * 2 if isinstance(value, int) else value,\n }.get(prefix, value),\n ),\n )\n == \"a=b&c=d&e[f]=123&e[g][0]=4\"\n )\n\n # Second case: using a list as filter\n assert qs.encode(\n {'a': 'b', 'c': 'd', 'e': 'f'},\n qs.EncodeOptions(\n encode=False,\n filter=['a', 'e']\n )\n ) == 'a=b&e=f'\n\n # Third case: using a list as filter with indices\n assert qs.encode(\n {\n 'a': ['b', 'c', 'd'],\n 'e': 'f',\n },\n qs.EncodeOptions(\n encode=False,\n filter=['a', 0, 2]\n )\n ) == 'a[0]=b&a[2]=d'\n\nHandling ``None`` values\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBy default, ``None`` values are treated like empty ``str``\\ings:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode({'a': None, 'b': ''}) == 'a=&b='\n\nTo distinguish between ``None`` values and empty ``str``\\s use the\n`strict_null_handling <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.strict_null_handling>`__ flag.\nIn the result string the ``None`` values have no ``=`` sign:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': None, 'b': ''},\n qs.EncodeOptions(strict_null_handling=True),\n ) == 'a&b='\n\nTo decode values without ``=`` back to ``None`` use the\n`strict_null_handling <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.strict_null_handling>`__ flag:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.decode(\n 'a&b=',\n qs.DecodeOptions(strict_null_handling=True),\n ) == {'a': None, 'b': ''}\n\nTo completely skip rendering keys with ``None`` values, use the\n`skip_nulls <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.skip_nulls>`__ flag:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': 'b', 'c': None},\n qs.EncodeOptions(skip_nulls=True),\n ) == 'a=b'\n\nIf you\u2019re communicating with legacy systems, you can switch to\n`LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__ using the\n`charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.charset>`__ option:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'\u00e6': '\u00e6'},\n qs.EncodeOptions(charset=qs.Charset.LATIN1)\n ) == '%E6=%E6'\n\nCharacters that don\u2019t exist in `LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__\nwill be converted to numeric entities, similar to what browsers do:\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': '\u263a'},\n qs.EncodeOptions(charset=qs.Charset.LATIN1)\n ) == 'a=%26%239786%3B'\n\nYou can use the `charset_sentinel <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.charset_sentinel>`__\noption to announce the character by including an ``utf8=\u2713`` parameter with the proper\nencoding of the checkmark, similar to what Ruby on Rails and others do when submitting forms.\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': '\u263a'},\n qs.EncodeOptions(charset_sentinel=True)\n ) == 'utf8=%E2%9C%93&a=%E2%98%BA'\n\n assert qs.encode(\n {'a': '\u00e6'},\n qs.EncodeOptions(charset=qs.Charset.LATIN1, charset_sentinel=True)\n ) == 'utf8=%26%2310003%3B&a=%E6'\n\nDealing with special character sets\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBy default, the encoding and decoding of characters is done in\n`UTF8 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.UTF8>`__, and\n`LATIN1 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.charset.Charset.LATIN1>`__ support is also built in via\nthe `charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.charset>`__\nand `charset <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.charset>`__ parameter,\nrespectively.\n\nIf you wish to encode query strings to a different character set (i.e.\n`Shift JIS <https://en.wikipedia.org/wiki/Shift_JIS>`__)\n\n.. code:: python\n\n import qs_codec as qs\n import codecs\n import typing as t\n\n def custom_encoder(\n string: str,\n charset: t.Optional[qs.Charset],\n format: t.Optional[qs.Format],\n ) -> str:\n if string:\n buf: bytes = codecs.encode(string, 'shift_jis')\n result: t.List[str] = ['{:02x}'.format(b) for b in buf]\n return '%' + '%'.join(result)\n return ''\n\n assert qs.encode(\n {'a': '\u3053\u3093\u306b\u3061\u306f\uff01'},\n qs.EncodeOptions(encoder=custom_encoder)\n ) == '%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49'\n\nThis also works for decoding of query strings:\n\n.. code:: python\n\n import qs_codec as qs\n import re\n import codecs\n import typing as t\n\n def custom_decoder(\n string: str,\n charset: t.Optional[qs.Charset],\n ) -> t.Optional[str]:\n if string:\n result: t.List[int] = []\n while string:\n match: t.Optional[t.Match[str]] = re.search(r'%([0-9A-F]{2})', string, re.IGNORECASE)\n if match:\n result.append(int(match.group(1), 16))\n string = string[match.end():]\n else:\n break\n buf: bytes = bytes(result)\n return codecs.decode(buf, 'shift_jis')\n return None\n\n assert qs.decode(\n '%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49',\n qs.DecodeOptions(decoder=custom_decoder)\n ) == {'a': '\u3053\u3093\u306b\u3061\u306f\uff01'}\n\nRFC 3986 and RFC 1738 space encoding\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe default `format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.format>`__ is\n`RFC3986 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.format.Format.RFC3986>`__ which encodes\n``' '`` to ``%20`` which is backward compatible. You can also set the\n`format <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.encode_options.EncodeOptions.format>`__ to\n`RFC1738 <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.enums.format.Format.RFC1738>`__ which encodes ``' '`` to ``+``.\n\n.. code:: python\n\n import qs_codec as qs\n\n assert qs.encode(\n {'a': 'b c'},\n qs.EncodeOptions(format=qs.Format.RFC3986)\n ) == 'a=b%20c'\n\n assert qs.encode(\n {'a': 'b c'},\n qs.EncodeOptions(format=qs.Format.RFC3986)\n ) == 'a=b%20c'\n\n assert qs.encode(\n {'a': 'b c'},\n qs.EncodeOptions(format=qs.Format.RFC1738)\n ) == 'a=b+c'\n\n--------------\n\nSpecial thanks to the authors of\n`qs <https://www.npmjs.com/package/qs>`__ for JavaScript: - `Jordan\nHarband <https://github.com/ljharb>`__ - `TJ\nHolowaychuk <https://github.com/visionmedia/node-querystring>`__\n\n.. |PyPI - Version| image:: https://img.shields.io/pypi/v/qs_codec\n :target: https://pypi.org/project/qs-codec/\n.. |PyPI - Downloads| image:: https://img.shields.io/pypi/dm/qs_codec\n :target: https://pypistats.org/packages/qs-codec\n.. |PyPI - Status| image:: https://img.shields.io/pypi/status/qs_codec\n.. |PyPI - Python Version| image:: https://img.shields.io/pypi/pyversions/qs_codec\n.. |PyPI - Format| image:: https://img.shields.io/pypi/format/qs_codec\n.. |Test| image:: https://github.com/techouse/qs_codec/actions/workflows/test.yml/badge.svg\n :target: https://github.com/techouse/qs_codec/actions/workflows/test.yml\n.. |CodeQL| image:: https://github.com/techouse/qs_codec/actions/workflows/github-code-scanning/codeql/badge.svg\n :target: https://github.com/techouse/qs_codec/actions/workflows/github-code-scanning/codeql\n.. |Publish| image:: https://github.com/techouse/qs_codec/actions/workflows/publish.yml/badge.svg\n :target: https://github.com/techouse/qs_codec/actions/workflows/publish.yml\n.. |Docs| image:: https://github.com/techouse/qs_codec/actions/workflows/docs.yml/badge.svg\n :target: https://github.com/techouse/qs_codec/actions/workflows/docs.yml\n.. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg\n :target: https://github.com/psf/black\n.. |codecov| image:: https://codecov.io/gh/techouse/qs_codec/graph/badge.svg?token=Vp0z05yj2l\n :target: https://codecov.io/gh/techouse/qs_codec\n.. |Codacy| image:: https://app.codacy.com/project/badge/Grade/7ead208221ae4f6785631043064647e4\n :target: https://app.codacy.com/gh/techouse/qs_codec/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade\n.. |License| image:: https://img.shields.io/github/license/techouse/qs_codec\n :target: LICENSE\n.. |GitHub Sponsors| image:: https://img.shields.io/github/sponsors/techouse\n :target: https://github.com/sponsors/techouse\n.. |GitHub Repo stars| image:: https://img.shields.io/github/stars/techouse/qs_codec\n :target: https://github.com/techouse/qs_codec/stargazers\n.. |Contributor Covenant| image:: https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg\n :target: CODE-OF-CONDUCT.md\n.. |flake8| image:: https://img.shields.io/badge/flake8-checked-blueviolet.svg\n :target: https://flake8.pycqa.org/en/latest/\n.. |mypy| image:: https://img.shields.io/badge/mypy-checked-blue.svg\n :target: https://mypy.readthedocs.io/en/stable/\n.. |pylint| image:: https://img.shields.io/badge/linting-pylint-yellowgreen.svg\n :target: https://github.com/pylint-dev/pylint\n.. |isort| image:: https://img.shields.io/badge/imports-isort-blue.svg\n :target: https://pycqa.github.io/isort/\n.. |bandit| image:: https://img.shields.io/badge/security-bandit-blue.svg\n :target: https://github.com/PyCQA/bandit\n :alt: Security Status",
"bugtrack_url": null,
"license": "BSD-3-Clause",
"summary": "A query string encoding and decoding library for Python. Ported from qs for JavaScript.",
"version": "1.0.5",
"project_urls": {
"Changelog": "https://github.com/techouse/qs_codec/blob/master/CHANGELOG.md",
"Documentation": "https://techouse.github.io/qs_codec/",
"Homepage": "https://techouse.github.io/qs_codec/",
"PayPal": "https://paypal.me/ktusar",
"Source": "https://github.com/techouse/qs_codec",
"Sponsor": "https://github.com/sponsors/techouse"
},
"split_keywords": [
"codec",
" qs",
" query",
" query-string",
" querystring",
" url"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "6716ff93a35798c1ebdd402fbb102b7b57acf6902f471320122bd36c6532d1cc",
"md5": "ab1ec7d6ef3d03e9f87173ecbb13ea66",
"sha256": "1eecf49c29523478ac008da5a7ecd8c327cd250a77d17c5dec293736f7b45621"
},
"downloads": -1,
"filename": "qs_codec-1.0.5-py3-none-any.whl",
"has_sig": false,
"md5_digest": "ab1ec7d6ef3d03e9f87173ecbb13ea66",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 29408,
"upload_time": "2024-11-24T16:51:58",
"upload_time_iso_8601": "2024-11-24T16:51:58.351022Z",
"url": "https://files.pythonhosted.org/packages/67/16/ff93a35798c1ebdd402fbb102b7b57acf6902f471320122bd36c6532d1cc/qs_codec-1.0.5-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "c053c81596e96927f4d8867664cc74b697883e43afee32385c5a8514a87a5478",
"md5": "3f9a5c2cd30d304ae4c117b8789f1dbd",
"sha256": "451747a059c93f1f446ca6ee3ba320f456a71de9d63377da809d47363e0b1862"
},
"downloads": -1,
"filename": "qs_codec-1.0.5.tar.gz",
"has_sig": false,
"md5_digest": "3f9a5c2cd30d304ae4c117b8789f1dbd",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 63064,
"upload_time": "2024-11-24T16:52:00",
"upload_time_iso_8601": "2024-11-24T16:52:00.612484Z",
"url": "https://files.pythonhosted.org/packages/c0/53/c81596e96927f4d8867664cc74b697883e43afee32385c5a8514a87a5478/qs_codec-1.0.5.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-11-24 16:52:00",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "techouse",
"github_project": "qs_codec",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"tox": true,
"lcname": "qs-codec"
}