Skip to content

Commit c7b436f

Browse files
committed
Merge pull request #1023 from tseaver/bigquery-table_api_methods
Bigquery table api methods
2 parents 21f385c + 7210c8c commit c7b436f

4 files changed

Lines changed: 872 additions & 5 deletions

File tree

gcloud/bigquery/dataset.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from gcloud.exceptions import NotFound
1919
from gcloud.bigquery._helpers import _datetime_from_prop
20+
from gcloud.bigquery.table import Table
2021

2122

2223
class Dataset(object):
@@ -211,8 +212,10 @@ def _set_properties(self, api_response):
211212
"""
212213
self._properties.clear()
213214
cleaned = api_response.copy()
214-
cleaned['creationTime'] = float(cleaned['creationTime'])
215-
cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime'])
215+
if 'creationTime' in cleaned:
216+
cleaned['creationTime'] = float(cleaned['creationTime'])
217+
if 'lastModifiedTime' in cleaned:
218+
cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime'])
216219
self._properties.update(cleaned)
217220

218221
def _build_resource(self):
@@ -353,3 +356,17 @@ def delete(self, client=None):
353356
"""
354357
client = self._require_client(client)
355358
client.connection.api_request(method='DELETE', path=self.path)
359+
360+
def table(self, name, schema=()):
361+
"""Construct a table bound to this dataset.
362+
363+
:type name: string
364+
:param name: Name of the table.
365+
366+
:type schema: list of :class:`gcloud.bigquery.table.SchemaField`
367+
:param schema: The table's schema
368+
369+
:rtype: :class:`gcloud.bigquery.table.Table`
370+
:returns: a new ``Table`` instance
371+
"""
372+
return Table(name, dataset=self, schema=schema)

gcloud/bigquery/table.py

Lines changed: 233 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818

1919
import six
2020

21+
from gcloud.exceptions import NotFound
2122
from gcloud.bigquery._helpers import _datetime_from_prop
2223
from gcloud.bigquery._helpers import _prop_from_datetime
2324

2425

26+
_MARKER = object()
27+
28+
2529
class SchemaField(object):
2630
"""Describe a single field within a table schema.
2731
@@ -281,7 +285,7 @@ def view_query(self, value):
281285
"""Update SQL query defining the table as a view.
282286
283287
:type value: string
284-
:param value: new location
288+
:param value: new query
285289
286290
:raises: ValueError for invalid value types.
287291
"""
@@ -293,3 +297,231 @@ def view_query(self, value):
293297
def view_query(self):
294298
"""Delete SQL query defining the table as a view."""
295299
self._properties.pop('view', None)
300+
301+
def _require_client(self, client):
302+
"""Check client or verify over-ride.
303+
304+
:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
305+
:param client: the client to use. If not passed, falls back to the
306+
``client`` stored on the current dataset.
307+
308+
:rtype: :class:`gcloud.bigquery.client.Client`
309+
:returns: The client passed in or the currently bound client.
310+
"""
311+
if client is None:
312+
client = self._dataset._client
313+
return client
314+
315+
def _set_properties(self, api_response):
316+
"""Update properties from resource in body of ``api_response``
317+
318+
:type api_response: httplib2.Response
319+
:param api_response: response returned from an API call
320+
"""
321+
self._properties.clear()
322+
cleaned = api_response.copy()
323+
if 'creationTime' in cleaned:
324+
cleaned['creationTime'] = float(cleaned['creationTime'])
325+
if 'lastModifiedTime' in cleaned:
326+
cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime'])
327+
if 'expirationTime' in cleaned:
328+
cleaned['expirationTime'] = float(cleaned['expirationTime'])
329+
self._properties.update(cleaned)
330+
331+
def _build_schema_resource(self, fields=None):
332+
"""Generate a resource fragment for table's schema."""
333+
if fields is None:
334+
fields = self._schema
335+
infos = []
336+
for field in fields:
337+
info = {'name': field.name,
338+
'type': field.field_type,
339+
'mode': field.mode}
340+
if field.description is not None:
341+
info['description'] = field.description
342+
if field.fields is not None:
343+
info['fields'] = self._build_schema_resource(field.fields)
344+
infos.append(info)
345+
return infos
346+
347+
def _build_resource(self):
348+
"""Generate a resource for ``create`` or ``update``."""
349+
resource = {
350+
'tableReference': {
351+
'projectId': self._dataset.project,
352+
'datasetId': self._dataset.name,
353+
'tableId': self.name},
354+
'schema': {'fields': self._build_schema_resource()},
355+
}
356+
if self.description is not None:
357+
resource['description'] = self.description
358+
359+
if self.expires is not None:
360+
value = _prop_from_datetime(self.expires)
361+
resource['expirationTime'] = value
362+
363+
if self.friendly_name is not None:
364+
resource['friendlyName'] = self.friendly_name
365+
366+
if self.location is not None:
367+
resource['location'] = self.location
368+
369+
if self.view_query is not None:
370+
view = resource['view'] = {}
371+
view['query'] = self.view_query
372+
373+
return resource
374+
375+
def create(self, client=None):
376+
"""API call: create the dataset via a PUT request
377+
378+
See:
379+
https://cloud.google.com/bigquery/reference/rest/v2/tables/insert
380+
381+
:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
382+
:param client: the client to use. If not passed, falls back to the
383+
``client`` stored on the current dataset.
384+
"""
385+
client = self._require_client(client)
386+
path = '/projects/%s/datasets/%s/tables' % (
387+
self._dataset.project, self._dataset.name)
388+
api_response = client.connection.api_request(
389+
method='POST', path=path, data=self._build_resource())
390+
self._set_properties(api_response)
391+
392+
def exists(self, client=None):
393+
"""API call: test for the existence of the table via a GET request
394+
395+
See
396+
https://cloud.google.com/bigquery/docs/reference/v2/tables/get
397+
398+
:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
399+
:param client: the client to use. If not passed, falls back to the
400+
``client`` stored on the current dataset.
401+
"""
402+
client = self._require_client(client)
403+
404+
try:
405+
client.connection.api_request(method='GET', path=self.path,
406+
query_params={'fields': 'id'})
407+
except NotFound:
408+
return False
409+
else:
410+
return True
411+
412+
def reload(self, client=None):
413+
"""API call: refresh table properties via a GET request
414+
415+
See
416+
https://cloud.google.com/bigquery/docs/reference/v2/tables/get
417+
418+
:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
419+
:param client: the client to use. If not passed, falls back to the
420+
``client`` stored on the current dataset.
421+
"""
422+
client = self._require_client(client)
423+
424+
api_response = client.connection.api_request(
425+
method='GET', path=self.path)
426+
self._set_properties(api_response)
427+
428+
def patch(self,
429+
client=None,
430+
friendly_name=_MARKER,
431+
description=_MARKER,
432+
location=_MARKER,
433+
expires=_MARKER,
434+
view_query=_MARKER,
435+
schema=_MARKER):
436+
"""API call: update individual table properties via a PATCH request
437+
438+
See
439+
https://cloud.google.com/bigquery/docs/reference/v2/tables/patch
440+
441+
:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
442+
:param client: the client to use. If not passed, falls back to the
443+
``client`` stored on the current dataset.
444+
445+
:type friendly_name: string or ``NoneType``
446+
:param friendly_name: point in time at which the table expires.
447+
448+
:type description: string or ``NoneType``
449+
:param description: point in time at which the table expires.
450+
451+
:type location: string or ``NoneType``
452+
:param location: point in time at which the table expires.
453+
454+
:type expires: :class:`datetime.datetime` or ``NoneType``
455+
:param expires: point in time at which the table expires.
456+
457+
:type view_query: string
458+
:param view_query: SQL query defining the table as a view
459+
460+
:type schema: list of :class:`SchemaField`
461+
:param schema: fields describing the schema
462+
463+
:raises: ValueError for invalid value types.
464+
"""
465+
client = self._require_client(client)
466+
467+
partial = {}
468+
469+
if expires is not _MARKER:
470+
if (not isinstance(expires, datetime.datetime) and
471+
expires is not None):
472+
raise ValueError("Pass a datetime, or None")
473+
partial['expirationTime'] = _prop_from_datetime(expires)
474+
475+
if description is not _MARKER:
476+
partial['description'] = description
477+
478+
if friendly_name is not _MARKER:
479+
partial['friendlyName'] = friendly_name
480+
481+
if location is not _MARKER:
482+
partial['location'] = location
483+
484+
if view_query is not _MARKER:
485+
if view_query is None:
486+
partial['view'] = None
487+
else:
488+
partial['view'] = {'query': view_query}
489+
490+
if schema is not _MARKER:
491+
if schema is None:
492+
partial['schema'] = None
493+
else:
494+
partial['schema'] = {
495+
'fields': self._build_schema_resource(schema)}
496+
497+
api_response = client.connection.api_request(
498+
method='PATCH', path=self.path, data=partial)
499+
self._set_properties(api_response)
500+
501+
def update(self, client=None):
502+
"""API call: update table properties via a PUT request
503+
504+
See
505+
https://cloud.google.com/bigquery/docs/reference/v2/tables/update
506+
507+
:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
508+
:param client: the client to use. If not passed, falls back to the
509+
``client`` stored on the current dataset.
510+
"""
511+
client = self._require_client(client)
512+
api_response = client.connection.api_request(
513+
method='PUT', path=self.path, data=self._build_resource())
514+
self._set_properties(api_response)
515+
516+
def delete(self, client=None):
517+
"""API call: delete the table via a DELETE request
518+
519+
See:
520+
https://cloud.google.com/bigquery/reference/rest/v2/tables/delete
521+
522+
:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
523+
:param client: the client to use. If not passed, falls back to the
524+
``client`` stored on the current dataset.
525+
"""
526+
client = self._require_client(client)
527+
client.connection.api_request(method='DELETE', path=self.path)

gcloud/bigquery/test_dataset.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,31 @@ def test_create_w_alternate_client(self):
179179
self.assertEqual(req['data'], SENT)
180180
self._verifyResourceProperties(dataset, RESOURCE)
181181

182+
def test_create_w_missing_output_properties(self):
183+
# In the wild, the resource returned from 'dataset.create' sometimes
184+
# lacks 'creationTime' / 'lastModifiedTime'
185+
PATH = 'projects/%s/datasets' % (self.PROJECT,)
186+
RESOURCE = self._makeResource()
187+
del RESOURCE['creationTime']
188+
del RESOURCE['lastModifiedTime']
189+
self.WHEN = None
190+
conn = _Connection(RESOURCE)
191+
CLIENT = _Client(project=self.PROJECT, connection=conn)
192+
dataset = self._makeOne(self.DS_NAME, client=CLIENT)
193+
194+
dataset.create()
195+
196+
self.assertEqual(len(conn._requested), 1)
197+
req = conn._requested[0]
198+
self.assertEqual(req['method'], 'POST')
199+
self.assertEqual(req['path'], '/%s' % PATH)
200+
SENT = {
201+
'datasetReference':
202+
{'projectId': self.PROJECT, 'datasetId': self.DS_NAME},
203+
}
204+
self.assertEqual(req['data'], SENT)
205+
self._verifyResourceProperties(dataset, RESOURCE)
206+
182207
def test_exists_miss_w_bound_client(self):
183208
PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME)
184209
conn = _Connection()
@@ -393,6 +418,31 @@ def test_delete_w_alternate_client(self):
393418
self.assertEqual(req['method'], 'DELETE')
394419
self.assertEqual(req['path'], '/%s' % PATH)
395420

421+
def test_table_wo_schema(self):
422+
from gcloud.bigquery.table import Table
423+
conn = _Connection({})
424+
CLIENT = _Client(project=self.PROJECT, connection=conn)
425+
dataset = self._makeOne(self.DS_NAME, client=CLIENT)
426+
table = dataset.table('table_name')
427+
self.assertTrue(isinstance(table, Table))
428+
self.assertEqual(table.name, 'table_name')
429+
self.assertTrue(table._dataset is dataset)
430+
self.assertEqual(table.schema, [])
431+
432+
def test_table_w_schema(self):
433+
from gcloud.bigquery.table import SchemaField
434+
from gcloud.bigquery.table import Table
435+
conn = _Connection({})
436+
CLIENT = _Client(project=self.PROJECT, connection=conn)
437+
dataset = self._makeOne(self.DS_NAME, client=CLIENT)
438+
full_name = SchemaField('full_name', 'STRING', mode='REQUIRED')
439+
age = SchemaField('age', 'INTEGER', mode='REQUIRED')
440+
table = dataset.table('table_name', schema=[full_name, age])
441+
self.assertTrue(isinstance(table, Table))
442+
self.assertEqual(table.name, 'table_name')
443+
self.assertTrue(table._dataset is dataset)
444+
self.assertEqual(table.schema, [full_name, age])
445+
396446

397447
class _Client(object):
398448

0 commit comments

Comments
 (0)