text: # Copyright 2019 NTRLab
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  # ------------------------------------------------------------------------------
  
  import asyncio
  import re
  import logging
  import json
  import base64
  from aiohttp import web
  
  # pylint: disable=no-name-in-module,import-error
  # needed for the google.protobuf imports to pass pylint
  from google.protobuf.json_format import MessageToDict
  from google.protobuf.message import DecodeError
  
  
  
  import dgt_rest_api.exceptions as errors
  from dgt_rest_api import error_handlers
  from dgt_rest_api.messaging import DisconnectError
  from dgt_rest_api.messaging import SendBackoffTimeoutError
  from dgt_sdk.protobuf.validator_pb2 import Message
  from dgt_sdk.protobuf import client_transaction_pb2
  from dgt_sdk.protobuf import client_list_control_pb2
  from dgt_sdk.protobuf import client_batch_submit_pb2
  from dgt_sdk.protobuf import client_state_pb2
  from dgt_sdk.protobuf import client_block_pb2
  from dgt_sdk.protobuf import client_batch_pb2
  from dgt_sdk.protobuf import client_receipt_pb2
  from dgt_sdk.protobuf.client_peers_pb2 import ClientPeersGetResponse,ClientPeersGetRequest
  from dgt_sdk.protobuf import client_status_pb2
  from dgt_sdk.protobuf import client_topology_pb2
  from dgt_sdk.protobuf.block_pb2 import BlockHeader
  from dgt_sdk.protobuf.batch_pb2 import BatchList
  from dgt_sdk.protobuf.batch_pb2 import BatchHeader
  from dgt_sdk.protobuf.transaction_pb2 import TransactionHeader
  from dgt_sdk.protobuf import client_heads_pb2,client_topology_pb2
  # pylint: disable=too-many-lines
  
  DEFAULT_TIMEOUT = 300
  LOGGER = logging.getLogger(__name__)
  MALICIOUS_LIST = {'ok' : ClientPeersGetRequest.OK,'ma' : ClientPeersGetRequest.MALICIOUS,'ma1' : ClientPeersGetRequest.MALICIOUS1,'ma2' : ClientPeersGetRequest.MALICIOUS2 }
  
  class CounterWrapper():
      def __init__(self, counter=None):
          self._counter = counter
  
      def inc(self):
          if self._counter:
              self._counter.inc()
  
  
  class NoopTimerContext():
      def __enter__(self):
          pass
  
      def __exit__(self, exception_type, exception_value, traceback):
          pass
  
      def stop(self):
          pass
  
  
  class TimerWrapper():
      def __init__(self, timer=None):
          self._timer = timer
          self._noop = NoopTimerContext()
  
      def time(self):
          if self._timer:
              return self._timer.time()
          return self._noop
  
  
  class RouteHandler:
      """Contains a number of aiohttp handlers for endpoints in the Rest Api.
  
      Each handler takes an aiohttp Request object, and uses the data in
      that request to send Protobuf message to a validator. The Protobuf response
      is then parsed, and finally an aiohttp Response object is sent back
      to the client with JSON formatted data and metadata.
  
      If something goes wrong, an aiohttp HTTP exception is raised or returned
      instead.
  
      Args:
          connection (:obj: messaging.Connection): The object that communicates
              with the validator.
          timeout (int, optional): The time in seconds before the Api should
              cancel a request and report that the validator is unavailable.
      """
  
      def __init__(
              self, loop, connection,
              timeout=DEFAULT_TIMEOUT, metrics_registry=None):
          self._loop = loop
          self._connection = connection
          self._timeout = timeout
          if metrics_registry:
              self._post_batches_count = CounterWrapper(
                  metrics_registry.counter('post_batches_count'))
              self._post_batches_error = CounterWrapper(
                  metrics_registry.counter('post_batches_error'))
              self._post_batches_total_time = TimerWrapper(
                  metrics_registry.timer('post_batches_total_time'))
              self._post_batches_validator_time = TimerWrapper(
                  metrics_registry.timer('post_batches_validator_time'))
          else:
              self._post_batches_count = CounterWrapper()
              self._post_batches_error = CounterWrapper()
              self._post_batches_total_time = TimerWrapper()
              self._post_batches_validator_time = TimerWrapper()
  
      async def submit_batches(self, request):
          """Accepts a binary encoded BatchList and submits it to the validator.
  
          Request:
              body: octet-stream BatchList of one or more Batches
          Response:
              status:
                   - 202: Batches submitted and pending
              link: /batches or /batch_statuses link for submitted batches
  
          """
          timer_ctx = self._post_batches_total_time.time()
          self._post_batches_count.inc()
  
          # Parse request
          if request.headers['Content-Type'] != 'application/octet-stream':
              LOGGER.debug(
                  'Submission headers had wrong Content-Type: %s',
                  request.headers['Content-Type'])
              self._post_batches_error.inc()
              raise errors.SubmissionWrongContentType()
  
          body = await request.read()
          if not body:
              LOGGER.debug('Submission contained an empty body')
              self._post_batches_error.inc()
              raise errors.NoBatchesSubmitted()
  
          try:
              batch_list = BatchList()
              batch_list.ParseFromString(body)
          except DecodeError:
              LOGGER.debug('Submission body could not be decoded: %s', body)
              self._post_batches_error.inc()
              raise errors.BadProtobufSubmitted()
  
          # Query validator
          error_traps = [error_handlers.BatchInvalidTrap,
                         error_handlers.BatchQueueFullTrap]
          validator_query = client_batch_submit_pb2.ClientBatchSubmitRequest(
              batches=batch_list.batches)
  
          with self._post_batches_validator_time.time():
              await self._query_validator(
                  Message.CLIENT_BATCH_SUBMIT_REQUEST,
                  client_batch_submit_pb2.ClientBatchSubmitResponse,
                  validator_query,
                  error_traps)
  
          # Build response envelope
          id_string = ','.join(b.header_signature for b in batch_list.batches)
  
          status = 202
          link = self._build_url(request, path='/batch_statuses', id=id_string)
  
          retval = self._wrap_response(
              request,
              metadata={'link': link},
              status=status)
  
          timer_ctx.stop()
          return retval
  
      async def list_statuses(self, request):
          """Fetches the committed status of batches by either a POST or GET.
  
          Request:
              body: A JSON array of one or more id strings (if POST)
              query:
                  - id: A comma separated list of up to 15 ids (if GET)
                  - wait: Request should not return until all batches committed
  
          Response:
              data: A JSON object, with batch ids as keys, and statuses as values
              link: The /batch_statuses link queried (if GET)
          """
          error_traps = [error_handlers.StatusResponseMissing]
  
          # Parse batch ids from POST body, or query paramaters
          if request.method == 'POST':
              if request.headers['Content-Type'] != 'application/json':
                  LOGGER.debug(
                      'Request headers had wrong Content-Type: %s',
                      request.headers['Content-Type'])
                  raise errors.StatusWrongContentType()
  
              ids = await request.json()
  
              if (not ids
                      or not isinstance(ids, list)
                      or not all(isinstance(i, str) for i in ids)):
                  LOGGER.debug('Request body was invalid: %s', ids)
                  raise errors.StatusBodyInvalid()
              for i in ids:
                  self._validate_id(i)
  
          else:
              ids = self._get_filter_ids(request)
              if not ids:
                  LOGGER.debug('Request for statuses missing id query')
                  raise errors.StatusIdQueryInvalid()
  
          # Query validator
          validator_query = \
              client_batch_submit_pb2.ClientBatchStatusRequest(
                  batch_ids=ids)
          self._set_wait(request, validator_query)
  
          response = await self._query_validator(
              Message.CLIENT_BATCH_STATUS_REQUEST,
              client_batch_submit_pb2.ClientBatchStatusResponse,
              validator_query,
              error_traps)
  
          # Send response
          if request.method != 'POST':
              metadata = self._get_metadata(request, response)
          else:
              metadata = None
  
          data = self._drop_id_prefixes(
              self._drop_empty_props(response['batch_statuses']))
  
          return self._wrap_response(request, data=data, metadata=metadata)
  
      async def list_state(self, request):
          """Fetches list of data entries, optionally filtered by address prefix.
  
          Request:
              query:
                  - head: The id of the block to use as the head of the chain
                  - address: Return entries whose addresses begin with this
                  prefix
  
          Response:
              data: An array of leaf objects with address and data keys
              head: The head used for this query (most recent if unspecified)
              link: The link to this exact query, including head block
              paging: Paging info and nav, like total resources and a next link
          """
          paging_controls = self._get_paging_controls(request)
          # for DAG ask head of chain for getting merkle root is incorrect way 
          # FIXME - add special method for asking real merkle root
          head, root = await self._head_to_root(request.url.query.get('head', None))
          LOGGER.debug('LIST_STATE STATE=%s',root[:10])
                  
          validator_query = client_state_pb2.ClientStateListRequest(
              state_root='',#root,
              address=request.url.query.get('address', None),
              sorting=self._get_sorting_message(request, "default"),
              paging=self._make_paging_message(paging_controls))
  
          response = await self._query_validator(
              Message.CLIENT_STATE_LIST_REQUEST,
              client_state_pb2.ClientStateListResponse,
              validator_query)
  
          return self._wrap_paginated_response(
              request=request,
              response=response,
              controls=paging_controls,
              data=response.get('entries', []),
              head=head)
  
      async def fetch_state(self, request):
          """Fetches data from a specific address in the validator's state tree.
  
          Request:
              query:
                  - head: The id of the block to use as the head of the chain
                  - address: The 70 character address of the data to be fetched
  
          Response:
              data: The base64 encoded binary data stored at that address
              head: The head used for this query (most recent if unspecified)
              link: The link to this exact query, including head block
          """
          error_traps = [
              error_handlers.InvalidAddressTrap,
              error_handlers.StateNotFoundTrap]
  
          address = request.match_info.get('address', '')
          head = request.url.query.get('head', None)
          LOGGER.debug('fetch_state head=%s',head[:8] if head is not None else None)
          #FIXME for DAG we should ask real merkle root
          head, root = await self._head_to_root(head)
          response = await self._query_validator(
              Message.CLIENT_STATE_GET_REQUEST,
              client_state_pb2.ClientStateGetResponse,
              client_state_pb2.ClientStateGetRequest(
                  state_root='',#root,
                  address=address),
              error_traps)
  
          return self._wrap_response(
              request,
              data=response['value'],
              metadata=self._get_metadata(request, response, head=head))
  
      async def list_blocks(self, request):
          """Fetches list of blocks from validator, optionally filtered by id.
  
          Request:
              query:
                  - head: The id of the block to use as the head of the chain
                  - id: Comma separated list of block ids to include in results
  
          Response:
              data: JSON array of fully expanded Block objects
              head: The head used for this query (most recent if unspecified)
              link: The link to this exact query, including head block
              paging: Paging info and nav, like total resources and a next link
          """
          paging_controls = self._get_paging_controls(request)
          validator_query = client_block_pb2.ClientBlockListRequest(
              head_id=self._get_head_id(request),
              block_ids=self._get_filter_ids(request),
              sorting=self._get_sorting_message(request, "block_num"),
              paging=self._make_paging_message(paging_controls))
  
          response = await self._query_validator(
              Message.CLIENT_BLOCK_LIST_REQUEST,
              client_block_pb2.ClientBlockListResponse,
              validator_query)
  
          return self._wrap_paginated_response(
              request=request,
              response=response,
              controls=paging_controls,
              data=[self._expand_block(b) for b in response['blocks']])
  
      async def fetch_block(self, request):
          """Fetches a specific block from the validator, specified by id.
          Request:
              path:
                  - block_id: The 128-character id of the block to be fetched
  
          Response:
              data: A JSON object with the data from the fully expanded Block
              link: The link to this exact query
          """
          error_traps = [error_handlers.BlockNotFoundTrap]
  
          block_id = request.match_info.get('block_id', '')
          self._validate_id(block_id)
  
          response = await self._query_validator(
              Message.CLIENT_BLOCK_GET_BY_ID_REQUEST,
              client_block_pb2.ClientBlockGetResponse,
              client_block_pb2.ClientBlockGetByIdRequest(block_id=block_id),
              error_traps)
  
          return self._wrap_response(
              request,
              data=self._expand_block(response['block']),
              metadata=self._get_metadata(request, response))
  
      async def list_batches(self, request):
          """Fetches list of batches from validator, optionally filtered by id.
  
          Request:
              query:
                  - head: The id of the block to use as the head of the chain
                  - id: Comma separated list of batch ids to include in results
  
          Response:
              data: JSON array of fully expanded Batch objects
              head: The head used for this query (most recent if unspecified)
              link: The link to this exact query, including head block
              paging: Paging info and nav, like total resources and a next link
          """
          paging_controls = self._get_paging_controls(request)
          validator_query = client_batch_pb2.ClientBatchListRequest(
              head_id=self._get_head_id(request),
              batch_ids=self._get_filter_ids(request),
              sorting=self._get_sorting_message(request, "default"),
              paging=self._make_paging_message(paging_controls))
  
          response = await self._query_validator(
              Message.CLIENT_BATCH_LIST_REQUEST,
              client_batch_pb2.ClientBatchListResponse,
              validator_query)
  
          return self._wrap_paginated_response(
              request=request,
              response=response,
              controls=paging_controls,
              data=[self._expand_batch(b) for b in response['batches']])
  
      async def fetch_batch(self, request):
          """Fetches a specific batch from the validator, specified by id.
  
          Request:
              path:
                  - batch_id: The 128-character id of the batch to be fetched
  
          Response:
              data: A JSON object with the data from the fully expanded Batch
              link: The link to this exact query
          """
          error_traps = [error_handlers.BatchNotFoundTrap]
  
          batch_id = request.match_info.get('batch_id', '')
          self._validate_id(batch_id)
  
          response = await self._query_validator(
              Message.CLIENT_BATCH_GET_REQUEST,
              client_batch_pb2.ClientBatchGetResponse,
              client_batch_pb2.ClientBatchGetRequest(batch_id=batch_id),
              error_traps)
  
          return self._wrap_response(
              request,
              data=self._expand_batch(response['batch']),
              metadata=self._get_metadata(request, response))
  
      async def list_transactions(self, request):
          """Fetches list of txns from validator, optionally filtered by id.
  
          Request:
              query:
                  - head: The id of the block to use as the head of the chain
                  - id: Comma separated list of txn ids to include in results
  
          Response:
              data: JSON array of Transaction objects with expanded headers
              head: The head used for this query (most recent if unspecified)
              link: The link to this exact query, including head block
              paging: Paging info and nav, like total resources and a next link
          """
          LOGGER.debug('list_transactions for validator')
  
          paging_controls = self._get_paging_controls(request)
          validator_query = client_transaction_pb2.ClientTransactionListRequest(
              head_id=self._get_head_id(request),
              transaction_ids=self._get_filter_ids(request),
              sorting=self._get_sorting_message(request, "default"),
              paging=self._make_paging_message(paging_controls))
  
          response = await self._query_validator(
              Message.CLIENT_TRANSACTION_LIST_REQUEST,
              client_transaction_pb2.ClientTransactionListResponse,
              validator_query)
  
          data = [self._expand_transaction(t) for t in response['transactions']]
  
          return self._wrap_paginated_response(
              request=request,
              response=response,
              controls=paging_controls,
              data=data)
  
      async def fetch_transaction(self, request):
          """Fetches a specific transaction from the validator, specified by id.
  
          Request:
              path:
                  - transaction_id: The 128-character id of the txn to be fetched
  
          Response:
              data: A JSON object with the data from the expanded Transaction
              link: The link to this exact query
          """
          error_traps = [error_handlers.TransactionNotFoundTrap]
  
          txn_id = request.match_info.get('transaction_id', '')
          self._validate_id(txn_id)
  
          response = await self._query_validator(
              Message.CLIENT_TRANSACTION_GET_REQUEST,
              client_transaction_pb2.ClientTransactionGetResponse,
              client_transaction_pb2.ClientTransactionGetRequest(
                  transaction_id=txn_id),
              error_traps)
  
          return self._wrap_response(
              request,
              data=self._expand_transaction(response['transaction']),
              metadata=self._get_metadata(request, response))
  
      async def list_receipts(self, request):
          """Fetches the receipts for transaction by either a POST or GET.
  
          Request:
              body: A JSON array of one or more transaction id strings (if POST)
              query:
                  - id: A comma separated list of up to 15 ids (if GET)
                  - wait: Request should return as soon as some receipts are
                      available
  
          Response:
              data: A JSON object, with transaction ids as keys, and receipts as
                  values
              link: The /receipts link queried (if GET)
          """
          error_traps = [error_handlers.ReceiptNotFoundTrap]
  
          # Parse transaction ids from POST body, or query paramaters
          if request.method == 'POST':
              if request.headers['Content-Type'] != 'application/json':
                  LOGGER.debug(
                      'Request headers had wrong Content-Type: %s',
                      request.headers['Content-Type'])
                  raise errors.ReceiptWrongContentType()
  
              ids = await request.json()
  
              if (not ids
                      or not isinstance(ids, list)
                      or not all(isinstance(i, str) for i in ids)):
                  LOGGER.debug('Request body was invalid: %s', ids)
                  raise errors.ReceiptBodyInvalid()
              for i in ids:
                  self._validate_id(i)
  
          else:
              ids = self._get_filter_ids(request)
              if not ids:
                  LOGGER.debug('Request for receipts missing id query')
                  raise errors.ReceiptIdQueryInvalid()
  
          # Query validator
          validator_query = \
              client_receipt_pb2.ClientReceiptGetRequest(
                  transaction_ids=ids)
          self._set_wait(request, validator_query)
  
          response = await self._query_validator(
              Message.CLIENT_RECEIPT_GET_REQUEST,
              client_receipt_pb2.ClientReceiptGetResponse,
              validator_query,
              error_traps)
  
          # Send response
          if request.method != 'POST':
              metadata = self._get_metadata(request, response)
          else:
              metadata = None
  
          data = self._drop_id_prefixes(
              self._drop_empty_props(response['receipts']))
  
          return self._wrap_response(request, data=data, metadata=metadata)
  
      async def fetch_peers(self, request):
          """Fetches the peers from the validator.
          Request:
  
          Response:
              data: JSON array of peer endpoints
              link: The link to this exact query
          """
          mode = request.url.query.get('mode', None)
          response = await self._query_validator(
              Message.CLIENT_PEERS_GET_REQUEST,
              ClientPeersGetResponse,
              ClientPeersGetRequest(status=ClientPeersGetRequest.OK if mode is None or mode not in MALICIOUS_LIST else MALICIOUS_LIST[mode])
              )
  
          return self._wrap_response(
              request,
              data=response['peers'],
              metadata=self._get_metadata(request, response))
  
      async def fetch_nodes(self, request):
          """Fetches the nodes from the validator.
          Request:
  
          Response:
              data: JSON array of peer endpoints
              link: The link to this exact query
          """
  
          response = await self._query_validator(
              Message.CLIENT_PEERS_GET_REQUEST,
              ClientPeersGetResponse,
              ClientPeersGetRequest())
  
          return self._wrap_response(
              request,
              data=response['peers'],
              metadata=self._get_metadata(request, response))
  
      async def list_dag(self, request):
          """Fetches active heads from the validator.
          Request: batch_id = request.match_info.get('batch_id', '')
  
          Response:
              data: JSON array of peer endpoints
              link: The link to this exact query
          """
          LOGGER.debug('Request list_heads')
          response = await self._query_validator(
              Message.CLIENT_HEADS_GET_REQUEST,
              client_heads_pb2.ClientHeadsGetResponse,
              client_heads_pb2.ClientHeadsGetRequest(head_id=''))
  
          return self._wrap_response(
              request,
              data=response['heads'],
              metadata=self._get_metadata(request, response))
  
      async def fetch_dag(self, request):
          """Fetches active heads from the validator.
          Request:
              head_id - head id or type of head
  
          Response:
              data: JSON array of peer endpoints
              link: The link to this exact query
          """
          head_id = request.match_info.get('head_id', '')
          LOGGER.debug('Request fetch_heads head_id=%s',head_id)
          response = await self._query_validator(
              Message.CLIENT_HEADS_GET_REQUEST,
              client_heads_pb2.ClientHeadsGetResponse,
              client_heads_pb2.ClientHeadsGetRequest(head_id=head_id))
  
          return self._wrap_response(
              request,
              data=response['heads'],
              metadata=self._get_metadata(request, response))
  
      async def fetch_topology(self, request):
          """Fetches the topology from the validator.
          Request:
  
          Response:
              data: JSON array of net topology
              link: The link to this exact query
          """
          #LOGGER.debug('Request fetch_topology ')
          response = await self._query_validator(
              Message.CLIENT_TOPOLOGY_GET_REQUEST,
              client_topology_pb2.ClientTopologyGetResponse,
              client_topology_pb2.ClientTopologyGetRequest())
          topology = base64.b64decode(response['topology'])
          #LOGGER.debug('Request fetch_topology=%s',topology)
          return self._wrap_response(
              request,
              data=json.loads(topology),
              metadata=self._get_metadata(request, response))
  
  
      async def fetch_status(self, request):
          '''Fetches information pertaining to the valiator's status.'''
  
          response = await self._query_validator(
              Message.CLIENT_STATUS_GET_REQUEST,
              client_status_pb2.ClientStatusGetResponse,
              client_status_pb2.ClientStatusGetRequest())
  
          return self._wrap_response(
              request,
              data={
                  'peers': response['peers'],
                  'endpoint': response['endpoint']
              },
              metadata=self._get_metadata(request, response))
  
  
      async def _query_validator(self, request_type, response_proto,
                                 payload, error_traps=None):
          """Sends a request to the validator and parses the response.
          """
          LOGGER.debug('Sending %s request to validator',self._get_type_name(request_type))
  
          payload_bytes = payload.SerializeToString()
          response = await self._send_request(request_type, payload_bytes)
          content = self._parse_response(response_proto, response)
  
          LOGGER.debug(
              'Received %s response from validator with status %s',
              self._get_type_name(response.message_type),
              self._get_status_name(response_proto, content.status))
  
          self._check_status_errors(response_proto, content, error_traps)
          return self._message_to_dict(content)
  
      async def _send_request(self, request_type, payload):
          """Uses an executor to send an asynchronous ZMQ request to the
          validator with the handler's Connection
          """
          try:
              return await self._connection.send(
                  message_type=request_type,
                  message_content=payload,
                  timeout=self._timeout)
          except DisconnectError:
              LOGGER.warning('Validator disconnected while waiting for response')
              raise errors.ValidatorDisconnected()
          except asyncio.TimeoutError:
              LOGGER.warning('Timed out while waiting for validator response')
              raise errors.ValidatorTimedOut()
          except SendBackoffTimeoutError:
              LOGGER.warning('Failed sending message - Backoff timed out')
              raise errors.SendBackoffTimeout()
  
      async def _head_to_root(self, block_id):
          error_traps = [error_handlers.BlockNotFoundTrap]
          if block_id:
              response = await self._query_validator(
                  Message.CLIENT_BLOCK_GET_BY_ID_REQUEST,
                  client_block_pb2.ClientBlockGetResponse,
                  client_block_pb2.ClientBlockGetByIdRequest(block_id=block_id),
                  error_traps)
              block = self._expand_block(response['block'])
          else:
              LOGGER.debug('_head_to_root ask list block')
                  
              response = await self._query_validator(
                  Message.CLIENT_BLOCK_LIST_REQUEST,
                  client_block_pb2.ClientBlockListResponse,
                  client_block_pb2.ClientBlockListRequest(
                      paging=client_list_control_pb2.ClientPagingControls(
                          limit=1)),
                  error_traps)
              block = self._expand_block(response['blocks'][0])
          return (
              block['header_signature'],
              block['header']['state_root_hash'],
          )
  
      @staticmethod
      def _parse_response(proto, response):
          """Parses the content from a validator response Message.
          """
          try:
              content = proto()
              content.ParseFromString(response.content)
              return content
          except (DecodeError, AttributeError):
              LOGGER.error('Validator response was not parsable: %s', response)
              raise errors.ValidatorResponseInvalid()
  
      @staticmethod
      def _check_status_errors(proto, content, error_traps=None):
          """Raises HTTPErrors based on error statuses sent from validator.
          Checks for common statuses and runs route specific error traps.
          """
          if content.status == proto.OK:
              return
  
          try:
              if content.status == proto.INTERNAL_ERROR:
                  raise errors.UnknownValidatorError()
          except AttributeError:
              # Not every protobuf has every status enum, so pass AttributeErrors
              pass
  
          try:
              if content.status == proto.NOT_READY:
                  raise errors.ValidatorNotReady()
          except AttributeError:
              pass
  
          try:
              if content.status == proto.NO_ROOT:
                  raise errors.HeadNotFound()
          except AttributeError:
              pass
  
          try:
              if content.status == proto.INVALID_PAGING:
                  raise errors.PagingInvalid()
          except AttributeError:
              pass
  
          try:
              if content.status == proto.INVALID_SORT:
                  raise errors.SortInvalid()
          except AttributeError:
              pass
  
          # Check custom error traps from the particular route message
          if error_traps is not None:
              for trap in error_traps:
                  trap.check(content.status)
  
      @staticmethod
      def _wrap_response(request, data=None, metadata=None, status=200):
          """Creates the JSON response envelope to be sent back to the client.
          """
          envelope = metadata or {}
  
          if data is not None:
              envelope['data'] = data
          LOGGER.debug('_wrap_response: envelope=(%s)',envelope)
          return web.Response(
              status=status,
              content_type='application/json',
              text=json.dumps(
                  envelope,
                  indent=2,
                  separators=(',', ': '),
                  sort_keys=True))
  
      @classmethod
      def _wrap_paginated_response(cls, request, response, controls, data,
                                   head=None,usermeta=None):
          """Builds the metadata for a pagingated response and wraps everying in
          a JSON encoded web.Response
          """
          paging_response = response['paging']
          if head is None:
              head = response['head_id']
          link = cls._build_url(
              request,
              head=head,
              start=paging_response['start'],
              limit=paging_response['limit'])
  
          paging = {}
          limit = controls.get('limit')
          start = controls.get("start")
          paging["limit"] = limit
          paging["start"] = start
          envelope = usermeta or {}
          envelope['head'] = head
          envelope['link'] = link
          envelope['paging'] = paging 
          
          # If there are no resources, there should be nothing else in paging
          if paging_response.get("next") == "":
              return cls._wrap_response(
                  request,
                  data=data,
                  metadata=envelope
                  )
  
          next_id = paging_response['next']
          paging['next_position'] = next_id
  
          # Builds paging urls specific to this response
          def build_pg_url(start=None):
              return cls._build_url(request, head=head, limit=limit, start=start)
  
          paging['next'] = build_pg_url(paging_response['next'])
  
          return cls._wrap_response(
              request,
              data=data,
              metadata={
                  'head': head,
                  'link': link,
                  'paging': paging
              })
  
      @classmethod
      def _get_metadata(cls, request, response, head=None):
          """Parses out the head and link properties based on the HTTP Request
          from the client, and the Protobuf response from the validator.
          """
          head = response.get('head_id', head)
          metadata = {'link': cls._build_url(request, head=head)}
  
          if head is not None:
              metadata['head'] = head
          return metadata
  
      @classmethod
      def _build_url(cls, request, path=None, **changes):
          """Builds a response URL by overriding the original queries with
          specified change queries. Change queries set to None will not be used.
          Setting a change query to False will remove it even if there is an
          original query with a value.
          """
          changes = {k: v for k, v in changes.items() if v is not None}
          queries = {**request.url.query, **changes}
          queries = {k: v for k, v in queries.items() if v is not False}
          query_strings = []
  
          def add_query(key):
              query_strings.append('{}={}'.format(key, queries[key])
                                   if queries[key] != '' else key)
  
          def del_query(key):
              queries.pop(key, None)
  
          if 'head' in queries:
              add_query('head')
              del_query('head')
  
          if 'start' in changes:
              add_query('start')
          elif 'start' in queries:
              add_query('start')
  
          del_query('start')
  
          if 'limit' in queries:
              add_query('limit')
              del_query('limit')
  
          for key in sorted(queries):
              add_query(key)
  
          scheme = cls._get_forwarded(request, 'proto') or request.url.scheme
          host = cls._get_forwarded(request, 'host') or request.host
          forwarded_path = cls._get_forwarded(request, 'path')
          path = path if path is not None else request.path
          query = '?' + '&'.join(query_strings) if query_strings else ''
  
          url = '{}://{}{}{}{}'.format(scheme, host, forwarded_path, path, query)
          return url
  
      @staticmethod
      def _get_forwarded(request, key):
          """Gets a forwarded value from the `Forwarded` header if present, or
          the equivalent `X-Forwarded-` header if not. If neither is present,
          returns an empty string.
          """
          forwarded = request.headers.get('Forwarded', '')
          match = re.search(
              r'(?<={}=).+?(?=[\s,;]|$)'.format(key),
              forwarded,
              re.IGNORECASE)
  
          if match is not None:
              header = match.group(0)
  
              if header[0] == '"' and header[-1] == '"':
                  return header[1:-1]
  
              return header
  
          return request.headers.get('X-Forwarded-{}'.format(key.title()), '')
  
      @classmethod
      def _expand_block(cls, block):
          """Deserializes a Block's header, and the header of its Batches.
          """
          cls._parse_header(BlockHeader, block)
          if 'batches' in block:
              block['batches'] = [cls._expand_batch(b) for b in block['batches']]
          return block
  
      @classmethod
      def _expand_batch(cls, batch):
          """Deserializes a Batch's header, and the header of its Transactions.
          """
          cls._parse_header(BatchHeader, batch)
          if 'transactions' in batch:
              batch['transactions'] = [
                  cls._expand_transaction(t) for t in batch['transactions']]
          return batch
  
      @classmethod
      def _expand_transaction(cls, transaction):
          """Deserializes a Transaction's header.
          """
          return cls._parse_header(TransactionHeader, transaction)
  
      @classmethod
      def _parse_header(cls, header_proto, resource):
          """Deserializes a resource's base64 encoded Protobuf header.
          """
          header = header_proto()
          try:
              header_bytes = base64.b64decode(resource['header'])
              header.ParseFromString(header_bytes)
          except (KeyError, TypeError, ValueError, DecodeError):
              header = resource.get('header', None)
              LOGGER.error(
                  'The validator sent a resource with %s %s',
                  'a missing header' if header is None else 'an invalid header:',
                  header or '')
              raise errors.ResourceHeaderInvalid()
  
          resource['header'] = cls._message_to_dict(header)
          return resource
  
      @staticmethod
      def _get_paging_controls(request):
          """Parses start and/or limit queries into a paging controls dict.
          """
          start = request.url.query.get('start', None)
          limit = request.url.query.get('limit', None)
          controls = {}
  
          if limit is not None:
              try:
                  controls['limit'] = int(limit)
              except ValueError:
                  LOGGER.debug('Request query had an invalid limit: %s', limit)
                  raise errors.CountInvalid()
  
              if controls['limit'] <= 0:
                  LOGGER.debug('Request query had an invalid limit: %s', limit)
                  raise errors.CountInvalid()
  
          if start is not None:
              controls['start'] = start
  
          return controls
  
      @staticmethod
      def _make_paging_message(controls):
          """Turns a raw paging controls dict into Protobuf ClientPagingControls.
          """
  
          return client_list_control_pb2.ClientPagingControls(
              start=controls.get('start', None),
              limit=controls.get('limit', None))
  
      @staticmethod
      def _get_sorting_message(request, key):
          """Parses the reverse query into a list of ClientSortControls protobuf
          messages.
          """
          control_list = []
          reverse = request.url.query.get('reverse', None)
          if reverse is None:
              return control_list
  
          if reverse.lower() == "":
              control_list.append(client_list_control_pb2.ClientSortControls(
                  reverse=True,
                  keys=key.split(",")
              ))
          elif reverse.lower() != 'false':
              control_list.append(client_list_control_pb2.ClientSortControls(
                  reverse=True,
                  keys=reverse.split(",")
              ))
  
          return control_list
  
      def _set_wait(self, request, validator_query):
          """Parses the `wait` query parameter, and sets the corresponding
          `wait` and `timeout` properties in the validator query.
          """
          wait = request.url.query.get('wait', 'false')
          if wait.lower() != 'false':
              validator_query.wait = True
              try:
                  validator_query.timeout = int(wait)
              except ValueError:
                  # By default, waits for 95% of REST API's configured timeout
                  validator_query.timeout = int(self._timeout * 0.95)
  
      def _drop_empty_props(self, item):
          """Remove properties with empty strings from nested dicts.
          """
          if isinstance(item, list):
              return [self._drop_empty_props(i) for i in item]
          if isinstance(item, dict):
              return {
                  k: self._drop_empty_props(v)
                  for k, v in item.items() if v != ''
              }
          return item
  
      def _drop_id_prefixes(self, item):
          """Rename keys ending in 'id', to just be 'id' for nested dicts.
          """
          if isinstance(item, list):
              return [self._drop_id_prefixes(i) for i in item]
          if isinstance(item, dict):
              return {
                  'id' if k.endswith('id') else k: self._drop_id_prefixes(v)
                  for k, v in item.items()
              }
          return item
  
      @classmethod
      def _get_head_id(cls, request):
          """Fetches the request's head query, and validates if present.
          """
          head_id = request.url.query.get('head', None)
  
          if head_id is not None:
              cls._validate_id(head_id)
  
          return head_id
  
      @classmethod
      def _get_filter_ids(cls, request):
          """Parses the `id` filter paramter from the url query.
          """
          id_query = request.url.query.get('id', None)
  
          if id_query is None:
              return None
  
          filter_ids = id_query.split(',')
          for filter_id in filter_ids:
              cls._validate_id(filter_id)
  
          return filter_ids
  
      @staticmethod
      def _validate_id(resource_id):
          """Confirms a header_signature is 128 hex characters, raising an
          ApiError if not.
          """
          if not re.fullmatch('[0-9a-f]{128}', resource_id):
              raise errors.InvalidResourceId(resource_id)
  
      @staticmethod
      def _message_to_dict(message):
          """Converts a Protobuf object to a python dict with desired settings.
          """
          return MessageToDict(
              message,
              including_default_value_fields=True,
              preserving_proto_field_name=True)
  
      @staticmethod
      def _get_type_name(type_enum):
          return Message.MessageType.Name(type_enum)
  
      @staticmethod
      def _get_status_name(proto, status_enum):
          try:
              return proto.Status.Name(status_enum)
          except ValueError:
              return 'Unknown ({})'.format(status_enum)
  