text: from kiwoom import config
  from kiwoom.config import valid_event
  
  from functools import wraps
  from textwrap import dedent
  from types import LambdaType
  from inspect import (
      getattr_static,
      ismethod,
      isfunction,
      isclass,
      ismodule
  )
  
  
  class Connector:
      """
      Decorator class for mapping empty events to user implementing slots.
  
      This class helps mapping events to specific slots. 'Kiwoom' instance
      contains mapping information which had been set from two methods.
  
      1) Kiwoom.connect(event, signal, slot)
      2) Kiwoom.set_connect_hook(event, param)
  
      This decorator does not contain those mapping information but only
      uses the one defined in instance. This is because decorator should
      work on various contexts. The first parameter of wrapper function
      in __call__ method, i.e. api, is 'self' argument for Kiwoom object.
  
      This class has three main methods.
      1) Connector.map(event)
        - act as a decorator for pre-defined Kiwoom events
        - below is the usage example
  
          class Kiwoom(API):
              ...
              @map
              def on_event_connect(self, err_code):
                  pass
              ...
  
      2) Connector.mute(bool)  # static method
        - warning message can be turned on/off.
  
      3) Connector.connectable(fn)  # static method
        - Check if given fn is a bounded method to an instance.
        - If fn is not a bounded method, it should be static or lambda.
        - Bounded method is important to handle continuous information
      """
      # Class variable
      nargs = {
          'on_event_connect': 1,
          'on_receive_msg': 4,
          'on_receive_tr_data': 5,
          'on_receive_real_data': 3,
          'on_receive_chejan_data': 3,
          'on_receive_condition_ver': 2,
          'on_receive_tr_condition': 5,
          'on_receive_real_condition': 4
      }
  
      def __init__(self):
          # If no hook is set, dic[event] returns signal/slot.
          # If hook is set, dic[event][key] returns signal/slot.
          self._hooks = dict()
          self._signals = dict()
          self._slots = dict()
          self._indices = dict()
  
      def signal(self, event, key=None):
          """
          Returns signal methods connected to the event.
  
          If signal and slot are connected to a specific event by Kiwoom.connect() method,
          then this method returns the connected signal method. If signal is not connected,
          or wrong key is given, raise a KeyError.
  
          'key' is needed when hook is set by Kiwoom.set_connect_hook(). 'key' is set to
          be the name of signal method by default unless another string is set on purpose
          when connecting.
  
          When requesting data to server is needed, specifically if more data is available,
          Kiwoom.signal() returns the exact signal method that can request more data.
  
          :param event: str
              One of the pre-defined event names in string. See kiwoom.config.EVENTS.
          :param key: str, optional
              If hook is set by Kiwoom.set_connect_hook() method and signal is connected
              by Kiwoom.connect(), then key is needed. 'key' is set to be name of the
              signal method by default unless another 'key' is given when connecting.
          :return: method
              Signal method connected to the given event. If wrong event, returns None.
          """
          if not valid_event(event):
              return None
          if not self.connect_hook(event):
              return self._signals[event]
          return self._signals[event][key]
  
      def slot(self, event, key=None):
          """
          Returns slot methods connected to the event.
  
          If signal and slot are connected to specific event by Kiwoom.connect() method,
          then this method returns the connected slot method. If slot is not connected,
          or wrong key is given, this raises a KeyError.
  
          'key' is needed when hook is set by Kiwoom.set_connect_hook(). 'key' is set to
          be the name of slot method by default unless another string is set on purpose
          when connecting.
  
          When an event is called, Kiwoom.slot() returns the exact slot method that can
          handle data received from the event. This method is used in Connector decorator
          that wraps events to execute connected slot with the event.
  
          :param event: str
              One of the pre-defined event names in string. See kiwoom.config.EVENTS.
          :param key: str, optional
              If hook is set by Kiwoom.set_connect_hook() method and slot is connected
              by Kiwoom.connect(), then key is needed. 'key' is set to be name of the
              slot method by default unless another 'key' is given when connecting.
          :return: method or None
              Slot method connected to the given event. If wrong event, returns None.
          """
          if not valid_event(event):
              return None
          if not self.connect_hook(event):
              return self._slots[event]
          return self._slots[event][key]
  
      def connect(self, event, signal=None, slot=None, key=None):
          """
          Connects signals and slots to one of pre-defined events.
  
          Information saved in this method is used by decorator @Connector() which wraps
          the events and automatically calls the right slot connected to the events. In
          addition to the decorator, Kiwoom.signal(event, key) and Kiwoom.slot(event, key)
          returns the one connected to the event.
  
          1) If no hook is set on the event, then the connected signal/slot can be retrieved
             by Kiwoom.signal(event) and Kiwoom.slot(event). There is no need to use key.
  
          2) If hook is set by Kiwoom.set_connect_hook() on the event, in which case there
             needs multiple slots to connect on one event, then connection requires a key
             which is to be the name of signal/slot methods by default.
  
             The convention to utilizing this module recommends to define the name of related
             signal and slot to be the same. Then it becomes easier to manage and develop codes.
  
             Use 'key' arg only when there is a special need. The connected signal/slot can be
             retrieved by Kiwoom.signal(event, key='name') and Kiwoom.slot(event, key='name').
             Here 'name' can be a method name or special 'key' used in this method.
  
          This method checks whether or not given signal/slot can be called without any
          problem. If given method is not bounded to an instance, method should be static
          or lambda function. This is because normally 'self' argument is needed to call
          methods, therefore method must be bounded to an instance unless given method is
          a function.
  
          Please see tutorials example on the link below.
          https://github.com/breadum/kiwoom/blob/main/tutorials/4.%20TR%20Data.py
  
          :param event: str
              One of the pre-defined event names in string. See kiwoom.config.EVENTS.
          :param signal: method, optional
              A method that requests to the server
          :param slot: method, optional
              A method that reacts the server's response
          :param key: str, optional
              Key is needed only if hook is set by Kiwoom.set_connect_hook() method.
              Key is set to be name of the given signal and/or slot method by default.
              If key is given other than method name, the connected signal can be
              retrieved by Kiwoom.siganl(event, key) and slot by Kiwoom.slot(event, key)
          """
          valid = False
          connectable = Connector.connectable
  
          if not valid_event(event):
              return
  
          # Directly connect slot to the event
          if not self.connect_hook(event):
              # Key can't be used here
              if key is not None:
                  raise RuntimeError(
                      "Key can't be used. Remove key argument or Try to set_connect_hook() first."
                  )
  
              elif connectable(signal):
                  if connectable(slot):
                      valid = True
                      self._signals[event] = signal
                      self._slots[event] = slot
  
              elif connectable(slot):
                  valid = True
                  self._slots[event] = slot
  
          # Connect slot to the event when
          else:
              if connectable(signal):
                  if connectable(slot):
                      valid = True
                      # Key other than method's name
                      if key is not None:
                          self._signals[event][key] = signal
                          self._slots[event][key] = slot
                      # Default key is method's name
                      else:
                          self._signals[event][getattr(signal, '__name__')] = signal
                          self._slots[event][getattr(slot, '__name__')] = slot
  
              elif connectable(slot):
                  valid = True
                  if key is not None:
                      self._slots[event][key] = slot
                  else:
                      self._slots[event][getattr(slot, '__name__')] = slot
  
          # Nothing is connected
          if not valid:
              raise RuntimeError(f"Unsupported combination of inputs. Please read below.\n\n{self.connect.__doc__}")
  
      def connect_hook(self, event):
          """
          Returns whether a hook is set for given event.
  
          :param event: str
              One of the pre-defined event names in string. See kiwoom.config.EVENTS.
          :return: bool
          """
          if event in self._hooks:
              return True
          return False
  
      def set_connect_hook(self, event, param):
          """
          Set parameter defined in event as a hook to find the right slot when event is called.
  
          When an event needs multiple slots to connect, depending on specific tasks, set
          a hook(key) to select which slot to map. The hook must be one of the parameters
          in the definition of the event method. Parameters can be found by help built-in
          function or Kiwoom.api_arg_spec(event). This raises a KeyError if given param is
          not defined in event method.
  
          If hook is set to the given parameter, argument passed into the parameter when
          the event is called, is going to be a key to connect event, signal and slot.
  
          Convention is that the name of signal and slot that deal with the related task
          is recommended to be the same, so that 'key' is set to be the method name of
          signal and slot by default. See examples on the tutorials link below.
          https://github.com/breadum/kiwoom/blob/main/tutorials/5.%20TR%20Data.py
  
          :param event: str
              One of the pre-defined event names in string. See kiwoom.config.EVENTS.
          :param param: str
              Parameter name defined in given event. To see all parameters to event,
              use Kiwoom.api_arg_spec(event) method or help(...) built-in function.
          """
          if not valid_event(event):
              return
  
          # To check given arg is valid
          from kiwoom import Kiwoom  # lazy import
          args = Kiwoom.api_arg_spec(event)
          if param not in args:
              raise KeyError(f"{param} is not valid.\nSelect one of {args}.")
  
          # To set connect hook and its index in args
          self._hooks[event] = param
          self._indices[event] = list(args.keys()).index(param) - 1  # except 'self'
  
          # Initialize structure to get signal/slot method by dic[event][key]
          self._signals[event] = dict()
          self._slots[event] = dict()
  
      def get_connect_hook(self, event):
          """
          Returns a hook (i.e. name of parameter) set in given event.
  
          :param event: str
              One of the pre-defined event names in string. See kiwoom.config.EVENTS.
          :return: str or None
              If exists, returns hook in string else None. If not a valid event is given,
              this returns None.
          """
          if event not in self._hooks:
              return None
          return self._hooks[event]
  
      def remove_connect_hook(self, event):
          """
          Remove hook which is set in given event if exists.
  
          This method removes all information of signals and slots connected to given
          event as well as hook. If hook of given event does not exist, this raises
          a KeyError.
  
          :param event: str
              One of the pre-defined event names in string. See kiwoom.config.EVENTS.
          """
          del self._hooks[event]
          del self._signals[event]
          del self._slots[event]
          del self._indices[event]
  
      def get_hook_index(self, event):
          """
          Returns index of hook in method arguments
  
          :param event: str
          :return: int
          """
          if event in self._indices:
              return self._indices[event]
          return None
  
      @staticmethod
      def map(ehandler):
          """
          Decorator method to call slot method when event connected to slot has been called.
  
          When event has been called, this decorator method is called with event method as arg,
          'ehandler'. Then wrapper function wraps the event and will be executed.
  
          Inside the wrapper, it takes all args from the event. The First arg is 'self' which is
          an instance of Kiwoom class. The rest of args depends on which event has been called.
  
          Firstly, execute event handler which is initially an empty method in the module. But
          this process is needed for when an empty default method is overridden. Then, find the
          right slot connected to the event. If found, execute it with the same args forwarded
          from event. If not, just print a warning message. This message can be turned on/off.
  
          Usage example
          >>  class Kiwoom(API):
          >>      ...
          >>      @map  # decorator
          >>      def on_event_connect(self, err_code):
          >>          pass  # empty event
          >>      ...
  
          :param ehandler: method
              One of pre-defined event handlers, see kiwoom.config.EVENTS.
          :return: function
              Wrapper function that executes a slot method connected to the event.
          """
          @wraps(ehandler)  # keep docstring of event handler
          def wrapper(api, *args):
              # Variables
              event = getattr(ehandler, '__name__')
              idx = api.get_hook_index(event)
              hook = api.get_connect_hook(event)
              args = args[:Connector.nargs[event]]
  
              # To execute the default event handler in case of overriding
              ehandler(api, *args)
  
              # To find connected slot
              try:
                  # If hook is set on the event, then key becomes arg that corresponds to the hook
                  # ex) if hook is rq_name for on_receive_tr_data, then key becomes arg passed into rq_name
                  key = args[idx] if hook else None
                  # To retrieve the right slot
                  slot = api.slot(event, key)
  
              except KeyError:
                  if not config.MUTE:
                      msg = dedent(
                          f"""
                          kiwoom.{event}({', '.join(map(str, args))}) has been called.
      
                          But the event handler, '{event}', is not connected to any slot.
                          Please try to connect event and slot by using kiwoom.connect() method.
                            >> api.connect('{event}', slot=slot_method)
      
                          This warning message can disappear by the following. 
                            >> kiwoom.config.MUTE = True  # global variable
                          """
                      )
                      print(msg)
                  # To prevent executing slot that does not exist, just return
                  return
  
              # Execute the connected slot
              slot(*args)
  
          # Return wrapper function to decorate
          return wrapper
  
      @classmethod
      def mute(cls, bool):
          """
          Class method to turn on/off printing warning message of no connected slot to an event.
  
          Usage example
          >> Connector.mute(True/False)
  
          :param bool: bool
              If True, no warning message else warning.
          """
          config.MUTE = bool
  
      @staticmethod
      def connectable(fn):
          """
          Static function that checks given fn is callable and bounded method.
  
          When event is called, a slot mapped to the event is to be called by decorator.
          If fn is not a static function nor lambda function, fn needs 'self' argument to
          work correctly. This is why fn should be bounded method to instance object.
          Bounded method contains 'self' argument by itself. This method is used in
          Kiwoom.connect(event, signal, slot) to check validity before making connections.
          When given fn is not valid to connect, this function raises TypeError.
  
          :param fn: method, function or None
              Any callables to be tested. None is exceptionally accepted.
          :return: bool
              If a callable can be executed without any problem, returns True.
          """
          # None by default False
          if fn is None:
              return False
  
          # Instance method, Class method
          if ismethod(fn):
              return True
  
          # Normal function, Class function, Lambda function
          if isfunction(fn):
              # Lambda function
              if isinstance(fn, LambdaType):
                  return True
  
              qname = getattr(fn, '__qualname__')
              if '.' in qname:
                  idx = qname.rfind('.')
                  prefix, fname = qname[:idx], qname[idx + 1:]
                  obj = eval(prefix)
  
                  # Normal function
                  if ismodule(obj):
                      return True
  
                  # Class function
                  if isclass(obj):
  
                      # Static method
                      if isinstance(getattr_static(obj, fname), staticmethod):
                          return True
  
                      # Class function should be bound to an instance
                      if not hasattr(fn, '__self__'):
                          raise TypeError(dedent(
                              f"""
                                  Given '{fname}' must be instance method, not function.
                                  Try to make an instance first as followings.
      
                                    >> var = {prefix}(*args, **kwargs)
                                    >> kiwoom.connect(.., var.{fname}, ..)
                                  """
                          ))  # False
  
          # Just a callable object (a class that has '__call__')
          elif callable(fn):
              # Static call
              if isinstance(getattr_static(fn, '__call__'), staticmethod):
                  return True
  
              # Non-static call should be bound to an instance
              if not hasattr(fn, '__self__'):
                  raise TypeError(dedent(
                      f"""
                      Given '{getattr(fn, '__qualname__')} must be bound to instance.
                      Try to make an instance first as followings.
  
                        >> var = {getattr(fn, '__name__')}(*args, **kwargs)
                        >> kiwoom.connect(.., var, ..)
                      """
                  ))  # False
  
          # Not a valid argument
          else:
              from kiwoom.core.kiwoom import Kiwoom  # Dynamic import to avoid circular import
              raise TypeError(
                  f"Unsupported type, {type(fn)}. Please try with valid args.\n\n{Kiwoom.connect.__doc__}."
              )  # False
  