docs: def insert_data(self, data, include_index=False, index_name=None):
      """ Insert the contents of a Pandas DataFrame or a list of dictionaries into the table.
  
      The insertion will be performed using at most 500 rows per POST, and at most 10 POSTs per
      second, as BigQuery has some limits on streaming rates.
  
      Args:
        data: the DataFrame or list to insert.
        include_index: whether to include the DataFrame or list index as a column in the BQ table.
        index_name: for a list, if include_index is True, this should be the name for the index.
            If not specified, 'Index' will be used.
      Returns:
        The table.
      Raises:
        Exception if the table doesn't exist, the table's schema differs from the data's schema,
        or the insert failed.
      """
      # TODO(gram): we could create the Table here is it doesn't exist using a schema derived
      # from the data. IIRC we decided not to but doing so seems less unwieldy that having to
      # create it first and then validate the schema against it itself.
  
      # There are BigQuery limits on the streaming API:
      #
      # max_rows_per_post = 500
      # max_bytes_per_row = 20000
      # max_rows_per_second = 10000
      # max_bytes_per_post = 1000000
      # max_bytes_per_second = 10000000
      #
      # It is non-trivial to enforce these here, and the max bytes per row is not something we
      # can really control. As an approximation we enforce the 500 row limit
      # with a 0.05 sec POST interval (to enforce the 10,000 rows per sec limit).
      max_rows_per_post = 500
      post_interval = 0.05
  
      # TODO(gram): add different exception types for each failure case.
      if not self.exists():
        raise Exception('Table %s does not exist.' % self._full_name)
  
      data_schema = _schema.Schema.from_data(data)
      if isinstance(data, list):
        if include_index:
          if not index_name:
            index_name = 'Index'
          data_schema._add_field(index_name, 'INTEGER')
  
      table_schema = self.schema
  
      # Do some validation of the two schema to make sure they are compatible.
      for data_field in data_schema:
        name = data_field.name
        table_field = table_schema[name]
        if table_field is None:
          raise Exception('Table does not contain field %s' % name)
        data_type = data_field.data_type
        table_type = table_field.data_type
        if table_type != data_type:
          raise Exception('Field %s in data has type %s but in table has type %s' %
                          (name, data_type, table_type))
  
      total_rows = len(data)
      total_pushed = 0
  
      job_id = uuid.uuid4().hex
      rows = []
      column_name_map = {}
  
      is_dataframe = isinstance(data, pandas.DataFrame)
      if is_dataframe:
        # reset_index creates a new dataframe so we don't affect the original. reset_index(drop=True)
        # drops the original index and uses an integer range.
        gen = data.reset_index(drop=not include_index).iterrows()
      else:
        gen = enumerate(data)
  
      for index, row in gen:
        if is_dataframe:
          row = row.to_dict()
        elif include_index:
          row[index_name] = index
  
        rows.append({
          'json': self._encode_dict_as_row(row, column_name_map),
          'insertId': job_id + str(index)
        })
  
        total_pushed += 1
  
        if (total_pushed == total_rows) or (len(rows) == max_rows_per_post):
          try:
            response = self._api.tabledata_insert_all(self._name_parts, rows)
          except Exception as e:
            raise e
          if 'insertErrors' in response:
            raise Exception('insertAll failed: %s' % response['insertErrors'])
  
          time.sleep(post_interval)  # Streaming API is rate-limited
          rows = []
  
      # Block until data is ready
      while True:
        self._info = self._api.tables_get(self._name_parts)
        if 'streamingBuffer' not in self._info or \
                'estimatedRows' not in self._info['streamingBuffer'] or \
                int(self._info['streamingBuffer']['estimatedRows']) > 0:
          break
        time.sleep(2)
  
      return self