qs-codec


Nameqs-codec JSON
Version 0.2.1 PyPI version JSON
download
home_pageNone
SummaryA query string encoding and decoding library for Python. Ported from qs for JavaScript.
upload_time2024-05-06 11:11:39
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseBSD-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

   # Encoding
   assert qs_codec.encode({'a': 'b'}) == 'a=b'

   # Decoding
   assert qs_codec.decode('a=b') == {'a': 'b'}

Decoding
~~~~~~~~

dictionaries
^^^^^^^^^^^^

.. code:: python

   import qs_codec, typing as t

   def decode(
       value: t.Optional[t.Union[str, t.Mapping]],
       options: qs_codec.DecodeOptions = qs_codec.DecodeOptions(),
   ) -> dict:
       """Decodes a str or Mapping into a Dict. 
       
       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

   assert qs_codec.decode('foo[bar]=baz') == {'foo': {'bar': 'baz'}}

URI encoded strings work too:

.. code:: python

   import qs_codec

   assert qs_codec.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

   assert qs_codec.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

   assert qs_codec.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

   assert qs_codec.decode(
       'a[b][c][d][e][f][g][h][i]=j',
       qs_codec.DecodeOptions(depth=1),
   ) == {'a': {'b': {'[c][d][e][f][g][h][i]': 'j'}}}

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.

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

   assert qs_codec.decode(
       'a=b&c=d',
       qs_codec.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

   assert qs_codec.decode(
       '?a=b&c=d',
       qs_codec.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

   assert qs_codec.decode(
       'a=b;c=d',
       qs_codec.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 re, qs_codec

   assert qs_codec.decode(
       'a=b;c=d',
       qs_codec.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

   assert qs_codec.decode(
       'a.b=c',
       qs_codec.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

   assert qs_codec.decode(
       'name%252Eobj.first=John&name%252Eobj.last=Doe',
       qs_codec.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

   assert qs_codec.decode(
       'foo[]&bar=baz',
       qs_codec.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

   assert qs_codec.decode('foo=bar&foo=baz') == {'foo': ['bar', 'baz']}

   assert qs_codec.decode(
       'foo=bar&foo=baz',
       qs_codec.DecodeOptions(duplicates=qs_codec.Duplicates.COMBINE),
   ) == {'foo': ['bar', 'baz']}

   assert qs_codec.decode(
       'foo=bar&foo=baz',
       qs_codec.DecodeOptions(duplicates=qs_codec.Duplicates.FIRST),
   ) == {'foo': 'bar'}

   assert qs_codec.decode(
       'foo=bar&foo=baz',
       qs_codec.DecodeOptions(duplicates=qs_codec.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

   assert qs_codec.decode(
       'a=%A7',
       qs_codec.DecodeOptions(charset=qs_codec.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

   assert qs_codec.decode(
       'utf8=%E2%9C%93&a=%C3%B8',
       qs_codec.DecodeOptions(
           charset=qs_codec.Charset.LATIN1,
           charset_sentinel=True,
       ),
   ) == {'a': 'ø'}

   assert qs_codec.decode(
       'utf8=%26%2310003%3B&a=%F8',
       qs_codec.DecodeOptions(
           charset=qs_codec.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

   assert qs_codec.decode(
       'a=%26%239786%3B',
       qs_codec.DecodeOptions(
           charset=qs_codec.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

   assert qs_codec.decode('a[]=b&a[]=c') == {'a': ['b', 'c']}

You may specify an index as well:

.. code:: python

   import qs_codec

   assert qs_codec.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

   assert qs_codec.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

   assert qs_codec.decode('a[]=&a[]=b') == {'a': ['', 'b']}

   assert qs_codec.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

   assert qs_codec.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

   assert qs_codec.decode(
       'a[1]=b',
       qs_codec.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

   assert qs_codec.decode(
       'a[]=b',
       qs_codec.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

   assert qs_codec.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

   assert qs_codec.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

   assert qs_codec.decode(
       'a=15&b=true&c=null',
   ) == {'a': '15', 'b': 'true', 'c': 'null'}

Encoding
~~~~~~~~

.. code:: python

   import qs_codec, typing as t

   def encode(
       value: t.Any,
       options: qs_codec.EncodeOptions = qs_codec.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

   assert qs_codec.encode({'a': 'b'}) == 'a=b'
   assert qs_codec.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

   assert qs_codec.encode(
       {'a': {'b': 'c'}},
       qs_codec.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

   assert qs_codec.encode(
       {
           'a': 'b',
           'c': ['d', 'e=f'],
           'f': [
               ['g'],
               ['h']
           ]
       },
       qs_codec.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, typing as t


   def custom_encoder(
       value: str,
       charset: t.Optional[qs_codec.Charset],
       format: t.Optional[qs_codec.Format],
   ) -> str:
       if value == 'č':
           return 'c'
       return value


   assert qs_codec.encode(
       {'a': {'b': 'č'}},
       qs_codec.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, typing as t

   def custom_decoder(
       value: t.Any,
       charset: t.Optional[qs_codec.Charset],
   ) -> t.Union[int, str]:
       try:
           return int(value)
       except ValueError:
           return value

   assert qs_codec.decode(
       'foo=123',
       qs_codec.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

   assert qs_codec.encode(
       {'a': ['b', 'c', 'd']},
       qs_codec.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

   assert qs_codec.encode(
       {'a': ['b', 'c', 'd']},
       qs_codec.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

   # ListFormat.INDICES
   assert qs_codec.encode(
       {'a': ['b', 'c']},
       qs_codec.EncodeOptions(
           encode=False,
           list_format=qs_codec.ListFormat.INDICES,
       ),
   ) == 'a[0]=b&a[1]=c'

   # ListFormat.BRACKETS
   assert qs_codec.encode(
       {'a': ['b', 'c']},
       qs_codec.EncodeOptions(
           encode=False,
           list_format=qs_codec.ListFormat.BRACKETS,
       ),
   ) == 'a[]=b&a[]=c'

   # ListFormat.REPEAT
   assert qs_codec.encode(
       {'a': ['b', 'c']},
       qs_codec.EncodeOptions(
           encode=False,
           list_format=qs_codec.ListFormat.REPEAT,
       ),
   ) == 'a=b&a=c'

   # ListFormat.COMMA
   assert qs_codec.encode(
       {'a': ['b', 'c']},
       qs_codec.EncodeOptions(
           encode=False,
           list_format=qs_codec.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

   assert qs_codec.encode(
       {'a': {'b': {'c': 'd', 'e': 'f'}}},
       qs_codec.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

   assert qs_codec.encode(
       {'a': {'b': {'c': 'd', 'e': 'f'}}},
       qs_codec.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

   assert qs_codec.encode(
       {'name.obj': {'first': 'John', 'last': 'Doe'}},
       qs_codec.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

   assert qs_codec.encode(
       {'foo': [], 'bar': 'baz', },
       qs_codec.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

   assert qs_codec.encode({'a': ''}) == 'a='

Keys with no values (such as an empty ``dict`` or ``list``) will return nothing:

.. code:: python

   import qs_codec

   assert qs_codec.encode({'a': []}) == ''

   assert qs_codec.encode({'a': {}}) == ''

   assert qs_codec.encode({'a': [{}]}) == ''

   assert qs_codec.encode({'a': {'b': []}}) == ''

   assert qs_codec.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

   assert qs_codec.encode({'a': None, 'b': qs_codec.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

   assert qs_codec.encode(
       {'a': 'b', 'c': 'd'},
       qs_codec.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

   assert qs_codec.encode(
       {'a': 'b', 'c': 'd', },
       qs_codec.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, datetime, sys

   # First case: encoding a datetime object to an ISO 8601 string
   assert (
       qs_codec.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_codec.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_codec.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_codec.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

   assert qs_codec.encode(
       {'a': 'c', 'z': 'y', 'b': 'f'},
       qs_codec.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, datetime, sys

   # First case: using a Callable as filter
   assert (
       qs_codec.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_codec.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_codec.encode(
       {'a': 'b', 'c': 'd', 'e': 'f'},
       qs_codec.EncodeOptions(
           encode=False,
           filter=['a', 'e']
       )
   ) == 'a=b&e=f'

   # Third case: using a list as filter with indices
   assert qs_codec.encode(
       {
           'a': ['b', 'c', 'd'],
           'e': 'f',
       },
       qs_codec.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

   assert qs_codec.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

   assert qs_codec.encode(
       {'a': None, 'b': ''},
       qs_codec.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

   assert qs_codec.decode(
       'a&b=',
       qs_codec.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

   assert qs_codec.encode(
       {'a': 'b', 'c': None},
       qs_codec.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

   assert qs_codec.encode(
       {'æ': 'æ'},
       qs_codec.EncodeOptions(charset=qs_codec.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

   assert qs_codec.encode(
       {'a': '☺'},
       qs_codec.EncodeOptions(charset=qs_codec.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

   assert qs_codec.encode(
       {'a': '☺'},
       qs_codec.EncodeOptions(charset_sentinel=True)
   ) == 'utf8=%E2%9C%93&a=%E2%98%BA'

   assert qs_codec.encode(
       {'a': 'æ'},
       qs_codec.EncodeOptions(charset=qs_codec.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, codecs, typing as t

   def custom_encoder(
       string: str,
       charset: t.Optional[qs_codec.Charset],
       format: t.Optional[qs_codec.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_codec.encode(
       {'a': 'こんにちは!'},
       qs_codec.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, re, codecs, typing as t

   def custom_decoder(
       string: str,
       charset: t.Optional[qs_codec.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_codec.decode(
       '%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49',
       qs_codec.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

   assert qs_codec.encode(
       {'a': 'b c'},
       qs_codec.EncodeOptions(format=qs_codec.Format.RFC3986)
   ) == 'a=b%20c'

   assert qs_codec.encode(
       {'a': 'b c'},
       qs_codec.EncodeOptions(format=qs_codec.Format.RFC3986)
   ) == 'a=b%20c'

   assert qs_codec.encode(
       {'a': 'b c'},
       qs_codec.EncodeOptions(format=qs_codec.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/2c/83/0108b30f623db5d28089a296b0809972ea27a98395044bf1ec6b2b6d3c67/qs_codec-0.2.1.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\n\n   # Encoding\n   assert qs_codec.encode({'a': 'b'}) == 'a=b'\n\n   # Decoding\n   assert qs_codec.decode('a=b') == {'a': 'b'}\n\nDecoding\n~~~~~~~~\n\ndictionaries\n^^^^^^^^^^^^\n\n.. code:: python\n\n   import qs_codec, typing as t\n\n   def decode(\n       value: t.Optional[t.Union[str, t.Mapping]],\n       options: qs_codec.DecodeOptions = qs_codec.DecodeOptions(),\n   ) -> dict:\n       \"\"\"Decodes a str or Mapping into a Dict. \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\n\n   assert qs_codec.decode('foo[bar]=baz') == {'foo': {'bar': 'baz'}}\n\nURI encoded strings work too:\n\n.. code:: python\n\n   import qs_codec\n\n   assert qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.decode(\n       'a[b][c][d][e][f][g][h][i]=j',\n       qs_codec.DecodeOptions(depth=1),\n   ) == {'a': {'b': {'[c][d][e][f][g][h][i]': 'j'}}}\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.\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\n\n   assert qs_codec.decode(\n       'a=b&c=d',\n       qs_codec.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\n\n   assert qs_codec.decode(\n       '?a=b&c=d',\n       qs_codec.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\n\n   assert qs_codec.decode(\n       'a=b;c=d',\n       qs_codec.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 re, qs_codec\n\n   assert qs_codec.decode(\n       'a=b;c=d',\n       qs_codec.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\n\n   assert qs_codec.decode(\n       'a.b=c',\n       qs_codec.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\n\n   assert qs_codec.decode(\n       'name%252Eobj.first=John&name%252Eobj.last=Doe',\n       qs_codec.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\n\n   assert qs_codec.decode(\n       'foo[]&bar=baz',\n       qs_codec.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\n\n   assert qs_codec.decode('foo=bar&foo=baz') == {'foo': ['bar', 'baz']}\n\n   assert qs_codec.decode(\n       'foo=bar&foo=baz',\n       qs_codec.DecodeOptions(duplicates=qs_codec.Duplicates.COMBINE),\n   ) == {'foo': ['bar', 'baz']}\n\n   assert qs_codec.decode(\n       'foo=bar&foo=baz',\n       qs_codec.DecodeOptions(duplicates=qs_codec.Duplicates.FIRST),\n   ) == {'foo': 'bar'}\n\n   assert qs_codec.decode(\n       'foo=bar&foo=baz',\n       qs_codec.DecodeOptions(duplicates=qs_codec.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\n\n   assert qs_codec.decode(\n       'a=%A7',\n       qs_codec.DecodeOptions(charset=qs_codec.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\n\n   assert qs_codec.decode(\n       'utf8=%E2%9C%93&a=%C3%B8',\n       qs_codec.DecodeOptions(\n           charset=qs_codec.Charset.LATIN1,\n           charset_sentinel=True,\n       ),\n   ) == {'a': '\u00f8'}\n\n   assert qs_codec.decode(\n       'utf8=%26%2310003%3B&a=%F8',\n       qs_codec.DecodeOptions(\n           charset=qs_codec.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\n\n   assert qs_codec.decode(\n       'a=%26%239786%3B',\n       qs_codec.DecodeOptions(\n           charset=qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.decode('a[]=&a[]=b') == {'a': ['', 'b']}\n\n   assert qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.decode(\n       'a[1]=b',\n       qs_codec.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\n\n   assert qs_codec.decode(\n       'a[]=b',\n       qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.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, typing as t\n\n   def encode(\n       value: t.Any,\n       options: qs_codec.EncodeOptions = qs_codec.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\n\n   assert qs_codec.encode({'a': 'b'}) == 'a=b'\n   assert qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': {'b': 'c'}},\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {\n           'a': 'b',\n           'c': ['d', 'e=f'],\n           'f': [\n               ['g'],\n               ['h']\n           ]\n       },\n       qs_codec.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, typing as t\n\n\n   def custom_encoder(\n       value: str,\n       charset: t.Optional[qs_codec.Charset],\n       format: t.Optional[qs_codec.Format],\n   ) -> str:\n       if value == '\u010d':\n           return 'c'\n       return value\n\n\n   assert qs_codec.encode(\n       {'a': {'b': '\u010d'}},\n       qs_codec.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, typing as t\n\n   def custom_decoder(\n       value: t.Any,\n       charset: t.Optional[qs_codec.Charset],\n   ) -> t.Union[int, str]:\n       try:\n           return int(value)\n       except ValueError:\n           return value\n\n   assert qs_codec.decode(\n       'foo=123',\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': ['b', 'c', 'd']},\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': ['b', 'c', 'd']},\n       qs_codec.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\n\n   # ListFormat.INDICES\n   assert qs_codec.encode(\n       {'a': ['b', 'c']},\n       qs_codec.EncodeOptions(\n           encode=False,\n           list_format=qs_codec.ListFormat.INDICES,\n       ),\n   ) == 'a[0]=b&a[1]=c'\n\n   # ListFormat.BRACKETS\n   assert qs_codec.encode(\n       {'a': ['b', 'c']},\n       qs_codec.EncodeOptions(\n           encode=False,\n           list_format=qs_codec.ListFormat.BRACKETS,\n       ),\n   ) == 'a[]=b&a[]=c'\n\n   # ListFormat.REPEAT\n   assert qs_codec.encode(\n       {'a': ['b', 'c']},\n       qs_codec.EncodeOptions(\n           encode=False,\n           list_format=qs_codec.ListFormat.REPEAT,\n       ),\n   ) == 'a=b&a=c'\n\n   # ListFormat.COMMA\n   assert qs_codec.encode(\n       {'a': ['b', 'c']},\n       qs_codec.EncodeOptions(\n           encode=False,\n           list_format=qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': {'b': {'c': 'd', 'e': 'f'}}},\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': {'b': {'c': 'd', 'e': 'f'}}},\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'name.obj': {'first': 'John', 'last': 'Doe'}},\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'foo': [], 'bar': 'baz', },\n       qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.encode({'a': []}) == ''\n\n   assert qs_codec.encode({'a': {}}) == ''\n\n   assert qs_codec.encode({'a': [{}]}) == ''\n\n   assert qs_codec.encode({'a': {'b': []}}) == ''\n\n   assert qs_codec.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\n\n   assert qs_codec.encode({'a': None, 'b': qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': 'b', 'c': 'd'},\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': 'b', 'c': 'd', },\n       qs_codec.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, datetime, sys\n\n   # First case: encoding a datetime object to an ISO 8601 string\n   assert (\n       qs_codec.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_codec.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_codec.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_codec.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\n\n   assert qs_codec.encode(\n       {'a': 'c', 'z': 'y', 'b': 'f'},\n       qs_codec.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, datetime, sys\n\n   # First case: using a Callable as filter\n   assert (\n       qs_codec.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_codec.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_codec.encode(\n       {'a': 'b', 'c': 'd', 'e': 'f'},\n       qs_codec.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_codec.encode(\n       {\n           'a': ['b', 'c', 'd'],\n           'e': 'f',\n       },\n       qs_codec.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\n\n   assert qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': None, 'b': ''},\n       qs_codec.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\n\n   assert qs_codec.decode(\n       'a&b=',\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': 'b', 'c': None},\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'\u00e6': '\u00e6'},\n       qs_codec.EncodeOptions(charset=qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': '\u263a'},\n       qs_codec.EncodeOptions(charset=qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': '\u263a'},\n       qs_codec.EncodeOptions(charset_sentinel=True)\n   ) == 'utf8=%E2%9C%93&a=%E2%98%BA'\n\n   assert qs_codec.encode(\n       {'a': '\u00e6'},\n       qs_codec.EncodeOptions(charset=qs_codec.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, codecs, typing as t\n\n   def custom_encoder(\n       string: str,\n       charset: t.Optional[qs_codec.Charset],\n       format: t.Optional[qs_codec.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_codec.encode(\n       {'a': '\u3053\u3093\u306b\u3061\u306f\uff01'},\n       qs_codec.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, re, codecs, typing as t\n\n   def custom_decoder(\n       string: str,\n       charset: t.Optional[qs_codec.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_codec.decode(\n       '%61=%82%b1%82%f1%82%c9%82%bf%82%cd%81%49',\n       qs_codec.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\n\n   assert qs_codec.encode(\n       {'a': 'b c'},\n       qs_codec.EncodeOptions(format=qs_codec.Format.RFC3986)\n   ) == 'a=b%20c'\n\n   assert qs_codec.encode(\n       {'a': 'b c'},\n       qs_codec.EncodeOptions(format=qs_codec.Format.RFC3986)\n   ) == 'a=b%20c'\n\n   assert qs_codec.encode(\n       {'a': 'b c'},\n       qs_codec.EncodeOptions(format=qs_codec.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": "0.2.1",
    "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": "add64e9ab973bd93718b6f18ec35a1e8ba045d6cb12dacb0781071b5fa13c0db",
                "md5": "360ba6dc0f274bb295535fdec6db0312",
                "sha256": "9f42073baf205785398a8bb9fe2ef027ef3ace878da345a9fcc8dd63ee2277f9"
            },
            "downloads": -1,
            "filename": "qs_codec-0.2.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "360ba6dc0f274bb295535fdec6db0312",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 28683,
            "upload_time": "2024-05-06T11:11:37",
            "upload_time_iso_8601": "2024-05-06T11:11:37.726821Z",
            "url": "https://files.pythonhosted.org/packages/ad/d6/4e9ab973bd93718b6f18ec35a1e8ba045d6cb12dacb0781071b5fa13c0db/qs_codec-0.2.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "2c830108b30f623db5d28089a296b0809972ea27a98395044bf1ec6b2b6d3c67",
                "md5": "5dabad787b7494f54feac44d6404ac78",
                "sha256": "33fe23e3f0fda7d9a0f12195af7d3b282b01ad43b22d4d2e39aea80aeeb2c889"
            },
            "downloads": -1,
            "filename": "qs_codec-0.2.1.tar.gz",
            "has_sig": false,
            "md5_digest": "5dabad787b7494f54feac44d6404ac78",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 59153,
            "upload_time": "2024-05-06T11:11:39",
            "upload_time_iso_8601": "2024-05-06T11:11:39.622316Z",
            "url": "https://files.pythonhosted.org/packages/2c/83/0108b30f623db5d28089a296b0809972ea27a98395044bf1ec6b2b6d3c67/qs_codec-0.2.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-05-06 11:11:39",
    "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"
}
        
Elapsed time: 0.27391s