| from __future__ import annotations |
|
|
| from typing import Any, List, Tuple, Union, Mapping, TypeVar |
| from urllib.parse import parse_qs, urlencode |
| from typing_extensions import Literal, get_args |
|
|
| from ._types import NotGiven, not_given |
| from ._utils import flatten |
|
|
| _T = TypeVar("_T") |
|
|
|
|
| ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] |
| NestedFormat = Literal["dots", "brackets"] |
|
|
| PrimitiveData = Union[str, int, float, bool, None] |
| |
| |
| Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] |
| Params = Mapping[str, Data] |
|
|
|
|
| class Querystring: |
| array_format: ArrayFormat |
| nested_format: NestedFormat |
|
|
| def __init__( |
| self, |
| *, |
| array_format: ArrayFormat = "repeat", |
| nested_format: NestedFormat = "brackets", |
| ) -> None: |
| self.array_format = array_format |
| self.nested_format = nested_format |
|
|
| def parse(self, query: str) -> Mapping[str, object]: |
| |
| return parse_qs(query) |
|
|
| def stringify( |
| self, |
| params: Params, |
| *, |
| array_format: ArrayFormat | NotGiven = not_given, |
| nested_format: NestedFormat | NotGiven = not_given, |
| ) -> str: |
| return urlencode( |
| self.stringify_items( |
| params, |
| array_format=array_format, |
| nested_format=nested_format, |
| ) |
| ) |
|
|
| def stringify_items( |
| self, |
| params: Params, |
| *, |
| array_format: ArrayFormat | NotGiven = not_given, |
| nested_format: NestedFormat | NotGiven = not_given, |
| ) -> list[tuple[str, str]]: |
| opts = Options( |
| qs=self, |
| array_format=array_format, |
| nested_format=nested_format, |
| ) |
| return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) |
|
|
| def _stringify_item( |
| self, |
| key: str, |
| value: Data, |
| opts: Options, |
| ) -> list[tuple[str, str]]: |
| if isinstance(value, Mapping): |
| items: list[tuple[str, str]] = [] |
| nested_format = opts.nested_format |
| for subkey, subvalue in value.items(): |
| items.extend( |
| self._stringify_item( |
| |
| f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", |
| subvalue, |
| opts, |
| ) |
| ) |
| return items |
|
|
| if isinstance(value, (list, tuple)): |
| array_format = opts.array_format |
| if array_format == "comma": |
| return [ |
| ( |
| key, |
| ",".join(self._primitive_value_to_str(item) for item in value if item is not None), |
| ), |
| ] |
| elif array_format == "repeat": |
| items = [] |
| for item in value: |
| items.extend(self._stringify_item(key, item, opts)) |
| return items |
| elif array_format == "indices": |
| raise NotImplementedError("The array indices format is not supported yet") |
| elif array_format == "brackets": |
| items = [] |
| key = key + "[]" |
| for item in value: |
| items.extend(self._stringify_item(key, item, opts)) |
| return items |
| else: |
| raise NotImplementedError( |
| f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" |
| ) |
|
|
| serialised = self._primitive_value_to_str(value) |
| if not serialised: |
| return [] |
| return [(key, serialised)] |
|
|
| def _primitive_value_to_str(self, value: PrimitiveData) -> str: |
| |
| if value is True: |
| return "true" |
| elif value is False: |
| return "false" |
| elif value is None: |
| return "" |
| return str(value) |
|
|
|
|
| _qs = Querystring() |
| parse = _qs.parse |
| stringify = _qs.stringify |
| stringify_items = _qs.stringify_items |
|
|
|
|
| class Options: |
| array_format: ArrayFormat |
| nested_format: NestedFormat |
|
|
| def __init__( |
| self, |
| qs: Querystring = _qs, |
| *, |
| array_format: ArrayFormat | NotGiven = not_given, |
| nested_format: NestedFormat | NotGiven = not_given, |
| ) -> None: |
| self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format |
| self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format |
|
|