Skip to content

Commit e63b1d8

Browse files
authored
Merge pull request #2165 from tseaver/factor-out-operations
Factor out shared 'Operation' class and helpers.
2 parents a1ae30b + 01ffb45 commit e63b1d8

File tree

10 files changed

+568
-871
lines changed

10 files changed

+568
-871
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
gcloud-api
77
gcloud-config
88
gcloud-auth
9+
operation-api
910

1011
.. toctree::
1112
:maxdepth: 0

docs/operation-api.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Long-Running Operations
2+
~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
.. automodule:: gcloud.operation
5+
:members:
6+
:show-inheritance:
7+

gcloud/bigtable/cluster.py

Lines changed: 22 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,29 @@
1717

1818
import re
1919

20-
from google.longrunning import operations_pb2
21-
2220
from gcloud.bigtable._generated import (
2321
instance_pb2 as data_v2_pb2)
2422
from gcloud.bigtable._generated import (
2523
bigtable_instance_admin_pb2 as messages_v2_pb2)
24+
from gcloud.operation import Operation
25+
from gcloud.operation import _compute_type_url
26+
from gcloud.operation import _register_type_url
2627

2728

2829
_CLUSTER_NAME_RE = re.compile(r'^projects/(?P<project>[^/]+)/'
2930
r'instances/(?P<instance>[^/]+)/clusters/'
3031
r'(?P<cluster_id>[a-z][-a-z0-9]*)$')
31-
_OPERATION_NAME_RE = re.compile(r'^operations/'
32-
r'projects/([^/]+)/'
33-
r'instances/([^/]+)/'
34-
r'clusters/([a-z][-a-z0-9]*)/'
35-
r'operations/(?P<operation_id>\d+)$')
36-
_TYPE_URL_MAP = {
37-
}
3832

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

4236

37+
_UPDATE_CLUSTER_METADATA_URL = _compute_type_url(
38+
messages_v2_pb2.UpdateClusterMetadata)
39+
_register_type_url(
40+
_UPDATE_CLUSTER_METADATA_URL, messages_v2_pb2.UpdateClusterMetadata)
41+
42+
4343
def _prepare_create_request(cluster):
4444
"""Creates a protobuf request for a CreateCluster request.
4545
@@ -58,109 +58,6 @@ def _prepare_create_request(cluster):
5858
)
5959

6060

61-
def _parse_pb_any_to_native(any_val, expected_type=None):
62-
"""Convert a serialized "google.protobuf.Any" value to actual type.
63-
64-
:type any_val: :class:`google.protobuf.any_pb2.Any`
65-
:param any_val: A serialized protobuf value container.
66-
67-
:type expected_type: str
68-
:param expected_type: (Optional) The type URL we expect ``any_val``
69-
to have.
70-
71-
:rtype: object
72-
:returns: The de-serialized object.
73-
:raises: :class:`ValueError <exceptions.ValueError>` if the
74-
``expected_type`` does not match the ``type_url`` on the input.
75-
"""
76-
if expected_type is not None and expected_type != any_val.type_url:
77-
raise ValueError('Expected type: %s, Received: %s' % (
78-
expected_type, any_val.type_url))
79-
container_class = _TYPE_URL_MAP[any_val.type_url]
80-
return container_class.FromString(any_val.value)
81-
82-
83-
def _process_operation(operation_pb):
84-
"""Processes a create protobuf response.
85-
86-
:type operation_pb: :class:`google.longrunning.operations_pb2.Operation`
87-
:param operation_pb: The long-running operation response from a
88-
Create/Update/Undelete cluster request.
89-
90-
:rtype: tuple
91-
:returns: integer ID of the operation (``operation_id``).
92-
:raises: :class:`ValueError <exceptions.ValueError>` if the operation name
93-
doesn't match the :data:`_OPERATION_NAME_RE` regex.
94-
"""
95-
match = _OPERATION_NAME_RE.match(operation_pb.name)
96-
if match is None:
97-
raise ValueError('Operation name was not in the expected '
98-
'format after a cluster modification.',
99-
operation_pb.name)
100-
operation_id = int(match.group('operation_id'))
101-
102-
return operation_id
103-
104-
105-
class Operation(object):
106-
"""Representation of a Google API Long-Running Operation.
107-
108-
In particular, these will be the result of operations on
109-
clusters using the Cloud Bigtable API.
110-
111-
:type op_type: str
112-
:param op_type: The type of operation being performed. Expect
113-
``create``, ``update`` or ``undelete``.
114-
115-
:type op_id: int
116-
:param op_id: The ID of the operation.
117-
118-
:type cluster: :class:`Cluster`
119-
:param cluster: The cluster that created the operation.
120-
"""
121-
122-
def __init__(self, op_type, op_id, cluster=None):
123-
self.op_type = op_type
124-
self.op_id = op_id
125-
self._cluster = cluster
126-
self._complete = False
127-
128-
def __eq__(self, other):
129-
if not isinstance(other, self.__class__):
130-
return False
131-
return (other.op_type == self.op_type and
132-
other.op_id == self.op_id and
133-
other._cluster == self._cluster and
134-
other._complete == self._complete)
135-
136-
def __ne__(self, other):
137-
return not self.__eq__(other)
138-
139-
def finished(self):
140-
"""Check if the operation has finished.
141-
142-
:rtype: bool
143-
:returns: A boolean indicating if the current operation has completed.
144-
:raises: :class:`ValueError <exceptions.ValueError>` if the operation
145-
has already completed.
146-
"""
147-
if self._complete:
148-
raise ValueError('The operation has completed.')
149-
150-
operation_name = ('operations/' + self._cluster.name +
151-
'/operations/%d' % (self.op_id,))
152-
request_pb = operations_pb2.GetOperationRequest(name=operation_name)
153-
# We expect a `google.longrunning.operations_pb2.Operation`.
154-
client = self._cluster._instance._client
155-
operation_pb = client._operations_stub.GetOperation(request_pb)
156-
157-
if operation_pb.done:
158-
self._complete = True
159-
return True
160-
else:
161-
return False
162-
163-
16461
class Cluster(object):
16562
"""Representation of a Google Cloud Bigtable Cluster.
16663
@@ -317,11 +214,13 @@ def create(self):
317214
"""
318215
request_pb = _prepare_create_request(self)
319216
# We expect a `google.longrunning.operations_pb2.Operation`.
320-
operation_pb = self._instance._client._instance_stub.CreateCluster(
321-
request_pb)
217+
client = self._instance._client
218+
operation_pb = client._instance_stub.CreateCluster(request_pb)
322219

323-
op_id = _process_operation(operation_pb)
324-
return Operation('create', op_id, cluster=self)
220+
operation = Operation.from_pb(operation_pb, client)
221+
operation.target = self
222+
operation.metadata['request_type'] = 'CreateCluster'
223+
return operation
325224

326225
def update(self):
327226
"""Update this cluster.
@@ -345,12 +244,14 @@ def update(self):
345244
name=self.name,
346245
serve_nodes=self.serve_nodes,
347246
)
348-
# Ignore expected `._generated.instance_pb2.Cluster`.
349-
operation_pb = self._instance._client._instance_stub.UpdateCluster(
350-
request_pb)
247+
# We expect a `google.longrunning.operations_pb2.Operation`.
248+
client = self._instance._client
249+
operation_pb = client._instance_stub.UpdateCluster(request_pb)
351250

352-
op_id = _process_operation(operation_pb)
353-
return Operation('update', op_id, cluster=self)
251+
operation = Operation.from_pb(operation_pb, client)
252+
operation.target = self
253+
operation.metadata['request_type'] = 'UpdateCluster'
254+
return operation
354255

355256
def delete(self):
356257
"""Delete this cluster.

gcloud/bigtable/instance.py

Lines changed: 13 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717

1818
import re
1919

20-
from google.longrunning import operations_pb2
21-
22-
from gcloud._helpers import _pb_timestamp_to_datetime
2320
from gcloud.bigtable._generated import (
2421
instance_pb2 as data_v2_pb2)
2522
from gcloud.bigtable._generated import (
@@ -29,21 +26,20 @@
2926
from gcloud.bigtable.cluster import Cluster
3027
from gcloud.bigtable.cluster import DEFAULT_SERVE_NODES
3128
from gcloud.bigtable.table import Table
29+
from gcloud.operation import Operation
30+
from gcloud.operation import _compute_type_url
31+
from gcloud.operation import _register_type_url
3232

3333

3434
_EXISTING_INSTANCE_LOCATION_ID = 'see-existing-cluster'
3535
_INSTANCE_NAME_RE = re.compile(r'^projects/(?P<project>[^/]+)/'
3636
r'instances/(?P<instance_id>[a-z][-a-z0-9]*)$')
37-
_OPERATION_NAME_RE = re.compile(r'^operations/projects/([^/]+)/'
38-
r'instances/([a-z][-a-z0-9]*)/'
39-
r'locations/(?P<location_id>[a-z][-a-z0-9]*)/'
40-
r'operations/(?P<operation_id>\d+)$')
41-
_TYPE_URL_BASE = 'type.googleapis.com/google.bigtable.'
42-
_ADMIN_TYPE_URL_BASE = _TYPE_URL_BASE + 'admin.v2.'
43-
_INSTANCE_CREATE_METADATA = _ADMIN_TYPE_URL_BASE + 'CreateInstanceMetadata'
44-
_TYPE_URL_MAP = {
45-
_INSTANCE_CREATE_METADATA: messages_v2_pb2.CreateInstanceMetadata,
46-
}
37+
38+
39+
_CREATE_INSTANCE_METADATA_URL = _compute_type_url(
40+
messages_v2_pb2.CreateInstanceMetadata)
41+
_register_type_url(
42+
_CREATE_INSTANCE_METADATA_URL, messages_v2_pb2.CreateInstanceMetadata)
4743

4844

4945
def _prepare_create_request(instance):
@@ -71,125 +67,6 @@ def _prepare_create_request(instance):
7167
return message
7268

7369

74-
def _parse_pb_any_to_native(any_val, expected_type=None):
75-
"""Convert a serialized "google.protobuf.Any" value to actual type.
76-
77-
:type any_val: :class:`google.protobuf.any_pb2.Any`
78-
:param any_val: A serialized protobuf value container.
79-
80-
:type expected_type: str
81-
:param expected_type: (Optional) The type URL we expect ``any_val``
82-
to have.
83-
84-
:rtype: object
85-
:returns: The de-serialized object.
86-
:raises: :class:`ValueError <exceptions.ValueError>` if the
87-
``expected_type`` does not match the ``type_url`` on the input.
88-
"""
89-
if expected_type is not None and expected_type != any_val.type_url:
90-
raise ValueError('Expected type: %s, Received: %s' % (
91-
expected_type, any_val.type_url))
92-
container_class = _TYPE_URL_MAP[any_val.type_url]
93-
return container_class.FromString(any_val.value)
94-
95-
96-
def _process_operation(operation_pb):
97-
"""Processes a create protobuf response.
98-
99-
:type operation_pb: :class:`google.longrunning.operations_pb2.Operation`
100-
:param operation_pb: The long-running operation response from a
101-
Create/Update/Undelete instance request.
102-
103-
:rtype: (int, str, datetime)
104-
:returns: (operation_id, location_id, operation_begin).
105-
:raises: :class:`ValueError <exceptions.ValueError>` if the operation name
106-
doesn't match the :data:`_OPERATION_NAME_RE` regex.
107-
"""
108-
match = _OPERATION_NAME_RE.match(operation_pb.name)
109-
if match is None:
110-
raise ValueError('Operation name was not in the expected '
111-
'format after instance creation.',
112-
operation_pb.name)
113-
location_id = match.group('location_id')
114-
operation_id = int(match.group('operation_id'))
115-
116-
request_metadata = _parse_pb_any_to_native(operation_pb.metadata)
117-
operation_begin = _pb_timestamp_to_datetime(
118-
request_metadata.request_time)
119-
120-
return operation_id, location_id, operation_begin
121-
122-
123-
class Operation(object):
124-
"""Representation of a Google API Long-Running Operation.
125-
126-
In particular, these will be the result of operations on
127-
instances using the Cloud Bigtable API.
128-
129-
:type op_type: str
130-
:param op_type: The type of operation being performed. Expect
131-
``create``, ``update`` or ``undelete``.
132-
133-
:type op_id: int
134-
:param op_id: The ID of the operation.
135-
136-
:type begin: :class:`datetime.datetime`
137-
:param begin: The time when the operation was started.
138-
139-
:type location_id: str
140-
:param location_id: ID of the location in which the operation is running
141-
142-
:type instance: :class:`Instance`
143-
:param instance: The instance that created the operation.
144-
"""
145-
146-
def __init__(self, op_type, op_id, begin, location_id, instance=None):
147-
self.op_type = op_type
148-
self.op_id = op_id
149-
self.begin = begin
150-
self.location_id = location_id
151-
self._instance = instance
152-
self._complete = False
153-
154-
def __eq__(self, other):
155-
if not isinstance(other, self.__class__):
156-
return False
157-
return (other.op_type == self.op_type and
158-
other.op_id == self.op_id and
159-
other.begin == self.begin and
160-
other.location_id == self.location_id and
161-
other._instance == self._instance and
162-
other._complete == self._complete)
163-
164-
def __ne__(self, other):
165-
return not self.__eq__(other)
166-
167-
def finished(self):
168-
"""Check if the operation has finished.
169-
170-
:rtype: bool
171-
:returns: A boolean indicating if the current operation has completed.
172-
:raises: :class:`ValueError <exceptions.ValueError>` if the operation
173-
has already completed.
174-
"""
175-
if self._complete:
176-
raise ValueError('The operation has completed.')
177-
178-
operation_name = (
179-
'operations/%s/locations/%s/operations/%d' %
180-
(self._instance.name, self.location_id, self.op_id))
181-
request_pb = operations_pb2.GetOperationRequest(name=operation_name)
182-
# We expect a `google.longrunning.operations_pb2.Operation`.
183-
operation_pb = self._instance._client._operations_stub.GetOperation(
184-
request_pb)
185-
186-
if operation_pb.done:
187-
self._complete = True
188-
return True
189-
else:
190-
return False
191-
192-
19370
class Instance(object):
19471
"""Representation of a Google Cloud Bigtable Instance.
19572
@@ -359,8 +236,10 @@ def create(self):
359236
# We expect a `google.longrunning.operations_pb2.Operation`.
360237
operation_pb = self._client._instance_stub.CreateInstance(request_pb)
361238

362-
op_id, loc_id, op_begin = _process_operation(operation_pb)
363-
return Operation('create', op_id, op_begin, loc_id, instance=self)
239+
operation = Operation.from_pb(operation_pb, self._client)
240+
operation.target = self
241+
operation.metadata['request_type'] = 'CreateInstance'
242+
return operation
364243

365244
def update(self):
366245
"""Update this instance.

0 commit comments

Comments
 (0)