class Response(dict):
    """A response from OANDA.

    Allows dotted attribute access
    """
    def __init__(self, data, status, bool, datetime_format):
        if data:
            super().__init__(data)
        self.status = status
        self.bool = bool
        self.datetime_format = datetime_format

    def __bool__(self):
        """Returns True if response contains data as per the OANDA spec.

        Returns false if a status code not defined in the endpoint spec was returned
        """
        return self.bool

    def __repr__(self):
        keys = ', '.join(self.keys())
        return f'<Status [{self.status}]: {keys}>'

    def __getattr__(self, name):
        try:
            return self[name]
        except KeyError:
            msg = f'No such attribute `{name}`'
            raise AttributeError(msg)

    def dict(self, json=False, datetime_format=None):
        """Convert the response to a nested dictionary

        Args:

            json: Convert object attributes to the :term:`JSON` representation

        """
        if json and datetime_format is None:
            datetime_format = self.datetime_format


        def value_to_dict(value):
            if isinstance(value, Model):
                result = value.dict(json, datetime_format)
            elif isinstance(value, Array):
                try:
                    result = [obj.dict(json, datetime_format) for obj in value]
                except AttributeError:
                    result = [obj for obj in value]
            elif isinstance(value, Specifier) and json:
                    # Specifiers need to be strings for JSON
                result = str(value)
            elif isinstance(value, pd.Timestamp) and json:
                result = value.json(datetime_format)
            else:
                result = value
            return result

        return {key: value_to_dict(value) for key, value in self.items()}

    def json(self, datetime_format=None):
        """Return the json equivalent of the response"""
        return json.dumps(self.dict(json=True, datetime_format=datetime_format)) def update_account(self, changes, changes_state):
    """Update an existing account with changes

    Args:
        self: -- OandaClient instance
        changes: -- Changes from account_changes API call
        changes_state: ChangesState from account_changes API call

    Returns: None
    """

    # Add / Replace / Remove items from the AccountChanges object
    orders = (order if not changes.orders_filled.get_id(order.id)
              else changes.orders_filled.get_id(order.id)
              for order in chain(self._account.orders, changes.orders_created)
              if not changes.orders_cancelled.get_id(order.id)
              and not changes.orders_filled.get_id(order.id))

    trades = (trade if not changes.trades_reduced.get_id(trade.id)
              else changes.trades_reduced.get_id(trade.id)
              for trade in chain(self._account.trades, changes.trades_opened)
              if not changes.trades_closed.get_id(trade.id))

    # We need to replace any positions in the stored account with the changed positions
    # and then we need to update the dynamic state
    positions = {position.instrument: position
                 for position in self._account.positions}

    positions.update({
        position.instrument: position for position in changes.positions
    })

    positions = ((position, changes_state.positions.get_instrument(instrument))
                 for instrument, position in positions.items())

    # Update the Dynamic state
    orders = tuple(order if not changes_state.orders.get_id(order.id)
                   else order.replace(**changes_state.orders.get_id(order.id).dict())
                   for order in orders)

    trades = tuple(trade if not changes_state.trades.get_id(trade.id)
                   else trade.replace(**changes_state.trades.get_id(trade.id).dict())
                   for trade in trades)

    positions = tuple(
        position.replace(
            unrealized_pl=0,
            long=position.long.replace(unrealized_pl=0),
            short=position.short.replace(unrealized_pl=0))
        if not state
        else position.replace(
            unrealized_pl=state.net_unrealized_pl,
            long=position.long.replace(unrealized_pl=state.long_unrealized_pl),
            short=position.short.replace(unrealized_pl=state.short_unrealized_pl))
        for position, state in positions)

    self._account = self._account.replace(**dict(changes_state.dict(json=False),
                                                 orders=orders, trades=trades, positions=positions))

    self.transactions = ArrayTransaction(
        *sorted((changes.transactions + self.transactions)[-self.max_transaction_history:],
                key=lambda x: x.id, reverse=True)) def create_attribute(typ, data):
    """Correctly instantiate object based upon type of argument passed"""
    try:
        if isinstance(data, dict):
            result = typ(**data)
        elif isinstance(data, Specifier):
            if not issubclass(typ, Specifier):
                msg = f'{data} must be a {Specifier} is {type(data)}'
                logger.error(msg)
                raise IncompatibleValue(msg)
            result = typ(data)
        elif isinstance(data, InstrumentName):
            result = typ(data)
        elif isinstance(data, (Model, Array, Primitive)):
            if not issubclass(type(data), typ):
                msg = f'{data} must be of type {typ} is {type(data)}'
                logger.error(msg)
                raise IncompatibleValue(msg)
            result = data
        elif isinstance(data, (tuple, list)):
            result = typ(*data)
        else:
            result = typ(data)
    except (TypeError, ValueError, UnknownKeywordArgument):
        # This error handling is required when there is no
        # schema available to parse the data. Typically
        # when an error code has been returned
        # A none value should be returned if this is the case
        if typ is not None:
            msg = f'Could not create {typ}. DATA: {data}, TYPE: {type(data)}'
            logger.error(msg)
            raise InstantiationFailure(msg)
    else:
        return result class GETAccountID(EndPoint):
    # the HTTP verb to use for this endpoint
    method = 'GET'

    # path to endpoint
    path = ('/v3/accounts/', AccountID)

    # description of endpoint
    description = 'Get the full details for a single Account that a client has access to. ' \
                  'Full pending Order, open Trade and open Position representations are provided.'

    # parameters required to send to endpoint
    parameters = {Authorization: (HEADER, 'Authorization'), AcceptDatetimeFormat: (HEADER, 'Accept-Datetime-Format'),
                  AccountID: (PATH, 'accountID')}
    # valid responses
    responses = {200: {'account': Account, 'lastTransactionID': TransactionID}}

    # error msgs'
    error = (400, 401, 405) class LastTransactionID(TransactionID):
    """Contains the most recent TransactionID"""
    pass class SinceTransactionID(TransactionID):
    """The account changes to get Since LastTransactionID for account_changes() method"""
    pass class GETPricingStream(EndPoint):
    # host to use for this endpoint
    host = 'STREAM'

    # the HTTP verb to use for this endpoint
    method = 'GET'

    # path to endpoint
    path = ('/v3/accounts/', AccountID, '/pricing/stream')

    # description of endpoint
    description = '''Get a stream of Account Prices starting from when the request is made.
    This pricing stream does not include every single price created for the Account, 
    but instead will provide at most 4 prices per second (every 250 milliseconds) 
    for each instrument being requested. If more than one price is created for an instrument 
    during the 250 millisecond window, only the price in effect at the end of the window is sent.
    This means that during periods of rapid price movement, subscribers to this stream will 
    not be sent every price. Pricing windows for different connections to the price stream 
    are not all aligned in the same way (i.e.they are not all aligned to the top of the second).
    This means that during periods of rapid price movement, different subscribers may observe 
    different prices depending on their alignment.'''

    # parameters required to send to endpoint
    parameters = {Authorization: (HEADER, 'Authorization'), AcceptDatetimeFormat: (HEADER, 'Accept-Datetime-Format'),
                  AccountID: (PATH, 'accountID'), Instruments: (QUERY, 'instruments'), Snapshot: (QUERY, 'snapshot')}

    # valid responses
    responses = {200: {'PRICE': Price, 'HEARTBEAT': PricingHeartbeat}}

    # error msgs'
    error = (400, 401, 404, 405) class GETTransactionsStream(EndPoint):
    # host to use for this endpoint
    host = 'STREAM'

    # the HTTP verb to use for this endpoint
    method = 'GET'

    # path to endpoint
    path = ('/v3/accounts/', AccountID, '/transactions/stream')

    # description of endpoint
    description = 'Get a stream of Transactions for an Account starting from when the request is made.'

    # parameters required to send to endpoint
    parameters = {Authorization: (HEADER, 'Authorization'), AccountID: (PATH, 'accountID')}

    # valid responses
    responses = {200: {'HEARTBEAT': TransactionHeartbeat,
                       'CREATE': CreateTransaction,
                       'CLOSE': CloseTransaction,
                       'REOPEN': ReopenTransaction,
                       'CLIENT_CONFIGURE': ClientConfigureTransaction,
                       'CLIENT_CONFIGURE_REJECT': ClientConfigureRejectTransaction,
                       'TRANSFER_FUNDS': TransferFundsTransaction,
                       'TRANSFER_FUNDS_REJECT': TransferFundsRejectTransaction,
                       'MARKET_ORDER': MarketOrderTransaction,
                       'MARKET_ORDER_REJECT': MarketOrderRejectTransaction,
                       'LIMIT_ORDER': LimitOrderTransaction,
                       'LIMIT_ORDER_REJECT': LimitOrderRejectTransaction,
                       'STOP_ORDER': StopOrderTransaction,
                       'STOP_ORDER_REJECT': StopOrderRejectTransaction,
                       'MARKET_IF_TOUCHED_ORDER': MarketIfTouchedOrderTransaction,
                       'MARKET_IF_TOUCHED_ORDER_REJECT': MarketIfTouchedOrderRejectTransaction,
                       'TAKE_PROFIT_ORDER': TakeProfitOrderTransaction,
                       'TAKE_PROFIT_ORDER_REJECT': TakeProfitOrderRejectTransaction,
                       'STOP_LOSS_ORDER': StopLossOrderTransaction,
                       'STOP_LOSS_ORDER_REJECT': StopOrderRejectTransaction,
                       'TRAILING_STOP_LOSS_ORDER': TrailingStopLossOrderTransaction,
                       'TRAILING_STOP_LOSS_ORDER_REJECT': TrailingStopLossOrderRejectTransaction,
                       'ORDER_FILL': OrderFillTransaction,
                       'ORDER_CANCEL': OrderCancelTransaction,
                       'ORDER_CANCEL_REJECT': OrderCancelRejectTransaction,
                       'ORDER_CLIENT_EXTENSIONS_MODIFY': OrderClientExtensionsModifyTransaction,
                       'ORDER_CLIENT_EXTENSIONS_MODIFY_REJECT': OrderClientExtensionsModifyRejectTransaction,
                       'TRADE_CLIENT_EXTENSIONS_MODIFY': TradeClientExtensionsModifyTransaction,
                       'TRADE_CLIENT_EXTENSIONS_MODIFY_REJECT': TradeClientExtensionsModifyRejectTransaction,
                       'MARGIN_CALL_ENTER': MarginCallEnterTransaction,
                       'MARGIN_CALL_EXTEND': MarginCallExtendTransaction,
                       'MARGIN_CALL_EXIT': MarginCallExitTransaction,
                       'DELAYED_TRADE_CLOSURE': DelayedTradeClosureTransaction,
                       'DAILY_FINANCING': DailyFinancingTransaction,
                       'RESET_RESETTABLE_PL': ResetResettablePLTransaction}
                 }

    # error msgs'
    error = (400, 401, 404, 405) class ResponseTimeout(AsyncV20Exception):
    """The server took to long to respond"""
    pass class UnexpectedStatus(AsyncV20Exception):
    """The server returned an unexpected HTTP status"""
    pass
import ujson as json
import logging
from asyncio import TimeoutError as AsyncTimeOutError
from async_timeout import timeout
from .response import Response
from .rest import update_account
from ..definitions.base import create_attribute
from ..endpoints.account import GETAccountID
from ..endpoints.annotations import LastTransactionID
from ..endpoints.annotations import SinceTransactionID
from ..endpoints.other_responses import other_responses
from ..endpoints.pricing import GETPricingStream
from ..endpoints.transaction import GETTransactionsStream
from ..exceptions import ResponseTimeout, UnexpectedStatus
from time import time


TRANSACTION = 'transaction'
HEARTBEAT = 'heartbeat'
PRICE = 'price'

logger = logging.getLogger(__name__)

def _lookup_schema(endpoint, status):
    try:
        schema = endpoint.responses[status]  # look up the template to process the data
    except KeyError:
        try:
            schema = other_responses[status]  # See if a response status is an error code
        except KeyError:
            msg = str(status)
            logger.error(msg)
            raise UnexpectedStatus(msg)
        else:
            # Returns False if the status wasn't in the endpoints expected response
            return schema, status, False
    else:
        # Returns true if the status was a valid response
        return schema, status, True


async def _create_response(json_body, endpoint, schema, status, boolean, datetime_format):
    # Here we iterate through all the json objects returned in the response
    # and construct the corresponding async_v20 type as determined by the endpoints
    # Schema
    if isinstance(schema, dict):
        data = []
        for json_object, json_field in json_body.items():
            data.append((json_object, create_attribute(schema.get(json_object), json_field)))
    else:
        obj = schema(**json_body)
        data = [(obj.__class__.__name__, obj)]
    return Response(data, status, boolean, datetime_format)


async def _rest_response(self, response, endpoint, enable_rest, method_name):
    try:
        async with timeout(self.rest_timeout):
            async with response as resp:
                schema, status, boolean = _lookup_schema(endpoint, resp.status)
                # Update client headers.
                self.default_parameters.update(resp.raw_headers)
                json_body = await resp.json()

    except AsyncTimeOutError:
        msg = f'{method_name} took longer than {self.rest_timeout} seconds'
        logger.error(msg)

        raise ResponseTimeout(msg)