blob: bc73d82bcd9bb7ae35add47f44222bfe74a9231e [file]
#!/usr/bin/env python
#
# Copyright 2007 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.
#
"""Conversion API providing document conversion service for applications."""
from google.appengine.api import apiproxy_stub_map
from google.appengine.api.conversion import conversion_service_pb2
from google.appengine.runtime import apiproxy_errors
CONVERSION_MAX_NUM_PER_REQUEST = 10
CONVERSION_MAX_DOC_SIZE_BYTES = 10 * (2 ** 20)
_CONVERSION_SERVICE = "conversion"
_CONVERT_METHOD = "Convert"
class Error(Exception):
"""Base-class for exceptions in this module."""
class BackendDeadlineExceeded(Error):
"""Communication to backend service timed-out."""
class TransientError(Error):
"""Transient error while accessing the backend, please try again later."""
class BackendError(Error):
"""Something wrong in the backend that can't be sent back to application."""
class ConversionUnsupported(Error):
"""Unsupported conversion attempted."""
class ConversionTooLarge(Error):
"""The conversion is too large."""
class TooManyConversions(Error):
"""Too many conversions in the request."""
class InvalidRequest(Error):
"""The request was not formed properly."""
def _to_conversion_error(error):
"""Translate an application error to a conversion Error, if possible.
Args:
error: An ApplicationError to translate.
Returns:
error: ConversionApi specific error message.
"""
error_map = {
conversion_service_pb2.ConversionServiceError.TIMEOUT:
BackendDeadlineExceeded,
conversion_service_pb2.ConversionServiceError.TRANSIENT_ERROR:
TransientError,
conversion_service_pb2.ConversionServiceError.INTERNAL_ERROR:
BackendError,
conversion_service_pb2.ConversionServiceError.UNSUPPORTED_CONVERSION:
ConversionUnsupported,
conversion_service_pb2.ConversionServiceError.CONVERSION_TOO_LARGE:
ConversionTooLarge,
conversion_service_pb2.ConversionServiceError.TOO_MANY_CONVERSIONS:
TooManyConversions,
conversion_service_pb2.ConversionServiceError.INVALID_REQUEST:
InvalidRequest,
}
if error.application_error in error_map:
return error_map[error.application_error](error.error_detail)
else:
return error
def _to_error_text(error_code):
"""Translate an error code to an error message, if possible.
Args:
error_code: An conversion_service_pb2.ConversionServiceError error code.
Returns:
Human readable error message.
"""
error_map = {
conversion_service_pb2.ConversionServiceError.TIMEOUT:
"BackendDeadlineExceeded",
conversion_service_pb2.ConversionServiceError.TRANSIENT_ERROR:
"TransientError",
conversion_service_pb2.ConversionServiceError.INTERNAL_ERROR:
"BackendError",
conversion_service_pb2.ConversionServiceError.UNSUPPORTED_CONVERSION:
"ConversionUnsupported",
conversion_service_pb2.ConversionServiceError.CONVERSION_TOO_LARGE:
"ConversionTooLarge",
conversion_service_pb2.ConversionServiceError.TOO_MANY_CONVERSIONS:
"TooManyConversions",
conversion_service_pb2.ConversionServiceError.INVALID_REQUEST:
"InvalidRequest",
}
if error_code in error_map:
return error_map[error_code]
else:
return "UnknownError"
class Asset(object):
"""Represents a single asset in the request.
An asset is a generic blob of data. A conversion document must contain
at least one asset, typically the document contents. Additional assets
are those needed for the conversion, for example images in HTML.
"""
def __init__(self, mime_type, data, name=None):
"""Constructor.
Args:
mime_type: mime type of the asset (string).
data: data to be converted (string).
name: name of the asset (string).
Raises:
TypeError: if input arguments are not string or data is missing.
"""
if mime_type is not None:
if not isinstance(mime_type, (str, unicode)):
raise TypeError("mime_type %r is not a string" % mime_type)
self._mime_type = mime_type
if data is None:
raise TypeError("Asset must have a data field")
if not isinstance(data, (str, unicode)):
raise TypeError("data %r is not a string" % data)
self._data = data
if name is not None:
if not isinstance(name, (str, unicode)):
raise TypeError("name %r is not a string" % name)
self._name = name
@property
def mime_type(self):
"""The mime type of the asset (string)."""
return self._mime_type
@property
def data(self):
"""The data of the asset (string)."""
return self._data
@property
def name(self):
"""The name of the asset (string)."""
return self._name
def to_proto(self):
"""Create and return an AssetInfo protocol buffer.
Returns:
An conversion_service_pb2.AssetInfo protocol buffer.
"""
asset_info_pb = conversion_service_pb2.AssetInfo()
if self.mime_type is not None:
asset_info_pb.mime_type = self._mime_type
asset_info_pb.data = self._data
if self.name is not None:
asset_info_pb.name = self._name
return asset_info_pb
class ConversionRequest(object):
"""Represents a single conversion from one file format to another.
A conversion document must contain at least one asset, typically the
document contents. Additional assets are those needed for the conversion,
for example images in HTML.
"""
def __init__(self, asset, output_type):
"""Create a single conversion.
Args:
asset: An Asset instance.
output_type: output data mime type (string), put into the
output_mime_type field.
Raises:
TypeError: if input arguments are not string or data is missing.
"""
self.input_assets = []
if not asset.mime_type:
raise TypeError("Asset's mime type %r should not be None or empty" %
asset.mime_type)
if not output_type:
raise TypeError("Output mime type %r should not be None or empty" %
output_type)
if not isinstance(output_type, (str, unicode)):
raise TypeError("Output mime type %r is not a string" % output_type)
self.add_asset(asset)
self.output_mime_type = output_type
def add_asset(self, asset):
"""Add an asset into the conversion request.
Args:
asset: An Asset instance.
Raises:
TypeError: if the asset is not an Asset instance.
"""
if not isinstance(asset, Asset):
raise TypeError("Input %r is not an Asset instance" % asset)
self.input_assets.append(asset.to_proto())
def to_proto(self):
"""Create and return a ConversionInput protocol buffer."""
conversion = conversion_service_pb2.ConversionInput()
for asset in self.input_assets:
conversion.input.asset.extend([asset])
conversion.output_mime_type = self.output_mime_type
return conversion
class ConversionOutput(object):
"""Represents a single conversion output from the response.
A conversion output includes the error code and a list of converted assets.
"""
def __init__(self, conversion_output_proto):
"""Constructor.
Args:
conversion_output_proto: the ConversionOutput protocol buffer.
Raises:
AssertionError: if asset_info_proto is not an AssetInfo protocol buffer.
"""
assert isinstance(conversion_output_proto,
conversion_service_pb2.ConversionOutput)
self._error_code = conversion_output_proto.error_code
self._error_text = "OK"
if self._error_code != conversion_service_pb2.ConversionServiceError.OK:
self._error_text = _to_error_text(self._error_code)
self._assets = []
for asset_pb in conversion_output_proto.output.asset:
self._assets.append(Asset(
asset_pb.mime_type, asset_pb.data, asset_pb.name))
@property
def error_code(self):
"""The error code of this conversion."""
return self._error_code
@property
def error_text(self):
"""The error message of this conversion if not successful."""
return self._error_text
@property
def assets(self):
"""A list of converted assets in the format of Asset instances."""
return self._assets
def convert(conversion_request, deadline=None):
"""Makes all conversions in parallel, blocking until all results are returned.
Args:
conversion_request: A ConversionRequest instance or a list of
ConversionRequest instances.
deadline: Optional deadline in seconds for all the conversions.
Returns:
A ConverionOutput instance if conversion_request is a ConversionRequest
instance. Or a list of ConversionOutput instances,
one per ConversionRequest in the same order.
Raises:
TypeError: Input conversion_requests with wrong type.
See more details in _to_conversion_error function.
"""
rpc = create_rpc(deadline=deadline)
make_convert_call(rpc, conversion_request)
return rpc.get_result()
def create_rpc(deadline=None, callback=None):
"""Creates an RPC object for use with the Conversion API.
Args:
deadline: Optional deadline in seconds for the operation; the default
is a system-specific deadline (typically 5 seconds).
callback: Optional callable to invoke on completion.
Returns:
An apiproxy_stub_map.UserRPC object specialized for this service.
"""
return apiproxy_stub_map.UserRPC(_CONVERSION_SERVICE, deadline, callback)
def make_convert_call(rpc, conversion_request):
"""Executes the RPC call to do the conversions.
The result can then be got from rpc.get_result which will call
_get_convert_result. See the docstring there for more details.
Args:
rpc: a UserRPC instance.
conversion_request: A ConversionRequest instance or a list of
ConversionRequest instances.
Raises:
TypeError: Input conversion_requests with wrong type.
See more details in _to_conversion_error function.
"""
request = conversion_service_pb2.ConversionRequest()
response = conversion_service_pb2.ConversionResponse()
try:
conversion_requests = list(iter(conversion_request))
except TypeError:
conversion_requests = [conversion_request]
multiple = False
else:
multiple = True
for conversion in conversion_requests:
if isinstance(conversion, ConversionRequest):
request.conversion.extend([conversion.to_proto()])
else:
raise TypeError("conversion_request must be a ConversionRequest instance "
"or a list of ConversionRequest instances")
rpc.make_call(_CONVERT_METHOD, request, response,
_get_convert_result, user_data=multiple)
def _get_convert_result(rpc):
"""Check success, handle exceptions, and return conversion results.
Args:
rpc: A UserRPC instance.
Returns:
A ConverionOutput instance if conversion_request is a ConversionRequest
instance. Or a list of ConversionOutput instances,
one per ConversionRequest in the same order.
Raises:
See more details in _to_conversion_error function.
"""
assert rpc.service == _CONVERSION_SERVICE, repr(rpc.service)
assert rpc.method == _CONVERT_METHOD, repr(rpc.method)
try:
rpc.check_success()
except apiproxy_errors.ApplicationError, e:
raise _to_conversion_error(e)
results = []
for output_pb in rpc.response.result:
results.append(ConversionOutput(output_pb))
multiple = rpc.user_data
if multiple:
return results
else:
assert len(results) == 1
return results[0]