Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
blpapi / test / messageformatter.py
Size: Mime:
# messageformatter.py

"""Class for formatting mock messages."""

# pylint: disable=function-redefined,ungrouped-imports,no-name-in-module

from functools import singledispatch, update_wrapper
from datetime import date, time, datetime
from json import dumps
from typing import Any, Callable

from blpapi import internals, Name
from blpapi.element import Element
from blpapi.typehints import (
    AnyPythonDatetime,
    BlpapiMessageFormatterHandle,
    SupportedElementTypes,
)
from blpapi.datetime import _DatetimeUtil
from blpapi.exception import _ExceptionUtil
from blpapi.utils import (
    conv2str,
    get_handle,
    isstr,
    MAX_32BIT_INT,
    MIN_32BIT_INT,
    MIN_64BIT_INT,
    MAX_64BIT_INT,
)
from blpapi.chandle import CHandle


def _singledispatchmethod(arg_index: int) -> Callable:
    """Decorator to implement singledispatch on a class method.
    `arg_index` indicates the argument whose type will be used to determine
    the function to which to dispatch.
    """

    def _singleDispatchMethodImpl(func: Callable) -> Callable:
        """Implementation of `_singledispatchmethod`."""
        dispatcher = singledispatch(func)

        # `dispatcher` is `func` wrapped by singledispatch
        # `dispatcher` has an attribute `register()`, which is a decorator,
        # which registers the typed funcs to call based on the arg passed to
        # `func`
        def _wrapper(*args: Any, **kw: Any) -> Any:
            # singledispatch bases the type on the first arg, which is `self`
            # in a method. Instead, we need the 3rd argument, skipping over
            # `self` and `name` in `setElement(self, name, value)`
            return dispatcher.dispatch(args[arg_index].__class__)(*args, **kw)

        # give `_wrapper` a `.register()` attribute
        # pylint: disable=no-member
        _wrapper.__setattr__("register", dispatcher.register)
        # makes `_wrapper` look like `func` with regards to metadata and
        # attributes
        update_wrapper(_wrapper, func)
        return _wrapper

    return _singleDispatchMethodImpl


class MessageFormatter(CHandle):
    """:class:`MessageFormatter` is used to populate/format a message. It
    supports writing only once to each field. Attempting to set an already set
    element will fail.

    Note that the behavior is undefined if:

    * A message is formatted with :meth:`formatMessageJson` or
      :meth:`formatMessageXml` or :meth:`formatMessageDict` is further
      formatted.

    * A message formatted with `set*()` or `append*()` is further formatted
      using :meth:`formatMessageJson` or :meth:`formatMessageXml` or
      :meth:`formatMessageDict`.

    Currently, the JSON and XML formatting methods have some known limitations:

    * The parsers can not differentiate complex field types
      (sequences, choices, arrays) that are empty with complex field types that
      are missing / null. These fields are dropped and absent in the message
      contents.

    * Enumerations of type "Datetime", or any "Datetime" element with timezone
      offset or sub-microsecond precision (e.g. nanoseconds) are not supported.
    """

    def __init__(self, handle: BlpapiMessageFormatterHandle) -> None:
        """For internal use only."""
        super(MessageFormatter, self).__init__(
            handle, internals.blpapi_MessageFormatter_destroy
        )
        self.__handle = handle

    def _handle(self) -> BlpapiMessageFormatterHandle:
        return self.__handle

    def setElement(self, name: Name, value: SupportedElementTypes) -> None:
        """Set the element with the specified ``name`` to the specified
        ``value`` in the current :class:`blpapi.Message` referenced by this
        :class:`MessageFormatter`.

        Args:
            name: Identifies the element which will be set to ``value``.
            value: The value to assign to the specified element.

        Raises:
            Exception: An exception is raised if any of the following are true:
                - The ``name`` is invalid for the current message
                - The element identified by ``name`` has already been set
                - The ``value`` cannot be assigned to the element identified by
                    ``name``

        Note:
            **Please use** :class:`blpapi.Name` **over** :class:`str` **where possible for**
            ``name``. :class:`blpapi.Name` **objects should be initialized
            once and then reused** as each :class:`blpapi.Name` instance refers to an
            entry in a global static table which grows in an unbounded manner.
        """

        # Although the function type hint is `name: Name` because we want to encourage using Names,
        # clients may actually pass a string, so we check and convert if needed
        # mypy complains that conv2str doesn't take Name, which we can ignore after isstr check
        if isstr(name):
            name = Name(conv2str(name))  # type: ignore
        self._setElement(name, value)

    # pylint: disable=unused-argument,no-self-use
    @_singledispatchmethod(2)
    def _setElement(self, name: Name, value: SupportedElementTypes) -> None:
        """Implementation for :meth:`setElement`.

        Args:
            name (Name): Identifies the element which will be set to ``value``
            value (bool or str or bytes or int or float or date or time or
                datetime or Name or None):
                The value to assign to the specified element.

        Raises:
            Exception: An exception is raised if any of the following are true:
                - The ``name`` is invalid for the current message
                - The element identified by ``name`` has already been set
                - The ``value`` cannot be assigned to the element identified by
                    ``name``
        """
        raise TypeError(
            f"The type of value {value!r} is not supported. Type"
            f" is {type(value)}. Please refer to the"
            " documentation for the supported types."
        )

    @_setElement.register(bool)
    def _(self, name: Name, value: bool) -> None:
        """Dispatch method for setting `bool` types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_setValueBool(
                self.__handle, get_handle(name), value
            )
        )

    @_setElement.register(datetime)
    @_setElement.register(date)
    @_setElement.register(time)
    def _(self, name: Name, value: AnyPythonDatetime) -> None:
        """Dispatch method for setting time types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_setValueHighPrecisionDatetime(
                self.__handle,
                get_handle(name),
                _DatetimeUtil.convertToBlpapi(value),
            )
        )

    @_setElement.register(int)
    def _setElementInt(self, name: Name, value: int) -> None:
        """Dispatch method for setting integer types."""
        if MIN_32BIT_INT <= value <= MAX_32BIT_INT:
            _ExceptionUtil.raiseOnError(
                internals.blpapi_MessageFormatter_setValueInt32(
                    self.__handle, get_handle(name), value
                )
            )
        elif MIN_64BIT_INT <= value <= MAX_64BIT_INT:
            _ExceptionUtil.raiseOnError(
                internals.blpapi_MessageFormatter_setValueInt64(
                    self.__handle, get_handle(name), value
                )
            )
        else:
            raise ValueError("Value is out of element's supported range")

    @_setElement.register(float)
    def _(self, name: Name, value: float) -> None:
        """Dispatch method for setting `float` types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_setValueFloat(
                self.__handle, get_handle(name), value
            )
        )

    @_setElement.register(Name)
    def _(self, name: Name, value: Name) -> None:
        """Dispatch method for setting `Name` types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_setValueFromName(
                self.__handle, get_handle(name), get_handle(value)
            )
        )

    @_setElement.register(type(None))
    def _(self, name: Name, _: Any) -> None:
        """Dispatch method for setting `None` types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_setValueNull(
                self.__handle, get_handle(name)
            )
        )

    @_setElement.register(str)
    def _(self, name: Name, value: str) -> None:
        """Dispatch method for setting string types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_setValueString(
                self.__handle, get_handle(name), value
            )
        )

    @_setElement.register(bytes)
    def _(self, name: Name, value: bytes) -> None:
        """Dispatch method for setting `bytes` types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_setValueBytes(
                self.__handle, get_handle(name), value
            )
        )

    def pushElement(self, name: Name) -> None:
        """Change the level at which this :class:`MessageFormatter` is
        operating to the element specified by ``name``.

        After this returns, the context of the :class:`MessageFormatter` is set
        to the element specified by ``name`` in the schema and any calls to
        :meth:`setElement` or meth:`pushElement` are applied at that level.

        If ``name`` represents an array of scalars then :meth:`appendValue`
        must be used to add values.

        If ``name`` represents an array of complex types then
        :meth:`appendElement()` must be used.

        Args:
            name: Specifies the :class:`blpapi.Element` on which to operate.
                The element must identify either a choice, a sequence or an
                array at the current level of the schema or the behavior is
                undefined.

        Raises:
            Exception: If the ``name`` is invalid for the current message or if
                the element identified by ``name`` has already been set.

        Note:
            **Please use** :class:`blpapi.Name` **over** :class:`str` **where possible for**
            ``name``. :class:`blpapi.Name` **objects should be initialized
            once and then reused** as each :class:`blpapi.Name` instance refers to an
            entry in a global static table which grows in an unbounded manner.
        """

        # Although the function type hint is `name: Name` because we want to encourage using Names,
        # clients may actually pass a string, so we check and convert if needed
        # mypy complains that conv2str doesn't take Name, which we can ignore after isstr check
        if isstr(name):
            name = Name(conv2str(name))  # type: ignore
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_pushElement(
                self.__handle, get_handle(name)
            )
        )

    def popElement(self) -> None:
        """Undo the most recent call to :meth:`pushElement` or
        :meth:`appendElement` on this :class:`MessageFormatter` and return the
        context of the :class:`MessageFormatter` to where it was before the
        call to :meth:`pushElement` or :meth:`appendElement`. Once
        :meth:`popElement` has been called, it is invalid to attempt to
        re-visit the same context.
        """
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_popElement(self.__handle)
        )

    def getElement(self) -> Element:
        """Get the current :class:`blpapi.Element` that this :class:`MessageFormatter`
        is formatting.

        Returns:
            The current element being formatted

        Raises:
            InvalidStateException: If there is no current element being
                formatted.
            Exception: For other errors when retrieving the element.

        Note:
            This method provides access to the underlying element structure
            for direct operations on the element.
        """
        res = internals.blpapi_MessageFormatter_getElement(self.__handle)
        _ExceptionUtil.raiseOnError(res[0])
        return Element(res[1], None)

    @_singledispatchmethod(1)
    def appendValue(self, value: SupportedElementTypes) -> None:
        """Append the specified ``value`` to the element on which this
        :class:`MessageFormatter` is operating.

        Args:
            value: The value to append.

        Raises:
            Exception: If the schema of the message is flat or the current
                element to which ``value`` is appended is not an array.
        """
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_appendValueString(
                self.__handle, value
            )
        )

    @appendValue.register(bool)
    def _(self, value: bool) -> None:
        """Dispatch method for appending `bool` types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_appendValueBool(
                self.__handle, value
            )
        )

    @appendValue.register(datetime)
    @appendValue.register(date)
    @appendValue.register(time)
    def _(self, value: AnyPythonDatetime) -> None:
        """Dispatch method for appending time types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_appendValueHighPrecisionDatetime(
                self.__handle, _DatetimeUtil.convertToBlpapi(value)
            )
        )

    @appendValue.register(int)
    def _appendValueInt(self, value: int) -> None:
        """Dispatch method for appending integer types."""
        if MIN_32BIT_INT <= value <= MAX_32BIT_INT:
            _ExceptionUtil.raiseOnError(
                internals.blpapi_MessageFormatter_appendValueInt32(
                    self.__handle, value
                )
            )
        elif MIN_64BIT_INT <= value <= MAX_64BIT_INT:
            _ExceptionUtil.raiseOnError(
                internals.blpapi_MessageFormatter_appendValueInt64(
                    self.__handle, value
                )
            )
        else:
            raise ValueError("Value is out of element's supported range")

    @appendValue.register(float)
    def _(self, value: float) -> None:
        """Dispatch method for appending `float` types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_appendValueFloat(
                self.__handle, value
            )
        )

    @appendValue.register(Name)
    def _(self, value: Name) -> None:
        """Dispatch method for appending `Name` types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_appendValueFromName(
                self.__handle, get_handle(value)
            )
        )

    @appendValue.register(str)
    def _(self, value: str) -> None:
        """Dispatch method for appending string types."""
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_appendValueString(
                self.__handle, value
            )
        )

    def appendElement(self) -> None:
        """Create an array :class:`blpapi.Element` and append it to the
        :class:`blpapi.Element` on which this :class:`MessageFormatter` is
        operating.

        Raises:
            Exception: If the schema of the :class:`blpapi.Message` is flat or
                the current :class:`blpapi.Element` to which this new
                :class:`blpapi.Element` is being appended is not an array, a
                sequence or a choice.
        """
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_appendElement(self.__handle)
        )

    def formatMessageJson(self, jsonMessage: str) -> None:
        """Format the :class:`blpapi.Message` from its ``JSON`` representation.

        Args:
            jsonMessage (str): A ``JSON``-formatted ``str`` representing the
                content used to format the message.

        Raises:
            Exception: If the method fails to format the
                :class:`blpapi.Message`.

        The behavior is undefined if further formatting is done using
        this :class:`MessageFormatter`.
        """
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_FormatMessageJson(
                self.__handle, jsonMessage
            )
        )

    def formatMessageXml(self, xmlMessage: str) -> None:
        """Format the :class:`blpapi.Message` from its ``XML`` representation.

        Args:
            xmlMessage (str): An ``XML``-formatted ``str`` representing the
                content used to format the message.

        Raises:
            Exception: If the method fails to format the
                :class:`blpapi.Message`.

        The behavior is undefined if further formatting is done using
        this :class:`MessageFormatter`.
        """
        _ExceptionUtil.raiseOnError(
            internals.blpapi_MessageFormatter_FormatMessageXml(
                self.__handle, xmlMessage
            )
        )

    def formatMessageDict(self, dictMessage: dict) -> None:
        """Format the :class:`blpapi.Message` from its ``dict`` representation.

        Args:
            dictMessage: A dictionary representing the content used to
                format the message.

        Raises:
            Exception: If the method fails to format the
                :class:`blpapi.Message`.

        The behavior is undefined if further formatting is done using
        this :class:`MessageFormatter`.
        """
        self.formatMessageJson(dumps(dictMessage))


__copyright__ = """
Copyright 2020. Bloomberg Finance L.P.

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