Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1278)

Side by Side Diff: third_party/google-endpoints/endpoints/apiserving.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 Google Inc. All Rights Reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """A library supporting use of the Google API Server.
16
17 This library helps you configure a set of ProtoRPC services to act as
18 Endpoints backends. In addition to translating ProtoRPC to Endpoints
19 compatible errors, it exposes a helper service that describes your services.
20
21 Usage:
22 1) Create an endpoints.api_server instead of a webapp.WSGIApplication.
23 2) Annotate your ProtoRPC Service class with @endpoints.api to give your
24 API a name, version, and short description
25 3) To return an error from Google API Server raise an endpoints.*Exception
26 The ServiceException classes specify the http status code returned.
27
28 For example:
29 raise endpoints.UnauthorizedException("Please log in as an admin user")
30
31
32 Sample usage:
33 - - - - app.yaml - - - -
34
35 handlers:
36 # Path to your API backend.
37 # /_ah/api/.* is the default. Using the base_path parameter, you can
38 # customize this to whichever base path you desire.
39 - url: /_ah/api/.*
40 # For the legacy python runtime this would be "script: services.py"
41 script: services.app
42
43 - - - - services.py - - - -
44
45 import endpoints
46 import postservice
47
48 app = endpoints.api_server([postservice.PostService], debug=True)
49
50 - - - - postservice.py - - - -
51
52 @endpoints.api(name='guestbook', version='v0.2', description='Guestbook API')
53 class PostService(remote.Service):
54 ...
55 @endpoints.method(GetNotesRequest, Notes, name='notes.list', path='notes',
56 http_method='GET')
57 def list(self, request):
58 raise endpoints.UnauthorizedException("Please log in as an admin user")
59 """
60
61 import cgi
62 import httplib
63 import json
64 import logging
65 import os
66
67 import api_backend_service
68 import api_config
69 import api_exceptions
70 import endpoints_dispatcher
71 import protojson
72
73 from google.appengine.api import app_identity
74 from google.api.control import client as control_client
75 from google.api.control import wsgi as control_wsgi
76
77 from protorpc import messages
78 from protorpc import remote
79 from protorpc.wsgi import service as wsgi_service
80
81 import util
82
83
84 _logger = logging.getLogger(__name__)
85 package = 'google.appengine.endpoints'
86
87
88 __all__ = [
89 'api_server',
90 'EndpointsErrorMessage',
91 'package',
92 ]
93
94
95 class _Remapped405Exception(api_exceptions.ServiceException):
96 """Method Not Allowed (405) ends up being remapped to 501.
97
98 This is included here for compatibility with the Java implementation. The
99 Google Cloud Endpoints server remaps HTTP 405 to 501.
100 """
101 http_status = httplib.METHOD_NOT_ALLOWED
102
103
104 class _Remapped408Exception(api_exceptions.ServiceException):
105 """Request Timeout (408) ends up being remapped to 503.
106
107 This is included here for compatibility with the Java implementation. The
108 Google Cloud Endpoints server remaps HTTP 408 to 503.
109 """
110 http_status = httplib.REQUEST_TIMEOUT
111
112
113 _ERROR_NAME_MAP = dict((httplib.responses[c.http_status], c) for c in [
114 api_exceptions.BadRequestException,
115 api_exceptions.UnauthorizedException,
116 api_exceptions.ForbiddenException,
117 api_exceptions.NotFoundException,
118 _Remapped405Exception,
119 _Remapped408Exception,
120 api_exceptions.ConflictException,
121 api_exceptions.GoneException,
122 api_exceptions.PreconditionFailedException,
123 api_exceptions.RequestEntityTooLargeException,
124 api_exceptions.InternalServerErrorException
125 ])
126
127 _ALL_JSON_CONTENT_TYPES = frozenset(
128 [protojson.EndpointsProtoJson.CONTENT_TYPE] +
129 protojson.EndpointsProtoJson.ALTERNATIVE_CONTENT_TYPES)
130
131
132 # Message format for returning error back to Google Endpoints frontend.
133 class EndpointsErrorMessage(messages.Message):
134 """Message for returning error back to Google Endpoints frontend.
135
136 Fields:
137 state: State of RPC, should be 'APPLICATION_ERROR'.
138 error_message: Error message associated with status.
139 """
140
141 class State(messages.Enum):
142 """Enumeration of possible RPC states.
143
144 Values:
145 OK: Completed successfully.
146 RUNNING: Still running, not complete.
147 REQUEST_ERROR: Request was malformed or incomplete.
148 SERVER_ERROR: Server experienced an unexpected error.
149 NETWORK_ERROR: An error occured on the network.
150 APPLICATION_ERROR: The application is indicating an error.
151 When in this state, RPC should also set application_error.
152 """
153 OK = 0
154 RUNNING = 1
155
156 REQUEST_ERROR = 2
157 SERVER_ERROR = 3
158 NETWORK_ERROR = 4
159 APPLICATION_ERROR = 5
160 METHOD_NOT_FOUND_ERROR = 6
161
162 state = messages.EnumField(State, 1, required=True)
163 error_message = messages.StringField(2)
164
165
166 # pylint: disable=g-bad-name
167 def _get_app_revision(environ=None):
168 """Gets the app revision (minor app version) of the current app.
169
170 Args:
171 environ: A dictionary with a key CURRENT_VERSION_ID that maps to a version
172 string of the format <major>.<minor>.
173
174 Returns:
175 The app revision (minor version) of the current app, or None if one couldn't
176 be found.
177 """
178 if environ is None:
179 environ = os.environ
180 if 'CURRENT_VERSION_ID' in environ:
181 return environ['CURRENT_VERSION_ID'].split('.')[1]
182
183
184 class _ApiServer(object):
185 """ProtoRPC wrapper, registers APIs and formats errors for Google API Server.
186
187 - - - - ProtoRPC error format - - - -
188 HTTP/1.0 400 Please log in as an admin user.
189 content-type: application/json
190
191 {
192 "state": "APPLICATION_ERROR",
193 "error_message": "Please log in as an admin user",
194 "error_name": "unauthorized",
195 }
196
197 - - - - Reformatted error format - - - -
198 HTTP/1.0 401 UNAUTHORIZED
199 content-type: application/json
200
201 {
202 "state": "APPLICATION_ERROR",
203 "error_message": "Please log in as an admin user"
204 }
205 """
206 # Silence lint warning about invalid const name
207 # pylint: disable=g-bad-name
208 __SERVER_SOFTWARE = 'SERVER_SOFTWARE'
209 __HEADER_NAME_PEER = 'HTTP_X_APPENGINE_PEER'
210 __GOOGLE_PEER = 'apiserving'
211 # A common EndpointsProtoJson for all _ApiServer instances. At the moment,
212 # EndpointsProtoJson looks to be thread safe.
213 __PROTOJSON = protojson.EndpointsProtoJson()
214
215 def __init__(self, api_services, **kwargs):
216 """Initialize an _ApiServer instance.
217
218 The primary function of this method is to set up the WSGIApplication
219 instance for the service handlers described by the services passed in.
220 Additionally, it registers each API in ApiConfigRegistry for later use
221 in the BackendService.getApiConfigs() (API config enumeration service).
222
223 Args:
224 api_services: List of protorpc.remote.Service classes implementing the API
225 or a list of _ApiDecorator instances that decorate the service classes
226 for an API.
227 **kwargs: Passed through to protorpc.wsgi.service.service_handlers except:
228 protocols - ProtoRPC protocols are not supported, and are disallowed.
229
230 Raises:
231 TypeError: if protocols are configured (this feature is not supported).
232 ApiConfigurationError: if there's a problem with the API config.
233 """
234 self.base_paths = set()
235
236 for entry in api_services[:]:
237 # pylint: disable=protected-access
238 if isinstance(entry, api_config._ApiDecorator):
239 api_services.remove(entry)
240 api_services.extend(entry.get_api_classes())
241 self.base_paths.add(entry.base_path)
242
243 # Record the API services for quick discovery doc generation
244 self.api_services = api_services
245
246 # Record the base paths
247 for entry in api_services:
248 self.base_paths.add(entry.api_info.base_path)
249
250 self.api_config_registry = api_backend_service.ApiConfigRegistry()
251 self.api_name_version_map = self.__create_name_version_map(api_services)
252 protorpc_services = self.__register_services(self.api_name_version_map,
253 self.api_config_registry)
254
255 # Disallow protocol configuration for now, Lily is json-only.
256 if 'protocols' in kwargs:
257 raise TypeError('__init__() got an unexpected keyword argument '
258 "'protocols'")
259 protocols = remote.Protocols()
260 protocols.add_protocol(self.__PROTOJSON, 'protojson')
261 remote.Protocols.set_default(protocols)
262
263 # This variable is not used in Endpoints 1.1, but let's pop it out here
264 # so it doesn't result in an unexpected keyword argument downstream.
265 kwargs.pop('restricted', None)
266
267 self.service_app = wsgi_service.service_mappings(protorpc_services,
268 **kwargs)
269
270 @staticmethod
271 def __create_name_version_map(api_services):
272 """Create a map from API name/version to Service class/factory.
273
274 This creates a map from an API name and version to a list of remote.Service
275 factories that implement that API.
276
277 Args:
278 api_services: A list of remote.Service-derived classes or factories
279 created with remote.Service.new_factory.
280
281 Returns:
282 A mapping from (api name, api version) to a list of service factories,
283 for service classes that implement that API.
284
285 Raises:
286 ApiConfigurationError: If a Service class appears more than once
287 in api_services.
288 """
289 api_name_version_map = {}
290 for service_factory in api_services:
291 try:
292 service_class = service_factory.service_class
293 except AttributeError:
294 service_class = service_factory
295 service_factory = service_class.new_factory()
296
297 key = service_class.api_info.name, service_class.api_info.version
298 service_factories = api_name_version_map.setdefault(key, [])
299 if service_factory in service_factories:
300 raise api_config.ApiConfigurationError(
301 'Can\'t add the same class to an API twice: %s' %
302 service_factory.service_class.__name__)
303
304 service_factories.append(service_factory)
305 return api_name_version_map
306
307 @staticmethod
308 def __register_services(api_name_version_map, api_config_registry):
309 """Register & return a list of each URL and class that handles that URL.
310
311 This finds every service class in api_name_version_map, registers it with
312 the given ApiConfigRegistry, builds the URL for that class, and adds
313 the URL and its factory to a list that's returned.
314
315 Args:
316 api_name_version_map: A mapping from (api name, api version) to a list of
317 service factories, as returned by __create_name_version_map.
318 api_config_registry: The ApiConfigRegistry where service classes will
319 be registered.
320
321 Returns:
322 A list of (URL, service_factory) for each service class in
323 api_name_version_map.
324
325 Raises:
326 ApiConfigurationError: If a Service class appears more than once
327 in api_name_version_map. This could happen if one class is used to
328 implement multiple APIs.
329 """
330 generator = api_config.ApiConfigGenerator()
331 protorpc_services = []
332 for service_factories in api_name_version_map.itervalues():
333 service_classes = [service_factory.service_class
334 for service_factory in service_factories]
335 config_file = generator.pretty_print_config_to_json(service_classes)
336 api_config_registry.register_backend(config_file)
337
338 for service_factory in service_factories:
339 protorpc_class_name = service_factory.service_class.__name__
340 root = '%s%s' % (service_factory.service_class.api_info.base_path,
341 protorpc_class_name)
342 if any(service_map[0] == root or service_map[1] == service_factory
343 for service_map in protorpc_services):
344 raise api_config.ApiConfigurationError(
345 'Can\'t reuse the same class in multiple APIs: %s' %
346 protorpc_class_name)
347 protorpc_services.append((root, service_factory))
348 return protorpc_services
349
350 def __is_json_error(self, status, headers):
351 """Determine if response is an error.
352
353 Args:
354 status: HTTP status code.
355 headers: Dictionary of (lowercase) header name to value.
356
357 Returns:
358 True if the response was an error, else False.
359 """
360 content_header = headers.get('content-type', '')
361 content_type, unused_params = cgi.parse_header(content_header)
362 return (status.startswith('400') and
363 content_type.lower() in _ALL_JSON_CONTENT_TYPES)
364
365 def __write_error(self, status_code, error_message=None):
366 """Return the HTTP status line and body for a given error code and message.
367
368 Args:
369 status_code: HTTP status code to be returned.
370 error_message: Error message to be returned.
371
372 Returns:
373 Tuple (http_status, body):
374 http_status: HTTP status line, e.g. 200 OK.
375 body: Body of the HTTP request.
376 """
377 if error_message is None:
378 error_message = httplib.responses[status_code]
379 status = '%d %s' % (status_code, httplib.responses[status_code])
380 message = EndpointsErrorMessage(
381 state=EndpointsErrorMessage.State.APPLICATION_ERROR,
382 error_message=error_message)
383 return status, self.__PROTOJSON.encode_message(message)
384
385 def protorpc_to_endpoints_error(self, status, body):
386 """Convert a ProtoRPC error to the format expected by Google Endpoints.
387
388 If the body does not contain an ProtoRPC message in state APPLICATION_ERROR
389 the status and body will be returned unchanged.
390
391 Args:
392 status: HTTP status of the response from the backend
393 body: JSON-encoded error in format expected by Endpoints frontend.
394
395 Returns:
396 Tuple of (http status, body)
397 """
398 try:
399 rpc_error = self.__PROTOJSON.decode_message(remote.RpcStatus, body)
400 except (ValueError, messages.ValidationError):
401 rpc_error = remote.RpcStatus()
402
403 if rpc_error.state == remote.RpcStatus.State.APPLICATION_ERROR:
404
405 # Try to map to HTTP error code.
406 error_class = _ERROR_NAME_MAP.get(rpc_error.error_name)
407 if error_class:
408 status, body = self.__write_error(error_class.http_status,
409 rpc_error.error_message)
410 return status, body
411
412 def get_api_configs(self):
413 return {
414 'items': [json.loads(c) for c in
415 self.api_config_registry.all_api_configs()]}
416
417 def __call__(self, environ, start_response):
418 """Wrapper for the Endpoints server app.
419
420 Args:
421 environ: WSGI request environment.
422 start_response: WSGI start response function.
423
424 Returns:
425 Response from service_app or appropriately transformed error response.
426 """
427 # Call the ProtoRPC App and capture its response
428 with util.StartResponseProxy() as start_response_proxy:
429 body_iter = self.service_app(environ, start_response_proxy.Proxy)
430 status = start_response_proxy.response_status
431 headers = start_response_proxy.response_headers
432 exception = start_response_proxy.response_exc_info
433
434 # Get response body
435 body = start_response_proxy.response_body
436 # In case standard WSGI behavior is implemented later...
437 if not body:
438 body = ''.join(body_iter)
439
440 # Transform ProtoRPC error into format expected by endpoints.
441 headers_dict = dict([(k.lower(), v) for k, v in headers])
442 if self.__is_json_error(status, headers_dict):
443 status, body = self.protorpc_to_endpoints_error(status, body)
444 # If the content-length header is present, update it with the new
445 # body length.
446 if 'content-length' in headers_dict:
447 for index, (header_name, _) in enumerate(headers):
448 if header_name.lower() == 'content-length':
449 headers[index] = (header_name, str(len(body)))
450 break
451
452 start_response(status, headers, exception)
453 return [body]
454
455
456 # Silence lint warning about invalid function name
457 # pylint: disable=g-bad-name
458 def api_server(api_services, **kwargs):
459 """Create an api_server.
460
461 The primary function of this method is to set up the WSGIApplication
462 instance for the service handlers described by the services passed in.
463 Additionally, it registers each API in ApiConfigRegistry for later use
464 in the BackendService.getApiConfigs() (API config enumeration service).
465 It also configures service control.
466
467 Args:
468 api_services: List of protorpc.remote.Service classes implementing the API
469 or a list of _ApiDecorator instances that decorate the service classes
470 for an API.
471 **kwargs: Passed through to protorpc.wsgi.service.service_handlers except:
472 protocols - ProtoRPC protocols are not supported, and are disallowed.
473
474 Returns:
475 A new WSGIApplication that serves the API backend and config registry.
476
477 Raises:
478 TypeError: if protocols are configured (this feature is not supported).
479 """
480 # Disallow protocol configuration for now, Lily is json-only.
481 if 'protocols' in kwargs:
482 raise TypeError("__init__() got an unexpected keyword argument 'protocols'")
483
484 # Construct the api serving app
485 apis_app = _ApiServer(api_services, **kwargs)
486 dispatcher = endpoints_dispatcher.EndpointsDispatcherMiddleware(apis_app)
487
488 # Determine the service name
489 service_name = os.environ.get('ENDPOINTS_SERVICE_NAME')
490 if not service_name:
491 _logger.warn('Did not specify the ENDPOINTS_SERVICE_NAME environment'
492 ' variable so service control is disabled. Please specify'
493 ' the name of service in ENDPOINTS_SERVICE_NAME to enable'
494 ' it.')
495 return dispatcher
496
497 # If we're using a local server, just return the dispatcher now to bypass
498 # control client.
499 if control_wsgi.running_on_devserver():
500 _logger.warn('Running on local devserver, so service control is disabled.')
501 return dispatcher
502
503 # The DEFAULT 'config' should be tuned so that it's always OK for python
504 # App Engine workloads. The config can be adjusted, but that's probably
505 # unnecessary on App Engine.
506 controller = control_client.Loaders.DEFAULT.load(service_name)
507
508 # Start the GAE background thread that powers the control client's cache.
509 control_client.use_gae_thread()
510 controller.start()
511
512 return control_wsgi.add_all(
513 dispatcher,
514 app_identity.get_application_id(),
515 controller)
OLDNEW
« no previous file with comments | « third_party/google-endpoints/endpoints/api_request.py ('k') | third_party/google-endpoints/endpoints/discovery_api_proxy.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698