Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions gcloud/bigquery/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from gcloud.exceptions import NotFound
from gcloud.bigquery._helpers import _datetime_from_prop
from gcloud.bigquery.table import Table


class Dataset(object):
Expand Down Expand Up @@ -211,8 +212,10 @@ def _set_properties(self, api_response):
"""
self._properties.clear()
cleaned = api_response.copy()
cleaned['creationTime'] = float(cleaned['creationTime'])
cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime'])
if 'creationTime' in cleaned:
cleaned['creationTime'] = float(cleaned['creationTime'])
if 'lastModifiedTime' in cleaned:
cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime'])

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

self._properties.update(cleaned)

def _build_resource(self):
Expand Down Expand Up @@ -353,3 +356,17 @@ def delete(self, client=None):
"""
client = self._require_client(client)
client.connection.api_request(method='DELETE', path=self.path)

def table(self, name, schema=()):
"""Construct a table bound to this dataset.

:type name: string
:param name: Name of the table.

:type schema: list of :class:`gcloud.bigquery.table.SchemaField`
:param schema: The table's schema

This comment was marked as spam.

This comment was marked as spam.


:rtype: :class:`gcloud.bigquery.table.Table`
:returns: a new ``Table`` instance
"""
return Table(name, dataset=self, schema=schema)
234 changes: 233 additions & 1 deletion gcloud/bigquery/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@

import six

from gcloud.exceptions import NotFound
from gcloud.bigquery._helpers import _datetime_from_prop
from gcloud.bigquery._helpers import _prop_from_datetime


_MARKER = object()


class SchemaField(object):
"""Describe a single field within a table schema.

Expand Down Expand Up @@ -281,7 +285,7 @@ def view_query(self, value):
"""Update SQL query defining the table as a view.

:type value: string
:param value: new location
:param value: new query

:raises: ValueError for invalid value types.
"""
Expand All @@ -293,3 +297,231 @@ def view_query(self, value):
def view_query(self):
"""Delete SQL query defining the table as a view."""
self._properties.pop('view', None)

def _require_client(self, client):
"""Check client or verify over-ride.

:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current dataset.

:rtype: :class:`gcloud.bigquery.client.Client`
:returns: The client passed in or the currently bound client.
"""
if client is None:
client = self._dataset._client
return client

def _set_properties(self, api_response):
"""Update properties from resource in body of ``api_response``

:type api_response: httplib2.Response
:param api_response: response returned from an API call
"""
self._properties.clear()
cleaned = api_response.copy()
if 'creationTime' in cleaned:
cleaned['creationTime'] = float(cleaned['creationTime'])
if 'lastModifiedTime' in cleaned:
cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime'])
if 'expirationTime' in cleaned:
cleaned['expirationTime'] = float(cleaned['expirationTime'])

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

self._properties.update(cleaned)

def _build_schema_resource(self, fields=None):
"""Generate a resource fragment for table's schema."""
if fields is None:
fields = self._schema

This comment was marked as spam.

This comment was marked as spam.

infos = []
for field in fields:
info = {'name': field.name,
'type': field.field_type,
'mode': field.mode}
if field.description is not None:
info['description'] = field.description
if field.fields is not None:
info['fields'] = self._build_schema_resource(field.fields)
infos.append(info)
return infos

def _build_resource(self):
"""Generate a resource for ``create`` or ``update``."""
resource = {
'tableReference': {
'projectId': self._dataset.project,
'datasetId': self._dataset.name,
'tableId': self.name},
'schema': {'fields': self._build_schema_resource()},
}
if self.description is not None:
resource['description'] = self.description

if self.expires is not None:
value = _prop_from_datetime(self.expires)
resource['expirationTime'] = value

if self.friendly_name is not None:
resource['friendlyName'] = self.friendly_name

if self.location is not None:
resource['location'] = self.location

if self.view_query is not None:
view = resource['view'] = {}
view['query'] = self.view_query

return resource

def create(self, client=None):
"""API call: create the dataset via a PUT request

See:
https://cloud.google.com/bigquery/reference/rest/v2/tables/insert

:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current dataset.
"""
client = self._require_client(client)
path = '/projects/%s/datasets/%s/tables' % (
self._dataset.project, self._dataset.name)
api_response = client.connection.api_request(
method='POST', path=path, data=self._build_resource())
self._set_properties(api_response)

def exists(self, client=None):
"""API call: test for the existence of the table via a GET request

See
https://cloud.google.com/bigquery/docs/reference/v2/tables/get

:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current dataset.
"""
client = self._require_client(client)

try:
client.connection.api_request(method='GET', path=self.path,
query_params={'fields': 'id'})
except NotFound:
return False
else:
return True

def reload(self, client=None):
"""API call: refresh table properties via a GET request

See
https://cloud.google.com/bigquery/docs/reference/v2/tables/get

:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current dataset.
"""
client = self._require_client(client)

api_response = client.connection.api_request(
method='GET', path=self.path)
self._set_properties(api_response)

def patch(self,
client=None,
friendly_name=_MARKER,
description=_MARKER,
location=_MARKER,
expires=_MARKER,
view_query=_MARKER,
schema=_MARKER):
"""API call: update individual table properties via a PATCH request

See
https://cloud.google.com/bigquery/docs/reference/v2/tables/patch

This comment was marked as spam.


:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current dataset.

:type friendly_name: string or ``NoneType``
:param friendly_name: point in time at which the table expires.

:type description: string or ``NoneType``
:param description: point in time at which the table expires.

:type location: string or ``NoneType``
:param location: point in time at which the table expires.

:type expires: :class:`datetime.datetime` or ``NoneType``
:param expires: point in time at which the table expires.

:type view_query: string
:param view_query: SQL query defining the table as a view

:type schema: list of :class:`SchemaField`
:param schema: fields describing the schema

:raises: ValueError for invalid value types.
"""
client = self._require_client(client)

partial = {}

if expires is not _MARKER:
if (not isinstance(expires, datetime.datetime) and
expires is not None):
raise ValueError("Pass a datetime, or None")
partial['expirationTime'] = _prop_from_datetime(expires)

if description is not _MARKER:
partial['description'] = description

if friendly_name is not _MARKER:
partial['friendlyName'] = friendly_name

if location is not _MARKER:
partial['location'] = location

if view_query is not _MARKER:
if view_query is None:
partial['view'] = None
else:
partial['view'] = {'query': view_query}

if schema is not _MARKER:
if schema is None:
partial['schema'] = None
else:
partial['schema'] = {
'fields': self._build_schema_resource(schema)}

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

api_response = client.connection.api_request(
method='PATCH', path=self.path, data=partial)
self._set_properties(api_response)

def update(self, client=None):
"""API call: update table properties via a PUT request

See
https://cloud.google.com/bigquery/docs/reference/v2/tables/update

:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current dataset.
"""
client = self._require_client(client)
api_response = client.connection.api_request(
method='PUT', path=self.path, data=self._build_resource())
self._set_properties(api_response)

def delete(self, client=None):
"""API call: delete the table via a DELETE request

See:
https://cloud.google.com/bigquery/reference/rest/v2/tables/delete

:type client: :class:`gcloud.bigquery.client.Client` or ``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current dataset.
"""
client = self._require_client(client)
client.connection.api_request(method='DELETE', path=self.path)
50 changes: 50 additions & 0 deletions gcloud/bigquery/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,31 @@ def test_create_w_alternate_client(self):
self.assertEqual(req['data'], SENT)
self._verifyResourceProperties(dataset, RESOURCE)

def test_create_w_missing_output_properties(self):
# In the wild, the resource returned from 'dataset.create' sometimes
# lacks 'creationTime' / 'lastModifiedTime'
PATH = 'projects/%s/datasets' % (self.PROJECT,)
RESOURCE = self._makeResource()
del RESOURCE['creationTime']
del RESOURCE['lastModifiedTime']
self.WHEN = None
conn = _Connection(RESOURCE)
CLIENT = _Client(project=self.PROJECT, connection=conn)
dataset = self._makeOne(self.DS_NAME, client=CLIENT)

dataset.create()

self.assertEqual(len(conn._requested), 1)
req = conn._requested[0]
self.assertEqual(req['method'], 'POST')
self.assertEqual(req['path'], '/%s' % PATH)
SENT = {
'datasetReference':
{'projectId': self.PROJECT, 'datasetId': self.DS_NAME},
}
self.assertEqual(req['data'], SENT)
self._verifyResourceProperties(dataset, RESOURCE)

def test_exists_miss_w_bound_client(self):
PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME)
conn = _Connection()
Expand Down Expand Up @@ -393,6 +418,31 @@ def test_delete_w_alternate_client(self):
self.assertEqual(req['method'], 'DELETE')
self.assertEqual(req['path'], '/%s' % PATH)

def test_table_wo_schema(self):
from gcloud.bigquery.table import Table
conn = _Connection({})
CLIENT = _Client(project=self.PROJECT, connection=conn)
dataset = self._makeOne(self.DS_NAME, client=CLIENT)
table = dataset.table('table_name')
self.assertTrue(isinstance(table, Table))
self.assertEqual(table.name, 'table_name')
self.assertTrue(table._dataset is dataset)
self.assertEqual(table.schema, [])

def test_table_w_schema(self):
from gcloud.bigquery.table import SchemaField
from gcloud.bigquery.table import Table
conn = _Connection({})
CLIENT = _Client(project=self.PROJECT, connection=conn)
dataset = self._makeOne(self.DS_NAME, client=CLIENT)
full_name = SchemaField('full_name', 'STRING', mode='REQUIRED')
age = SchemaField('age', 'INTEGER', mode='REQUIRED')
table = dataset.table('table_name', schema=[full_name, age])
self.assertTrue(isinstance(table, Table))
self.assertEqual(table.name, 'table_name')
self.assertTrue(table._dataset is dataset)
self.assertEqual(table.schema, [full_name, age])


class _Client(object):

Expand Down
Loading