docs: def generate_token(self, session_id, role=Roles.publisher, expire_time=None, data=None,
          initial_layout_class_list=[]):
          """
          Generates a token for a given session.
  
          :param String session_id: The session ID of the session to be accessed by the client using
            the token.
  
          :param String role: The role for the token. Valid values are defined in the Role
            class:
  
            * `Roles.subscriber` -- A subscriber can only subscribe to streams.
  
            * `Roles.publisher` -- A publisher can publish streams, subscribe to
              streams, and signal. (This is the default value if you do not specify a role.)
  
            * `Roles.moderator` -- In addition to the privileges granted to a
              publisher, in clients using the OpenTok.js 2.2 library, a moderator can call the
              `forceUnpublish()` and `forceDisconnect()` method of the
              Session object.
  
          :param int expire_time: The expiration time of the token, in seconds since the UNIX epoch.
            The maximum expiration time is 30 days after the creation time. The default expiration
            time is 24 hours after the token creation time.
  
          :param String data: A string containing connection metadata describing the
            end-user. For example, you can pass the user ID, name, or other data describing the
            end-user. The length of the string is limited to 1000 characters. This data cannot be
            updated once it is set.
  
          :param list initial_layout_class_list: An array of class names (strings)
            to be used as the initial layout classes for streams published by the client. Layout
            classes are used in customizing the layout of videos in
            `live streaming broadcasts <https://tokbox.com/developer/guides/broadcast/#live-streaming>`_ and
            `composed archives <https://tokbox.com/developer/guides/archiving/layout-control.html>`_
  
          :rtype:
            The token string.
          """
  
          # normalize
          # expire_time can be an integer, a datetime object, or anything else that can be coerced into an integer
          # after this block it will only be an integer
          if expire_time is not None:
              if isinstance(expire_time, datetime):
                  expire_time = calendar.timegm(expire_time.utctimetuple())
              else:
                  try:
                      expire_time = int(expire_time)
                  except (ValueError, TypeError):
                      raise OpenTokException(u('Cannot generate token, invalid expire time {0}').format(expire_time))
          else:
              expire_time = int(time.time()) + (60*60*24) # 1 day
  
          # validations
          if not text_type(session_id):
              raise OpenTokException(u('Cannot generate token, session_id was not valid {0}').format(session_id))
          if not isinstance(role, Roles):
              raise OpenTokException(u('Cannot generate token, {0} is not a valid role').format(role))
          now = int(time.time())
          if expire_time < now:
              raise OpenTokException(u('Cannot generate token, expire_time is not in the future {0}').format(expire_time))
          if expire_time > now + (60*60*24*30):  # 30 days
              raise OpenTokException(u('Cannot generate token, expire_time is not in the next 30 days {0}').format(expire_time))
          if data and len(data) > 1000:
              raise OpenTokException(u('Cannot generate token, data must be less than 1000 characters'))
          if initial_layout_class_list and not all(text_type(c) for c in initial_layout_class_list):
              raise OpenTokException(u('Cannot generate token, all items in initial_layout_class_list must be strings'))
          initial_layout_class_list_serialized = u(' ').join(initial_layout_class_list)
          if len(initial_layout_class_list_serialized) > 1000:
              raise OpenTokException(u('Cannot generate token, initial_layout_class_list must be less than 1000 characters'))
  
          # decode session id to verify api_key
          sub_session_id = session_id[2:]
          sub_session_id_bytes = sub_session_id.encode('utf-8')
          sub_session_id_bytes_padded = sub_session_id_bytes + (b('=') * (-len(sub_session_id_bytes) % 4))
          try:
              decoded_session_id = base64.b64decode(sub_session_id_bytes_padded, b('-_'))
              parts = decoded_session_id.decode('utf-8').split(u('~'))
          except Exception as e:
              raise OpenTokException(u('Cannot generate token, the session_id {0} was not valid').format(session_id))
          if self.api_key not in parts:
              raise OpenTokException(u('Cannot generate token, the session_id {0} does not belong to the api_key {1}').format(session_id, self.api_key))
  
          data_params = dict(
              session_id                = session_id,
              create_time               = now,
              expire_time               = expire_time,
              role                      = role.value,
              nonce                     = random.randint(0,999999),
              initial_layout_class_list = initial_layout_class_list_serialized
          )
          if data:
              data_params['connection_data'] = data
          data_string = urlencode(data_params, True)
  
          sig = self._sign_string(data_string, self.api_secret)
          decoded_base64_bytes = u('partner_id={api_key}&sig={sig}:{payload}').format(
              api_key = self.api_key,
              sig     = sig,
              payload = data_string
          )
          if PY3:
              decoded_base64_bytes = decoded_base64_bytes.encode('utf-8')
          token = u('{sentinal}{base64_data}').format(
              sentinal    = self.TOKEN_SENTINEL,
              base64_data = base64.b64encode(decoded_base64_bytes).decode()
          )
          return token