class AbstractOPServiceMonitor(object):
  """
  An abstract base class that can be used to execute custom code upon starting and finishing each request to an
  OPService instance. Add a monitor instance to an OPService instance using the 'add_monitor' function. Note that
  monitors will be evaluated in the same order in which they are added.

  Note that these monitors are only triggered on calls that explicitly follow the OP pattern (see the
  'OPService._is_op_request' function).

  The functions 'start' and 'finish' may be called asynchronously, though you are guaranteed that 'start' will complete
  before 'finish' is called.

  Example - a monitor that prints the route name and time taken for each request (regardless of success):

  import threading
  from datetime import datetime
  from op_service.service import OPService

  class TimerMonitor(AbstractOPServiceMonitor):

    def on_route_start(self, state):
      threading.local().start = datetime.now()

    def on_route_end(self, state):
      start = threading.local().start
      end = datetime.now()
      print("%s: %fs" % (state['route'], (end - start).total_seconds()))

  OP = OPService.instance()
  OP.add_monitor(TimerMonitor())
  """


  def on_route_start(self, state):
    """
    This function will be called immediately as soon as a request is received (even before the request data is
    deserialized or the auth headers have been processed).

    Parameters:
      state: a dictionary that describes the request state. Treat it immutably as it can be shared between threads.
        Keys:
        - metadata: the service metadata object: { 'pkg_id': ..., 'commit_id': ..., 'config_id': ... }
        - route: the name of the route at which this request is directed
    """


  def on_route_end(self, state):
    """
    This function will be called after a request ends. It cannot affect the response. The 'state' dictionary contains
    properties that can be used to determine what response was sent.

    Parameters:
      state: a dictionary that describes the request state. Treat it immutably as it can be shared between threads. Note
        that not all of the keys listed below will be present depending on whether or not the request failed (and at
        what point) - the reason in brackets indicates why the key may be missing.
        Keys:
        - metadata: the service metadata object: { 'pkg_id': ..., 'commit_id': ..., 'config_id': ... }
        - route: the name of the route at which this request is directed
        - response_code: the HTTP integer code of the response (i.e. 200)
        - response_data: the serialized JSON response data
        - exc_info: [only if an exception was thrown] the python exc_info object of the thrown exception
        - profile: [only if the the route specifies a profile validation format] the decoded JWT profile from the call
          headers
        - profile_validated: [only if the the route specifies a profile validation format and the raw profile passes that
          validation] the validated JWT profile (see 'profile' key)
        - input_data: [only if the route specifies an input data validation format] the deserialized input data
        - input_data_validated: [only if the route specifies an input data validation format and the input data passes
          that validation] the validated input data (see 'input_data' key)
    """
