Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding HTTP support to core Operation class. #2645

Merged
merged 2 commits into from
Oct 31, 2016
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
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.
"""

_from_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._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 @@ -166,12 +198,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 @@ -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._from_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._from_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._from_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._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 @@ -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