Merge branch 'main' into avm99963-monorail
Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266
GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/third_party/protorpc/LICENSE b/third_party/protorpc/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/third_party/protorpc/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/third_party/protorpc/README.monorail b/third_party/protorpc/README.monorail
new file mode 100644
index 0000000..5f8eaf1
--- /dev/null
+++ b/third_party/protorpc/README.monorail
@@ -0,0 +1,14 @@
+Name: ProtoRPC
+Short Name: protorpc
+URL: https://github.com/google/protorpc
+Version: 0.12.0
+License: Apache 2.0
+License File: LICENSE
+Security Critical: no
+Description:
+Local Modifications:
+1. Retain only the protorpc/remote.py file.
+2. Rename my_service.async to my_service.async_
+ We don't use the async feature, and it's a reserved keyword in Python 3.
+3. array.array.tostring() and array.array.fromstring() are renamed in Python 3.
+ Use array.array.tobytes() and array.array.frombytes(), respectively.
diff --git a/third_party/protorpc/protobuf.py b/third_party/protorpc/protobuf.py
new file mode 100644
index 0000000..6de3bce
--- /dev/null
+++ b/third_party/protorpc/protobuf.py
@@ -0,0 +1,360 @@
+#!/usr/bin/env python
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Protocol buffer support for message types.
+
+For more details about protocol buffer encoding and decoding please see:
+
+ http://code.google.com/apis/protocolbuffers/docs/encoding.html
+
+Public Exceptions:
+ DecodeError: Raised when a decode error occurs from incorrect protobuf format.
+
+Public Functions:
+ encode_message: Encodes a message in to a protocol buffer string.
+ decode_message: Decode from a protocol buffer string to a message.
+"""
+import six
+
+__author__ = 'rafek@google.com (Rafe Kaplan)'
+
+
+import array
+
+from . import message_types
+from . import messages
+from . import util
+from .google_imports import ProtocolBuffer
+
+
+__all__ = ['ALTERNATIVE_CONTENT_TYPES',
+ 'CONTENT_TYPE',
+ 'encode_message',
+ 'decode_message',
+ ]
+
+CONTENT_TYPE = 'application/octet-stream'
+
+ALTERNATIVE_CONTENT_TYPES = ['application/x-google-protobuf']
+
+
+class _Encoder(ProtocolBuffer.Encoder):
+ """Extension of protocol buffer encoder.
+
+ Original protocol buffer encoder does not have complete set of methods
+ for handling required encoding. This class adds them.
+ """
+
+ # TODO(rafek): Implement the missing encoding types.
+ def no_encoding(self, value):
+ """No encoding available for type.
+
+ Args:
+ value: Value to encode.
+
+ Raises:
+ NotImplementedError at all times.
+ """
+ raise NotImplementedError()
+
+ def encode_enum(self, value):
+ """Encode an enum value.
+
+ Args:
+ value: Enum to encode.
+ """
+ self.putVarInt32(value.number)
+
+ def encode_message(self, value):
+ """Encode a Message in to an embedded message.
+
+ Args:
+ value: Message instance to encode.
+ """
+ self.putPrefixedString(encode_message(value))
+
+
+ def encode_unicode_string(self, value):
+ """Helper to properly pb encode unicode strings to UTF-8.
+
+ Args:
+ value: String value to encode.
+ """
+ if isinstance(value, six.text_type):
+ value = value.encode('utf-8')
+ self.putPrefixedString(value)
+
+
+class _Decoder(ProtocolBuffer.Decoder):
+ """Extension of protocol buffer decoder.
+
+ Original protocol buffer decoder does not have complete set of methods
+ for handling required decoding. This class adds them.
+ """
+
+ # TODO(rafek): Implement the missing encoding types.
+ def no_decoding(self):
+ """No decoding available for type.
+
+ Raises:
+ NotImplementedError at all times.
+ """
+ raise NotImplementedError()
+
+ def decode_string(self):
+ """Decode a unicode string.
+
+ Returns:
+ Next value in stream as a unicode string.
+ """
+ return self.getPrefixedString().decode('UTF-8')
+
+ def decode_boolean(self):
+ """Decode a boolean value.
+
+ Returns:
+ Next value in stream as a boolean.
+ """
+ return bool(self.getBoolean())
+
+
+# Number of bits used to describe a protocol buffer bits used for the variant.
+_WIRE_TYPE_BITS = 3
+_WIRE_TYPE_MASK = 7
+
+
+# Maps variant to underlying wire type. Many variants map to same type.
+_VARIANT_TO_WIRE_TYPE = {
+ messages.Variant.DOUBLE: _Encoder.DOUBLE,
+ messages.Variant.FLOAT: _Encoder.FLOAT,
+ messages.Variant.INT64: _Encoder.NUMERIC,
+ messages.Variant.UINT64: _Encoder.NUMERIC,
+ messages.Variant.INT32: _Encoder.NUMERIC,
+ messages.Variant.BOOL: _Encoder.NUMERIC,
+ messages.Variant.STRING: _Encoder.STRING,
+ messages.Variant.MESSAGE: _Encoder.STRING,
+ messages.Variant.BYTES: _Encoder.STRING,
+ messages.Variant.UINT32: _Encoder.NUMERIC,
+ messages.Variant.ENUM: _Encoder.NUMERIC,
+ messages.Variant.SINT32: _Encoder.NUMERIC,
+ messages.Variant.SINT64: _Encoder.NUMERIC,
+}
+
+
+# Maps variant to encoder method.
+_VARIANT_TO_ENCODER_MAP = {
+ messages.Variant.DOUBLE: _Encoder.putDouble,
+ messages.Variant.FLOAT: _Encoder.putFloat,
+ messages.Variant.INT64: _Encoder.putVarInt64,
+ messages.Variant.UINT64: _Encoder.putVarUint64,
+ messages.Variant.INT32: _Encoder.putVarInt32,
+ messages.Variant.BOOL: _Encoder.putBoolean,
+ messages.Variant.STRING: _Encoder.encode_unicode_string,
+ messages.Variant.MESSAGE: _Encoder.encode_message,
+ messages.Variant.BYTES: _Encoder.encode_unicode_string,
+ messages.Variant.UINT32: _Encoder.no_encoding,
+ messages.Variant.ENUM: _Encoder.encode_enum,
+ messages.Variant.SINT32: _Encoder.no_encoding,
+ messages.Variant.SINT64: _Encoder.no_encoding,
+}
+
+
+# Basic wire format decoders. Used for reading unknown values.
+_WIRE_TYPE_TO_DECODER_MAP = {
+ _Encoder.NUMERIC: _Decoder.getVarInt64,
+ _Encoder.DOUBLE: _Decoder.getDouble,
+ _Encoder.STRING: _Decoder.getPrefixedString,
+ _Encoder.FLOAT: _Decoder.getFloat,
+}
+
+
+# Map wire type to variant. Used to find a variant for unknown values.
+_WIRE_TYPE_TO_VARIANT_MAP = {
+ _Encoder.NUMERIC: messages.Variant.INT64,
+ _Encoder.DOUBLE: messages.Variant.DOUBLE,
+ _Encoder.STRING: messages.Variant.STRING,
+ _Encoder.FLOAT: messages.Variant.FLOAT,
+}
+
+
+# Wire type to name mapping for error messages.
+_WIRE_TYPE_NAME = {
+ _Encoder.NUMERIC: 'NUMERIC',
+ _Encoder.DOUBLE: 'DOUBLE',
+ _Encoder.STRING: 'STRING',
+ _Encoder.FLOAT: 'FLOAT',
+}
+
+
+# Maps variant to decoder method.
+_VARIANT_TO_DECODER_MAP = {
+ messages.Variant.DOUBLE: _Decoder.getDouble,
+ messages.Variant.FLOAT: _Decoder.getFloat,
+ messages.Variant.INT64: _Decoder.getVarInt64,
+ messages.Variant.UINT64: _Decoder.getVarUint64,
+ messages.Variant.INT32: _Decoder.getVarInt32,
+ messages.Variant.BOOL: _Decoder.decode_boolean,
+ messages.Variant.STRING: _Decoder.decode_string,
+ messages.Variant.MESSAGE: _Decoder.getPrefixedString,
+ messages.Variant.BYTES: _Decoder.getPrefixedString,
+ messages.Variant.UINT32: _Decoder.no_decoding,
+ messages.Variant.ENUM: _Decoder.getVarInt32,
+ messages.Variant.SINT32: _Decoder.no_decoding,
+ messages.Variant.SINT64: _Decoder.no_decoding,
+}
+
+
+def encode_message(message):
+ """Encode Message instance to protocol buffer.
+
+ Args:
+ Message instance to encode in to protocol buffer.
+
+ Returns:
+ String encoding of Message instance in protocol buffer format.
+
+ Raises:
+ messages.ValidationError if message is not initialized.
+ """
+ message.check_initialized()
+ encoder = _Encoder()
+
+ # Get all fields, from the known fields we parsed and the unknown fields
+ # we saved. Note which ones were known, so we can process them differently.
+ all_fields = [(field.number, field) for field in message.all_fields()]
+ all_fields.extend((key, None)
+ for key in message.all_unrecognized_fields()
+ if isinstance(key, six.integer_types))
+ all_fields.sort()
+ for field_num, field in all_fields:
+ if field:
+ # Known field.
+ value = message.get_assigned_value(field.name)
+ if value is None:
+ continue
+ variant = field.variant
+ repeated = field.repeated
+ else:
+ # Unrecognized field.
+ value, variant = message.get_unrecognized_field_info(field_num)
+ if not isinstance(variant, messages.Variant):
+ continue
+ repeated = isinstance(value, (list, tuple))
+
+ tag = ((field_num << _WIRE_TYPE_BITS) | _VARIANT_TO_WIRE_TYPE[variant])
+
+ # Write value to wire.
+ if repeated:
+ values = value
+ else:
+ values = [value]
+ for next in values:
+ encoder.putVarInt32(tag)
+ if isinstance(field, messages.MessageField):
+ next = field.value_to_message(next)
+ field_encoder = _VARIANT_TO_ENCODER_MAP[variant]
+ field_encoder(encoder, next)
+
+ buffer = encoder.buffer()
+ return buffer.tobytes()
+
+
+def decode_message(message_type, encoded_message):
+ """Decode protocol buffer to Message instance.
+
+ Args:
+ message_type: Message type to decode data to.
+ encoded_message: Encoded version of message as string.
+
+ Returns:
+ Decoded instance of message_type.
+
+ Raises:
+ DecodeError if an error occurs during decoding, such as incompatible
+ wire format for a field.
+ messages.ValidationError if merged message is not initialized.
+ """
+ message = message_type()
+ message_array = array.array('B')
+ message_array.frombytes(encoded_message)
+ try:
+ decoder = _Decoder(message_array, 0, len(message_array))
+
+ while decoder.avail() > 0:
+ # Decode tag and variant information.
+ encoded_tag = decoder.getVarInt32()
+ tag = encoded_tag >> _WIRE_TYPE_BITS
+ wire_type = encoded_tag & _WIRE_TYPE_MASK
+ try:
+ found_wire_type_decoder = _WIRE_TYPE_TO_DECODER_MAP[wire_type]
+ except:
+ raise messages.DecodeError('No such wire type %d' % wire_type)
+
+ if tag < 1:
+ raise messages.DecodeError('Invalid tag value %d' % tag)
+
+ try:
+ field = message.field_by_number(tag)
+ except KeyError:
+ # Unexpected tags are ok.
+ field = None
+ wire_type_decoder = found_wire_type_decoder
+ else:
+ expected_wire_type = _VARIANT_TO_WIRE_TYPE[field.variant]
+ if expected_wire_type != wire_type:
+ raise messages.DecodeError('Expected wire type %s but found %s' % (
+ _WIRE_TYPE_NAME[expected_wire_type],
+ _WIRE_TYPE_NAME[wire_type]))
+
+ wire_type_decoder = _VARIANT_TO_DECODER_MAP[field.variant]
+
+ value = wire_type_decoder(decoder)
+
+ # Save unknown fields and skip additional processing.
+ if not field:
+ # When saving this, save it under the tag number (which should
+ # be unique), and set the variant and value so we know how to
+ # interpret the value later.
+ variant = _WIRE_TYPE_TO_VARIANT_MAP.get(wire_type)
+ if variant:
+ message.set_unrecognized_field(tag, value, variant)
+ continue
+
+ # Special case Enum and Message types.
+ if isinstance(field, messages.EnumField):
+ try:
+ value = field.type(value)
+ except TypeError:
+ raise messages.DecodeError('Invalid enum value %s' % value)
+ elif isinstance(field, messages.MessageField):
+ value = decode_message(field.message_type, value)
+ value = field.value_from_message(value)
+
+ # Merge value in to message.
+ if field.repeated:
+ values = getattr(message, field.name)
+ if values is None:
+ setattr(message, field.name, [value])
+ else:
+ values.append(value)
+ else:
+ setattr(message, field.name, value)
+ except ProtocolBuffer.ProtocolBufferDecodeError as err:
+ raise messages.DecodeError('Decoding error: %s' % str(err))
+
+ message.check_initialized()
+ return message
diff --git a/third_party/protorpc/remote.py b/third_party/protorpc/remote.py
new file mode 100644
index 0000000..7983573
--- /dev/null
+++ b/third_party/protorpc/remote.py
@@ -0,0 +1,1248 @@
+#!/usr/bin/env python
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Remote service library.
+
+This module contains classes that are useful for building remote services that
+conform to a standard request and response model. To conform to this model
+a service must be like the following class:
+
+ # Each service instance only handles a single request and is then discarded.
+ # Make these objects light weight.
+ class Service(object):
+
+ # It must be possible to construct service objects without any parameters.
+ # If your constructor needs extra information you should provide a
+ # no-argument factory function to create service instances.
+ def __init__(self):
+ ...
+
+ # Each remote method must use the 'method' decorator, passing the request
+ # and response message types. The remote method itself must take a single
+ # parameter which is an instance of RequestMessage and return an instance
+ # of ResponseMessage.
+ @method(RequestMessage, ResponseMessage)
+ def remote_method(self, request):
+ # Return an instance of ResponseMessage.
+
+ # A service object may optionally implement an 'initialize_request_state'
+ # method that takes as a parameter a single instance of a RequestState. If
+ # a service does not implement this method it will not receive the request
+ # state.
+ def initialize_request_state(self, state):
+ ...
+
+The 'Service' class is provided as a convenient base class that provides the
+above functionality. It implements all required and optional methods for a
+service. It also has convenience methods for creating factory functions that
+can pass persistent global state to a new service instance.
+
+The 'method' decorator is used to declare which methods of a class are
+meant to service RPCs. While this decorator is not responsible for handling
+actual remote method invocations, such as handling sockets, handling various
+RPC protocols and checking messages for correctness, it does attach information
+to methods that responsible classes can examine and ensure the correctness
+of the RPC.
+
+When the method decorator is used on a method, the wrapper method will have a
+'remote' property associated with it. The 'remote' property contains the
+request_type and response_type expected by the methods implementation.
+
+On its own, the method decorator does not provide any support for subclassing
+remote methods. In order to extend a service, one would need to redecorate
+the sub-classes methods. For example:
+
+ class MyService(Service):
+
+ @method(DoSomethingRequest, DoSomethingResponse)
+ def do_stuff(self, request):
+ ... implement do_stuff ...
+
+ class MyBetterService(MyService):
+
+ @method(DoSomethingRequest, DoSomethingResponse)
+ def do_stuff(self, request):
+ response = super(MyBetterService, self).do_stuff.remote.method(request)
+ ... do stuff with response ...
+ return response
+
+A Service subclass also has a Stub class that can be used with a transport for
+making RPCs. When a stub is created, it is capable of doing both synchronous
+and asynchronous RPCs if the underlying transport supports it. To make a stub
+using an HTTP transport do:
+
+ my_service = MyService.Stub(HttpTransport('<my service URL>'))
+
+For synchronous calls, just call the expected methods on the service stub:
+
+ request = DoSomethingRequest()
+ ...
+ response = my_service.do_something(request)
+
+Each stub instance has an async object that can be used for initiating
+asynchronous RPCs if the underlying protocol transport supports it. To
+make an asynchronous call, do:
+
+ rpc = my_service.async_.do_something(request)
+ response = rpc.get_response()
+"""
+
+from __future__ import with_statement
+import six
+
+__author__ = 'rafek@google.com (Rafe Kaplan)'
+
+import functools
+import logging
+import sys
+import threading
+from wsgiref import headers as wsgi_headers
+
+from . import message_types
+from . import messages
+from . import protobuf
+from . import protojson
+from . import util
+
+
+__all__ = [
+ 'ApplicationError',
+ 'MethodNotFoundError',
+ 'NetworkError',
+ 'RequestError',
+ 'RpcError',
+ 'ServerError',
+ 'ServiceConfigurationError',
+ 'ServiceDefinitionError',
+
+ 'HttpRequestState',
+ 'ProtocolConfig',
+ 'Protocols',
+ 'RequestState',
+ 'RpcState',
+ 'RpcStatus',
+ 'Service',
+ 'StubBase',
+ 'check_rpc_status',
+ 'get_remote_method_info',
+ 'is_error_status',
+ 'method',
+ 'remote',
+]
+
+
+class ServiceDefinitionError(messages.Error):
+ """Raised when a service is improperly defined."""
+
+
+class ServiceConfigurationError(messages.Error):
+ """Raised when a service is incorrectly configured."""
+
+
+# TODO: Use error_name to map to specific exception message types.
+class RpcStatus(messages.Message):
+ """Status of on-going or complete RPC.
+
+ Fields:
+ state: State of RPC.
+ error_name: Error name set by application. Only set when
+ status is APPLICATION_ERROR. For use by application to transmit
+ specific reason for error.
+ error_message: Error message associated with status.
+ """
+
+ class State(messages.Enum):
+ """Enumeration of possible RPC states.
+
+ Values:
+ OK: Completed successfully.
+ RUNNING: Still running, not complete.
+ REQUEST_ERROR: Request was malformed or incomplete.
+ SERVER_ERROR: Server experienced an unexpected error.
+ NETWORK_ERROR: An error occured on the network.
+ APPLICATION_ERROR: The application is indicating an error.
+ When in this state, RPC should also set application_error.
+ """
+ OK = 0
+ RUNNING = 1
+
+ REQUEST_ERROR = 2
+ SERVER_ERROR = 3
+ NETWORK_ERROR = 4
+ APPLICATION_ERROR = 5
+ METHOD_NOT_FOUND_ERROR = 6
+
+ state = messages.EnumField(State, 1, required=True)
+ error_message = messages.StringField(2)
+ error_name = messages.StringField(3)
+
+
+RpcState = RpcStatus.State
+
+
+class RpcError(messages.Error):
+ """Base class for RPC errors.
+
+ Each sub-class of RpcError is associated with an error value from RpcState
+ and has an attribute STATE that refers to that value.
+ """
+
+ def __init__(self, message, cause=None):
+ super(RpcError, self).__init__(message)
+ self.cause = cause
+
+ @classmethod
+ def from_state(cls, state):
+ """Get error class from RpcState.
+
+ Args:
+ state: RpcState value. Can be enum value itself, string or int.
+
+ Returns:
+ Exception class mapped to value if state is an error. Returns None
+ if state is OK or RUNNING.
+ """
+ return _RPC_STATE_TO_ERROR.get(RpcState(state))
+
+
+class RequestError(RpcError):
+ """Raised when wrong request objects received during method invocation."""
+
+ STATE = RpcState.REQUEST_ERROR
+
+
+class MethodNotFoundError(RequestError):
+ """Raised when unknown method requested by RPC."""
+
+ STATE = RpcState.METHOD_NOT_FOUND_ERROR
+
+
+class NetworkError(RpcError):
+ """Raised when network error occurs during RPC."""
+
+ STATE = RpcState.NETWORK_ERROR
+
+
+class ServerError(RpcError):
+ """Unexpected error occured on server."""
+
+ STATE = RpcState.SERVER_ERROR
+
+
+class ApplicationError(RpcError):
+ """Raised for application specific errors.
+
+ Attributes:
+ error_name: Application specific error name for exception.
+ """
+
+ STATE = RpcState.APPLICATION_ERROR
+
+ def __init__(self, message, error_name=None):
+ """Constructor.
+
+ Args:
+ message: Application specific error message.
+ error_name: Application specific error name. Must be None, string
+ or unicode string.
+ """
+ super(ApplicationError, self).__init__(message)
+ self.error_name = error_name
+
+ def __str__(self):
+ return self.args[0] or ''
+
+ def __repr__(self):
+ if self.error_name is None:
+ error_format = ''
+ else:
+ error_format = ', %r' % self.error_name
+ return '%s(%r%s)' % (type(self).__name__, self.args[0], error_format)
+
+
+_RPC_STATE_TO_ERROR = {
+ RpcState.REQUEST_ERROR: RequestError,
+ RpcState.NETWORK_ERROR: NetworkError,
+ RpcState.SERVER_ERROR: ServerError,
+ RpcState.APPLICATION_ERROR: ApplicationError,
+ RpcState.METHOD_NOT_FOUND_ERROR: MethodNotFoundError,
+}
+
+class _RemoteMethodInfo(object):
+ """Object for encapsulating remote method information.
+
+ An instance of this method is associated with the 'remote' attribute
+ of the methods 'invoke_remote_method' instance.
+
+ Instances of this class are created by the remote decorator and should not
+ be created directly.
+ """
+
+ def __init__(self,
+ method,
+ request_type,
+ response_type):
+ """Constructor.
+
+ Args:
+ method: The method which implements the remote method. This is a
+ function that will act as an instance method of a class definition
+ that is decorated by '@method'. It must always take 'self' as its
+ first parameter.
+ request_type: Expected request type for the remote method.
+ response_type: Expected response type for the remote method.
+ """
+ self.__method = method
+ self.__request_type = request_type
+ self.__response_type = response_type
+
+ @property
+ def method(self):
+ """Original undecorated method."""
+ return self.__method
+
+ @property
+ def request_type(self):
+ """Expected request type for remote method."""
+ if isinstance(self.__request_type, six.string_types):
+ self.__request_type = messages.find_definition(
+ self.__request_type,
+ relative_to=sys.modules[self.__method.__module__])
+ return self.__request_type
+
+ @property
+ def response_type(self):
+ """Expected response type for remote method."""
+ if isinstance(self.__response_type, six.string_types):
+ self.__response_type = messages.find_definition(
+ self.__response_type,
+ relative_to=sys.modules[self.__method.__module__])
+ return self.__response_type
+
+
+def method(request_type=message_types.VoidMessage,
+ response_type=message_types.VoidMessage):
+ """Method decorator for creating remote methods.
+
+ Args:
+ request_type: Message type of expected request.
+ response_type: Message type of expected response.
+
+ Returns:
+ 'remote_method_wrapper' function.
+
+ Raises:
+ TypeError: if the request_type or response_type parameters are not
+ proper subclasses of messages.Message.
+ """
+ if (not isinstance(request_type, six.string_types) and
+ (not isinstance(request_type, type) or
+ not issubclass(request_type, messages.Message) or
+ request_type is messages.Message)):
+ raise TypeError(
+ 'Must provide message class for request-type. Found %s',
+ request_type)
+
+ if (not isinstance(response_type, six.string_types) and
+ (not isinstance(response_type, type) or
+ not issubclass(response_type, messages.Message) or
+ response_type is messages.Message)):
+ raise TypeError(
+ 'Must provide message class for response-type. Found %s',
+ response_type)
+
+ def remote_method_wrapper(method):
+ """Decorator used to wrap method.
+
+ Args:
+ method: Original method being wrapped.
+
+ Returns:
+ 'invoke_remote_method' function responsible for actual invocation.
+ This invocation function instance is assigned an attribute 'remote'
+ which contains information about the remote method:
+ request_type: Expected request type for remote method.
+ response_type: Response type returned from remote method.
+
+ Raises:
+ TypeError: If request_type or response_type is not a subclass of Message
+ or is the Message class itself.
+ """
+
+ @functools.wraps(method)
+ def invoke_remote_method(service_instance, request):
+ """Function used to replace original method.
+
+ Invoke wrapped remote method. Checks to ensure that request and
+ response objects are the correct types.
+
+ Does not check whether messages are initialized.
+
+ Args:
+ service_instance: The service object whose method is being invoked.
+ This is passed to 'self' during the invocation of the original
+ method.
+ request: Request message.
+
+ Returns:
+ Results of calling wrapped remote method.
+
+ Raises:
+ RequestError: Request object is not of the correct type.
+ ServerError: Response object is not of the correct type.
+ """
+ if not isinstance(request, remote_method_info.request_type):
+ raise RequestError('Method %s.%s expected request type %s, '
+ 'received %s' %
+ (type(service_instance).__name__,
+ method.__name__,
+ remote_method_info.request_type,
+ type(request)))
+ response = method(service_instance, request)
+ if not isinstance(response, remote_method_info.response_type):
+ raise ServerError('Method %s.%s expected response type %s, '
+ 'sent %s' %
+ (type(service_instance).__name__,
+ method.__name__,
+ remote_method_info.response_type,
+ type(response)))
+ return response
+
+ remote_method_info = _RemoteMethodInfo(method,
+ request_type,
+ response_type)
+
+ invoke_remote_method.remote = remote_method_info
+ return invoke_remote_method
+
+ return remote_method_wrapper
+
+
+def remote(request_type, response_type):
+ """Temporary backward compatibility alias for method."""
+ logging.warning('The remote decorator has been renamed method. It will be '
+ 'removed in very soon from future versions of ProtoRPC.')
+ return method(request_type, response_type)
+
+
+def get_remote_method_info(method):
+ """Get remote method info object from remote method.
+
+ Returns:
+ Remote method info object if method is a remote method, else None.
+ """
+ if not callable(method):
+ return None
+
+ try:
+ method_info = method.remote
+ except AttributeError:
+ return None
+
+ if not isinstance(method_info, _RemoteMethodInfo):
+ return None
+
+ return method_info
+
+
+class StubBase(object):
+ """Base class for client side service stubs.
+
+ The remote method stubs are created by the _ServiceClass meta-class
+ when a Service class is first created. The resulting stub will
+ extend both this class and the service class it handles communications for.
+
+ Assume that there is a service:
+
+ class NewContactRequest(messages.Message):
+
+ name = messages.StringField(1, required=True)
+ phone = messages.StringField(2)
+ email = messages.StringField(3)
+
+ class NewContactResponse(message.Message):
+
+ contact_id = messages.StringField(1)
+
+ class AccountService(remote.Service):
+
+ @remote.method(NewContactRequest, NewContactResponse):
+ def new_contact(self, request):
+ ... implementation ...
+
+ A stub of this service can be called in two ways. The first is to pass in a
+ correctly initialized NewContactRequest message:
+
+ request = NewContactRequest()
+ request.name = 'Bob Somebody'
+ request.phone = '+1 415 555 1234'
+
+ response = account_service_stub.new_contact(request)
+
+ The second way is to pass in keyword parameters that correspond with the root
+ request message type:
+
+ account_service_stub.new_contact(name='Bob Somebody',
+ phone='+1 415 555 1234')
+
+ The second form will create a request message of the appropriate type.
+ """
+
+ def __init__(self, transport):
+ """Constructor.
+
+ Args:
+ transport: Underlying transport to communicate with remote service.
+ """
+ self.__transport = transport
+
+ @property
+ def transport(self):
+ """Transport used to communicate with remote service."""
+ return self.__transport
+
+
+class _ServiceClass(type):
+ """Meta-class for service class."""
+
+ def __new_async_method(cls, remote):
+ """Create asynchronous method for Async handler.
+
+ Args:
+ remote: RemoteInfo to create method for.
+ """
+ def async_method(self, *args, **kwargs):
+ """Asynchronous remote method.
+
+ Args:
+ self: Instance of StubBase.Async subclass.
+
+ Stub methods either take a single positional argument when a full
+ request message is passed in, or keyword arguments, but not both.
+
+ See docstring for StubBase for more information on how to use remote
+ stub methods.
+
+ Returns:
+ Rpc instance used to represent asynchronous RPC.
+ """
+ if args and kwargs:
+ raise TypeError('May not provide both args and kwargs')
+
+ if not args:
+ # Construct request object from arguments.
+ request = remote.request_type()
+ for name, value in six.iteritems(kwargs):
+ setattr(request, name, value)
+ else:
+ # First argument is request object.
+ request = args[0]
+
+ return self.transport.send_rpc(remote, request)
+
+ async_method.__name__ = remote.method.__name__
+ async_method = util.positional(2)(async_method)
+ async_method.remote = remote
+ return async_method
+
+ def __new_sync_method(cls, async_method):
+ """Create synchronous method for stub.
+
+ Args:
+ async_method: asynchronous method to delegate calls to.
+ """
+ def sync_method(self, *args, **kwargs):
+ """Synchronous remote method.
+
+ Args:
+ self: Instance of StubBase.Async subclass.
+ args: Tuple (request,):
+ request: Request object.
+ kwargs: Field values for request. Must be empty if request object
+ is provided.
+
+ Returns:
+ Response message from synchronized RPC.
+ """
+ return async_method(self.async_, *args, **kwargs).response
+ sync_method.__name__ = async_method.__name__
+ sync_method.remote = async_method.remote
+ return sync_method
+
+ def __create_async_methods(cls, remote_methods):
+ """Construct a dictionary of asynchronous methods based on remote methods.
+
+ Args:
+ remote_methods: Dictionary of methods with associated RemoteInfo objects.
+
+ Returns:
+ Dictionary of asynchronous methods with assocaited RemoteInfo objects.
+ Results added to AsyncStub subclass.
+ """
+ async_methods = {}
+ for method_name, method in remote_methods.items():
+ async_methods[method_name] = cls.__new_async_method(method.remote)
+ return async_methods
+
+ def __create_sync_methods(cls, async_methods):
+ """Construct a dictionary of synchronous methods based on remote methods.
+
+ Args:
+ async_methods: Dictionary of async methods to delegate calls to.
+
+ Returns:
+ Dictionary of synchronous methods with assocaited RemoteInfo objects.
+ Results added to Stub subclass.
+ """
+ sync_methods = {}
+ for method_name, async_method in async_methods.items():
+ sync_methods[method_name] = cls.__new_sync_method(async_method)
+ return sync_methods
+
+ def __new__(cls, name, bases, dct):
+ """Instantiate new service class instance."""
+ if StubBase not in bases:
+ # Collect existing remote methods.
+ base_methods = {}
+ for base in bases:
+ try:
+ remote_methods = base.__remote_methods
+ except AttributeError:
+ pass
+ else:
+ base_methods.update(remote_methods)
+
+ # Set this class private attribute so that base_methods do not have
+ # to be recacluated in __init__.
+ dct['_ServiceClass__base_methods'] = base_methods
+
+ for attribute, value in dct.items():
+ base_method = base_methods.get(attribute, None)
+ if base_method:
+ if not callable(value):
+ raise ServiceDefinitionError(
+ 'Must override %s in %s with a method.' % (
+ attribute, name))
+
+ if get_remote_method_info(value):
+ raise ServiceDefinitionError(
+ 'Do not use method decorator when overloading remote method %s '
+ 'on service %s.' %
+ (attribute, name))
+
+ base_remote_method_info = get_remote_method_info(base_method)
+ remote_decorator = method(
+ base_remote_method_info.request_type,
+ base_remote_method_info.response_type)
+ new_remote_method = remote_decorator(value)
+ dct[attribute] = new_remote_method
+
+ return type.__new__(cls, name, bases, dct)
+
+ def __init__(cls, name, bases, dct):
+ """Create uninitialized state on new class."""
+ type.__init__(cls, name, bases, dct)
+
+ # Only service implementation classes should have remote methods and stub
+ # sub classes created. Stub implementations have their own methods passed
+ # in to the type constructor.
+ if StubBase not in bases:
+ # Create list of remote methods.
+ cls.__remote_methods = dict(cls.__base_methods)
+
+ for attribute, value in dct.items():
+ value = getattr(cls, attribute)
+ remote_method_info = get_remote_method_info(value)
+ if remote_method_info:
+ cls.__remote_methods[attribute] = value
+
+ # Build asynchronous stub class.
+ stub_attributes = {'Service': cls}
+ async_methods = cls.__create_async_methods(cls.__remote_methods)
+ stub_attributes.update(async_methods)
+ async_class = type('AsyncStub', (StubBase, cls), stub_attributes)
+ cls.AsyncStub = async_class
+
+ # Constructor for synchronous stub class.
+ def __init__(self, transport):
+ """Constructor.
+
+ Args:
+ transport: Underlying transport to communicate with remote service.
+ """
+ super(cls.Stub, self).__init__(transport)
+ self.async_ = cls.AsyncStub(transport)
+
+ # Build synchronous stub class.
+ stub_attributes = {'Service': cls,
+ '__init__': __init__}
+ stub_attributes.update(cls.__create_sync_methods(async_methods))
+
+ cls.Stub = type('Stub', (StubBase, cls), stub_attributes)
+
+ @staticmethod
+ def all_remote_methods(cls):
+ """Get all remote methods of service.
+
+ Returns:
+ Dict from method name to unbound method.
+ """
+ return dict(cls.__remote_methods)
+
+
+class RequestState(object):
+ """Request state information.
+
+ Properties:
+ remote_host: Remote host name where request originated.
+ remote_address: IP address where request originated.
+ server_host: Host of server within which service resides.
+ server_port: Post which service has recevied request from.
+ """
+
+ @util.positional(1)
+ def __init__(self,
+ remote_host=None,
+ remote_address=None,
+ server_host=None,
+ server_port=None):
+ """Constructor.
+
+ Args:
+ remote_host: Assigned to property.
+ remote_address: Assigned to property.
+ server_host: Assigned to property.
+ server_port: Assigned to property.
+ """
+ self.__remote_host = remote_host
+ self.__remote_address = remote_address
+ self.__server_host = server_host
+ self.__server_port = server_port
+
+ @property
+ def remote_host(self):
+ return self.__remote_host
+
+ @property
+ def remote_address(self):
+ return self.__remote_address
+
+ @property
+ def server_host(self):
+ return self.__server_host
+
+ @property
+ def server_port(self):
+ return self.__server_port
+
+ def _repr_items(self):
+ for name in ['remote_host',
+ 'remote_address',
+ 'server_host',
+ 'server_port']:
+ yield name, getattr(self, name)
+
+ def __repr__(self):
+ """String representation of state."""
+ state = [self.__class__.__name__]
+ for name, value in self._repr_items():
+ if value:
+ state.append('%s=%r' % (name, value))
+
+ return '<%s>' % (' '.join(state),)
+
+
+class HttpRequestState(RequestState):
+ """HTTP request state information.
+
+ NOTE: Does not attempt to represent certain types of information from the
+ request such as the query string as query strings are not permitted in
+ ProtoRPC URLs unless required by the underlying message format.
+
+ Properties:
+ headers: wsgiref.headers.Headers instance of HTTP request headers.
+ http_method: HTTP method as a string.
+ service_path: Path on HTTP service where service is mounted. This path
+ will not include the remote method name.
+ """
+
+ @util.positional(1)
+ def __init__(self,
+ http_method=None,
+ service_path=None,
+ headers=None,
+ **kwargs):
+ """Constructor.
+
+ Args:
+ Same as RequestState, including:
+ http_method: Assigned to property.
+ service_path: Assigned to property.
+ headers: HTTP request headers. If instance of Headers, assigned to
+ property without copying. If dict, will convert to name value pairs
+ for use with Headers constructor. Otherwise, passed as parameters to
+ Headers constructor.
+ """
+ super(HttpRequestState, self).__init__(**kwargs)
+
+ self.__http_method = http_method
+ self.__service_path = service_path
+
+ # Initialize headers.
+ if isinstance(headers, dict):
+ header_list = []
+ for key, value in sorted(headers.items()):
+ if not isinstance(value, list):
+ value = [value]
+ for item in value:
+ header_list.append((key, item))
+ headers = header_list
+ self.__headers = wsgi_headers.Headers(headers or [])
+
+ @property
+ def http_method(self):
+ return self.__http_method
+
+ @property
+ def service_path(self):
+ return self.__service_path
+
+ @property
+ def headers(self):
+ return self.__headers
+
+ def _repr_items(self):
+ for item in super(HttpRequestState, self)._repr_items():
+ yield item
+
+ for name in ['http_method', 'service_path']:
+ yield name, getattr(self, name)
+
+ yield 'headers', list(self.headers.items())
+
+
+class Service(six.with_metaclass(_ServiceClass, object)):
+ """Service base class.
+
+ Base class used for defining remote services. Contains reflection functions,
+ useful helpers and built-in remote methods.
+
+ Services are expected to be constructed via either a constructor or factory
+ which takes no parameters. However, it might be required that some state or
+ configuration is passed in to a service across multiple requests.
+
+ To do this, define parameters to the constructor of the service and use
+ the 'new_factory' class method to build a constructor that will transmit
+ parameters to the constructor. For example:
+
+ class MyService(Service):
+
+ def __init__(self, configuration, state):
+ self.configuration = configuration
+ self.state = state
+
+ configuration = MyServiceConfiguration()
+ global_state = MyServiceState()
+
+ my_service_factory = MyService.new_factory(configuration,
+ state=global_state)
+
+ The contract with any service handler is that a new service object is created
+ to handle each user request, and that the construction does not take any
+ parameters. The factory satisfies this condition:
+
+ new_instance = my_service_factory()
+ assert new_instance.state is global_state
+
+ Attributes:
+ request_state: RequestState set via initialize_request_state.
+ """
+
+ __request_state = None
+
+ @classmethod
+ def all_remote_methods(cls):
+ """Get all remote methods for service class.
+
+ Built-in methods do not appear in the dictionary of remote methods.
+
+ Returns:
+ Dictionary mapping method name to remote method.
+ """
+ return _ServiceClass.all_remote_methods(cls)
+
+ @classmethod
+ def new_factory(cls, *args, **kwargs):
+ """Create factory for service.
+
+ Useful for passing configuration or state objects to the service. Accepts
+ arbitrary parameters and keywords, however, underlying service must accept
+ also accept not other parameters in its constructor.
+
+ Args:
+ args: Args to pass to service constructor.
+ kwargs: Keyword arguments to pass to service constructor.
+
+ Returns:
+ Factory function that will create a new instance and forward args and
+ keywords to the constructor.
+ """
+
+ def service_factory():
+ return cls(*args, **kwargs)
+
+ # Update docstring so that it is easier to debug.
+ full_class_name = '%s.%s' % (cls.__module__, cls.__name__)
+ service_factory.__doc__ = (
+ 'Creates new instances of service %s.\n\n'
+ 'Returns:\n'
+ ' New instance of %s.'
+ % (cls.__name__, full_class_name))
+
+ # Update name so that it is easier to debug the factory function.
+ service_factory.__name__ = '%s_service_factory' % cls.__name__
+
+ service_factory.service_class = cls
+
+ return service_factory
+
+ def initialize_request_state(self, request_state):
+ """Save request state for use in remote method.
+
+ Args:
+ request_state: RequestState instance.
+ """
+ self.__request_state = request_state
+
+ @classmethod
+ def definition_name(cls):
+ """Get definition name for Service class.
+
+ Package name is determined by the global 'package' attribute in the
+ module that contains the Service definition. If no 'package' attribute
+ is available, uses module name. If no module is found, just uses class
+ name as name.
+
+ Returns:
+ Fully qualified service name.
+ """
+ try:
+ return cls.__definition_name
+ except AttributeError:
+ outer_definition_name = cls.outer_definition_name()
+ if outer_definition_name is None:
+ cls.__definition_name = cls.__name__
+ else:
+ cls.__definition_name = '%s.%s' % (outer_definition_name, cls.__name__)
+
+ return cls.__definition_name
+
+ @classmethod
+ def outer_definition_name(cls):
+ """Get outer definition name.
+
+ Returns:
+ Package for service. Services are never nested inside other definitions.
+ """
+ return cls.definition_package()
+
+ @classmethod
+ def definition_package(cls):
+ """Get package for service.
+
+ Returns:
+ Package name for service.
+ """
+ try:
+ return cls.__definition_package
+ except AttributeError:
+ cls.__definition_package = util.get_package_for_module(cls.__module__)
+
+ return cls.__definition_package
+
+ @property
+ def request_state(self):
+ """Request state associated with this Service instance."""
+ return self.__request_state
+
+
+def is_error_status(status):
+ """Function that determines whether the RPC status is an error.
+
+ Args:
+ status: Initialized RpcStatus message to check for errors.
+ """
+ status.check_initialized()
+ return RpcError.from_state(status.state) is not None
+
+
+def check_rpc_status(status):
+ """Function converts an error status to a raised exception.
+
+ Args:
+ status: Initialized RpcStatus message to check for errors.
+
+ Raises:
+ RpcError according to state set on status, if it is an error state.
+ """
+ status.check_initialized()
+ error_class = RpcError.from_state(status.state)
+ if error_class is not None:
+ if error_class is ApplicationError:
+ raise error_class(status.error_message, status.error_name)
+ else:
+ raise error_class(status.error_message)
+
+
+class ProtocolConfig(object):
+ """Configuration for single protocol mapping.
+
+ A read-only protocol configuration provides a given protocol implementation
+ with a name and a set of content-types that it recognizes.
+
+ Properties:
+ protocol: The protocol implementation for configuration (usually a module,
+ for example, protojson, protobuf, etc.). This is an object that has the
+ following attributes:
+ CONTENT_TYPE: Used as the default content-type if default_content_type
+ is not set.
+ ALTERNATIVE_CONTENT_TYPES (optional): A list of alternative
+ content-types to the default that indicate the same protocol.
+ encode_message: Function that matches the signature of
+ ProtocolConfig.encode_message. Used for encoding a ProtoRPC message.
+ decode_message: Function that matches the signature of
+ ProtocolConfig.decode_message. Used for decoding a ProtoRPC message.
+ name: Name of protocol configuration.
+ default_content_type: The default content type for the protocol. Overrides
+ CONTENT_TYPE defined on protocol.
+ alternative_content_types: A list of alternative content-types supported
+ by the protocol. Must not contain the default content-type, nor
+ duplicates. Overrides ALTERNATIVE_CONTENT_TYPE defined on protocol.
+ content_types: A list of all content-types supported by configuration.
+ Combination of default content-type and alternatives.
+ """
+
+ def __init__(self,
+ protocol,
+ name,
+ default_content_type=None,
+ alternative_content_types=None):
+ """Constructor.
+
+ Args:
+ protocol: The protocol implementation for configuration.
+ name: The name of the protocol configuration.
+ default_content_type: The default content-type for protocol. If none
+ provided it will check protocol.CONTENT_TYPE.
+ alternative_content_types: A list of content-types. If none provided,
+ it will check protocol.ALTERNATIVE_CONTENT_TYPES. If that attribute
+ does not exist, will be an empty tuple.
+
+ Raises:
+ ServiceConfigurationError if there are any duplicate content-types.
+ """
+ self.__protocol = protocol
+ self.__name = name
+ self.__default_content_type = (default_content_type or
+ protocol.CONTENT_TYPE).lower()
+ if alternative_content_types is None:
+ alternative_content_types = getattr(protocol,
+ 'ALTERNATIVE_CONTENT_TYPES',
+ ())
+ self.__alternative_content_types = tuple(
+ content_type.lower() for content_type in alternative_content_types)
+ self.__content_types = (
+ (self.__default_content_type,) + self.__alternative_content_types)
+
+ # Detect duplicate content types in definition.
+ previous_type = None
+ for content_type in sorted(self.content_types):
+ if content_type == previous_type:
+ raise ServiceConfigurationError(
+ 'Duplicate content-type %s' % content_type)
+ previous_type = content_type
+
+ @property
+ def protocol(self):
+ return self.__protocol
+
+ @property
+ def name(self):
+ return self.__name
+
+ @property
+ def default_content_type(self):
+ return self.__default_content_type
+
+ @property
+ def alternate_content_types(self):
+ return self.__alternative_content_types
+
+ @property
+ def content_types(self):
+ return self.__content_types
+
+ def encode_message(self, message):
+ """Encode message.
+
+ Args:
+ message: Message instance to encode.
+
+ Returns:
+ String encoding of Message instance encoded in protocol's format.
+ """
+ return self.__protocol.encode_message(message)
+
+ def decode_message(self, message_type, encoded_message):
+ """Decode buffer to Message instance.
+
+ Args:
+ message_type: Message type to decode data to.
+ encoded_message: Encoded version of message as string.
+
+ Returns:
+ Decoded instance of message_type.
+ """
+ return self.__protocol.decode_message(message_type, encoded_message)
+
+
+class Protocols(object):
+ """Collection of protocol configurations.
+
+ Used to describe a complete set of content-type mappings for multiple
+ protocol configurations.
+
+ Properties:
+ names: Sorted list of the names of registered protocols.
+ content_types: Sorted list of supported content-types.
+ """
+
+ __default_protocols = None
+ __lock = threading.Lock()
+
+ def __init__(self):
+ """Constructor."""
+ self.__by_name = {}
+ self.__by_content_type = {}
+
+ def add_protocol_config(self, config):
+ """Add a protocol configuration to protocol mapping.
+
+ Args:
+ config: A ProtocolConfig.
+
+ Raises:
+ ServiceConfigurationError if protocol.name is already registered
+ or any of it's content-types are already registered.
+ """
+ if config.name in self.__by_name:
+ raise ServiceConfigurationError(
+ 'Protocol name %r is already in use' % config.name)
+ for content_type in config.content_types:
+ if content_type in self.__by_content_type:
+ raise ServiceConfigurationError(
+ 'Content type %r is already in use' % content_type)
+
+ self.__by_name[config.name] = config
+ self.__by_content_type.update((t, config) for t in config.content_types)
+
+ def add_protocol(self, *args, **kwargs):
+ """Add a protocol configuration from basic parameters.
+
+ Simple helper method that creates and registeres a ProtocolConfig instance.
+ """
+ self.add_protocol_config(ProtocolConfig(*args, **kwargs))
+
+ @property
+ def names(self):
+ return tuple(sorted(self.__by_name))
+
+ @property
+ def content_types(self):
+ return tuple(sorted(self.__by_content_type))
+
+ def lookup_by_name(self, name):
+ """Look up a ProtocolConfig by name.
+
+ Args:
+ name: Name of protocol to look for.
+
+ Returns:
+ ProtocolConfig associated with name.
+
+ Raises:
+ KeyError if there is no protocol for name.
+ """
+ return self.__by_name[name.lower()]
+
+ def lookup_by_content_type(self, content_type):
+ """Look up a ProtocolConfig by content-type.
+
+ Args:
+ content_type: Content-type to find protocol configuration for.
+
+ Returns:
+ ProtocolConfig associated with content-type.
+
+ Raises:
+ KeyError if there is no protocol for content-type.
+ """
+ return self.__by_content_type[content_type.lower()]
+
+ @classmethod
+ def new_default(cls):
+ """Create default protocols configuration.
+
+ Returns:
+ New Protocols instance configured for protobuf and protorpc.
+ """
+ protocols = cls()
+ protocols.add_protocol(protobuf, 'protobuf')
+ protocols.add_protocol(protojson.ProtoJson.get_default(), 'protojson')
+ return protocols
+
+ @classmethod
+ def get_default(cls):
+ """Get the global default Protocols instance.
+
+ Returns:
+ Current global default Protocols instance.
+ """
+ default_protocols = cls.__default_protocols
+ if default_protocols is None:
+ with cls.__lock:
+ default_protocols = cls.__default_protocols
+ if default_protocols is None:
+ default_protocols = cls.new_default()
+ cls.__default_protocols = default_protocols
+ return default_protocols
+
+ @classmethod
+ def set_default(cls, protocols):
+ """Set the global default Protocols instance.
+
+ Args:
+ protocols: A Protocols instance.
+
+ Raises:
+ TypeError: If protocols is not an instance of Protocols.
+ """
+ if not isinstance(protocols, Protocols):
+ raise TypeError(
+ 'Expected value of type "Protocols", found %r' % protocols)
+ with cls.__lock:
+ cls.__default_protocols = protocols