Repository: googleapis/google-cloud-python

Issue:
table.insert_data should allow for dictionaries
[`table.insert_data`][2] currently only accepts tuples/lists in the order of the schema. It'd be a great feature allow lists of dictionaries to be passed.

Would expect cases where if the keys in the dictionary do not match the schema, it would return back an error.

FWIW, here's an example of how another python library handles it for a typical database connection:

[https://github.com/pudo/dataset/blob/master/dataset/persistence/table.py#L62][1]

[1]: https://github.com/pudo/dataset/blob/5fd8b3ef3ebc3ed143be8f435d917746d10b0c51/dataset/persistence/table.py#L62
[2]: https://github.com/GoogleCloudPlatform/google-cloud-python/blob/ff4f492f04c9b4f5511f8b0b7d2a32df86328306/bigquery/google/cloud/bigquery/table.py#L688-L694


Discussion:
@lukesneeringer WDYT of this? It'd be somewhat easy to support with a helper that mapped a each `row` in `rows` [into `row_info`][1] (whether it started as a `tuple` or a `dict`).

[1]: https://github.com/GoogleCloudPlatform/google-cloud-python/blob/ff4f492f04c9b4f5511f8b0b7d2a32df86328306/bigquery/google/cloud/bigquery/table.py#L742-L750
@dhermes I think we would be better off with another method which handles dicts (and likely calls the current `insert_data`).
+1 to this, it would be really helpful (especially when handling 2-3 level nested RECORDS). Its currently quite a change from using both the Java client and the NodeJS client (both of which accept a TableRow / dictionary).
I'd like to add a `Table.row_from_mapping` method which uses the schema to generate row tuples from a dict.  Usage would then be:

```python
table.insert_data([table.row_from_mapping(mapping) for mapping in json_rows])
```

Resolving patch:
diff --git a/bigquery/google/cloud/bigquery/table.py b/bigquery/google/cloud/bigquery/table.py
--- a/bigquery/google/cloud/bigquery/table.py
+++ b/bigquery/google/cloud/bigquery/table.py
@@ -684,6 +684,35 @@ def fetch_data(self, max_results=None, page_token=None, client=None):
         iterator._NEXT_TOKEN = 'pageToken'
         return iterator
 
+    def row_from_mapping(self, mapping):
+        """Convert a mapping to a row tuple using the schema.
+
+        :type mapping: dict
+        :param mapping: Mapping of row data: must contain keys for all
+               required fields in the schema.  Keys which do not correspond
+               to a field in the schema are ignored.
+
+        :rtype: tuple
+        :returns: Tuple whose elements are ordered according to the table's
+                  schema.
+        :raises: ValueError if table's schema is not set
+        """
+        if len(self._schema) == 0:
+            raise ValueError(_TABLE_HAS_NO_SCHEMA)
+
+        row = []
+        for field in self.schema:
+            if field.mode == 'REQUIRED':
+                row.append(mapping[field.name])
+            elif field.mode == 'REPEATED':
+                row.append(mapping.get(field.name, ()))
+            elif field.mode == 'NULLABLE':
+                row.append(mapping.get(field.name))
+            else:
+                raise ValueError(
+                    "Unknown field mode: {}".format(field.mode))
+        return tuple(row)
+
     def insert_data(self,
                     rows,
                     row_ids=None,


Test patch:
diff --git a/bigquery/tests/unit/test_table.py b/bigquery/tests/unit/test_table.py
--- a/bigquery/tests/unit/test_table.py
+++ b/bigquery/tests/unit/test_table.py
@@ -1305,6 +1305,61 @@ def test_fetch_data_w_record_schema(self):
         self.assertEqual(req['method'], 'GET')
         self.assertEqual(req['path'], '/%s' % PATH)
 
+    def test_row_from_mapping_wo_schema(self):
+        from google.cloud.bigquery.table import _TABLE_HAS_NO_SCHEMA
+        MAPPING = {'full_name': 'Phred Phlyntstone', 'age': 32}
+        client = _Client(project=self.PROJECT)
+        dataset = _Dataset(client)
+        table = self._make_one(self.TABLE_NAME, dataset=dataset)
+
+        with self.assertRaises(ValueError) as exc:
+            table.row_from_mapping(MAPPING)
+
+        self.assertEqual(exc.exception.args, (_TABLE_HAS_NO_SCHEMA,))
+
+    def test_row_from_mapping_w_invalid_schema(self):
+        from google.cloud.bigquery.table import SchemaField
+        MAPPING = {
+            'full_name': 'Phred Phlyntstone',
+            'age': 32,
+            'colors': ['red', 'green'],
+            'bogus': 'WHATEVER',
+        }
+        client = _Client(project=self.PROJECT)
+        dataset = _Dataset(client)
+        full_name = SchemaField('full_name', 'STRING', mode='REQUIRED')
+        age = SchemaField('age', 'INTEGER', mode='REQUIRED')
+        colors = SchemaField('colors', 'DATETIME', mode='REPEATED')
+        bogus = SchemaField('joined', 'STRING', mode='BOGUS')
+        table = self._make_one(self.TABLE_NAME, dataset=dataset,
+                               schema=[full_name, age, colors, bogus])
+
+        with self.assertRaises(ValueError) as exc:
+            table.row_from_mapping(MAPPING)
+
+        self.assertIn('Unknown field mode: BOGUS', str(exc.exception))
+
+    def test_row_from_mapping_w_schema(self):
+        from google.cloud.bigquery.table import SchemaField
+        MAPPING = {
+            'full_name': 'Phred Phlyntstone',
+            'age': 32,
+            'colors': ['red', 'green'],
+            'extra': 'IGNORED',
+        }
+        client = _Client(project=self.PROJECT)
+        dataset = _Dataset(client)
+        full_name = SchemaField('full_name', 'STRING', mode='REQUIRED')
+        age = SchemaField('age', 'INTEGER', mode='REQUIRED')
+        colors = SchemaField('colors', 'DATETIME', mode='REPEATED')
+        joined = SchemaField('joined', 'STRING', mode='NULLABLE')
+        table = self._make_one(self.TABLE_NAME, dataset=dataset,
+                               schema=[full_name, age, colors, joined])
+
+        self.assertEqual(
+            table.row_from_mapping(MAPPING),
+            ('Phred Phlyntstone', 32, ['red', 'green'], None))
+
     def test_insert_data_wo_schema(self):
         from google.cloud.bigquery.table import _TABLE_HAS_NO_SCHEMA
 
@@ -1832,7 +1887,7 @@ def test__parse_schema_resource_subfields(self):
         RESOURCE['schema']['fields'].append(
             {'name': 'phone',
              'type': 'RECORD',
-             'mode': 'REPEATABLE',
+             'mode': 'REPEATED',
              'fields': [{'name': 'type',
                          'type': 'STRING',
                          'mode': 'REQUIRED'},
@@ -1900,7 +1955,7 @@ def test_w_subfields(self):
         full_name = SchemaField('full_name', 'STRING', mode='REQUIRED')
         ph_type = SchemaField('type', 'STRING', 'REQUIRED')
         ph_num = SchemaField('number', 'STRING', 'REQUIRED')
-        phone = SchemaField('phone', 'RECORD', mode='REPEATABLE',
+        phone = SchemaField('phone', 'RECORD', mode='REPEATED',
                             fields=[ph_type, ph_num])
         resource = self._call_fut([full_name, phone])
         self.assertEqual(len(resource), 2)
@@ -1911,7 +1966,7 @@ def test_w_subfields(self):
         self.assertEqual(resource[1],
                          {'name': 'phone',
                           'type': 'RECORD',
-                          'mode': 'REPEATABLE',
+                          'mode': 'REPEATED',
                           'fields': [{'name': 'type',
                                       'type': 'STRING',
                                       'mode': 'REQUIRED'},