diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000..15c4834 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: ccceffa458940e20969a232f6352bfc3 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/README.doctree b/.doctrees/README.doctree new file mode 100644 index 0000000..fbb4953 Binary files /dev/null and b/.doctrees/README.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle new file mode 100644 index 0000000..74326b2 Binary files /dev/null and b/.doctrees/environment.pickle differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree new file mode 100644 index 0000000..a3dbb60 Binary files /dev/null and b/.doctrees/index.doctree differ diff --git a/.doctrees/qs_codec.doctree b/.doctrees/qs_codec.doctree new file mode 100644 index 0000000..73835c9 Binary files /dev/null and b/.doctrees/qs_codec.doctree differ diff --git a/.doctrees/qs_codec.enums.doctree b/.doctrees/qs_codec.enums.doctree new file mode 100644 index 0000000..608e13b Binary files /dev/null and b/.doctrees/qs_codec.enums.doctree differ diff --git a/.doctrees/qs_codec.models.doctree b/.doctrees/qs_codec.models.doctree new file mode 100644 index 0000000..631ef26 Binary files /dev/null and b/.doctrees/qs_codec.models.doctree differ diff --git a/.doctrees/qs_codec.utils.doctree b/.doctrees/qs_codec.utils.doctree new file mode 100644 index 0000000..798af4c Binary files /dev/null and b/.doctrees/qs_codec.utils.doctree differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/README.html b/README.html new file mode 100644 index 0000000..605858d --- /dev/null +++ b/README.html @@ -0,0 +1,942 @@ + + + + + + + + Decoding — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Decoding

+
+

dictionaries

+
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 allows you to create nested dicts within your query +strings, by surrounding the name of sub-keys with square brackets +[]. For example, the string 'foo[bar]=baz' converts to:

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

URI encoded strings work too:

+
import qs_codec
+
+assert qs_codec.decode('a%5Bb%5D=c') == {'a': {'b': 'c'}}
+
+
+

You can also nest your dicts, like 'foo[bar][baz]=foobarbaz':

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

By default, when nesting dicts 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:

+
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:

+
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 is used to parse user +input, and it is recommended to keep it a reasonably small number.

+

For similar reasons, by default decode will only parse up to 1000 parameters. This can be overridden by passing a +parameter_limit option:

+
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:

+
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 can also be passed:

+
import qs_codec
+
+assert qs_codec.decode(
+    'a=b;c=d',
+    qs_codec.DecodeOptions(delimiter=';'),
+) == {'a': 'b', 'c': 'd'}
+
+
+

delimiter can be a regular expression too:

+
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 +can be used to enable dot notation:

+
import qs_codec
+
+assert qs_codec.decode(
+    'a.b=c',
+    qs_codec.DecodeOptions(allow_dots=True),
+) == {'a': {'b': 'c'}}
+
+
+

Option decode_dot_in_keys +can be used to decode dots in keys.

+

Note: it implies allow_dots, so +decode will error if you set decode_dot_in_keys +to True, and allow_dots to False.

+
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 can +be used to allowing empty list values in a dict

+
import qs_codec
+
+assert qs_codec.decode(
+    'foo[]&bar=baz',
+    qs_codec.DecodeOptions(allow_empty_lists=True),
+) == {'foo': [], 'bar': 'baz'}
+
+
+

Option duplicates can be used to +change the behavior when duplicate keys are encountered

+
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:

+
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 supports this mechanism via the +charset_sentinel option. +If specified, the utf8 parameter will be omitted from the returned +dict. It will be used to switch to LATIN1 or +UTF8 mode depending on how the checkmark is encoded.

+

Important: When you specify both the charset +option and the charset_sentinel option, the +charset will be overridden when the request contains a +utf8 parameter from which the actual charset can be deduced. In that +sense the charset will behave as the default charset +rather than the authoritative charset.

+
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 &#…; syntax to the actual character, you can specify the +interpret_numeric_entities +option as well:

+
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 mode.

+
+
+

lists

+

decode can also decode lists using a similar [] notation:

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

You may specify an index as well:

+
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 lists with specific indices, +decode will compact a sparse list to +only the existing values preserving their order:

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

Note that an empty string is also a value, and will be preserved:

+
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 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.

+
import qs_codec
+
+assert qs_codec.decode('a[100]=b') == {'a': {100: 'b'}}
+
+
+

This limit can be overridden by passing an list_limit +option:

+
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 +to False.

+
import qs_codec
+
+assert qs_codec.decode(
+    'a[]=b',
+    qs_codec.DecodeOptions(parse_lists=False),
+) == {'a': {0: 'b'}}
+
+
+

If you mix notations, decode will merge the two items into a dict:

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

You can also create lists of dicts:

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

(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 strings.

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

Encoding

+
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 by default URI encodes output. dicts are +encoded as you would expect:

+
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 +option to False:

+
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 option to True:

+
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 option:

+
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 option does not apply if +encode is False).

+

Similar to encoder there is a +decoder option for decode +to override decoding of properties and values:

+
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 lists are encoded, they follow the +list_format option, which defaults to +INDICES:

+
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 option to +False, or to be more explicit, the list_format +option to REPEAT:

+
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 option to specify the +format of the output list:

+
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 set to +COMMA, you can also pass the +comma_round_trip option set to True or +False, to append [] on single-item lists, so that they can round trip through a decoding.

+

BRACKETS notation is used for encoding dicts by default:

+
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 option to True:

+
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 dicts by setting +encode_dot_in_keys to True:

+
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 +and 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 option to True:

+
import qs_codec
+
+assert qs_codec.encode(
+    {'foo': [], 'bar': 'baz', },
+    qs_codec.EncodeOptions(
+        encode=False,
+        allow_empty_lists=True,
+    ),
+) == 'foo[]&bar=baz'
+
+
+

Empty strings and None values will be omitted, but the equals sign (=) remains in place:

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

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

+
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 properties will be omitted entirely:

+
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 to True:

+
import qs_codec
+
+assert qs_codec.encode(
+    {'a': 'b', 'c': 'd'},
+    qs_codec.EncodeOptions(add_query_prefix=True),
+) == '?a=b&c=d'
+
+
+

The delimiter may be overridden as well:

+
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 +objects, you can provide a Callable in the +serialize_date option:

+
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 option:

+
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 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:

+
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 strings:

+
import qs_codec
+
+assert qs_codec.encode({'a': None, 'b': ''}) == 'a=&b='
+
+
+

To distinguish between None values and empty strs use the +strict_null_handling flag. +In the result string the None values have no = sign:

+
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 flag:

+
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 flag:

+
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 using the +charset option:

+
import qs_codec
+
+assert qs_codec.encode(
+    {'æ': 'æ'},
+    qs_codec.EncodeOptions(charset=qs_codec.Charset.LATIN1)
+) == '%E6=%E6'
+
+
+

Characters that don’t exist in LATIN1 +will be converted to numeric entities, similar to what browsers do:

+
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 +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.

+
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, and +LATIN1 support is also built in via +the charset +and charset parameter, +respectively.

+

If you wish to encode query strings to a different character set (i.e. +Shift JIS)

+
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:

+
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 is +RFC3986 which encodes +' ' to %20 which is backward compatible. You can also set the +format to +RFC1738 which encodes ' ' to +.

+
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'
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 0000000..145cb90 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,118 @@ + + + + + + + Overview: module code — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/decode.html b/_modules/qs_codec/decode.html new file mode 100644 index 0000000..487e2d2 --- /dev/null +++ b/_modules/qs_codec/decode.html @@ -0,0 +1,300 @@ + + + + + + + qs_codec.decode — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.decode

+"""A query string decoder (parser)."""
+
+import re
+import typing as t
+from math import isinf
+
+from regex import regex
+
+from .enums.charset import Charset
+from .enums.duplicates import Duplicates
+from .enums.sentinel import Sentinel
+from .models.decode_options import DecodeOptions
+from .models.undefined import Undefined
+from .utils.utils import Utils
+
+
+
+[docs] +def decode(value: t.Optional[t.Union[str, t.Mapping]], options: DecodeOptions = DecodeOptions()) -> dict: + """ + Decodes a ``str`` or ``Mapping`` into a ``dict``. + + Providing custom ``DecodeOptions`` will override the default behavior. + """ + if not value: + return dict() + + if not isinstance(value, (str, t.Mapping)): + raise ValueError("The input must be a String or a Dict") + + temp_obj: t.Optional[t.Mapping] = _parse_query_string_values(value, options) if isinstance(value, str) else value + obj: t.Dict = dict() + + # Iterate over the keys and setup the new object + if temp_obj: + for key, val in temp_obj.items(): + new_obj: t.Any = _parse_keys(key, val, options, isinstance(value, str)) + obj = Utils.merge(obj, new_obj, options) # type: ignore [assignment] + + return Utils.compact(obj)
+ + + +def _interpret_numeric_entities(value: str) -> str: + return re.sub(r"&#(\d+);", lambda match: chr(int(match.group(1))), value) + + +def _parse_array_value(value: t.Any, options: DecodeOptions) -> t.Any: + if isinstance(value, str) and value and options.comma and "," in value: + return value.split(",") + + return value + + +def _parse_query_string_values(value: str, options: DecodeOptions) -> t.Dict: + obj: t.Dict = dict() + + clean_str: str = value.replace("?", "", 1) if options.ignore_query_prefix else value + limit: t.Optional[int] = None if isinf(options.parameter_limit) else options.parameter_limit # type: ignore [assignment] + parts: t.List[str] + if isinstance(options.delimiter, re.Pattern): + parts = re.split(options.delimiter, clean_str) if not limit else re.split(options.delimiter, clean_str)[:limit] + else: + parts = clean_str.split(options.delimiter) if not limit else clean_str.split(options.delimiter)[:limit] + + skip_index: int = -1 # Keep track of where the utf8 sentinel was found + i: int + + charset: Charset = options.charset + + if options.charset_sentinel: + for i, _part in enumerate(parts): + if _part.startswith("utf8="): + if _part == Sentinel.CHARSET.encoded: + charset = Charset.UTF8 + elif _part == Sentinel.ISO.encoded: + charset = Charset.LATIN1 + skip_index = i + break + + for i, _ in enumerate(parts): + if i == skip_index: + continue + + part: str = parts[i] + bracket_equals_pos: int = part.find("]=") + pos: int = part.find("=") if bracket_equals_pos == -1 else (bracket_equals_pos + 1) + + key: str + val: t.Union[t.List, t.Tuple, str, t.Any] + if pos == -1: + key = options.decoder(part, charset) + val = None if options.strict_null_handling else "" + else: + key = options.decoder(part[:pos], charset) + val = Utils.apply( + _parse_array_value(part[pos + 1 :], options), + lambda v: options.decoder(v, charset), + ) + + if val and options.interpret_numeric_entities and charset == Charset.LATIN1: + val = _interpret_numeric_entities(val) # type: ignore [arg-type] + + if "[]=" in part: + val = [val] if isinstance(val, (list, tuple)) else val + + existing: bool = key in obj + + if existing and options.duplicates == Duplicates.COMBINE: + obj[key] = Utils.combine(obj[key], val) + elif not existing or options.duplicates == Duplicates.LAST: + obj[key] = val + + return obj + + +def _parse_object( + chain: t.Union[t.List[str], t.Tuple[str, ...]], val: t.Any, options: DecodeOptions, values_parsed: bool +) -> t.Any: + leaf: t.Any = val if values_parsed else _parse_array_value(val, options) + + i: int + for i in reversed(range(len(chain))): + obj: t.Optional[t.Any] + root: str = chain[i] + + if root == "[]" and options.parse_lists: + if options.allow_empty_lists and leaf == "": + obj = [] + else: + obj = list(leaf) if isinstance(leaf, (list, tuple)) else [leaf] + else: + obj = dict() + + clean_root: str = root[1:-1] if root.startswith("[") and root.endswith("]") else root + + decoded_root: str = clean_root.replace(r"%2E", ".") if options.decode_dot_in_keys else clean_root + + index: t.Optional[int] + try: + index = int(decoded_root, 10) + except (ValueError, TypeError): + index = None + + if not options.parse_lists and decoded_root == "": + obj = {0: leaf} + elif ( + index is not None + and index >= 0 + and root != decoded_root + and str(index) == decoded_root + and options.parse_lists + and index <= options.list_limit + ): + obj = [Undefined() for _ in range(index + 1)] + obj[index] = leaf + else: + obj[index if index is not None else decoded_root] = leaf + + leaf = obj + + return leaf + + +def _parse_keys(given_key: t.Optional[str], val: t.Any, options: DecodeOptions, values_parsed: bool) -> t.Any: + if not given_key: + return + + # Transform dot notation to bracket notation + key: str = re.sub(r"\.([^.[]+)", r"[\1]", given_key) if options.allow_dots else given_key + + # The regex chunks + brackets: regex.Pattern[str] = regex.compile(r"\[(?:[^\[\]]|(?R))*\]") + + # Get the parent + segment: t.Optional[regex.Match] = brackets.search(key) if options.depth > 0 else None + parent: str = key[0 : segment.start()] if segment is not None else key + + # Stash the parent if it exists + keys: t.List[str] = [parent] if parent else [] + + # Loop through children appending to the array until we hit depth + i: int = 0 + while options.depth > 0 and (segment := brackets.search(key)) is not None and i < options.depth: + i += 1 + if segment is not None: + keys.append(segment.group()) + # Update the key to start searching from the next position + key = key[segment.end() :] + + # If there's a remainder, just add whatever is left + if segment is not None: + keys.append(f"[{key[segment.start():]}]") + + return _parse_object(keys, val, options, values_parsed) +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/encode.html b/_modules/qs_codec/encode.html new file mode 100644 index 0000000..0eca60e --- /dev/null +++ b/_modules/qs_codec/encode.html @@ -0,0 +1,404 @@ + + + + + + + qs_codec.encode — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.encode

+"""A query string encoder (stringifier)."""
+
+import typing as t
+from copy import deepcopy
+from datetime import datetime
+from functools import cmp_to_key
+from weakref import WeakKeyDictionary
+
+from .enums.charset import Charset
+from .enums.format import Format
+from .enums.list_format import ListFormat
+from .enums.sentinel import Sentinel
+from .models.encode_options import EncodeOptions
+from .models.undefined import Undefined
+from .models.weak_wrapper import WeakWrapper
+from .utils.utils import Utils
+
+
+
+[docs] +def encode(value: t.Any, options: EncodeOptions = EncodeOptions()) -> str: + """ + Encodes ``Any`` object into a query ``str`` ing. + + Providing custom ``EncodeOptions`` will override the default behavior. + """ + if value is None: + return "" + + obj: t.Mapping[str, t.Any] + if isinstance(value, t.Mapping): + obj = deepcopy(value) + elif isinstance(value, (list, tuple)): + obj = {str(key): value for key, value in enumerate(value)} + else: + obj = {} + + keys: t.List[t.Any] = [] + + if not obj: + return "" + + obj_keys: t.Optional[t.List[t.Any]] = None + + if options.filter is not None: + if callable(options.filter): + obj = options.filter("", obj) + elif isinstance(options.filter, (list, tuple)): + obj_keys = list(options.filter) + + comma_round_trip: bool = options.list_format == ListFormat.COMMA and options.comma_round_trip is True + + if obj_keys is None: + obj_keys = list(obj.keys()) + + if options.sort is not None and callable(options.sort): + obj_keys = sorted(obj_keys, key=cmp_to_key(options.sort)) + + side_channel: WeakKeyDictionary = WeakKeyDictionary() + + for _key in obj_keys: + if not isinstance(_key, str): + continue + if _key in obj and obj.get(_key) is None and options.skip_nulls: + continue + + _encoded: t.Union[t.List[t.Any], t.Tuple[t.Any, ...], t.Any] = _encode( + value=obj.get(_key), + is_undefined=_key not in obj, + side_channel=side_channel, + prefix=_key, + generate_array_prefix=options.list_format.generator, + comma_round_trip=comma_round_trip, + encoder=options.encoder if options.encode else None, + serialize_date=options.serialize_date, + sort=options.sort, + filter=options.filter, + formatter=options.format.formatter, + allow_empty_lists=options.allow_empty_lists, + strict_null_handling=options.strict_null_handling, + skip_nulls=options.skip_nulls, + encode_dot_in_keys=options.encode_dot_in_keys, + allow_dots=options.allow_dots, + format=options.format, + encode_values_only=options.encode_values_only, + charset=options.charset, + add_query_prefix=options.add_query_prefix, + ) + + if isinstance(_encoded, (list, tuple)): + keys.extend(_encoded) + else: + keys.append(_encoded) + + joined: str = options.delimiter.join(keys) + prefix: str = "?" if options.add_query_prefix else "" + + if options.charset_sentinel: + if options.charset == Charset.LATIN1: + prefix += f"{Sentinel.ISO.encoded}&" + elif options.charset == Charset.UTF8: + prefix += f"{Sentinel.CHARSET.encoded}&" + else: + raise ValueError("Invalid charset") + + return prefix + joined if joined else ""
+ + + +_sentinel: WeakWrapper = WeakWrapper({}) + + +def _encode( + value: t.Any, + is_undefined: bool, + side_channel: WeakKeyDictionary, + prefix: t.Optional[str], + comma_round_trip: t.Optional[bool], + encoder: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]], + serialize_date: t.Callable[[datetime], str], + sort: t.Optional[t.Callable[[t.Any, t.Any], int]], + filter: t.Optional[t.Union[t.Callable, t.List[t.Union[str, int]]]], + formatter: t.Optional[t.Callable[[str], str]], + format: Format = Format.RFC3986, + generate_array_prefix: t.Callable[[str, t.Optional[str]], str] = ListFormat.INDICES.generator, + allow_empty_lists: bool = False, + strict_null_handling: bool = False, + skip_nulls: bool = False, + encode_dot_in_keys: bool = False, + allow_dots: bool = False, + encode_values_only: bool = False, + charset: t.Optional[Charset] = Charset.UTF8, + add_query_prefix: bool = False, +) -> t.Union[t.List[t.Any], t.Tuple[t.Any, ...], t.Any]: + if prefix is None: + prefix = "?" if add_query_prefix else "" + + if comma_round_trip is None: + comma_round_trip = generate_array_prefix == ListFormat.COMMA.generator + + if formatter is None: + formatter = format.formatter + + obj: t.Any = deepcopy(value) + + tmp_sc: t.Optional[WeakKeyDictionary] = side_channel + step: int = 0 + find_flag: bool = False + + while (tmp_sc := tmp_sc.get(_sentinel)) and not find_flag: # type: ignore [union-attr] + # Where value last appeared in the ref tree + pos: t.Optional[int] = tmp_sc.get(WeakWrapper(value)) + step += 1 + if pos is not None: + if pos == step: + raise ValueError("Circular reference detected") + else: + find_flag = True # Break while + if tmp_sc.get(_sentinel) is None: + step = 0 + + if callable(filter): + obj = filter(prefix, obj) + elif isinstance(obj, datetime): + obj = serialize_date(obj) if callable(serialize_date) else obj.isoformat() + elif generate_array_prefix == ListFormat.COMMA.generator and isinstance(obj, (list, tuple)): + obj = Utils.apply( + obj, + lambda val: ( + (serialize_date(val) if callable(serialize_date) else val.isoformat()) + if isinstance(val, datetime) + else val + ), + ) + + if not is_undefined and obj is None: + if strict_null_handling: + return encoder(prefix, charset, format) if callable(encoder) and not encode_values_only else prefix + + obj = "" + + if Utils.is_non_nullish_primitive(obj, skip_nulls) or isinstance(obj, bytes): + if callable(encoder): + key_value = prefix if encode_values_only else encoder(prefix, charset, format) + return [f"{formatter(key_value)}={formatter(encoder(obj, charset, format))}"] + + return [f"{formatter(prefix)}={formatter(str(obj))}"] + + values: t.List = [] + + if is_undefined: + return values + + obj_keys: t.List + if generate_array_prefix == ListFormat.COMMA.generator and isinstance(obj, (list, tuple)): + # we need to join elements in + if encode_values_only and callable(encoder): + obj = Utils.apply(obj, encoder) + + if obj: + obj_keys_value = ",".join([str(e) if e is not None else "" for e in obj]) + obj_keys = [{"value": obj_keys_value if obj_keys_value else None}] + else: + obj_keys = [{"value": Undefined()}] + elif isinstance(filter, (list, tuple)): + obj_keys = list(filter) + else: + keys: t.List + if isinstance(obj, t.Mapping): + keys = list(obj.keys()) + elif isinstance(obj, (list, tuple)): + keys = [index for index in range(len(obj))] + else: + keys = [] + + obj_keys = sorted(keys, key=cmp_to_key(sort)) if sort is not None else list(keys) + + encoded_prefix: str = prefix.replace(".", "%2E") if encode_dot_in_keys else prefix + + adjusted_prefix: str = ( + f"{encoded_prefix}[]" + if comma_round_trip and isinstance(obj, (list, tuple)) and len(obj) == 1 + else encoded_prefix + ) + + if allow_empty_lists and isinstance(obj, (list, tuple)) and not obj: + return [f"{adjusted_prefix}[]"] + + for _key in obj_keys: + _value: t.Any + _value_undefined: bool + + if isinstance(_key, t.Mapping) and "value" in _key and not isinstance(_key.get("value"), Undefined): + _value = _key.get("value") + _value_undefined = False + else: + try: + if isinstance(obj, t.Mapping): + _value = obj.get(_key) + _value_undefined = _key not in obj + elif isinstance(obj, (list, tuple)): + _value = obj[_key] + _value_undefined = False + else: + _value = obj[_key] + _value_undefined = False + except Exception: # pylint: disable=W0718 + _value = None + _value_undefined = True + + if skip_nulls and _value is None: + continue + + encoded_key: str = str(_key).replace(".", "%2E") if allow_dots and encode_dot_in_keys else str(_key) + + key_prefix: str = ( + generate_array_prefix(adjusted_prefix, encoded_key) + if isinstance(obj, (list, tuple)) + else f"{adjusted_prefix}{f'.{encoded_key}' if allow_dots else f'[{encoded_key}]'}" + ) + + side_channel[WeakWrapper(value)] = step + value_side_channel: WeakKeyDictionary = WeakKeyDictionary() + value_side_channel[_sentinel] = side_channel + + encoded: t.Union[t.List[t.Any], t.Tuple[t.Any, ...], t.Any] = _encode( + value=_value, + is_undefined=_value_undefined, + side_channel=value_side_channel, + prefix=key_prefix, + comma_round_trip=comma_round_trip, + encoder=( + None + if generate_array_prefix == ListFormat.COMMA.generator + and encode_values_only + and isinstance(obj, (list, tuple)) + else encoder + ), + serialize_date=serialize_date, + sort=sort, + filter=filter, + formatter=formatter, + format=format, + generate_array_prefix=generate_array_prefix, + allow_empty_lists=allow_empty_lists, + strict_null_handling=strict_null_handling, + skip_nulls=skip_nulls, + encode_dot_in_keys=encode_dot_in_keys, + allow_dots=allow_dots, + encode_values_only=encode_values_only, + charset=charset, + ) + + if isinstance(encoded, (list, tuple)): + values.extend(encoded) + else: + values.append(encoded) + + return values +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/enums/charset.html b/_modules/qs_codec/enums/charset.html new file mode 100644 index 0000000..3a73eb5 --- /dev/null +++ b/_modules/qs_codec/enums/charset.html @@ -0,0 +1,129 @@ + + + + + + + qs_codec.enums.charset — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.enums.charset

+"""Charset enum module."""
+
+from dataclasses import dataclass
+from enum import Enum
+
+
+@dataclass(frozen=True)
+class _CharsetDataMixin:
+    """Character set data mixin."""
+
+    encoding: str
+
+
+
+[docs] +class Charset(_CharsetDataMixin, Enum): + """Character set.""" + + UTF8 = "utf-8" + """UTF-8 character encoding.""" + + LATIN1 = "iso-8859-1" + """ISO-8859-1 (Latin-1) character encoding."""
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/enums/duplicates.html b/_modules/qs_codec/enums/duplicates.html new file mode 100644 index 0000000..f29a45c --- /dev/null +++ b/_modules/qs_codec/enums/duplicates.html @@ -0,0 +1,124 @@ + + + + + + + qs_codec.enums.duplicates — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.enums.duplicates

+"""This module contains an enum of all available duplicate key handling strategies."""
+
+from enum import Enum
+
+
+
+[docs] +class Duplicates(Enum): + """An enum of all available duplicate key handling strategies.""" + + COMBINE = 1 + """Combine duplicate keys into a single key with an array of values.""" + + FIRST = 2 + """Use the first value for duplicate keys.""" + + LAST = 3 + """Use the last value for duplicate keys."""
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/enums/format.html b/_modules/qs_codec/enums/format.html new file mode 100644 index 0000000..202fbcf --- /dev/null +++ b/_modules/qs_codec/enums/format.html @@ -0,0 +1,154 @@ + + + + + + + qs_codec.enums.format — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.enums.format

+"""An enum of all supported URI component encoding formats."""
+
+import typing as t
+from dataclasses import dataclass
+from enum import Enum
+
+
+
+[docs] +class Formatter: + """A class for formatting URI components.""" + +
+[docs] + @staticmethod + def rfc1738(value: str) -> str: + """Format a string according to `RFC 1738 <https://datatracker.ietf.org/doc/html/rfc1738>`_.""" + return value.replace("%20", "+")
+ + +
+[docs] + @staticmethod + def rfc3986(value: str) -> str: + """Format a string according to `RFC 3986 <https://datatracker.ietf.org/doc/html/rfc3986>`_.""" + return value
+
+ + + +@dataclass(frozen=True) +class _FormatDataMixin: + """Format data mixin.""" + + format_name: str + formatter: t.Callable[[str], str] + + +
+[docs] +class Format(_FormatDataMixin, Enum): + """An enum of all supported URI component encoding formats.""" + + RFC1738 = "RFC1738", Formatter.rfc1738 + """`RFC 1738 <https://datatracker.ietf.org/doc/html/rfc1738>`_.""" + + RFC3986 = "RFC3986", Formatter.rfc3986 + """`RFC 3986 <https://datatracker.ietf.org/doc/html/rfc3986>`_."""
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/enums/list_format.html b/_modules/qs_codec/enums/list_format.html new file mode 100644 index 0000000..ee3347c --- /dev/null +++ b/_modules/qs_codec/enums/list_format.html @@ -0,0 +1,176 @@ + + + + + + + qs_codec.enums.list_format — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.enums.list_format

+"""An enum for all available list format options."""
+
+import typing as t
+from dataclasses import dataclass
+from enum import Enum
+
+
+
+[docs] +class ListFormatGenerator: + """A class for formatting list items.""" + +
+[docs] + @staticmethod + def brackets(prefix: str, key: t.Optional[str] = None) -> str: # pylint: disable=W0613 + """Format a list item using brackets.""" + return f"{prefix}[]"
+ + +
+[docs] + @staticmethod + def comma(prefix: str, key: t.Optional[str] = None) -> str: # pylint: disable=W0613 + """Format a list item using commas.""" + return prefix
+ + +
+[docs] + @staticmethod + def indices(prefix: str, key: t.Optional[str] = None) -> str: + """Format a list item using indices.""" + return f"{prefix}[{key}]"
+ + +
+[docs] + @staticmethod + def repeat(prefix: str, key: t.Optional[str] = None) -> str: # pylint: disable=W0613 + """Format a list item using repeats.""" + return prefix
+
+ + + +@dataclass(frozen=True) +class _ListFormatDataMixin: + """List format data mixin.""" + + list_format_name: str + generator: t.Callable[[str, t.Optional[str]], str] + + +
+[docs] +class ListFormat(_ListFormatDataMixin, Enum): + """An enum of all available list format options.""" + + BRACKETS = "BRACKETS", ListFormatGenerator.brackets + """Use brackets to represent list items, for example ``foo[]=123&foo[]=456&foo[]=789``""" + + COMMA = "COMMA", ListFormatGenerator.comma + """Use commas to represent list items, for example ``foo=123,456,789``""" + + REPEAT = "REPEAT", ListFormatGenerator.repeat + """Use a repeat key to represent list items, for example ``foo=123&foo=456&foo=789``""" + + INDICES = "INDICES", ListFormatGenerator.indices + """Use indices to represent list items, for example ``foo[0]=123&foo[1]=456&foo[2]=789``"""
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/enums/sentinel.html b/_modules/qs_codec/enums/sentinel.html new file mode 100644 index 0000000..80b5af2 --- /dev/null +++ b/_modules/qs_codec/enums/sentinel.html @@ -0,0 +1,134 @@ + + + + + + + qs_codec.enums.sentinel — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.enums.sentinel

+"""This module defines the Sentinel enum, which contains all available sentinel values."""
+
+from dataclasses import dataclass
+from enum import Enum
+
+
+@dataclass(frozen=True)
+class _SentinelDataMixin:
+    """Sentinel data mixin."""
+
+    raw: str
+    encoded: str
+
+
+
+[docs] +class Sentinel(_SentinelDataMixin, Enum): + """An enum of all available sentinel values.""" + + ISO = r"&#10003;", r"utf8=%26%2310003%3B" + """This is what browsers will submit when the ``✓`` character occurs in an ``application/x-www-form-urlencoded`` + body and the encoding of the page containing the form is ``iso-8859-1``, or when the submitted form has an + ``accept-charset`` attribute of ``iso-8859-1``. Presumably also with other charsets that do not contain the ``✓`` + character, such as ``us-ascii``.""" + + CHARSET = r"✓", r"utf8=%E2%9C%93" + """These are the percent-encoded ``utf-8`` octets representing a checkmark, indicating that the request actually is + ``utf-8`` encoded."""
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/models/decode_options.html b/_modules/qs_codec/models/decode_options.html new file mode 100644 index 0000000..22c61b9 --- /dev/null +++ b/_modules/qs_codec/models/decode_options.html @@ -0,0 +1,199 @@ + + + + + + + qs_codec.models.decode_options — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.models.decode_options

+"""This module contains the ``DecodeOptions`` class that configures the output of ``decode``."""
+
+import typing as t
+from dataclasses import dataclass, field
+
+from ..enums.charset import Charset
+from ..enums.duplicates import Duplicates
+from ..utils.decode_utils import DecodeUtils
+
+
+
+[docs] +@dataclass +class DecodeOptions: + """Options that configure the output of ``decode``.""" + + allow_dots: bool = field(default=None) # type: ignore [assignment] + """Set to ``True`` to decode dot ``dict`` notation in the encoded input.""" + + decode_dot_in_keys: bool = field(default=None) # type: ignore [assignment] + """Set to ``True`` to decode dots in keys. + Note: it implies ``allow_dots``, so ``decode`` will error if you set ``decode_dot_in_keys`` to ``True``, and + ``allow_dots`` to ``False``.""" + + allow_empty_lists: bool = False + """Set to ``True`` to allow empty ``list`` values inside ``dict``\\s in the encoded input.""" + + list_limit: int = 20 + """``qs_codec`` will 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``. + This limit can be overridden by passing a ``list_limit`` option.""" + + charset: Charset = Charset.UTF8 + """The character encoding to use when decoding the input.""" + + charset_sentinel: bool = False + """Some services add an initial ``utf8=✓`` value to forms so that old InternetExplorer 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. + + ``qs_codec`` supports this mechanism via the ``charset_sentinel`` option. + If specified, the ``utf-8`` parameter will be omitted from the returned ``dict``. + It will be used to switch to ``LATIN1`` or ``UTF8`` mode depending on how the checkmark is encoded. + + Important: When you specify both the ``charset`` option and the ``charset_sentinel`` option, + the ``charset`` will be overridden when the request contains a ``utf-8`` parameter from which the actual charset + can be deduced. In that sense the ``charset`` will behave as the default charset rather than the authoritative + charset.""" + + comma: bool = False + """Set to ``True`` to parse the input as a comma-separated value. + Note: nested ``dict`` s, such as ``'a={b:1},{c:d}'`` are not supported.""" + + delimiter: t.Union[str, t.Pattern[str]] = "&" + """The delimiter to use when splitting key-value pairs in the encoded input. Can be a ``str`` or a ``Pattern``.""" + + depth: int = 5 + """By default, when nesting ``dict``\\s ``qs_codec`` will only decode up to 5 children deep. + This depth can be overridden by setting the ``depth``. + The depth limit helps mitigate abuse when ``qs_codec`` is used to parse user input, + and it is recommended to keep it a reasonably small number.""" + + parameter_limit: t.Union[int, float] = 1000 + """For similar reasons, by default ``qs_codec`` will only parse up to 1000 parameters. This can be overridden by + passing a ``parameter_limit`` option.""" + + duplicates: Duplicates = Duplicates.COMBINE + """Change the duplicate key handling strategy.""" + + ignore_query_prefix: bool = False + """Set to ``True`` to ignore the leading question mark query prefix in the encoded input.""" + + interpret_numeric_entities: bool = False + """Set to ``True`` to interpret HTML numeric entities (``&#...;``) in the encoded input.""" + + parse_lists: bool = True + """To disable ``list`` parsing entirely, set ``parse_lists`` to ``False``.""" + + strict_null_handling: bool = False + """Set to true to decode values without ``=`` to ``None``.""" + + decoder: t.Callable[[t.Optional[str], t.Optional[Charset]], t.Any] = DecodeUtils.decode + """Set a ``Callable`` to affect the decoding of the input.""" + + def __post_init__(self): + """Post-initialization.""" + if self.allow_dots is None: + self.allow_dots = self.decode_dot_in_keys is True or False + if self.decode_dot_in_keys is None: + self.decode_dot_in_keys = False
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/models/encode_options.html b/_modules/qs_codec/models/encode_options.html new file mode 100644 index 0000000..723789a --- /dev/null +++ b/_modules/qs_codec/models/encode_options.html @@ -0,0 +1,225 @@ + + + + + + + qs_codec.models.encode_options — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.models.encode_options

+"""This module contains the ``EncodeOptions`` class that configures the output of ``encode``."""
+
+import typing as t
+from dataclasses import asdict, dataclass, field
+from datetime import datetime
+
+from ..enums.charset import Charset
+from ..enums.format import Format
+from ..enums.list_format import ListFormat
+from ..utils.encode_utils import EncodeUtils
+
+
+
+[docs] +@dataclass +class EncodeOptions: + """Options that configure the output of ``encode``.""" + + allow_dots: bool = field(default=None) # type: ignore [assignment] + """Set to ``True`` to use dot ``dict`` notation in the encoded output.""" + + add_query_prefix: bool = False + """Set to ``True`` to add a question mark ``?`` prefix to the encoded output.""" + + allow_empty_lists: bool = False + """Set to ``True`` to allow empty ``list`` s in the encoded output.""" + + indices: t.Optional[bool] = None + """Deprecated: Use ``list_format`` instead.""" + + list_format: ListFormat = ListFormat.INDICES + """The ``list`` encoding format to use.""" + + charset: Charset = Charset.UTF8 + """The character encoding to use.""" + + charset_sentinel: bool = False + """Set to ``True`` 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.""" + + delimiter: str = "&" + """The delimiter to use when joining key-value pairs in the encoded output.""" + + encode: bool = True + """Set to ``False`` to disable encoding.""" + + encode_dot_in_keys: bool = field(default=None) # type: ignore [assignment] + """Encode ``dict`` keys using dot notation by setting ``encode_dot_in_keys`` to ``True``. + Caveat: When ``encode_values_only`` is ``True`` as well as ``encode_dot_in_keys``, only dots in keys and nothing + else will be encoded.""" + + encode_values_only: bool = False + """Encoding can be disabled for keys by setting the ``encode_values_only`` to ``True``.""" + + format: Format = Format.RFC3986 + """The encoding format to use. + The default ``format`` is ``Format.RFC3986`` which encodes ``' '`` to ``%20`` which is backward compatible. + You can also set ``format`` to ``Format.RFC1738`` which encodes ``' '`` to ``+``.""" + + filter: t.Optional[t.Union[t.Callable, t.List[t.Union[str, int]]]] = field(default=None) + """Use the ``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. + If you pass a ``list``, it will be used to select properties and ``list`` indices to be encoded.""" + + skip_nulls: bool = False + """Set to ``True`` to completely skip encoding keys with ``None`` values.""" + + serialize_date: t.Callable[[datetime], str] = EncodeUtils.serialize_date + """If you only want to override the serialization of ``datetime`` objects, you can provide a ``Callable``.""" + + encoder: t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str] = field( # type: ignore [assignment] + default_factory=EncodeUtils.encode # type: ignore [arg-type] + ) + """Set an ``Encoder`` to affect the encoding of values. + Note: the ``encoder`` option does not apply if ``encode`` is ``False``.""" + + _encoder: t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str] = field(init=False, repr=False) + + @property # type: ignore [no-redef] + def encoder(self) -> t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]: # noqa: F811 + """Get the encoder function.""" + return lambda v, c=self.charset, f=self.format: self._encoder(v, c, f) # type: ignore [misc] + + @encoder.setter + def encoder(self, value: t.Optional[t.Callable[[t.Any, t.Optional[Charset], t.Optional[Format]], str]]) -> None: + self._encoder = value if callable(value) else EncodeUtils.encode # type: ignore [assignment] + + strict_null_handling: bool = False + """Set to ``True`` to distinguish between ``null`` values and empty ``str``\\ings. This way the encoded string + ``None`` values will have no ``=`` sign.""" + + comma_round_trip: t.Optional[bool] = None + """When ``list_format`` is set to ``ListFormat.COMMA``, you can also set ``comma_round_trip`` option to ``True`` or + ``False``, to append ``[]`` on single-item ``list``\\s, so that they can round trip through a parse.""" + + sort: t.Optional[t.Callable[[t.Any, t.Any], int]] = field(default=None) + """Set a ``Callable`` to affect the order of parameter keys.""" + + def __post_init__(self): + """Post-initialization.""" + if self.allow_dots is None: + self.allow_dots = self.encode_dot_in_keys is True or False + if self.encode_dot_in_keys is None: + self.encode_dot_in_keys = False + if self.indices is not None: + self.list_format = ListFormat.INDICES if self.indices else ListFormat.REPEAT + + def __eq__(self, other: object) -> bool: + """Compare two `EncodeOptions` objects.""" + if not isinstance(other, EncodeOptions): + return False + + self_dict = asdict(self) + other_dict = asdict(other) + + self_dict["encoder"] = self._encoder + other_dict["encoder"] = other._encoder + + return self_dict == other_dict
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/models/undefined.html b/_modules/qs_codec/models/undefined.html new file mode 100644 index 0000000..26aa528 --- /dev/null +++ b/_modules/qs_codec/models/undefined.html @@ -0,0 +1,121 @@ + + + + + + + qs_codec.models.undefined — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.models.undefined

+"""Undefined class definition."""
+
+
+
+[docs] +class Undefined: + """Singleton class to represent undefined values.""" + + _instance = None + + def __new__(cls): + """Create a new instance of the class.""" + if cls._instance is None: + cls._instance = super(Undefined, cls).__new__(cls) + return cls._instance
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/models/weak_wrapper.html b/_modules/qs_codec/models/weak_wrapper.html new file mode 100644 index 0000000..daa5357 --- /dev/null +++ b/_modules/qs_codec/models/weak_wrapper.html @@ -0,0 +1,147 @@ + + + + + + + qs_codec.models.weak_wrapper — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.models.weak_wrapper

+"""A wrapper that allows weak references to be used as dictionary keys."""
+
+import typing as t
+from dataclasses import dataclass
+
+
+
+[docs] +@dataclass(frozen=True) +class WeakWrapper: + """A wrapper that allows weak references to be used as dictionary keys.""" + + value: t.Any + + def __hash__(self) -> int: + """Return the hash of the value.""" + return hash(self._hash_recursive(self.value, seen=set(), stack=set())) + + def _hash_recursive( + self, value: t.Any, seen: set, stack: set, depth: int = 0, max_depth: int = 1000 + ) -> t.Union[t.Tuple, t.Any]: + """Recursively hash a value.""" + if id(value) in stack: + raise ValueError("Circular reference detected") + + seen.add(id(value)) + stack.add(id(value)) + + if depth > max_depth: + raise ValueError("Maximum recursion depth exceeded") + + if isinstance(value, t.Mapping): + result = tuple((k, self._hash_recursive(v, seen, stack, depth + 1)) for k, v in sorted(value.items())) + elif isinstance(value, (t.List, t.Set)): + result = tuple(self._hash_recursive(v, seen, stack, depth + 1) for v in value) + else: + result = value + + stack.remove(id(value)) + + return result
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/utils/decode_utils.html b/_modules/qs_codec/utils/decode_utils.html new file mode 100644 index 0000000..9227dfc --- /dev/null +++ b/_modules/qs_codec/utils/decode_utils.html @@ -0,0 +1,177 @@ + + + + + + + qs_codec.utils.decode_utils — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.utils.decode_utils

+"""Decode utility methods used by the library."""
+
+import re
+import typing as t
+from urllib.parse import unquote
+
+from ..enums.charset import Charset
+from .str_utils import code_unit_at
+
+
+
+[docs] +class DecodeUtils: + """A collection of decode utility methods used by the library.""" + +
+[docs] + @classmethod + def unescape(cls, string: str) -> str: + """A Python representation the deprecated JavaScript unescape function. + + https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/unescape + """ + buffer: t.List[str] = [] + + i: int = 0 + while i < len(string): + c: int = code_unit_at(string, i) + + if c == 0x25: + if string[i + 1] == "u": + buffer.append( + chr(int(string[i + 2 : i + 6], 16)), + ) + i += 6 + continue + + buffer.append(chr(int(string[i + 1 : i + 3], 16))) + i += 3 + continue + + buffer.append(string[i]) + i += 1 + + return "".join(buffer)
+ + +
+[docs] + @classmethod + def decode( + cls, + string: t.Optional[str], + charset: t.Optional[Charset] = Charset.UTF8, + ) -> t.Optional[str]: + """Decode a URL-encoded string.""" + if string is None: + return None + + string_without_plus: str = string.replace("+", " ") + + if charset == Charset.LATIN1: + return re.sub( + r"%[0-9a-f]{2}", + lambda match: cls.unescape(match.group(0)), + string_without_plus, + flags=re.IGNORECASE, + ) + + return unquote(string_without_plus)
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/utils/encode_utils.html b/_modules/qs_codec/utils/encode_utils.html new file mode 100644 index 0000000..cd24f14 --- /dev/null +++ b/_modules/qs_codec/utils/encode_utils.html @@ -0,0 +1,266 @@ + + + + + + + qs_codec.utils.encode_utils — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.utils.encode_utils

+"""A collection of encode utility methods used by the library."""
+
+import re
+import typing as t
+from datetime import datetime
+from decimal import Decimal
+from enum import Enum
+
+from ..enums.charset import Charset
+from ..enums.format import Format
+from .str_utils import code_unit_at
+
+
+
+[docs] +class EncodeUtils: + """A collection of encode utility methods used by the library.""" + + HEX_TABLE: t.Tuple[str, ...] = tuple(f"%{i.to_bytes(1, 'big').hex().upper().zfill(2)}" for i in range(256)) + """Hex table of all 256 characters""" + +
+[docs] + @classmethod + def escape( + cls, + string: str, + format: t.Optional[Format] = Format.RFC3986, + ) -> str: + """A Python representation the deprecated JavaScript escape function. + + https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/escape + """ + buffer: t.List[str] = [] + + i: int + for i, _ in enumerate(string): + c: int = code_unit_at(string, i) + + # These 69 characters are safe for escaping + # ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./ + if ( + (0x30 <= c <= 0x39) # 0-9 + or (0x41 <= c <= 0x5A) # A-Z + or (0x61 <= c <= 0x7A) # a-z + or c == 0x40 # @ + or c == 0x2A # * + or c == 0x5F # _ + or c == 0x2D # - + or c == 0x2B # + + or c == 0x2E # . + or c == 0x2F # / + or (format == Format.RFC1738 and (c == 0x28 or c == 0x29)) # ( ) + ): + buffer.append(string[i]) + continue + + if c < 256: + buffer.extend([f"%{c.to_bytes(1, 'big').hex().upper().zfill(2)}"]) + continue + + buffer.extend([f"%u{c.to_bytes(2, 'big').hex().upper().zfill(4)}"]) + + return "".join(buffer)
+ + +
+[docs] + @classmethod + def encode( + cls, + value: t.Any, + charset: t.Optional[Charset] = Charset.UTF8, + format: t.Optional[Format] = Format.RFC3986, + ) -> str: + """Encode a value to a URL-encoded string.""" + if value is None or not isinstance(value, (int, float, Decimal, Enum, str, bool, bytes)): + return "" + + string: str + if isinstance(value, bytes): + string = value.decode("utf-8") + elif isinstance(value, bool): + string = str(value).lower() + elif isinstance(value, str): + string = value + else: + string = str(value) + + if value == "": + return "" + + if charset == Charset.LATIN1: + return re.sub( + r"%u[0-9a-f]{4}", + lambda match: f"%26%23{int(match.group(0)[2:], 16)}%3B", + EncodeUtils.escape(string, format), + flags=re.IGNORECASE, + ) + + buffer: t.List[str] = [] + + i: int + for i, _ in enumerate(string): + c: int = code_unit_at(string, i) + + if ( + c == 0x2D # - + or c == 0x2E # . + or c == 0x5F # _ + or c == 0x7E # ~ + or (0x30 <= c <= 0x39) # 0-9 + or (0x41 <= c <= 0x5A) # a-z + or (0x61 <= c <= 0x7A) # A-Z + or (format == Format.RFC1738 and (c == 0x28 or c == 0x29)) # ( ) + ): + buffer.append(string[i]) + continue + elif c < 0x80: # ASCII + buffer.extend([cls.HEX_TABLE[c]]) + continue + elif c < 0x800: # 2 bytes + buffer.extend( + [ + cls.HEX_TABLE[0xC0 | (c >> 6)], + cls.HEX_TABLE[0x80 | (c & 0x3F)], + ], + ) + continue + elif c < 0xD800 or c >= 0xE000: # 3 bytes + buffer.extend( + [ + cls.HEX_TABLE[0xE0 | (c >> 12)], + cls.HEX_TABLE[0x80 | ((c >> 6) & 0x3F)], + cls.HEX_TABLE[0x80 | (c & 0x3F)], + ], + ) + continue + else: + i += 1 + c = 0x10000 + (((c & 0x3FF) << 10) | (code_unit_at(string, i) & 0x3FF)) + buffer.extend( + [ + cls.HEX_TABLE[0xF0 | (c >> 18)], + cls.HEX_TABLE[0x80 | ((c >> 12) & 0x3F)], + cls.HEX_TABLE[0x80 | ((c >> 6) & 0x3F)], + cls.HEX_TABLE[0x80 | (c & 0x3F)], + ], + ) + + return "".join(buffer)
+ + +
+[docs] + @staticmethod + def serialize_date(dt: datetime) -> str: + """Serialize a `datetime` object to an ISO 8601 string.""" + return dt.isoformat()
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/utils/str_utils.html b/_modules/qs_codec/utils/str_utils.html new file mode 100644 index 0000000..ecec379 --- /dev/null +++ b/_modules/qs_codec/utils/str_utils.html @@ -0,0 +1,123 @@ + + + + + + + qs_codec.utils.str_utils — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.utils.str_utils

+"""Utility functions for working with strings."""
+
+
+
+[docs] +def code_unit_at(string: str, index: int) -> int: + """Returns the 16-bit UTF-16 code unit at the given index. + + This function first encodes the string in UTF-16 little endian format, then calculates the code unit at the + given index. The code unit is calculated by taking the byte at the index and adding it to 256 times the next + byte. This is because UTF-16 represents each code unit with two bytes, and in little endian format, the least + significant byte comes first. + + Adapted from https://api.dart.dev/stable/3.3.3/dart-core/String/codeUnitAt.html + """ + encoded_string: bytes = string.encode("utf-16-le") + return encoded_string[index * 2] + 256 * encoded_string[index * 2 + 1]
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/qs_codec/utils/utils.html b/_modules/qs_codec/utils/utils.html new file mode 100644 index 0000000..01291fd --- /dev/null +++ b/_modules/qs_codec/utils/utils.html @@ -0,0 +1,347 @@ + + + + + + + qs_codec.utils.utils — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for qs_codec.utils.utils

+"""A collection of utility methods used by the library."""
+
+import copy
+import typing as t
+from datetime import datetime, timedelta
+from decimal import Decimal
+from enum import Enum
+
+from ..models.decode_options import DecodeOptions
+from ..models.undefined import Undefined
+
+
+
+[docs] +class Utils: + """A collection of utility methods used by the library.""" + +
+[docs] + @staticmethod + def merge( + target: t.Optional[t.Union[t.Mapping, t.List, t.Tuple]], + source: t.Optional[t.Union[t.Mapping, t.List, t.Tuple, t.Any]], + options: DecodeOptions = DecodeOptions(), + ) -> t.Union[t.Dict, t.List, t.Tuple, t.Any]: + """Merge two objects together.""" + if source is None: + return target + + if not isinstance(source, t.Mapping): + if isinstance(target, (list, tuple)): + if any(isinstance(el, Undefined) for el in target): + target_: t.Dict[int, t.Any] = dict(enumerate(target)) + + if isinstance(source, (list, tuple)): + for i, item in enumerate(source): + if not isinstance(item, Undefined): + target_[i] = item + else: + target_[len(target_)] = source + + target = list(filter(lambda el: not isinstance(el, Undefined), target_.values())) + else: + if isinstance(source, (list, tuple)): + if all((isinstance(el, t.Mapping) or isinstance(el, Undefined)) for el in target) and all( + (isinstance(el, t.Mapping) or isinstance(el, Undefined)) for el in source + ): + target__: t.Dict[int, t.Any] = dict(enumerate(target)) + target = list( + { + i: Utils.merge(target__[i], item, options) if i in target__ else item + for i, item in enumerate(source) + }.values() + ) + else: + if isinstance(target, tuple): + target = list(target) + target.extend(filter(lambda el: not isinstance(el, Undefined), source)) + elif source is not None: + if isinstance(target, tuple): + target = list(target) + target.append(source) + elif isinstance(target, t.Mapping): + if isinstance(source, (list, tuple)): + target = { + **target, + **{i: item for i, item in enumerate(source) if not isinstance(item, Undefined)}, + } + elif source is not None: + if not isinstance(target, (list, tuple)) and isinstance(source, (list, tuple)): + return [target, *filter(lambda el: not isinstance(el, Undefined), source)] + return [target, source] + + return target + + if target is None or not isinstance(target, t.Mapping): + if isinstance(target, (list, tuple)): + return { + **{i: item for i, item in enumerate(target) if not isinstance(item, Undefined)}, + **source, + } + + return [ + el + for el in (target if isinstance(target, (list, tuple)) else [target]) + if not isinstance(el, Undefined) + ] + [ + el + for el in (source if isinstance(source, (list, tuple)) else [source]) + if not isinstance(el, Undefined) + ] + + merge_target: t.Dict = ( + dict(enumerate(el for el in source if not isinstance(el, Undefined))) + if isinstance(target, (list, tuple)) and not isinstance(source, (list, tuple)) + else copy.deepcopy(dict(target) if not isinstance(target, dict) else target) + ) + + return { + **merge_target, + **{ + key: Utils.merge(merge_target[key], value, options) if key in merge_target else value + for key, value in source.items() + }, + }
+ + +
+[docs] + @staticmethod + def compact(value: t.Dict) -> t.Dict: + """Remove all `Undefined` values from a dictionary.""" + queue: t.List[t.Dict] = [{"obj": {"o": value}, "prop": "o"}] + refs: t.List = [] + + for i in range(len(queue)): # pylint: disable=C0200 + item: t.Mapping = queue[i] + obj: t.Mapping = item["obj"][item["prop"]] + + keys: t.List = list(obj.keys()) + for key in keys: + val = obj.get(key) + + if ( + val is not None + and not isinstance(val, Undefined) + and isinstance(val, t.Mapping) + and val not in refs + ): + queue.append({"obj": obj, "prop": key}) + refs.append(val) + + Utils._compact_queue(queue) + Utils._remove_undefined_from_map(value) + + return value
+ + + @staticmethod + def _compact_queue(queue: t.List[t.Dict]) -> None: + while len(queue) > 1: + item = queue.pop() + obj = item["obj"][item["prop"]] + + if isinstance(obj, (list, tuple)): + item["obj"][item["prop"]] = list(filter(lambda el: not isinstance(el, Undefined), obj)) + + @staticmethod + def _remove_undefined_from_list(value: t.List) -> None: + i: int = len(value) - 1 + while i >= 0: + item = value[i] + if isinstance(item, Undefined): + value.pop(i) + elif isinstance(item, dict): + Utils._remove_undefined_from_map(item) + elif isinstance(item, list): + Utils._remove_undefined_from_list(item) + elif isinstance(item, tuple): + value[i] = list(item) + Utils._remove_undefined_from_list(value[i]) + i -= 1 + + @staticmethod + def _remove_undefined_from_map(obj: t.Dict) -> None: + keys: t.List = list(obj.keys()) + for key in keys: + val = obj[key] + if isinstance(val, Undefined): + obj.pop(key) + elif isinstance(val, dict) and not Utils._dicts_are_equal(val, obj): + Utils._remove_undefined_from_map(val) + elif isinstance(val, list): + Utils._remove_undefined_from_list(val) + elif isinstance(val, tuple): + obj[key] = list(val) + Utils._remove_undefined_from_list(obj[key]) + + @staticmethod + def _dicts_are_equal(d1: t.Mapping, d2: t.Mapping, path=None) -> bool: + if path is None: + path = set() + + if id(d1) in path or id(d2) in path: + return True + + path.add(id(d1)) + path.add(id(d2)) + + if isinstance(d1, dict) and isinstance(d2, dict): + if len(d1) != len(d2): + return False + for k, v in d1.items(): + if k not in d2: + return False + if not Utils._dicts_are_equal(v, d2[k], path): + return False + return True + else: + return d1 == d2 + +
+[docs] + @staticmethod + def combine(a: t.Union[list, tuple, t.Any], b: t.Union[list, tuple, t.Any]) -> t.List: + """Combine two lists or values.""" + return [*(a if isinstance(a, (list, tuple)) else [a]), *(b if isinstance(b, (list, tuple)) else [b])]
+ + +
+[docs] + @staticmethod + def apply(val: t.Union[list, tuple, t.Any], fn: t.Callable) -> t.Union[t.List, t.Any]: + """Apply a function to a value or a list of values.""" + return [fn(item) for item in val] if isinstance(val, (list, tuple)) else fn(val)
+ + +
+[docs] + @staticmethod + def is_non_nullish_primitive(val: t.Any, skip_nulls: bool = False) -> bool: + """Check if a value is a non-nullish primitive.""" + if val is None: + return False + + if isinstance(val, str): + return val != "" if skip_nulls else True + + if isinstance(val, (int, float, Decimal, bool, Enum, datetime, timedelta)): + return True + + if isinstance(val, Undefined): + return False + + if isinstance(val, object): + if isinstance(val, (list, tuple, t.Mapping)): + return False + return True + + return False
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_sources/README.rst.txt b/_sources/README.rst.txt new file mode 100644 index 0000000..db8e9c8 --- /dev/null +++ b/_sources/README.rst.txt @@ -0,0 +1,951 @@ +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 + +:py:attr:`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 :py:attr:`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 :py:attr:`decode ` is used to parse user +input, and it is recommended to keep it a reasonably small number. + +For similar reasons, by default :py:attr:`decode ` will only parse up to 1000 parameters. This can be overridden by passing a +:py:attr:`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 +:py:attr:`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 :py:attr:`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'} + +:py:attr:`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 :py:attr:`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 :py:attr:`decode_dot_in_keys ` +can be used to decode dots in keys. + +**Note:** it implies :py:attr:`allow_dots `, so +:py:attr:`decode ` will error if you set :py:attr:`decode_dot_in_keys ` +to ``True``, and :py:attr:`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 :py:attr:`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 :py:attr:`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 :py:attr:`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. + +:py:attr:`decode ` supports this mechanism via the +:py:attr:`charset_sentinel ` option. +If specified, the ``utf8`` parameter will be omitted from the returned +``dict``. It will be used to switch to :py:attr:`LATIN1 ` or +:py:attr:`UTF8 ` mode depending on how the checkmark is encoded. + +**Important**: When you specify both the :py:attr:`charset ` +option and the :py:attr:`charset_sentinel ` option, the +:py:attr:`charset ` will be overridden when the request contains a +``utf8`` parameter from which the actual charset can be deduced. In that +sense the :py:attr:`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 `&#...; `_ syntax to the actual character, you can specify the +:py:attr:`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 +:py:attr:`charset_sentinel ` mode. + +lists +^^^^^ + +:py:attr:`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, +:py:attr:`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']} + +:py:attr:`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 :py:attr:`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 :py:attr:`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, :py:attr:`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'}]} + +(:py:attr:`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, :py:attr:`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 :py:attr:`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 +:py:attr:`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 +:py:attr:`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 :py:attr:`encoder ` option does not apply if +:py:attr:`encode ` is ``False``). + +Similar to :py:attr:`encoder ` there is a +:py:attr:`decoder ` option for :py:attr:`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 +:py:attr:`list_format ` option, which defaults to +:py:attr:`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 :py:attr:`indices ` option to +``False``, or to be more explicit, the :py:attr:`list_format ` +option to :py:attr:`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 :py:attr:`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 :py:attr:`list_format ` set to +:py:attr:`COMMA `, you can also pass the +:py:attr:`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. + +:py:attr:`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 +:py:attr:`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 +:py:attr:`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 :py:attr:`encode_values_only ` +and :py:attr:`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 +:py:attr:`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': {}}}) == '' + +:py:attr:`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 +:py:attr:`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 :py:attr:`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 `_ +objects, you can provide a ``Callable`` in the +:py:attr:`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 +:py:attr:`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 :py:attr:`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 +:py:attr:`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 +:py:attr:`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 +:py:attr:`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 +:py:attr:`LATIN1 ` using the +:py:attr:`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 :py:attr:`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 :py:attr:`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 +:py:attr:`UTF8 `, and +:py:attr:`LATIN1 ` support is also built in via +the :py:attr:`charset ` +and :py:attr:`charset ` parameter, +respectively. + +If you wish to encode query strings to a different character set (i.e. +`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 :py:attr:`format ` is +:py:attr:`RFC3986 ` which encodes +``' '`` to ``%20`` which is backward compatible. You can also set the +:py:attr:`format ` to +:py:attr:`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' diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt new file mode 100644 index 0000000..b0b2d69 --- /dev/null +++ b/_sources/index.rst.txt @@ -0,0 +1,92 @@ +.. qs-codec documentation master file, created by + sphinx-quickstart on Sun Apr 28 13:58:45 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +qs-codec +======== + +A query string encoding and decoding library for Python. + +Ported from `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'} + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + README + qs_codec + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +-------------- + +Special thanks to the authors of +`qs `__ for JavaScript: - `Jordan +Harband `__ - `TJ +Holowaychuk `__ + +.. |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 \ No newline at end of file diff --git a/_sources/qs_codec.enums.rst.txt b/_sources/qs_codec.enums.rst.txt new file mode 100644 index 0000000..49e6cd6 --- /dev/null +++ b/_sources/qs_codec.enums.rst.txt @@ -0,0 +1,53 @@ +qs\_codec.enums package +======================= + +Submodules +---------- + +qs\_codec.enums.charset module +------------------------------ + +.. automodule:: qs_codec.enums.charset + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.enums.duplicates module +--------------------------------- + +.. automodule:: qs_codec.enums.duplicates + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.enums.format module +----------------------------- + +.. automodule:: qs_codec.enums.format + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.enums.list\_format module +----------------------------------- + +.. automodule:: qs_codec.enums.list_format + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.enums.sentinel module +------------------------------- + +.. automodule:: qs_codec.enums.sentinel + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: qs_codec.enums + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/qs_codec.models.rst.txt b/_sources/qs_codec.models.rst.txt new file mode 100644 index 0000000..0cc38fd --- /dev/null +++ b/_sources/qs_codec.models.rst.txt @@ -0,0 +1,45 @@ +qs\_codec.models package +======================== + +Submodules +---------- + +qs\_codec.models.decode\_options module +--------------------------------------- + +.. automodule:: qs_codec.models.decode_options + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.models.encode\_options module +--------------------------------------- + +.. automodule:: qs_codec.models.encode_options + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.models.undefined module +--------------------------------- + +.. automodule:: qs_codec.models.undefined + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.models.weak\_wrapper module +------------------------------------- + +.. automodule:: qs_codec.models.weak_wrapper + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: qs_codec.models + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/qs_codec.rst.txt b/_sources/qs_codec.rst.txt new file mode 100644 index 0000000..a368826 --- /dev/null +++ b/_sources/qs_codec.rst.txt @@ -0,0 +1,39 @@ +qs\_codec package +================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + qs_codec.enums + qs_codec.models + qs_codec.utils + +Submodules +---------- + +qs\_codec.decode module +----------------------- + +.. automodule:: qs_codec.decode + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.encode module +----------------------- + +.. automodule:: qs_codec.encode + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: qs_codec + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/qs_codec.utils.rst.txt b/_sources/qs_codec.utils.rst.txt new file mode 100644 index 0000000..40fdf1f --- /dev/null +++ b/_sources/qs_codec.utils.rst.txt @@ -0,0 +1,45 @@ +qs\_codec.utils package +======================= + +Submodules +---------- + +qs\_codec.utils.decode\_utils module +------------------------------------ + +.. automodule:: qs_codec.utils.decode_utils + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.utils.encode\_utils module +------------------------------------ + +.. automodule:: qs_codec.utils.encode_utils + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.utils.str\_utils module +--------------------------------- + +.. automodule:: qs_codec.utils.str_utils + :members: + :undoc-members: + :show-inheritance: + +qs\_codec.utils.utils module +---------------------------- + +.. automodule:: qs_codec.utils.utils + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: qs_codec.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/_static/alabaster.css b/_static/alabaster.css new file mode 100644 index 0000000..e3174bf --- /dev/null +++ b/_static/alabaster.css @@ -0,0 +1,708 @@ +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar { + max-height: 100%; + overflow-y: auto; +} + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox input[type="text"] { + width: 160px; +} + +div.sphinxsidebar .search > div { + display: table-cell; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: #fff; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin-left: 0; + margin-right: 0; + margin-top: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #EEE; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Hide ugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} \ No newline at end of file diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 0000000..e5179b7 --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: inherit; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/custom.css b/_static/custom.css new file mode 100644 index 0000000..2a924f1 --- /dev/null +++ b/_static/custom.css @@ -0,0 +1 @@ +/* This file intentionally left blank. */ diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000..4d67807 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 0000000..add8e0c --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.2.1', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 0000000..367b8ed --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 0000000..04a4174 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,84 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #8f5902; font-style: italic } /* Comment */ +.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ +.highlight .g { color: #000000 } /* Generic */ +.highlight .k { color: #004461; font-weight: bold } /* Keyword */ +.highlight .l { color: #000000 } /* Literal */ +.highlight .n { color: #000000 } /* Name */ +.highlight .o { color: #582800 } /* Operator */ +.highlight .x { color: #000000 } /* Other */ +.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8f5902 } /* Comment.Preproc */ +.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #a40000 } /* Generic.Deleted */ +.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ +.highlight .ges { color: #000000 } /* Generic.EmphStrong */ +.highlight .gr { color: #ef2929 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #745334 } /* Generic.Prompt */ +.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ +.highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #000000 } /* Literal.Date */ +.highlight .m { color: #990000 } /* Literal.Number */ +.highlight .s { color: #4e9a06 } /* Literal.String */ +.highlight .na { color: #c4a000 } /* Name.Attribute */ +.highlight .nb { color: #004461 } /* Name.Builtin */ +.highlight .nc { color: #000000 } /* Name.Class */ +.highlight .no { color: #000000 } /* Name.Constant */ +.highlight .nd { color: #888888 } /* Name.Decorator */ +.highlight .ni { color: #ce5c00 } /* Name.Entity */ +.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000000 } /* Name.Function */ +.highlight .nl { color: #f57900 } /* Name.Label */ +.highlight .nn { color: #000000 } /* Name.Namespace */ +.highlight .nx { color: #000000 } /* Name.Other */ +.highlight .py { color: #000000 } /* Name.Property */ +.highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #000000 } /* Name.Variable */ +.highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ +.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #f8f8f8 } /* Text.Whitespace */ +.highlight .mb { color: #990000 } /* Literal.Number.Bin */ +.highlight .mf { color: #990000 } /* Literal.Number.Float */ +.highlight .mh { color: #990000 } /* Literal.Number.Hex */ +.highlight .mi { color: #990000 } /* Literal.Number.Integer */ +.highlight .mo { color: #990000 } /* Literal.Number.Oct */ +.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ +.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ +.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ +.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ +.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ +.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ +.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000000 } /* Name.Function.Magic */ +.highlight .vc { color: #000000 } /* Name.Variable.Class */ +.highlight .vg { color: #000000 } /* Name.Variable.Global */ +.highlight .vi { color: #000000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000000 } /* Name.Variable.Magic */ +.highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/searchtools.js b/_static/searchtools.js new file mode 100644 index 0000000..92da3f8 --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,619 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + "Search finished, found ${resultCount} page(s) matching the search query." + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlinks", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + let score = Math.round(100 * queryLower.length / title.length) + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/_static/sphinx_highlight.js b/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/genindex.html b/genindex.html new file mode 100644 index 0000000..66d70c8 --- /dev/null +++ b/genindex.html @@ -0,0 +1,613 @@ + + + + + + + Index — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Index

+ +
+ A + | B + | C + | D + | E + | F + | H + | I + | L + | M + | P + | Q + | R + | S + | U + | V + | W + +
+

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

H

+ + +
+ +

I

+ + + +
+ +

L

+ + + +
+ +

M

+ + +
+ +

P

+ + + +
+ +

Q

+ + + +
    +
  • + qs_codec + +
  • +
  • + qs_codec.decode + +
  • +
  • + qs_codec.encode + +
  • +
  • + qs_codec.enums + +
  • +
  • + qs_codec.enums.charset + +
  • +
  • + qs_codec.enums.duplicates + +
  • +
  • + qs_codec.enums.format + +
  • +
  • + qs_codec.enums.list_format + +
  • +
  • + qs_codec.enums.sentinel + +
  • +
  • + qs_codec.models + +
  • +
    +
  • + qs_codec.models.decode_options + +
  • +
  • + qs_codec.models.encode_options + +
  • +
  • + qs_codec.models.undefined + +
  • +
  • + qs_codec.models.weak_wrapper + +
  • +
  • + qs_codec.utils + +
  • +
  • + qs_codec.utils.decode_utils + +
  • +
  • + qs_codec.utils.encode_utils + +
  • +
  • + qs_codec.utils.str_utils + +
  • +
  • + qs_codec.utils.utils + +
  • +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

U

+ + + +
+ +

V

+ + +
+ +

W

+ + +
+ + + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..d7b3e4f --- /dev/null +++ b/index.html @@ -0,0 +1,201 @@ + + + + + + + + qs-codec — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000..2bc4706 Binary files /dev/null and b/objects.inv differ diff --git a/py-modindex.html b/py-modindex.html new file mode 100644 index 0000000..019a8c8 --- /dev/null +++ b/py-modindex.html @@ -0,0 +1,213 @@ + + + + + + + Python Module Index — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Python Module Index

+ +
+ q +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ q
+ qs_codec +
    + qs_codec.decode +
    + qs_codec.encode +
    + qs_codec.enums +
    + qs_codec.enums.charset +
    + qs_codec.enums.duplicates +
    + qs_codec.enums.format +
    + qs_codec.enums.list_format +
    + qs_codec.enums.sentinel +
    + qs_codec.models +
    + qs_codec.models.decode_options +
    + qs_codec.models.encode_options +
    + qs_codec.models.undefined +
    + qs_codec.models.weak_wrapper +
    + qs_codec.utils +
    + qs_codec.utils.decode_utils +
    + qs_codec.utils.encode_utils +
    + qs_codec.utils.str_utils +
    + qs_codec.utils.utils +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/qs_codec.enums.html b/qs_codec.enums.html new file mode 100644 index 0000000..bb4ed4c --- /dev/null +++ b/qs_codec.enums.html @@ -0,0 +1,329 @@ + + + + + + + + qs_codec.enums package — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

qs_codec.enums package

+
+

Submodules

+
+
+

qs_codec.enums.charset module

+

Charset enum module.

+
+
+class qs_codec.enums.charset.Charset(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: _CharsetDataMixin, Enum

+

Character set.

+
+
+LATIN1 = _CharsetDataMixin(encoding='iso-8859-1')
+

ISO-8859-1 (Latin-1) character encoding.

+
+ +
+
+UTF8 = _CharsetDataMixin(encoding='utf-8')
+

UTF-8 character encoding.

+
+ +
+ +
+
+

qs_codec.enums.duplicates module

+

This module contains an enum of all available duplicate key handling strategies.

+
+
+class qs_codec.enums.duplicates.Duplicates(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: Enum

+

An enum of all available duplicate key handling strategies.

+
+
+COMBINE = 1
+

Combine duplicate keys into a single key with an array of values.

+
+ +
+
+FIRST = 2
+

Use the first value for duplicate keys.

+
+ +
+
+LAST = 3
+

Use the last value for duplicate keys.

+
+ +
+ +
+
+

qs_codec.enums.format module

+

An enum of all supported URI component encoding formats.

+
+
+class qs_codec.enums.format.Format(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: _FormatDataMixin, Enum

+

An enum of all supported URI component encoding formats.

+
+
+RFC1738 = _FormatDataMixin(format_name='RFC1738', formatter=<function Formatter.rfc1738>)
+

RFC 1738.

+
+ +
+
+RFC3986 = _FormatDataMixin(format_name='RFC3986', formatter=<function Formatter.rfc3986>)
+

RFC 3986.

+
+ +
+ +
+
+class qs_codec.enums.format.Formatter[source]
+

Bases: object

+

A class for formatting URI components.

+
+
+static rfc1738(value: str) str[source]
+

Format a string according to RFC 1738.

+
+ +
+
+static rfc3986(value: str) str[source]
+

Format a string according to RFC 3986.

+
+ +
+ +
+
+

qs_codec.enums.list_format module

+

An enum for all available list format options.

+
+
+class qs_codec.enums.list_format.ListFormat(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: _ListFormatDataMixin, Enum

+

An enum of all available list format options.

+
+
+BRACKETS = _ListFormatDataMixin(list_format_name='BRACKETS', generator=<function ListFormatGenerator.brackets>)
+

Use brackets to represent list items, for example foo[]=123&foo[]=456&foo[]=789

+
+ +
+
+COMMA = _ListFormatDataMixin(list_format_name='COMMA', generator=<function ListFormatGenerator.comma>)
+

Use commas to represent list items, for example foo=123,456,789

+
+ +
+
+INDICES = _ListFormatDataMixin(list_format_name='INDICES', generator=<function ListFormatGenerator.indices>)
+

Use indices to represent list items, for example foo[0]=123&foo[1]=456&foo[2]=789

+
+ +
+
+REPEAT = _ListFormatDataMixin(list_format_name='REPEAT', generator=<function ListFormatGenerator.repeat>)
+

Use a repeat key to represent list items, for example foo=123&foo=456&foo=789

+
+ +
+ +
+
+class qs_codec.enums.list_format.ListFormatGenerator[source]
+

Bases: object

+

A class for formatting list items.

+
+
+static brackets(prefix: str, key: str | None = None) str[source]
+

Format a list item using brackets.

+
+ +
+
+static comma(prefix: str, key: str | None = None) str[source]
+

Format a list item using commas.

+
+ +
+
+static indices(prefix: str, key: str | None = None) str[source]
+

Format a list item using indices.

+
+ +
+
+static repeat(prefix: str, key: str | None = None) str[source]
+

Format a list item using repeats.

+
+ +
+ +
+
+

qs_codec.enums.sentinel module

+

This module defines the Sentinel enum, which contains all available sentinel values.

+
+
+class qs_codec.enums.sentinel.Sentinel(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
+

Bases: _SentinelDataMixin, Enum

+

An enum of all available sentinel values.

+
+
+CHARSET = _SentinelDataMixin(raw='✓', encoded='utf8=%E2%9C%93')
+

These are the percent-encoded utf-8 octets representing a checkmark, indicating that the request actually is +utf-8 encoded.

+
+ +
+
+ISO = _SentinelDataMixin(raw='&#10003;', encoded='utf8=%26%2310003%3B')
+

This is what browsers will submit when the character occurs in an application/x-www-form-urlencoded +body and the encoding of the page containing the form is iso-8859-1, or when the submitted form has an +accept-charset attribute of iso-8859-1. Presumably also with other charsets that do not contain the +character, such as us-ascii.

+
+ +
+ +
+
+

Module contents

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/qs_codec.html b/qs_codec.html new file mode 100644 index 0000000..a12f98b --- /dev/null +++ b/qs_codec.html @@ -0,0 +1,328 @@ + + + + + + + + qs_codec package — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

qs_codec package

+
+

Subpackages

+
+ +
+
+
+

Submodules

+
+
+

qs_codec.decode module

+

A query string decoder (parser).

+
+
+qs_codec.decode.decode(value: str | ~typing.Mapping | None, options: ~qs_codec.models.decode_options.DecodeOptions = DecodeOptions(allow_dots=False, decode_dot_in_keys=False, allow_empty_lists=False, list_limit=20, charset=<Charset.UTF8: encoding='utf-8'>, charset_sentinel=False, comma=False, delimiter='&', depth=5, parameter_limit=1000, duplicates=<Duplicates.COMBINE: 1>, ignore_query_prefix=False, interpret_numeric_entities=False, parse_lists=True, strict_null_handling=False, decoder=<bound method DecodeUtils.decode of <class 'qs_codec.utils.decode_utils.DecodeUtils'>>)) dict[source]
+

Decodes a str or Mapping into a dict.

+

Providing custom DecodeOptions will override the default behavior.

+
+ +
+
+

qs_codec.encode module

+

A query string encoder (stringifier).

+
+
+qs_codec.encode.encode(value: ~typing.Any, options: ~qs_codec.models.encode_options.EncodeOptions = EncodeOptions(allow_dots=False, add_query_prefix=False, allow_empty_lists=False, indices=None, list_format=<ListFormat.INDICES: list_format_name='INDICES', generator=<function ListFormatGenerator.indices>>, charset=<Charset.UTF8: encoding='utf-8'>, charset_sentinel=False, delimiter='&', encode=True, encode_dot_in_keys=False, encode_values_only=False, format=<Format.RFC3986: format_name='RFC3986', formatter=<function Formatter.rfc3986>>, filter=None, skip_nulls=False, serialize_date=<function EncodeUtils.serialize_date>, encoder=<function EncodeOptions.encoder.<locals>.<lambda>>, strict_null_handling=False, comma_round_trip=None, sort=None)) str[source]
+

Encodes Any object into a query str ing.

+

Providing custom EncodeOptions will override the default behavior.

+
+ +
+
+

Module contents

+

A query string encoding and decoding library for Python. Ported from qs_codec for JavaScript.

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/qs_codec.models.html b/qs_codec.models.html new file mode 100644 index 0000000..c99322d --- /dev/null +++ b/qs_codec.models.html @@ -0,0 +1,436 @@ + + + + + + + + qs_codec.models package — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

qs_codec.models package

+
+

Submodules

+
+
+

qs_codec.models.decode_options module

+

This module contains the DecodeOptions class that configures the output of decode.

+
+
+class qs_codec.models.decode_options.DecodeOptions(allow_dots: bool = None, decode_dot_in_keys: bool = None, allow_empty_lists: bool = False, list_limit: int = 20, charset: ~qs_codec.enums.charset.Charset = Charset.UTF8, charset_sentinel: bool = False, comma: bool = False, delimiter: str | ~typing.Pattern[str] = '&', depth: int = 5, parameter_limit: int | float = 1000, duplicates: ~qs_codec.enums.duplicates.Duplicates = Duplicates.COMBINE, ignore_query_prefix: bool = False, interpret_numeric_entities: bool = False, parse_lists: bool = True, strict_null_handling: bool = False, decoder: ~typing.Callable[[str | None, ~qs_codec.enums.charset.Charset | None], ~typing.Any] = <bound method DecodeUtils.decode of <class 'qs_codec.utils.decode_utils.DecodeUtils'>>)[source]
+

Bases: object

+

Options that configure the output of decode.

+
+
+allow_dots: bool = None
+

Set to True to decode dot dict notation in the encoded input.

+
+ +
+
+allow_empty_lists: bool = False
+

Set to True to allow empty list values inside dicts in the encoded input.

+
+ +
+
+charset: Charset = _CharsetDataMixin(encoding='utf-8')
+

The character encoding to use when decoding the input.

+
+ +
+
+charset_sentinel: bool = False
+

Some services add an initial utf8=✓ value to forms so that old InternetExplorer 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.

+

qs_codec supports this mechanism via the charset_sentinel option. +If specified, the utf-8 parameter will be omitted from the returned dict. +It will be used to switch to LATIN1 or UTF8 mode depending on how the checkmark is encoded.

+

Important: When you specify both the charset option and the charset_sentinel option, +the charset will be overridden when the request contains a utf-8 parameter from which the actual charset +can be deduced. In that sense the charset will behave as the default charset rather than the authoritative +charset.

+
+ +
+
+comma: bool = False
+

Set to True to parse the input as a comma-separated value. +Note: nested dict s, such as 'a={b:1},{c:d}' are not supported.

+
+ +
+
+decode_dot_in_keys: bool = None
+

Set to True to decode dots in keys. +Note: it implies allow_dots, so decode will error if you set decode_dot_in_keys to True, and +allow_dots to False.

+
+ +
+
+classmethod decoder(string: str | None, charset: Charset | None = Charset.UTF8) str | None
+

Set a Callable to affect the decoding of the input.

+
+ +
+
+delimiter: str | Pattern[str] = '&'
+

The delimiter to use when splitting key-value pairs in the encoded input. Can be a str or a Pattern.

+
+ +
+
+depth: int = 5
+

By default, when nesting dicts qs_codec will only decode up to 5 children deep. +This depth can be overridden by setting the depth. +The depth limit helps mitigate abuse when qs_codec is used to parse user input, +and it is recommended to keep it a reasonably small number.

+
+ +
+
+duplicates: Duplicates = 1
+

Change the duplicate key handling strategy.

+
+ +
+
+ignore_query_prefix: bool = False
+

Set to True to ignore the leading question mark query prefix in the encoded input.

+
+ +
+
+interpret_numeric_entities: bool = False
+

Set to True to interpret HTML numeric entities (&#...;) in the encoded input.

+
+ +
+
+list_limit: int = 20
+

qs_codec will 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. +This limit can be overridden by passing a list_limit option.

+
+ +
+
+parameter_limit: int | float = 1000
+

For similar reasons, by default qs_codec will only parse up to 1000 parameters. This can be overridden by +passing a parameter_limit option.

+
+ +
+
+parse_lists: bool = True
+

To disable list parsing entirely, set parse_lists to False.

+
+ +
+
+strict_null_handling: bool = False
+

Set to true to decode values without = to None.

+
+ +
+ +
+
+

qs_codec.models.encode_options module

+

This module contains the EncodeOptions class that configures the output of encode.

+
+
+class qs_codec.models.encode_options.EncodeOptions(allow_dots: bool = None, add_query_prefix: bool = False, allow_empty_lists: bool = False, indices: bool | None = None, list_format: ~qs_codec.enums.list_format.ListFormat = ListFormat.INDICES, charset: ~qs_codec.enums.charset.Charset = Charset.UTF8, charset_sentinel: bool = False, delimiter: str = '&', encode: bool = True, encode_dot_in_keys: bool = None, encode_values_only: bool = False, format: ~qs_codec.enums.format.Format = Format.RFC3986, filter: ~typing.Callable | ~typing.List[str | int] | None = None, skip_nulls: bool = False, serialize_date: ~typing.Callable[[~datetime.datetime], str] = <function EncodeUtils.serialize_date>, encoder: ~typing.Callable[[~typing.Any, ~qs_codec.enums.charset.Charset | None, ~qs_codec.enums.format.Format | None], str] = <property object>, strict_null_handling: bool = False, comma_round_trip: bool | None = None, sort: ~typing.Callable[[~typing.Any, ~typing.Any], int] | None = None)[source]
+

Bases: object

+

Options that configure the output of encode.

+
+
+add_query_prefix: bool = False
+

Set to True to add a question mark ? prefix to the encoded output.

+
+ +
+
+allow_dots: bool = None
+

Set to True to use dot dict notation in the encoded output.

+
+ +
+
+allow_empty_lists: bool = False
+

Set to True to allow empty list s in the encoded output.

+
+ +
+
+charset: Charset = _CharsetDataMixin(encoding='utf-8')
+

The character encoding to use.

+
+ +
+
+charset_sentinel: bool = False
+

Set to True 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.

+
+ +
+
+comma_round_trip: bool | None = None
+

When list_format is set to ListFormat.COMMA, you can also set comma_round_trip option to True or +False, to append [] on single-item lists, so that they can round trip through a parse.

+
+ +
+
+delimiter: str = '&'
+

The delimiter to use when joining key-value pairs in the encoded output.

+
+ +
+
+encode: bool = True
+

Set to False to disable encoding.

+
+ +
+
+encode_dot_in_keys: bool = None
+

Encode dict keys using dot notation by setting encode_dot_in_keys to True. +Caveat: When encode_values_only is True as well as encode_dot_in_keys, only dots in keys and nothing +else will be encoded.

+
+ +
+
+encode_values_only: bool = False
+

Encoding can be disabled for keys by setting the encode_values_only to True.

+
+ +
+
+property encoder: Callable[[Any, Charset | None, Format | None], str]
+

Set an Encoder to affect the encoding of values. +Note: the encoder option does not apply if encode is False.

+
+ +
+
+filter: Callable | List[str | int] | None = None
+

Use the 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. +If you pass a list, it will be used to select properties and list indices to be encoded.

+
+ +
+
+format: Format = _FormatDataMixin(format_name='RFC3986', formatter=<function Formatter.rfc3986>)
+

The encoding format to use. +The default format is Format.RFC3986 which encodes ' ' to %20 which is backward compatible. +You can also set format to Format.RFC1738 which encodes ' ' to +.

+
+ +
+
+indices: bool | None = None
+

Use list_format instead.

+
+
Type:
+

Deprecated

+
+
+
+ +
+
+list_format: ListFormat = _ListFormatDataMixin(list_format_name='INDICES', generator=<function ListFormatGenerator.indices>)
+

The list encoding format to use.

+
+ +
+
+serialize_date() str
+

If you only want to override the serialization of datetime objects, you can provide a Callable.

+
+ +
+
+skip_nulls: bool = False
+

Set to True to completely skip encoding keys with None values.

+
+ +
+
+sort: Callable[[Any, Any], int] | None = None
+

Set a Callable to affect the order of parameter keys.

+
+ +
+
+strict_null_handling: bool = False
+

Set to True to distinguish between null values and empty strings. This way the encoded string +None values will have no = sign.

+
+ +
+ +
+
+

qs_codec.models.undefined module

+

Undefined class definition.

+
+
+class qs_codec.models.undefined.Undefined[source]
+

Bases: object

+

Singleton class to represent undefined values.

+
+ +
+
+

qs_codec.models.weak_wrapper module

+

A wrapper that allows weak references to be used as dictionary keys.

+
+
+class qs_codec.models.weak_wrapper.WeakWrapper(value: Any)[source]
+

Bases: object

+

A wrapper that allows weak references to be used as dictionary keys.

+
+
+value: Any
+
+ +
+ +
+
+

Module contents

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/qs_codec.utils.html b/qs_codec.utils.html new file mode 100644 index 0000000..f2c848c --- /dev/null +++ b/qs_codec.utils.html @@ -0,0 +1,255 @@ + + + + + + + + qs_codec.utils package — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

qs_codec.utils package

+
+

Submodules

+
+
+

qs_codec.utils.decode_utils module

+

Decode utility methods used by the library.

+
+
+class qs_codec.utils.decode_utils.DecodeUtils[source]
+

Bases: object

+

A collection of decode utility methods used by the library.

+
+
+classmethod decode(string: str | None, charset: Charset | None = Charset.UTF8) str | None[source]
+

Decode a URL-encoded string.

+
+ +
+
+classmethod unescape(string: str) str[source]
+

A Python representation the deprecated JavaScript unescape function.

+

https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/unescape

+
+ +
+ +
+
+

qs_codec.utils.encode_utils module

+

A collection of encode utility methods used by the library.

+
+
+class qs_codec.utils.encode_utils.EncodeUtils[source]
+

Bases: object

+

A collection of encode utility methods used by the library.

+
+
+HEX_TABLE: Tuple[str, ...] = ('%00', '%01', '%02', '%03', '%04', '%05', '%06', '%07', '%08', '%09', '%0A', '%0B', '%0C', '%0D', '%0E', '%0F', '%10', '%11', '%12', '%13', '%14', '%15', '%16', '%17', '%18', '%19', '%1A', '%1B', '%1C', '%1D', '%1E', '%1F', '%20', '%21', '%22', '%23', '%24', '%25', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C', '%2D', '%2E', '%2F', '%30', '%31', '%32', '%33', '%34', '%35', '%36', '%37', '%38', '%39', '%3A', '%3B', '%3C', '%3D', '%3E', '%3F', '%40', '%41', '%42', '%43', '%44', '%45', '%46', '%47', '%48', '%49', '%4A', '%4B', '%4C', '%4D', '%4E', '%4F', '%50', '%51', '%52', '%53', '%54', '%55', '%56', '%57', '%58', '%59', '%5A', '%5B', '%5C', '%5D', '%5E', '%5F', '%60', '%61', '%62', '%63', '%64', '%65', '%66', '%67', '%68', '%69', '%6A', '%6B', '%6C', '%6D', '%6E', '%6F', '%70', '%71', '%72', '%73', '%74', '%75', '%76', '%77', '%78', '%79', '%7A', '%7B', '%7C', '%7D', '%7E', '%7F', '%80', '%81', '%82', '%83', '%84', '%85', '%86', '%87', '%88', '%89', '%8A', '%8B', '%8C', '%8D', '%8E', '%8F', '%90', '%91', '%92', '%93', '%94', '%95', '%96', '%97', '%98', '%99', '%9A', '%9B', '%9C', '%9D', '%9E', '%9F', '%A0', '%A1', '%A2', '%A3', '%A4', '%A5', '%A6', '%A7', '%A8', '%A9', '%AA', '%AB', '%AC', '%AD', '%AE', '%AF', '%B0', '%B1', '%B2', '%B3', '%B4', '%B5', '%B6', '%B7', '%B8', '%B9', '%BA', '%BB', '%BC', '%BD', '%BE', '%BF', '%C0', '%C1', '%C2', '%C3', '%C4', '%C5', '%C6', '%C7', '%C8', '%C9', '%CA', '%CB', '%CC', '%CD', '%CE', '%CF', '%D0', '%D1', '%D2', '%D3', '%D4', '%D5', '%D6', '%D7', '%D8', '%D9', '%DA', '%DB', '%DC', '%DD', '%DE', '%DF', '%E0', '%E1', '%E2', '%E3', '%E4', '%E5', '%E6', '%E7', '%E8', '%E9', '%EA', '%EB', '%EC', '%ED', '%EE', '%EF', '%F0', '%F1', '%F2', '%F3', '%F4', '%F5', '%F6', '%F7', '%F8', '%F9', '%FA', '%FB', '%FC', '%FD', '%FE', '%FF')
+

Hex table of all 256 characters

+
+ +
+
+classmethod encode(value: Any, charset: Charset | None = Charset.UTF8, format: Format | None = Format.RFC3986) str[source]
+

Encode a value to a URL-encoded string.

+
+ +
+
+classmethod escape(string: str, format: Format | None = Format.RFC3986) str[source]
+

A Python representation the deprecated JavaScript escape function.

+

https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/escape

+
+ +
+
+static serialize_date(dt: datetime) str[source]
+

Serialize a datetime object to an ISO 8601 string.

+
+ +
+ +
+
+

qs_codec.utils.str_utils module

+

Utility functions for working with strings.

+
+
+qs_codec.utils.str_utils.code_unit_at(string: str, index: int) int[source]
+

Returns the 16-bit UTF-16 code unit at the given index.

+

This function first encodes the string in UTF-16 little endian format, then calculates the code unit at the +given index. The code unit is calculated by taking the byte at the index and adding it to 256 times the next +byte. This is because UTF-16 represents each code unit with two bytes, and in little endian format, the least +significant byte comes first.

+

Adapted from https://api.dart.dev/stable/3.3.3/dart-core/String/codeUnitAt.html

+
+ +
+
+

qs_codec.utils.utils module

+

A collection of utility methods used by the library.

+
+
+class qs_codec.utils.utils.Utils[source]
+

Bases: object

+

A collection of utility methods used by the library.

+
+
+static apply(val: list | tuple | Any, fn: Callable) List | Any[source]
+

Apply a function to a value or a list of values.

+
+ +
+
+static combine(a: list | tuple | Any, b: list | tuple | Any) List[source]
+

Combine two lists or values.

+
+ +
+
+static compact(value: Dict) Dict[source]
+

Remove all Undefined values from a dictionary.

+
+ +
+
+static is_non_nullish_primitive(val: Any, skip_nulls: bool = False) bool[source]
+

Check if a value is a non-nullish primitive.

+
+ +
+
+static merge(target: ~typing.Mapping | ~typing.List | ~typing.Tuple | None, source: ~typing.Mapping | ~typing.List | ~typing.Tuple | ~typing.Any | None, options: ~qs_codec.models.decode_options.DecodeOptions = DecodeOptions(allow_dots=False, decode_dot_in_keys=False, allow_empty_lists=False, list_limit=20, charset=<Charset.UTF8: encoding='utf-8'>, charset_sentinel=False, comma=False, delimiter='&', depth=5, parameter_limit=1000, duplicates=<Duplicates.COMBINE: 1>, ignore_query_prefix=False, interpret_numeric_entities=False, parse_lists=True, strict_null_handling=False, decoder=<bound method DecodeUtils.decode of <class 'qs_codec.utils.decode_utils.DecodeUtils'>>)) Dict | List | Tuple | Any[source]
+

Merge two objects together.

+
+ +
+ +
+
+

Module contents

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/search.html b/search.html new file mode 100644 index 0000000..a90626e --- /dev/null +++ b/search.html @@ -0,0 +1,125 @@ + + + + + + + Search — qs-codec 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + + +
+ + +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 0000000..4a94a8a --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"Contents:": [[1, null]], "Dealing with special character sets": [[0, "dealing-with-special-character-sets"]], "Decoding": [[0, "decoding"]], "Encoding": [[0, "encoding"]], "Handling None values": [[0, "handling-none-values"]], "Indices and tables": [[1, "indices-and-tables"]], "Module contents": [[2, "module-qs_codec"], [3, "module-qs_codec.enums"], [4, "module-qs_codec.models"], [5, "module-qs_codec.utils"]], "RFC 3986 and RFC 1738 space encoding": [[0, "rfc-3986-and-rfc-1738-space-encoding"]], "Submodules": [[2, "submodules"], [3, "submodules"], [4, "submodules"], [5, "submodules"]], "Subpackages": [[2, "subpackages"]], "Usage": [[1, "usage"]], "dictionaries": [[0, "dictionaries"]], "lists": [[0, "lists"]], "primitive values (int, bool, None, etc.)": [[0, "primitive-values-int-bool-none-etc"]], "qs-codec": [[1, "qs-codec"]], "qs_codec package": [[2, "qs-codec-package"]], "qs_codec.decode module": [[2, "module-qs_codec.decode"]], "qs_codec.encode module": [[2, "module-qs_codec.encode"]], "qs_codec.enums package": [[3, "qs-codec-enums-package"]], "qs_codec.enums.charset module": [[3, "module-qs_codec.enums.charset"]], "qs_codec.enums.duplicates module": [[3, "module-qs_codec.enums.duplicates"]], "qs_codec.enums.format module": [[3, "module-qs_codec.enums.format"]], "qs_codec.enums.list_format module": [[3, "module-qs_codec.enums.list_format"]], "qs_codec.enums.sentinel module": [[3, "module-qs_codec.enums.sentinel"]], "qs_codec.models package": [[4, "qs-codec-models-package"]], "qs_codec.models.decode_options module": [[4, "module-qs_codec.models.decode_options"]], "qs_codec.models.encode_options module": [[4, "module-qs_codec.models.encode_options"]], "qs_codec.models.undefined module": [[4, "module-qs_codec.models.undefined"]], "qs_codec.models.weak_wrapper module": [[4, "module-qs_codec.models.weak_wrapper"]], "qs_codec.utils package": [[5, "qs-codec-utils-package"]], "qs_codec.utils.decode_utils module": [[5, "module-qs_codec.utils.decode_utils"]], "qs_codec.utils.encode_utils module": [[5, "module-qs_codec.utils.encode_utils"]], "qs_codec.utils.str_utils module": [[5, "module-qs_codec.utils.str_utils"]], "qs_codec.utils.utils module": [[5, "module-qs_codec.utils.utils"]]}, "docnames": ["README", "index", "qs_codec", "qs_codec.enums", "qs_codec.models", "qs_codec.utils"], "envversion": {"sphinx": 61, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.viewcode": 1}, "filenames": ["README.rst", "index.rst", "qs_codec.rst", "qs_codec.enums.rst", "qs_codec.models.rst", "qs_codec.utils.rst"], "indexentries": {"add_query_prefix (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.add_query_prefix", false]], "allow_dots (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.allow_dots", false]], "allow_dots (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.allow_dots", false]], "allow_empty_lists (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.allow_empty_lists", false]], "allow_empty_lists (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.allow_empty_lists", false]], "apply() (qs_codec.utils.utils.utils static method)": [[5, "qs_codec.utils.utils.Utils.apply", false]], "brackets (qs_codec.enums.list_format.listformat attribute)": [[3, "qs_codec.enums.list_format.ListFormat.BRACKETS", false]], "brackets() (qs_codec.enums.list_format.listformatgenerator static method)": [[3, "qs_codec.enums.list_format.ListFormatGenerator.brackets", false]], "charset (class in qs_codec.enums.charset)": [[3, "qs_codec.enums.charset.Charset", false]], "charset (qs_codec.enums.sentinel.sentinel attribute)": [[3, "qs_codec.enums.sentinel.Sentinel.CHARSET", false]], "charset (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.charset", false]], "charset (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.charset", false]], "charset_sentinel (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.charset_sentinel", false]], "charset_sentinel (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.charset_sentinel", false]], "code_unit_at() (in module qs_codec.utils.str_utils)": [[5, "qs_codec.utils.str_utils.code_unit_at", false]], "combine (qs_codec.enums.duplicates.duplicates attribute)": [[3, "qs_codec.enums.duplicates.Duplicates.COMBINE", false]], "combine() (qs_codec.utils.utils.utils static method)": [[5, "qs_codec.utils.utils.Utils.combine", false]], "comma (qs_codec.enums.list_format.listformat attribute)": [[3, "qs_codec.enums.list_format.ListFormat.COMMA", false]], "comma (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.comma", false]], "comma() (qs_codec.enums.list_format.listformatgenerator static method)": [[3, "qs_codec.enums.list_format.ListFormatGenerator.comma", false]], "comma_round_trip (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.comma_round_trip", false]], "compact() (qs_codec.utils.utils.utils static method)": [[5, "qs_codec.utils.utils.Utils.compact", false]], "decode() (in module qs_codec.decode)": [[2, "qs_codec.decode.decode", false]], "decode() (qs_codec.utils.decode_utils.decodeutils class method)": [[5, "qs_codec.utils.decode_utils.DecodeUtils.decode", false]], "decode_dot_in_keys (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.decode_dot_in_keys", false]], "decodeoptions (class in qs_codec.models.decode_options)": [[4, "qs_codec.models.decode_options.DecodeOptions", false]], "decoder() (qs_codec.models.decode_options.decodeoptions class method)": [[4, "qs_codec.models.decode_options.DecodeOptions.decoder", false]], "decodeutils (class in qs_codec.utils.decode_utils)": [[5, "qs_codec.utils.decode_utils.DecodeUtils", false]], "delimiter (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.delimiter", false]], "delimiter (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.delimiter", false]], "depth (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.depth", false]], "duplicates (class in qs_codec.enums.duplicates)": [[3, "qs_codec.enums.duplicates.Duplicates", false]], "duplicates (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.duplicates", false]], "encode (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.encode", false]], "encode() (in module qs_codec.encode)": [[2, "qs_codec.encode.encode", false]], "encode() (qs_codec.utils.encode_utils.encodeutils class method)": [[5, "qs_codec.utils.encode_utils.EncodeUtils.encode", false]], "encode_dot_in_keys (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.encode_dot_in_keys", false]], "encode_values_only (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.encode_values_only", false]], "encodeoptions (class in qs_codec.models.encode_options)": [[4, "qs_codec.models.encode_options.EncodeOptions", false]], "encoder (qs_codec.models.encode_options.encodeoptions property)": [[4, "qs_codec.models.encode_options.EncodeOptions.encoder", false]], "encodeutils (class in qs_codec.utils.encode_utils)": [[5, "qs_codec.utils.encode_utils.EncodeUtils", false]], "escape() (qs_codec.utils.encode_utils.encodeutils class method)": [[5, "qs_codec.utils.encode_utils.EncodeUtils.escape", false]], "filter (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.filter", false]], "first (qs_codec.enums.duplicates.duplicates attribute)": [[3, "qs_codec.enums.duplicates.Duplicates.FIRST", false]], "format (class in qs_codec.enums.format)": [[3, "qs_codec.enums.format.Format", false]], "format (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.format", false]], "formatter (class in qs_codec.enums.format)": [[3, "qs_codec.enums.format.Formatter", false]], "hex_table (qs_codec.utils.encode_utils.encodeutils attribute)": [[5, "qs_codec.utils.encode_utils.EncodeUtils.HEX_TABLE", false]], "ignore_query_prefix (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.ignore_query_prefix", false]], "indices (qs_codec.enums.list_format.listformat attribute)": [[3, "qs_codec.enums.list_format.ListFormat.INDICES", false]], "indices (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.indices", false]], "indices() (qs_codec.enums.list_format.listformatgenerator static method)": [[3, "qs_codec.enums.list_format.ListFormatGenerator.indices", false]], "interpret_numeric_entities (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.interpret_numeric_entities", false]], "is_non_nullish_primitive() (qs_codec.utils.utils.utils static method)": [[5, "qs_codec.utils.utils.Utils.is_non_nullish_primitive", false]], "iso (qs_codec.enums.sentinel.sentinel attribute)": [[3, "qs_codec.enums.sentinel.Sentinel.ISO", false]], "last (qs_codec.enums.duplicates.duplicates attribute)": [[3, "qs_codec.enums.duplicates.Duplicates.LAST", false]], "latin1 (qs_codec.enums.charset.charset attribute)": [[3, "qs_codec.enums.charset.Charset.LATIN1", false]], "list_format (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.list_format", false]], "list_limit (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.list_limit", false]], "listformat (class in qs_codec.enums.list_format)": [[3, "qs_codec.enums.list_format.ListFormat", false]], "listformatgenerator (class in qs_codec.enums.list_format)": [[3, "qs_codec.enums.list_format.ListFormatGenerator", false]], "merge() (qs_codec.utils.utils.utils static method)": [[5, "qs_codec.utils.utils.Utils.merge", false]], "module": [[2, "module-qs_codec", false], [2, "module-qs_codec.decode", false], [2, "module-qs_codec.encode", false], [3, "module-qs_codec.enums", false], [3, "module-qs_codec.enums.charset", false], [3, "module-qs_codec.enums.duplicates", false], [3, "module-qs_codec.enums.format", false], [3, "module-qs_codec.enums.list_format", false], [3, "module-qs_codec.enums.sentinel", false], [4, "module-qs_codec.models", false], [4, "module-qs_codec.models.decode_options", false], [4, "module-qs_codec.models.encode_options", false], [4, "module-qs_codec.models.undefined", false], [4, "module-qs_codec.models.weak_wrapper", false], [5, "module-qs_codec.utils", false], [5, "module-qs_codec.utils.decode_utils", false], [5, "module-qs_codec.utils.encode_utils", false], [5, "module-qs_codec.utils.str_utils", false], [5, "module-qs_codec.utils.utils", false]], "parameter_limit (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.parameter_limit", false]], "parse_lists (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.parse_lists", false]], "qs_codec": [[2, "module-qs_codec", false]], "qs_codec.decode": [[2, "module-qs_codec.decode", false]], "qs_codec.encode": [[2, "module-qs_codec.encode", false]], "qs_codec.enums": [[3, "module-qs_codec.enums", false]], "qs_codec.enums.charset": [[3, "module-qs_codec.enums.charset", false]], "qs_codec.enums.duplicates": [[3, "module-qs_codec.enums.duplicates", false]], "qs_codec.enums.format": [[3, "module-qs_codec.enums.format", false]], "qs_codec.enums.list_format": [[3, "module-qs_codec.enums.list_format", false]], "qs_codec.enums.sentinel": [[3, "module-qs_codec.enums.sentinel", false]], "qs_codec.models": [[4, "module-qs_codec.models", false]], "qs_codec.models.decode_options": [[4, "module-qs_codec.models.decode_options", false]], "qs_codec.models.encode_options": [[4, "module-qs_codec.models.encode_options", false]], "qs_codec.models.undefined": [[4, "module-qs_codec.models.undefined", false]], "qs_codec.models.weak_wrapper": [[4, "module-qs_codec.models.weak_wrapper", false]], "qs_codec.utils": [[5, "module-qs_codec.utils", false]], "qs_codec.utils.decode_utils": [[5, "module-qs_codec.utils.decode_utils", false]], "qs_codec.utils.encode_utils": [[5, "module-qs_codec.utils.encode_utils", false]], "qs_codec.utils.str_utils": [[5, "module-qs_codec.utils.str_utils", false]], "qs_codec.utils.utils": [[5, "module-qs_codec.utils.utils", false]], "repeat (qs_codec.enums.list_format.listformat attribute)": [[3, "qs_codec.enums.list_format.ListFormat.REPEAT", false]], "repeat() (qs_codec.enums.list_format.listformatgenerator static method)": [[3, "qs_codec.enums.list_format.ListFormatGenerator.repeat", false]], "rfc1738 (qs_codec.enums.format.format attribute)": [[3, "qs_codec.enums.format.Format.RFC1738", false]], "rfc1738() (qs_codec.enums.format.formatter static method)": [[3, "qs_codec.enums.format.Formatter.rfc1738", false]], "rfc3986 (qs_codec.enums.format.format attribute)": [[3, "qs_codec.enums.format.Format.RFC3986", false]], "rfc3986() (qs_codec.enums.format.formatter static method)": [[3, "qs_codec.enums.format.Formatter.rfc3986", false]], "sentinel (class in qs_codec.enums.sentinel)": [[3, "qs_codec.enums.sentinel.Sentinel", false]], "serialize_date() (qs_codec.models.encode_options.encodeoptions method)": [[4, "qs_codec.models.encode_options.EncodeOptions.serialize_date", false]], "serialize_date() (qs_codec.utils.encode_utils.encodeutils static method)": [[5, "qs_codec.utils.encode_utils.EncodeUtils.serialize_date", false]], "skip_nulls (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.skip_nulls", false]], "sort (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.sort", false]], "strict_null_handling (qs_codec.models.decode_options.decodeoptions attribute)": [[4, "qs_codec.models.decode_options.DecodeOptions.strict_null_handling", false]], "strict_null_handling (qs_codec.models.encode_options.encodeoptions attribute)": [[4, "qs_codec.models.encode_options.EncodeOptions.strict_null_handling", false]], "undefined (class in qs_codec.models.undefined)": [[4, "qs_codec.models.undefined.Undefined", false]], "unescape() (qs_codec.utils.decode_utils.decodeutils class method)": [[5, "qs_codec.utils.decode_utils.DecodeUtils.unescape", false]], "utf8 (qs_codec.enums.charset.charset attribute)": [[3, "qs_codec.enums.charset.Charset.UTF8", false]], "utils (class in qs_codec.utils.utils)": [[5, "qs_codec.utils.utils.Utils", false]], "value (qs_codec.models.weak_wrapper.weakwrapper attribute)": [[4, "qs_codec.models.weak_wrapper.WeakWrapper.value", false]], "weakwrapper (class in qs_codec.models.weak_wrapper)": [[4, "qs_codec.models.weak_wrapper.WeakWrapper", false]]}, "objects": {"": [[2, 0, 0, "-", "qs_codec"]], "qs_codec": [[2, 0, 0, "-", "decode"], [2, 0, 0, "-", "encode"], [3, 0, 0, "-", "enums"], [4, 0, 0, "-", "models"], [5, 0, 0, "-", "utils"]], "qs_codec.decode": [[2, 1, 1, "", "decode"]], "qs_codec.encode": [[2, 1, 1, "", "encode"]], "qs_codec.enums": [[3, 0, 0, "-", "charset"], [3, 0, 0, "-", "duplicates"], [3, 0, 0, "-", "format"], [3, 0, 0, "-", "list_format"], [3, 0, 0, "-", "sentinel"]], "qs_codec.enums.charset": [[3, 2, 1, "", "Charset"]], "qs_codec.enums.charset.Charset": [[3, 3, 1, "", "LATIN1"], [3, 3, 1, "", "UTF8"]], "qs_codec.enums.duplicates": [[3, 2, 1, "", "Duplicates"]], "qs_codec.enums.duplicates.Duplicates": [[3, 3, 1, "", "COMBINE"], [3, 3, 1, "", "FIRST"], [3, 3, 1, "", "LAST"]], "qs_codec.enums.format": [[3, 2, 1, "", "Format"], [3, 2, 1, "", "Formatter"]], "qs_codec.enums.format.Format": [[3, 3, 1, "", "RFC1738"], [3, 3, 1, "", "RFC3986"]], "qs_codec.enums.format.Formatter": [[3, 4, 1, "", "rfc1738"], [3, 4, 1, "", "rfc3986"]], "qs_codec.enums.list_format": [[3, 2, 1, "", "ListFormat"], [3, 2, 1, "", "ListFormatGenerator"]], "qs_codec.enums.list_format.ListFormat": [[3, 3, 1, "", "BRACKETS"], [3, 3, 1, "", "COMMA"], [3, 3, 1, "", "INDICES"], [3, 3, 1, "", "REPEAT"]], "qs_codec.enums.list_format.ListFormatGenerator": [[3, 4, 1, "", "brackets"], [3, 4, 1, "", "comma"], [3, 4, 1, "", "indices"], [3, 4, 1, "", "repeat"]], "qs_codec.enums.sentinel": [[3, 2, 1, "", "Sentinel"]], "qs_codec.enums.sentinel.Sentinel": [[3, 3, 1, "", "CHARSET"], [3, 3, 1, "", "ISO"]], "qs_codec.models": [[4, 0, 0, "-", "decode_options"], [4, 0, 0, "-", "encode_options"], [4, 0, 0, "-", "undefined"], [4, 0, 0, "-", "weak_wrapper"]], "qs_codec.models.decode_options": [[4, 2, 1, "", "DecodeOptions"]], "qs_codec.models.decode_options.DecodeOptions": [[4, 3, 1, "", "allow_dots"], [4, 3, 1, "", "allow_empty_lists"], [4, 3, 1, "", "charset"], [4, 3, 1, "", "charset_sentinel"], [4, 3, 1, "", "comma"], [4, 3, 1, "", "decode_dot_in_keys"], [4, 4, 1, "", "decoder"], [4, 3, 1, "", "delimiter"], [4, 3, 1, "", "depth"], [4, 3, 1, "", "duplicates"], [4, 3, 1, "", "ignore_query_prefix"], [4, 3, 1, "", "interpret_numeric_entities"], [4, 3, 1, "", "list_limit"], [4, 3, 1, "", "parameter_limit"], [4, 3, 1, "", "parse_lists"], [4, 3, 1, "", "strict_null_handling"]], "qs_codec.models.encode_options": [[4, 2, 1, "", "EncodeOptions"]], "qs_codec.models.encode_options.EncodeOptions": [[4, 3, 1, "", "add_query_prefix"], [4, 3, 1, "", "allow_dots"], [4, 3, 1, "", "allow_empty_lists"], [4, 3, 1, "", "charset"], [4, 3, 1, "", "charset_sentinel"], [4, 3, 1, "", "comma_round_trip"], [4, 3, 1, "", "delimiter"], [4, 3, 1, "", "encode"], [4, 3, 1, "", "encode_dot_in_keys"], [4, 3, 1, "", "encode_values_only"], [4, 5, 1, "", "encoder"], [4, 3, 1, "", "filter"], [4, 3, 1, "", "format"], [4, 3, 1, "", "indices"], [4, 3, 1, "", "list_format"], [4, 4, 1, "", "serialize_date"], [4, 3, 1, "", "skip_nulls"], [4, 3, 1, "", "sort"], [4, 3, 1, "", "strict_null_handling"]], "qs_codec.models.undefined": [[4, 2, 1, "", "Undefined"]], "qs_codec.models.weak_wrapper": [[4, 2, 1, "", "WeakWrapper"]], "qs_codec.models.weak_wrapper.WeakWrapper": [[4, 3, 1, "", "value"]], "qs_codec.utils": [[5, 0, 0, "-", "decode_utils"], [5, 0, 0, "-", "encode_utils"], [5, 0, 0, "-", "str_utils"], [5, 0, 0, "-", "utils"]], "qs_codec.utils.decode_utils": [[5, 2, 1, "", "DecodeUtils"]], "qs_codec.utils.decode_utils.DecodeUtils": [[5, 4, 1, "", "decode"], [5, 4, 1, "", "unescape"]], "qs_codec.utils.encode_utils": [[5, 2, 1, "", "EncodeUtils"]], "qs_codec.utils.encode_utils.EncodeUtils": [[5, 3, 1, "", "HEX_TABLE"], [5, 4, 1, "", "encode"], [5, 4, 1, "", "escape"], [5, 4, 1, "", "serialize_date"]], "qs_codec.utils.str_utils": [[5, 1, 1, "", "code_unit_at"]], "qs_codec.utils.utils": [[5, 2, 1, "", "Utils"]], "qs_codec.utils.utils.Utils": [[5, 4, 1, "", "apply"], [5, 4, 1, "", "combine"], [5, 4, 1, "", "compact"], [5, 4, 1, "", "is_non_nullish_primitive"], [5, 4, 1, "", "merge"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "attribute", "Python attribute"], "4": ["py", "method", "Python method"], "5": ["py", "property", "Python property"]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:attribute", "4": "py:method", "5": "py:property"}, "terms": {"": [0, 4], "0": [0, 3], "00": [0, 5], "01": [0, 5], "01t00": 0, "02": 5, "02x": 0, "03": 5, "04": 5, "05": 5, "06": 5, "07": [0, 5], "08": 5, "09": 5, "0a": 5, "0b": 5, "0c": 5, "0d": 5, "0e": 5, "0f": 5, "1": [0, 2, 3, 4, 5], "10": 5, "100": 0, "1000": [0, 2, 4, 5], "10003": 3, "11": [0, 5], "12": 5, "123": [0, 3], "13": 5, "14": 5, "15": [0, 5], "16": [0, 5], "17": 5, "1738": [1, 3], "18": 5, "19": 5, "1970": 0, "1a": 5, "1b": 5, "1c": 5, "1d": 5, "1e": 5, "1f": 5, "2": [0, 3], "20": [0, 2, 4, 5], "20c": 0, "21": 5, "22": 5, "23": 5, "2310003": [0, 3], "239786": 0, "24": 5, "25": 5, "252eobj": 0, "256": 5, "26": [0, 3, 5], "27": 5, "28": 5, "29": 5, "2a": 5, "2b": 5, "2c": 5, "2d": 5, "2e": 5, "2f": 5, "3": [0, 3, 5], "30": 5, "31": 5, "32": 5, "33": 5, "34": 5, "35": 5, "36": 5, "37": 5, "38": 5, "39": 5, "3986": [1, 3], "3a": 5, "3b": [0, 3, 5], "3c": 5, "3d": 5, "3df": 0, "3e": 5, "3f": 5, "4": 0, "40": 5, "41": 5, "42": 5, "43": 5, "44": 5, "45": 5, "456": 3, "46": 5, "47": 5, "48": 5, "49": [0, 5], "4a": 5, "4b": 5, "4c": 5, "4d": 5, "4e": 5, "4f": 5, "5": [0, 2, 4, 5], "50": 5, "51": 5, "52": 5, "53": 5, "54": 5, "55": 5, "56": 5, "57": 5, "58": 5, "59": 5, "5a": 5, "5b": 5, "5bb": 0, "5c": 5, "5d": [0, 5], "5e": 5, "5f": 5, "60": 5, "61": [0, 5], "62": 5, "63": 5, "64": 5, "65": 5, "66": 5, "67": 5, "68": 5, "69": 5, "6a": 5, "6b": 5, "6c": 5, "6d": 5, "6e": 5, "6f": 5, "7": 0, "70": 5, "71": 5, "72": 5, "73": 5, "74": 5, "75": 5, "76": 5, "77": 5, "78": 5, "789": 3, "79": 5, "7a": 5, "7b": 5, "7c": 5, "7d": 5, "7e": 5, "7f": 5, "8": [0, 2, 3, 4, 5], "80": 5, "81": [0, 5], "82": [0, 5], "83": 5, "84": 5, "85": 5, "86": 5, "8601": [0, 5], "87": 5, "88": 5, "8859": 3, "89": 5, "8a": 5, "8b": 5, "8c": 5, "8d": 5, "8e": 5, "8f": 5, "90": 5, "91": 5, "92": 5, "93": [0, 3, 5], "94": 5, "95": 5, "96": 5, "97": 5, "98": [0, 5], "99": 5, "999999999": [0, 4], "9a": [0, 5], "9b": 5, "9c": [0, 3, 5], "9d": 5, "9e": 5, "9f": 5, "A": [1, 2, 3, 4, 5], "BE": 5, "By": [0, 4], "For": [0, 4], "If": [0, 4], "In": [0, 4], "It": [0, 4], "The": [0, 4, 5], "These": 3, "To": [0, 4], "_charsetdatamixin": [3, 4], "_formatdatamixin": [3, 4], "_listformatdatamixin": [3, 4], "_sentineldatamixin": 3, "a0": 5, "a1": 5, "a2": 5, "a3": 5, "a4": 5, "a5": 5, "a6": 5, "a7": [0, 5], "a8": 5, "a9": 5, "aa": 5, "ab": 5, "abus": [0, 4], "ac": 5, "accept": [0, 3, 4], "accord": 3, "actual": [0, 3, 4], "ad": 5, "adapt": 5, "add": [0, 4], "add_query_prefix": [0, 2, 4], "addition": [0, 4], "ae": 5, "af": 5, "affect": [0, 4], "against": [0, 4], "all": [0, 3, 5], "allow": [0, 4], "allow_dot": [0, 2, 4, 5], "allow_empty_list": [0, 2, 4, 5], "also": [0, 3, 4], "an": [0, 3, 4, 5], "ani": [0, 2, 4, 5], "announc": [0, 4], "api": 5, "append": [0, 4], "appli": [0, 2, 4, 5], "applic": [0, 3, 4], "ar": [0, 3, 4], "arrai": 3, "ascii": 3, "assert": [0, 1], "attempt": 0, "attribut": 3, "author": 1, "authorit": [0, 4], "avail": 3, "b": [0, 1, 4, 5], "b0": 5, "b1": [0, 5], "b2": 5, "b3": 5, "b4": 5, "b5": 5, "b6": 5, "b7": 5, "b8": [0, 5], "b9": 5, "ba": [0, 5], "back": 0, "backward": [0, 4], "bar": 0, "base": [3, 4, 5], "baz": 0, "bb": 5, "bc": 5, "bd": 5, "becaus": 5, "been": 0, "behav": [0, 4], "behavior": [0, 2], "between": [0, 4], "beyond": 0, "bf": [0, 5], "bit": 5, "bodi": [0, 3, 4], "bool": [1, 4, 5], "both": [0, 4], "bound": [2, 4, 5], "boundari": 3, "bracket": [0, 2, 3], "break": 0, "browser": [0, 3], "buf": 0, "built": 0, "bypass": 0, "byte": [0, 5], "c": [0, 4], "c0": 5, "c1": 5, "c2": 5, "c3": [0, 5], "c4": 5, "c5": 5, "c6": 5, "c7": 5, "c8": 5, "c9": [0, 5], "ca": 5, "calcul": 5, "call": [0, 4], "callabl": [0, 4, 5], "can": [0, 4], "cannot": 0, "case": [0, 4], "caveat": [0, 4], "cb": 5, "cc": 5, "cd": [0, 5], "ce": 5, "cf": 5, "chang": [0, 4], "charact": [1, 3, 4, 5], "charset": [0, 1, 2, 4, 5], "charset_sentinel": [0, 2, 4, 5], "check": [0, 4, 5], "checkmark": [0, 3, 4], "children": [0, 4], "clariti": 0, "class": [2, 3, 4, 5], "classmethod": [4, 5], "code": 5, "code_unit_at": [2, 5], "codec": 0, "codeunitat": 5, "collect": 5, "combin": [0, 2, 3, 4, 5], "come": 5, "comma": [0, 2, 3, 4, 5], "comma_round_trip": [0, 2, 4], "commun": 0, "compact": [0, 2, 5], "compat": [0, 4], "compil": 0, "complet": [0, 4], "compon": 3, "configur": 4, "contain": [0, 3, 4], "convert": [0, 4], "core": 5, "creat": 0, "custom": [0, 2], "custom_decod": 0, "custom_encod": 0, "d": [0, 4], "d0": 5, "d1": 5, "d2": 5, "d3": 5, "d4": 5, "d5": 5, "d6": 5, "d7": 5, "d8": 5, "d9": 5, "da": 5, "dart": 5, "date": 0, "datetim": [0, 4, 5], "db": 5, "dc": 5, "dd": 5, "de": 5, "deal": 1, "decod": [1, 4, 5], "decode_dot_in_kei": [0, 2, 4, 5], "decode_opt": [1, 2, 5], "decode_util": [1, 2, 4], "decodeopt": [0, 2, 4, 5], "decodeutil": [2, 4, 5], "deduc": [0, 4], "deep": [0, 4], "def": 0, "default": [0, 2, 4], "defin": 3, "definit": 4, "delimit": [0, 2, 4, 5], "depend": [0, 4], "deprec": [4, 5], "depth": [0, 2, 4, 5], "detect": [0, 4], "dev": 5, "develop": 5, "df": 5, "dict": [0, 2, 4, 5], "dictionari": [1, 4, 5], "differ": [0, 4], "disabl": [0, 4], "distinguish": [0, 4], "do": [0, 3, 4], "doc": 5, "doe": [0, 4], "don": 0, "done": 0, "dot": [0, 4], "dt": 5, "duplic": [0, 1, 2, 4, 5], "dure": 0, "e": [0, 4], "e0": 5, "e1": 5, "e2": [0, 3, 5], "e3": 5, "e4": 5, "e5": 5, "e6": [0, 5], "e7": 5, "e8": 5, "e9": 5, "ea": 5, "each": [0, 4, 5], "eb": 5, "ec": 5, "ed": 5, "ee": 5, "ef": 5, "els": [0, 4], "empti": [0, 4], "en": 5, "enabl": 0, "encod": [1, 3, 4, 5], "encode_dot_in_kei": [0, 2, 4], "encode_opt": [1, 2], "encode_util": [1, 2], "encode_values_onli": [0, 2, 4], "encodeopt": [0, 2, 4], "encodeutil": [2, 4, 5], "encount": 0, "end": 0, "endian": 5, "entir": [0, 4], "entiti": [0, 4], "enum": [1, 2, 4], "equal": 0, "error": [0, 4], "escap": [2, 5], "etc": 1, "exampl": [0, 1, 3, 4], "except": 0, "exist": 0, "expect": 0, "explicit": 0, "explor": 0, "express": 0, "f": 0, "f0": 5, "f1": [0, 5], "f2": 5, "f3": 5, "f4": 5, "f5": 5, "f6": 5, "f7": 5, "f8": [0, 5], "f9": 5, "fa": 5, "fals": [0, 2, 4, 5], "fb": 5, "fc": 5, "fd": 5, "fe": 5, "ff": 5, "filter": [0, 2, 4], "final": 0, "first": [0, 2, 3, 5], "flag": 0, "float": 4, "fn": 5, "follow": 0, "foo": [0, 3], "foobarbaz": 0, "form": [0, 3, 4], "format": [0, 1, 2, 4, 5], "format_nam": [2, 3, 4], "formatt": [2, 3, 4], "from": [0, 1, 2, 4, 5], "fromtimestamp": 0, "function": [2, 3, 4, 5], "g": [0, 4], "gener": [2, 3, 4], "get": 0, "given": [3, 5], "global_object": 5, "greater": [0, 4], "group": 0, "h": 0, "ha": [0, 3], "had": [0, 4], "handl": [1, 3, 4], "harband": 1, "have": [0, 4], "help": [0, 4], "hex": 5, "hex_tabl": [2, 5], "holowaychuk": 1, "how": [0, 4], "html": [4, 5], "http": 5, "huge": [0, 4], "i": [0, 3, 4, 5], "ignor": 4, "ignore_query_prefix": [0, 2, 4, 5], "ignorecas": 0, "impli": [0, 4], "import": [0, 1, 4], "includ": [0, 4], "index": [0, 1, 4, 5], "indic": [0, 2, 3, 4], "ing": [0, 2, 4], "initi": [0, 4], "input": [0, 4], "insid": 4, "instead": [0, 4], "int": [1, 4, 5], "internet": 0, "internetexplor": 4, "interpret": 4, "interpret_numeric_ent": [0, 2, 4, 5], "is_non_nullish_primit": [2, 5], "isinst": 0, "iso": [0, 2, 3, 5], "item": [0, 3, 4], "iter": [0, 4], "j": 0, "javascript": [1, 2, 5], "ji": 0, "john": 0, "join": [0, 4], "jordan": 1, "keep": [0, 4], "kei": [0, 3, 4], "lambda": [0, 2], "last": [0, 2, 3], "latin": 3, "latin1": [0, 2, 3, 4], "lead": [0, 4], "least": 5, "legaci": 0, "librari": [1, 2, 5], "like": [0, 4], "limit": [0, 4], "list": [1, 3, 4, 5], "list_format": [0, 1, 2, 4], "list_format_nam": [2, 3, 4], "list_limit": [0, 2, 4, 5], "listformat": [0, 2, 3, 4], "listformatgener": [2, 3, 4], "littl": 5, "local": 2, "mai": 0, "major": 0, "map": [0, 2, 5], "mark": [0, 4], "match": 0, "maximum": [0, 4], "mean": 0, "mechan": [0, 4], "member": [0, 4], "merg": [0, 2, 5], "method": [2, 4, 5], "minor": 0, "mitig": [0, 4], "mix": 0, "mode": [0, 4], "model": [1, 2, 5], "modul": 1, "more": [0, 4], "mozilla": 5, "must": 0, "name": [0, 3], "need": [0, 4], "nest": [0, 4], "next": 5, "non": 5, "none": [1, 2, 3, 4, 5], "notat": [0, 4], "note": [0, 4], "noth": [0, 4], "null": [0, 4], "nullish": 5, "number": [0, 4], "numer": [0, 4], "obj": 0, "object": [0, 2, 3, 4, 5], "obtain": [0, 4], "occur": 3, "octet": [0, 3], "old": [0, 4], "omit": [0, 4], "onli": [0, 4], "option": [0, 2, 3, 4, 5], "order": [0, 4], "org": 5, "other": [0, 3, 4], "otherwis": 0, "output": [0, 4], "over": [0, 4], "overrid": [0, 2, 4], "overridden": [0, 4], "packag": 1, "page": [0, 1, 3, 4], "pair": 4, "paramet": [0, 4], "parameter_limit": [0, 2, 4, 5], "pars": [0, 4], "parse_list": [0, 2, 4, 5], "parser": 2, "pass": [0, 4], "pattern": 4, "percent": [0, 3], "place": 0, "pleas": 0, "point": 0, "port": [1, 2], "prefix": [0, 3, 4], "prepend": 0, "preserv": 0, "presum": 3, "primit": [1, 5], "proper": [0, 4], "properti": [0, 4], "provid": [0, 2, 4], "python": [1, 2, 5], "q": 0, "qs_codec": [0, 1], "qualnam": 3, "queri": [0, 1, 2, 4], "question": [0, 4], "r": 0, "rail": [0, 4], "rather": [0, 4], "raw": 3, "re": 0, "real": 0, "reason": [0, 4], "recommend": [0, 4], "refer": [4, 5], "regular": 0, "remain": 0, "remov": 5, "render": 0, "repeat": [0, 2, 3], "replac": [0, 4], "repres": [3, 4, 5], "represent": 5, "request": [0, 3, 4], "respect": 0, "restrict": [0, 4], "result": 0, "return": [0, 4, 5], "rfc": [1, 3], "rfc1738": [0, 2, 3, 4], "rfc3986": [0, 2, 3, 4, 5], "round": [0, 4], "rubi": [0, 4], "search": [0, 1], "second": 0, "select": [0, 4], "sens": [0, 4], "sent": [0, 4], "sentinel": [1, 2], "separ": 4, "serial": [0, 4, 5], "serialize_d": [0, 2, 4, 5], "server": [0, 4], "servic": [0, 4], "set": [1, 3, 4], "shift": 0, "shift_ji": 0, "shown": 0, "sign": [0, 4], "signific": [0, 4, 5], "similar": [0, 4], "simpl": 1, "singl": [0, 3, 4], "singleton": 4, "skip": [0, 4], "skip_nul": [0, 2, 4, 5], "small": [0, 4], "so": [0, 4], "some": [0, 4], "someon": [0, 4], "sort": [0, 2, 4], "sourc": [2, 3, 4, 5], "space": 1, "spars": 0, "special": 1, "specif": 0, "specifi": [0, 4], "split": 4, "squar": 0, "stabl": 5, "start": 3, "static": [3, 5], "str": [0, 2, 3, 4, 5], "str_util": [1, 2], "strategi": [3, 4], "strict_null_handl": [0, 2, 4, 5], "string": [0, 1, 2, 3, 4, 5], "stringifi": 2, "sub": 0, "submit": [0, 3, 4], "submodul": 1, "subpackag": 1, "support": [0, 3, 4], "surround": 0, "switch": [0, 4], "sy": 0, "syntax": 0, "system": 0, "t": 0, "tabl": 5, "take": [0, 4, 5], "target": 5, "than": [0, 4], "thank": 1, "thei": [0, 4], "thi": [0, 3, 4, 5], "third": 0, "though": 0, "through": [0, 4], "time": [0, 4, 5], "timestamp": 0, "tj": 1, "togeth": 5, "too": 0, "treat": 0, "trip": [0, 4], "true": [0, 2, 4, 5], "try": 0, "tupl": 5, "two": [0, 5], "type": [0, 2, 3, 4, 5], "u": [3, 5], "undefin": [0, 1, 2, 5], "unescap": [2, 5], "union": 0, "unit": 5, "up": [0, 4], "uri": [0, 3], "url": 5, "urlencod": [0, 3, 4], "us": [0, 3, 4, 5], "usag": 0, "user": [0, 4], "utc": 0, "utcfromtimestamp": 0, "utf": [0, 2, 3, 4, 5], "utf8": [0, 2, 3, 4, 5], "util": [1, 2, 4], "val": 5, "valu": [1, 2, 3, 4, 5], "valueerror": 0, "version": [0, 4], "version_info": 0, "via": [0, 4], "wa": [0, 4], "wai": 4, "want": [0, 4], "weak": 4, "weak_wrapp": [1, 2], "weakwrapp": [2, 4], "web": 5, "well": [0, 4], "what": [0, 3, 4], "when": [0, 3, 4], "which": [0, 3, 4], "while": 0, "wish": 0, "within": 0, "without": [0, 4], "work": [0, 5], "would": 0, "wrapper": 4, "wrong": [0, 4], "www": [0, 3, 4], "x": [0, 3, 4], "y": 0, "you": [0, 4], "your": 0, "z": 0, "\u00e6": 0, "\u00f8": 0, "\u010d": 0, "\u3053\u3093\u306b\u3061\u306f": 0}, "titles": ["Decoding", "qs-codec", "qs_codec package", "qs_codec.enums package", "qs_codec.models package", "qs_codec.utils package"], "titleterms": {"1738": 0, "3986": 0, "bool": 0, "charact": 0, "charset": 3, "codec": 1, "content": [1, 2, 3, 4, 5], "deal": 0, "decod": [0, 2], "decode_opt": 4, "decode_util": 5, "dictionari": 0, "duplic": 3, "encod": [0, 2], "encode_opt": 4, "encode_util": 5, "enum": 3, "etc": 0, "format": 3, "handl": 0, "indic": 1, "int": 0, "list": 0, "list_format": 3, "model": 4, "modul": [2, 3, 4, 5], "none": 0, "packag": [2, 3, 4, 5], "primit": 0, "q": 1, "qs_codec": [2, 3, 4, 5], "rfc": 0, "sentinel": 3, "set": 0, "space": 0, "special": 0, "str_util": 5, "submodul": [2, 3, 4, 5], "subpackag": 2, "tabl": 1, "undefin": 4, "usag": 1, "util": 5, "valu": 0, "weak_wrapp": 4}}) \ No newline at end of file