Skip to content

Commit

Permalink
Merge pull request #2165 from tseaver/factor-out-operations
Browse files Browse the repository at this point in the history
Factor out shared 'Operation' class and helpers.
  • Loading branch information
tseaver authored Aug 24, 2016
2 parents a1ae30b + 01ffb45 commit e63b1d8
Show file tree
Hide file tree
Showing 10 changed files with 568 additions and 871 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
gcloud-api
gcloud-config
gcloud-auth
operation-api

.. toctree::
:maxdepth: 0
Expand Down
7 changes: 7 additions & 0 deletions docs/operation-api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Long-Running Operations
~~~~~~~~~~~~~~~~~~~~~~~

.. automodule:: gcloud.operation
:members:
:show-inheritance:

143 changes: 22 additions & 121 deletions gcloud/bigtable/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,29 @@

import re

from google.longrunning import operations_pb2

from gcloud.bigtable._generated import (
instance_pb2 as data_v2_pb2)
from gcloud.bigtable._generated import (
bigtable_instance_admin_pb2 as messages_v2_pb2)
from gcloud.operation import Operation
from gcloud.operation import _compute_type_url
from gcloud.operation import _register_type_url


_CLUSTER_NAME_RE = re.compile(r'^projects/(?P<project>[^/]+)/'
r'instances/(?P<instance>[^/]+)/clusters/'
r'(?P<cluster_id>[a-z][-a-z0-9]*)$')
_OPERATION_NAME_RE = re.compile(r'^operations/'
r'projects/([^/]+)/'
r'instances/([^/]+)/'
r'clusters/([a-z][-a-z0-9]*)/'
r'operations/(?P<operation_id>\d+)$')
_TYPE_URL_MAP = {
}

DEFAULT_SERVE_NODES = 3
"""Default number of nodes to use when creating a cluster."""


_UPDATE_CLUSTER_METADATA_URL = _compute_type_url(
messages_v2_pb2.UpdateClusterMetadata)
_register_type_url(
_UPDATE_CLUSTER_METADATA_URL, messages_v2_pb2.UpdateClusterMetadata)


def _prepare_create_request(cluster):
"""Creates a protobuf request for a CreateCluster request.
Expand All @@ -58,109 +58,6 @@ def _prepare_create_request(cluster):
)


def _parse_pb_any_to_native(any_val, expected_type=None):
"""Convert a serialized "google.protobuf.Any" value to actual type.
:type any_val: :class:`google.protobuf.any_pb2.Any`
:param any_val: A serialized protobuf value container.
:type expected_type: str
:param expected_type: (Optional) The type URL we expect ``any_val``
to have.
:rtype: object
:returns: The de-serialized object.
:raises: :class:`ValueError <exceptions.ValueError>` if the
``expected_type`` does not match the ``type_url`` on the input.
"""
if expected_type is not None and expected_type != any_val.type_url:
raise ValueError('Expected type: %s, Received: %s' % (
expected_type, any_val.type_url))
container_class = _TYPE_URL_MAP[any_val.type_url]
return container_class.FromString(any_val.value)


def _process_operation(operation_pb):
"""Processes a create protobuf response.
:type operation_pb: :class:`google.longrunning.operations_pb2.Operation`
:param operation_pb: The long-running operation response from a
Create/Update/Undelete cluster request.
:rtype: tuple
:returns: integer ID of the operation (``operation_id``).
:raises: :class:`ValueError <exceptions.ValueError>` if the operation name
doesn't match the :data:`_OPERATION_NAME_RE` regex.
"""
match = _OPERATION_NAME_RE.match(operation_pb.name)
if match is None:
raise ValueError('Operation name was not in the expected '
'format after a cluster modification.',
operation_pb.name)
operation_id = int(match.group('operation_id'))

return operation_id


class Operation(object):
"""Representation of a Google API Long-Running Operation.
In particular, these will be the result of operations on
clusters using the Cloud Bigtable API.
:type op_type: str
:param op_type: The type of operation being performed. Expect
``create``, ``update`` or ``undelete``.
:type op_id: int
:param op_id: The ID of the operation.
:type cluster: :class:`Cluster`
:param cluster: The cluster that created the operation.
"""

def __init__(self, op_type, op_id, cluster=None):
self.op_type = op_type
self.op_id = op_id
self._cluster = cluster
self._complete = False

def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return (other.op_type == self.op_type and
other.op_id == self.op_id and
other._cluster == self._cluster and
other._complete == self._complete)

def __ne__(self, other):
return not self.__eq__(other)

def finished(self):
"""Check if the operation has finished.
:rtype: bool
:returns: A boolean indicating if the current operation has completed.
:raises: :class:`ValueError <exceptions.ValueError>` if the operation
has already completed.
"""
if self._complete:
raise ValueError('The operation has completed.')

operation_name = ('operations/' + self._cluster.name +
'/operations/%d' % (self.op_id,))
request_pb = operations_pb2.GetOperationRequest(name=operation_name)
# We expect a `google.longrunning.operations_pb2.Operation`.
client = self._cluster._instance._client
operation_pb = client._operations_stub.GetOperation(request_pb)

if operation_pb.done:
self._complete = True
return True
else:
return False


class Cluster(object):
"""Representation of a Google Cloud Bigtable Cluster.
Expand Down Expand Up @@ -317,11 +214,13 @@ def create(self):
"""
request_pb = _prepare_create_request(self)
# We expect a `google.longrunning.operations_pb2.Operation`.
operation_pb = self._instance._client._instance_stub.CreateCluster(
request_pb)
client = self._instance._client
operation_pb = client._instance_stub.CreateCluster(request_pb)

op_id = _process_operation(operation_pb)
return Operation('create', op_id, cluster=self)
operation = Operation.from_pb(operation_pb, client)
operation.target = self
operation.metadata['request_type'] = 'CreateCluster'
return operation

def update(self):
"""Update this cluster.
Expand All @@ -345,12 +244,14 @@ def update(self):
name=self.name,
serve_nodes=self.serve_nodes,
)
# Ignore expected `._generated.instance_pb2.Cluster`.
operation_pb = self._instance._client._instance_stub.UpdateCluster(
request_pb)
# We expect a `google.longrunning.operations_pb2.Operation`.
client = self._instance._client
operation_pb = client._instance_stub.UpdateCluster(request_pb)

op_id = _process_operation(operation_pb)
return Operation('update', op_id, cluster=self)
operation = Operation.from_pb(operation_pb, client)
operation.target = self
operation.metadata['request_type'] = 'UpdateCluster'
return operation

def delete(self):
"""Delete this cluster.
Expand Down
147 changes: 13 additions & 134 deletions gcloud/bigtable/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@

import re

from google.longrunning import operations_pb2

from gcloud._helpers import _pb_timestamp_to_datetime
from gcloud.bigtable._generated import (
instance_pb2 as data_v2_pb2)
from gcloud.bigtable._generated import (
Expand All @@ -29,21 +26,20 @@
from gcloud.bigtable.cluster import Cluster
from gcloud.bigtable.cluster import DEFAULT_SERVE_NODES
from gcloud.bigtable.table import Table
from gcloud.operation import Operation
from gcloud.operation import _compute_type_url
from gcloud.operation import _register_type_url


_EXISTING_INSTANCE_LOCATION_ID = 'see-existing-cluster'
_INSTANCE_NAME_RE = re.compile(r'^projects/(?P<project>[^/]+)/'
r'instances/(?P<instance_id>[a-z][-a-z0-9]*)$')
_OPERATION_NAME_RE = re.compile(r'^operations/projects/([^/]+)/'
r'instances/([a-z][-a-z0-9]*)/'
r'locations/(?P<location_id>[a-z][-a-z0-9]*)/'
r'operations/(?P<operation_id>\d+)$')
_TYPE_URL_BASE = 'type.googleapis.com/google.bigtable.'
_ADMIN_TYPE_URL_BASE = _TYPE_URL_BASE + 'admin.v2.'
_INSTANCE_CREATE_METADATA = _ADMIN_TYPE_URL_BASE + 'CreateInstanceMetadata'
_TYPE_URL_MAP = {
_INSTANCE_CREATE_METADATA: messages_v2_pb2.CreateInstanceMetadata,
}


_CREATE_INSTANCE_METADATA_URL = _compute_type_url(
messages_v2_pb2.CreateInstanceMetadata)
_register_type_url(
_CREATE_INSTANCE_METADATA_URL, messages_v2_pb2.CreateInstanceMetadata)


def _prepare_create_request(instance):
Expand Down Expand Up @@ -71,125 +67,6 @@ def _prepare_create_request(instance):
return message


def _parse_pb_any_to_native(any_val, expected_type=None):
"""Convert a serialized "google.protobuf.Any" value to actual type.
:type any_val: :class:`google.protobuf.any_pb2.Any`
:param any_val: A serialized protobuf value container.
:type expected_type: str
:param expected_type: (Optional) The type URL we expect ``any_val``
to have.
:rtype: object
:returns: The de-serialized object.
:raises: :class:`ValueError <exceptions.ValueError>` if the
``expected_type`` does not match the ``type_url`` on the input.
"""
if expected_type is not None and expected_type != any_val.type_url:
raise ValueError('Expected type: %s, Received: %s' % (
expected_type, any_val.type_url))
container_class = _TYPE_URL_MAP[any_val.type_url]
return container_class.FromString(any_val.value)


def _process_operation(operation_pb):
"""Processes a create protobuf response.
:type operation_pb: :class:`google.longrunning.operations_pb2.Operation`
:param operation_pb: The long-running operation response from a
Create/Update/Undelete instance request.
:rtype: (int, str, datetime)
:returns: (operation_id, location_id, operation_begin).
:raises: :class:`ValueError <exceptions.ValueError>` if the operation name
doesn't match the :data:`_OPERATION_NAME_RE` regex.
"""
match = _OPERATION_NAME_RE.match(operation_pb.name)
if match is None:
raise ValueError('Operation name was not in the expected '
'format after instance creation.',
operation_pb.name)
location_id = match.group('location_id')
operation_id = int(match.group('operation_id'))

request_metadata = _parse_pb_any_to_native(operation_pb.metadata)
operation_begin = _pb_timestamp_to_datetime(
request_metadata.request_time)

return operation_id, location_id, operation_begin


class Operation(object):
"""Representation of a Google API Long-Running Operation.
In particular, these will be the result of operations on
instances using the Cloud Bigtable API.
:type op_type: str
:param op_type: The type of operation being performed. Expect
``create``, ``update`` or ``undelete``.
:type op_id: int
:param op_id: The ID of the operation.
:type begin: :class:`datetime.datetime`
:param begin: The time when the operation was started.
:type location_id: str
:param location_id: ID of the location in which the operation is running
:type instance: :class:`Instance`
:param instance: The instance that created the operation.
"""

def __init__(self, op_type, op_id, begin, location_id, instance=None):
self.op_type = op_type
self.op_id = op_id
self.begin = begin
self.location_id = location_id
self._instance = instance
self._complete = False

def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return (other.op_type == self.op_type and
other.op_id == self.op_id and
other.begin == self.begin and
other.location_id == self.location_id and
other._instance == self._instance and
other._complete == self._complete)

def __ne__(self, other):
return not self.__eq__(other)

def finished(self):
"""Check if the operation has finished.
:rtype: bool
:returns: A boolean indicating if the current operation has completed.
:raises: :class:`ValueError <exceptions.ValueError>` if the operation
has already completed.
"""
if self._complete:
raise ValueError('The operation has completed.')

operation_name = (
'operations/%s/locations/%s/operations/%d' %
(self._instance.name, self.location_id, self.op_id))
request_pb = operations_pb2.GetOperationRequest(name=operation_name)
# We expect a `google.longrunning.operations_pb2.Operation`.
operation_pb = self._instance._client._operations_stub.GetOperation(
request_pb)

if operation_pb.done:
self._complete = True
return True
else:
return False


class Instance(object):
"""Representation of a Google Cloud Bigtable Instance.
Expand Down Expand Up @@ -359,8 +236,10 @@ def create(self):
# We expect a `google.longrunning.operations_pb2.Operation`.
operation_pb = self._client._instance_stub.CreateInstance(request_pb)

op_id, loc_id, op_begin = _process_operation(operation_pb)
return Operation('create', op_id, op_begin, loc_id, instance=self)
operation = Operation.from_pb(operation_pb, self._client)
operation.target = self
operation.metadata['request_type'] = 'CreateInstance'
return operation

def update(self):
"""Update this instance.
Expand Down
Loading

0 comments on commit e63b1d8

Please sign in to comment.