Skip to content

Commit

Permalink
Adding HTTP support to core Operation class.
Browse files Browse the repository at this point in the history
Will likely need updates to Speech operation class.
  • Loading branch information
dhermes committed Oct 29, 2016
1 parent 7aa2821 commit 1bde184
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 @@ -97,8 +98,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 @@ -127,6 +133,8 @@ class Operation(object):
converted into the correct types.
"""

_use_grpc = True

def __init__(self, name, client, **caller_metadata):
self.name = name
self.client = client
Expand All @@ -152,6 +160,30 @@ def from_pb(cls, operation_pb, client, **caller_metadata):
"""
result = cls(operation_pb.name, client, **caller_metadata)
result._update_state(operation_pb)
result._use_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._use_grpc = False
return result

@property
Expand All @@ -166,12 +198,39 @@ def complete(self):
def _get_operation_rpc(self):
"""Checks 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._use_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 @@ -202,7 +261,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 @@ -88,7 +88,7 @@ def test_w_conflict(self):
self.assertEqual(type_url_map, {TYPE_URI: other})


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

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

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

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

def test_from_pb_wo_metadata_or_kw(self):
from google.longrunning import operations_pb2
Expand Down Expand Up @@ -187,6 +189,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._use_grpc)

def test_complete_property(self):
client = _Client()
operation = self._makeOne(self.OPERATION_NAME, client)
Expand Down Expand Up @@ -234,6 +268,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._use_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 @@ -324,7 +387,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 1bde184

Please sign in to comment.