Skip to content

Commit

Permalink
Merge pull request #2645 from dhermes/add-http-support-to-operation
Browse files Browse the repository at this point in the history
Adding HTTP support to core Operation class.
  • Loading branch information
dhermes authored Oct 31, 2016
2 parents 262a0e7 + 0328268 commit 319fd8f
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 4 deletions.
63 changes: 61 additions & 2 deletions core/google/cloud/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""Wrap long-running operations returned from Google Cloud APIs."""

from google.longrunning import operations_pb2
from google.protobuf import json_format


_GOOGLE_APIS_PREFIX = 'type.googleapis.com'
Expand Down Expand Up @@ -100,8 +101,13 @@ class Operation(object):
:type name: str
:param name: The fully-qualified path naming the operation.
:type client: object: must provide ``_operations_stub`` accessor.
:type client: :class:`~google.cloud.client.Client`
:param client: The client used to poll for the status of the operation.
If the operation was created via JSON/HTTP, the client
must own a :class:`~google.cloud.connection.Connection`
to send polling requests. If created via protobuf, the
client must have a gRPC stub in the ``_operations_stub``
attribute.
:type caller_metadata: dict
:param caller_metadata: caller-assigned metadata about the operation
Expand Down Expand Up @@ -130,6 +136,8 @@ class Operation(object):
converted into the correct types.
"""

_from_grpc = True

def __init__(self, name, client, **caller_metadata):
self.name = name
self.client = client
Expand All @@ -155,6 +163,30 @@ def from_pb(cls, operation_pb, client, **caller_metadata):
"""
result = cls(operation_pb.name, client, **caller_metadata)
result._update_state(operation_pb)
result._from_grpc = True
return result

@classmethod
def from_dict(cls, operation, client, **caller_metadata):
"""Factory: construct an instance from a dictionary.
:type operation: dict
:param operation: Operation as a JSON object.
:type client: :class:`~google.cloud.client.Client`
:param client: The client used to poll for the status of the operation.
:type caller_metadata: dict
:param caller_metadata: caller-assigned metadata about the operation
:rtype: :class:`Operation`
:returns: new instance, with attributes based on the protobuf.
"""
operation_pb = json_format.ParseDict(
operation, operations_pb2.Operation())
result = cls(operation_pb.name, client, **caller_metadata)
result._update_state(operation_pb)
result._from_grpc = False
return result

@property
Expand All @@ -169,12 +201,39 @@ def complete(self):
def _get_operation_rpc(self):
"""Polls the status of the current operation.
Uses gRPC request to check.
:rtype: :class:`~google.longrunning.operations_pb2.Operation`
:returns: The latest status of the current operation.
"""
request_pb = operations_pb2.GetOperationRequest(name=self.name)
return self.client._operations_stub.GetOperation(request_pb)

def _get_operation_http(self):
"""Checks the status of the current operation.
Uses HTTP request to check.
:rtype: :class:`~google.longrunning.operations_pb2.Operation`
:returns: The latest status of the current operation.
"""
path = 'operations/%s' % (self.name,)
api_response = self.client.connection.api_request(
method='GET', path=path)
return json_format.ParseDict(
api_response, operations_pb2.Operation())

def _get_operation(self):
"""Checks the status of the current operation.
:rtype: :class:`~google.longrunning.operations_pb2.Operation`
:returns: The latest status of the current operation.
"""
if self._from_grpc:
return self._get_operation_rpc()
else:
return self._get_operation_http()

def _update_state(self, operation_pb):
"""Update the state of the current object based on operation.
Expand Down Expand Up @@ -205,7 +264,7 @@ def poll(self):
if self.complete:
raise ValueError('The operation has completed.')

operation_pb = self._get_operation_rpc()
operation_pb = self._get_operation()
self._update_state(operation_pb)

return self.complete
80 changes: 78 additions & 2 deletions core/unit_tests/test_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def test_w_conflict(self):
self.assertEqual(type_url_map, {type_url: other})


class OperationTests(unittest.TestCase):
class TestOperation(unittest.TestCase):

OPERATION_NAME = 'operations/projects/foo/instances/bar/operations/123'

Expand All @@ -125,6 +125,7 @@ def test_ctor_defaults(self):
self.assertIsNone(operation.error)
self.assertIsNone(operation.metadata)
self.assertEqual(operation.caller_metadata, {})
self.assertTrue(operation._from_grpc)

def test_ctor_explicit(self):
client = _Client()
Expand All @@ -138,6 +139,7 @@ def test_ctor_explicit(self):
self.assertIsNone(operation.error)
self.assertIsNone(operation.metadata)
self.assertEqual(operation.caller_metadata, {'foo': 'bar'})
self.assertTrue(operation._from_grpc)

def test_from_pb_wo_metadata_or_kw(self):
from google.longrunning import operations_pb2
Expand Down Expand Up @@ -202,6 +204,38 @@ def test_from_pb_w_metadata_and_kwargs(self):
self.assertEqual(operation.metadata, meta)
self.assertEqual(operation.caller_metadata, {'baz': 'qux'})

def test_from_dict(self):
from google.protobuf.struct_pb2 import Struct
from google.cloud._testing import _Monkey
from google.cloud import operation as MUT

type_url = 'type.googleapis.com/%s' % (Struct.DESCRIPTOR.full_name,)
api_response = {
'name': self.OPERATION_NAME,
'metadata': {
'@type': type_url,
'value': {'foo': 'Bar'},
},
}

client = _Client()
klass = self._getTargetClass()

with _Monkey(MUT, _TYPE_URL_MAP={type_url: Struct}):
operation = klass.from_dict(api_response, client)

self.assertEqual(operation.name, self.OPERATION_NAME)
self.assertIs(operation.client, client)
self.assertIsNone(operation.target)
self.assertIsNone(operation.response)
self.assertIsNone(operation.error)
self.assertIsInstance(operation.metadata, Struct)
self.assertEqual(len(operation.metadata.fields), 1)
self.assertEqual(
operation.metadata.fields['foo'].string_value, 'Bar')
self.assertEqual(operation.caller_metadata, {})
self.assertFalse(operation._from_grpc)

def test_complete_property(self):
client = _Client()
operation = self._makeOne(self.OPERATION_NAME, client)
Expand Down Expand Up @@ -249,6 +283,35 @@ def test_poll_true(self):
self.assertIsInstance(request_pb, operations_pb2.GetOperationRequest)
self.assertEqual(request_pb.name, self.OPERATION_NAME)

def test_poll_http(self):
from google.protobuf.struct_pb2 import Struct
from google.cloud._testing import _Monkey
from google.cloud import operation as MUT

type_url = 'type.googleapis.com/%s' % (Struct.DESCRIPTOR.full_name,)
name = '2302903294023'
api_response = {
'name': name,
'done': True,
'metadata': {
'@type': type_url,
'value': {'foo': 'Bar'},
},
}
connection = _Connection(api_response)
client = _Client(connection)
operation = self._makeOne(name, client)
operation._from_grpc = False

with _Monkey(MUT, _TYPE_URL_MAP={type_url: Struct}):
self.assertTrue(operation.poll())

expected_path = 'operations/%s' % (name,)
self.assertEqual(connection._requested, [{
'method': 'GET',
'path': expected_path,
}])

def test__update_state_done(self):
from google.longrunning import operations_pb2

Expand Down Expand Up @@ -339,7 +402,20 @@ def GetOperation(self, request_pb):
return self._get_operation_response


class _Connection(object):

def __init__(self, *responses):
self._responses = responses
self._requested = []

def api_request(self, **kw):
self._requested.append(kw)
response, self._responses = self._responses[0], self._responses[1:]
return response


class _Client(object):

def __init__(self):
def __init__(self, connection=None):
self._operations_stub = _OperationsStub()
self.connection = connection

0 comments on commit 319fd8f

Please sign in to comment.