Source code for hikari.internal.data_binding

# -*- coding: utf-8 -*-
# cython: language_level=3
# Copyright (c) 2020 Nekokatt
# Copyright (c) 2021-present davfsa
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Data binding utilities."""
from __future__ import annotations

__all__: typing.Sequence[str] = (
    "Headers",
    "Query",
    "JSONObject",
    "JSONArray",
    "JSONish",
    "dump_json",
    "load_json",
    "JSONDecodeError",
    "JSONObjectBuilder",
    "StringMapBuilder",
    "URLEncodedFormBuilder",
)

import typing

import aiohttp
import multidict

from hikari import errors
from hikari import files
from hikari import snowflakes
from hikari import undefined

if typing.TYPE_CHECKING:
    import concurrent.futures
    import contextlib

    T_co = typing.TypeVar("T_co", covariant=True)
    T = typing.TypeVar("T")

[docs]Headers = typing.Mapping[str, str]
"""Type hint for HTTP headers."""
[docs]Query = typing.Union[typing.Dict[str, str], multidict.MultiDict[str]]
"""Type hint for HTTP query string.""" # MyPy does not support recursive types yet. This has been ongoing for a long time, unfortunately. # See https://github.com/python/typing/issues/182
[docs]JSONObject = typing.Mapping[str, typing.Any]
"""Type hint for a JSON-decoded object representation as a mapping."""
[docs]JSONArray = typing.Sequence[typing.Any]
"""Type hint for a JSON-decoded array representation as a sequence."""
[docs]JSONish = typing.Union[str, int, float, bool, None, JSONArray, JSONObject]
"""Type hint for any valid JSON-decoded type.""" Stringish = typing.Union[str, int, bool, undefined.UndefinedType, None, snowflakes.Unique] """Type hint for any valid that can be put in a StringMapBuilder""" _StringMapBuilderArg = typing.Union[ typing.Mapping[str, str], multidict.MultiMapping[str], typing.Iterable[typing.Tuple[str, str]] ] _APPLICATION_OCTET_STREAM: typing.Final[str] = "application/octet-stream" if typing.TYPE_CHECKING:
[docs] JSONDecodeError: typing.Type[Exception] = Exception
"""Exception raised when loading an invalid JSON string.""" def dump_json(_: typing.Union[JSONArray, JSONObject], /, *, indent: int = ...) -> str: """Convert a Python type to a JSON string.""" raise NotImplementedError def load_json(_: typing.AnyStr, /) -> typing.Union[JSONArray, JSONObject]: """Convert a JSON string to a Python type.""" raise NotImplementedError else: import json dump_json = json.dumps """Convert a Python type to a JSON string.""" load_json = json.loads """Convert a JSON string to a Python type.""" JSONDecodeError = json.JSONDecodeError """Exception raised when loading an invalid JSON string.""" @typing.final
[docs]class URLEncodedFormBuilder: """Helper class to generate `aiohttp.FormData`.""" __slots__: typing.Sequence[str] = ("_executor", "_fields", "_resources") def __init__(self, executor: typing.Optional[concurrent.futures.Executor] = None) -> None: self._executor = executor self._fields: typing.List[typing.Tuple[str, str, typing.Optional[str]]] = [] self._resources: typing.List[typing.Tuple[str, files.Resource[files.AsyncReader]]] = [] def add_field(self, name: str, data: str, *, content_type: typing.Optional[str] = None) -> None: self._fields.append((name, data, content_type)) def add_resource(self, name: str, resource: files.Resource[files.AsyncReader]) -> None: self._resources.append((name, resource)) async def build(self, stack: contextlib.AsyncExitStack) -> aiohttp.FormData: form = aiohttp.FormData() for field in self._fields: form.add_field(field[0], field[1], content_type=field[2]) for name, resource in self._resources: stream = await stack.enter_async_context(resource.stream(executor=self._executor)) mimetype = stream.mimetype or _APPLICATION_OCTET_STREAM form.add_field(name, stream, filename=stream.filename, content_type=mimetype) return form
@typing.final
[docs]class StringMapBuilder(multidict.MultiDict[str]): """Helper class used to quickly build query strings or header maps. This will consume any items that are not `hikari.undefined.UNDEFINED`. If a value is unspecified, it will be ignored when inserting it. This reduces the amount of boilerplate needed for generating the headers and query strings for low-level HTTP API interaction, amongst other things. .. warning:: Because this subclasses `dict`, you should not use the index operator to set items on this object. Doing so will skip any form of validation on the type. Use the `put*` methods instead. """ __slots__: typing.Sequence[str] = () def __init__(self, arg: _StringMapBuilderArg = (), **kwargs: str) -> None: # We have to allow arguments to be passed to the init here otherwise the inherited copy behaviour from # multidict.MultiDict fails. super().__init__(arg, **kwargs) @typing.overload
[docs] def put( self, key: str, value: Stringish, /, ) -> None: ...
@typing.overload def put( self, key: str, value: undefined.UndefinedOr[T_co], /, *, conversion: typing.Callable[[T_co], Stringish], ) -> None: ... def put( self, key: str, value: undefined.UndefinedOr[typing.Any], /, *, conversion: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, ) -> None: """Add a key and value to the string map. .. note:: The value will always be cast to a `str` before inserting it. `True` will be translated to `"true"`, `False` will be translated to `"false"`, and `None` will be translated to `"null"`. Parameters ---------- key : str The string key. value : hikari.undefined.UndefinedOr[typing.Any] The value to set. Other Parameters ---------------- conversion : typing.Optional[typing.Callable[[typing.Any], typing.Any]] An optional conversion to perform. """ if value is not undefined.UNDEFINED: if conversion is not None: value = conversion(value) if value is True: value = "true" elif value is False: value = "false" elif value is None: value = "null" elif isinstance(value, snowflakes.Unique): value = str(value.id) else: value = str(value) # __setitem__ just overwrites the previous value. self.add(key, value)
@typing.final
[docs]class JSONObjectBuilder(typing.Dict[str, JSONish]): """Helper class used to quickly build JSON objects from various values. If provided with any values that are `hikari.undefined.UNDEFINED`, then these values will be ignored. This speeds up generation of JSON payloads for low level HTTP and websocket API interaction. .. warning:: Because this subclasses `dict`, you should not use the index operator to set items on this object. Doing so will skip any form of validation on the type. Use the `put*` methods instead. """ __slots__: typing.Sequence[str] = () def __init__(self) -> None: # Only allow use of empty constructor here. super().__init__() @typing.overload
[docs] def put(self, key: str, value: undefined.UndefinedNoneOr[JSONish], /) -> None: ...
@typing.overload def put( self, key: str, value: undefined.UndefinedNoneOr[T_co], /, *, conversion: typing.Callable[[T_co], JSONish], ) -> None: ... def put( self, key: str, value: undefined.UndefinedNoneOr[typing.Any], /, *, conversion: typing.Optional[typing.Callable[[typing.Any], JSONish]] = None, ) -> None: """Put a JSON value. If the value is `hikari.undefined.UNDEFINED` it will not be stored. Parameters ---------- key : str The key to give the element. value : hikari.undefined.UndefinedOr[typing.Any] The JSON type to put. This may be a non-JSON type if a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. Other Parameters ---------------- conversion : typing.Optional[typing.Callable[[typing.Any], JSONish]] The optional conversion to apply. """ if value is undefined.UNDEFINED: return if conversion is None or value is None: self[key] = value else: self[key] = conversion(value) @typing.overload
[docs] def put_array( self, key: str, values: undefined.UndefinedOr[typing.Iterable[JSONish]], /, ) -> None: ...
@typing.overload def put_array( self, key: str, values: undefined.UndefinedOr[typing.Iterable[T_co]], /, *, conversion: typing.Callable[[T_co], JSONish], ) -> None: ... def put_array( self, key: str, values: undefined.UndefinedOr[typing.Iterable[typing.Any]], /, *, conversion: typing.Optional[typing.Callable[[typing.Any], JSONish]] = None, ) -> None: """Put a JSON array. If the value is `hikari.undefined.UNDEFINED` it will not be stored. If provided, a conversion will be applied to each item. Parameters ---------- key : str The key to give the element. values : hikari.undefined.UndefinedOr[typing.Iterable[T_co]] The JSON types to put. This may be an iterable of non-JSON types if a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. Other Parameters ---------------- conversion : typing.Optional[typing.Callable[[typing.Any], JSONType]] The optional conversion to apply. """ if values is not undefined.UNDEFINED: if conversion is not None: self[key] = [conversion(value) for value in values] else: self[key] = list(values)
[docs] def put_snowflake( self, key: str, value: undefined.UndefinedNoneOr[snowflakes.SnowflakeishOr[snowflakes.Unique]], / ) -> None: """Put a key with a snowflake value into the builder. If the value is `hikari.undefined.UNDEFINED` it will not be stored. Parameters ---------- key : str The key to give the element. value : hikari.undefined.UndefinedNoneOr[hikari.snowflakes.SnowflakeishOr[hikari.snowflakes.Unique]] The JSON type to put. This may alternatively be undefined, in this case, nothing is performed. This may also be `None`, in this case the value isn't cast. """ if value is not undefined.UNDEFINED and value is not None: self[key] = str(int(value)) elif value is None: self[key] = value
[docs] def put_snowflake_array( self, key: str, values: undefined.UndefinedOr[typing.Iterable[snowflakes.SnowflakeishOr[snowflakes.Unique]]], /, ) -> None: """Put an array of snowflakes with the given key into this builder. If the value is `hikari.undefined.UNDEFINED` it will not be stored. Each snowflake should be castable to an `int`. Parameters ---------- key : str The key to give the element. values : hikari.undefined.UndefinedOr[typing.Iterable[hikari.snowflakes.SnowflakeishOr[hikari.snowflakes.Unique]]] The JSON snowflakes to put. This may alternatively be undefined. In the latter case, nothing is performed. """ if values is not undefined.UNDEFINED: self[key] = [str(int(value)) for value in values]
def cast_variants_array(cast: typing.Callable[[T_co], T], raw_values: typing.Iterable[T_co], /) -> typing.List[T]: """Cast an array of enum variants while ignoring unrecognised variant types. Parameters ---------- cast : typing.Callable[[T_co], T] Callback to cast each variant to. This will ignore any variants which raises `hikari.errors.UnrecognisedEntityError` on cast. raw_values : typing.Iterable[T_co] Iterable of the raw values to cast. Returns ------- typing.List[T] A list of the casted variants (with any unrecognised types ignored). """ results: typing.List[T] = [] for value in raw_values: try: results.append(cast(value)) except errors.UnrecognisedEntityError: pass return results