Adrià Vilanova MartÃnez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 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 | from __future__ import absolute_import |
| 62 | |
| 63 | import cgi |
| 64 | from six.moves import http_client |
| 65 | import json |
| 66 | import logging |
| 67 | import os |
| 68 | |
| 69 | from google.appengine.api import app_identity |
| 70 | |
| 71 | from . import api_config |
| 72 | from . import api_exceptions |
| 73 | from . import endpoints_dispatcher |
| 74 | from . import message_types |
| 75 | from . import messages |
| 76 | from . import protojson |
| 77 | from . import remote |
| 78 | from . import util |
| 79 | |
| 80 | _logger = logging.getLogger(__name__) |
| 81 | package = 'google.appengine.endpoints' |
| 82 | |
| 83 | |
| 84 | __all__ = [ |
| 85 | 'ApiConfigRegistry', |
| 86 | 'api_server', |
| 87 | 'EndpointsErrorMessage', |
| 88 | 'package', |
| 89 | ] |
| 90 | |
| 91 | |
| 92 | class _Remapped405Exception(api_exceptions.ServiceException): |
| 93 | """Method Not Allowed (405) ends up being remapped to 501. |
| 94 | |
| 95 | This is included here for compatibility with the Java implementation. The |
| 96 | Google Cloud Endpoints server remaps HTTP 405 to 501. |
| 97 | """ |
| 98 | http_status = http_client.METHOD_NOT_ALLOWED |
| 99 | |
| 100 | |
| 101 | class _Remapped408Exception(api_exceptions.ServiceException): |
| 102 | """Request Timeout (408) ends up being remapped to 503. |
| 103 | |
| 104 | This is included here for compatibility with the Java implementation. The |
| 105 | Google Cloud Endpoints server remaps HTTP 408 to 503. |
| 106 | """ |
| 107 | http_status = http_client.REQUEST_TIMEOUT |
| 108 | |
| 109 | |
| 110 | _ERROR_NAME_MAP = dict((http_client.responses[c.http_status], c) for c in [ |
| 111 | api_exceptions.BadRequestException, |
| 112 | api_exceptions.UnauthorizedException, |
| 113 | api_exceptions.ForbiddenException, |
| 114 | api_exceptions.NotFoundException, |
| 115 | _Remapped405Exception, |
| 116 | _Remapped408Exception, |
| 117 | api_exceptions.ConflictException, |
| 118 | api_exceptions.GoneException, |
| 119 | api_exceptions.PreconditionFailedException, |
| 120 | api_exceptions.RequestEntityTooLargeException, |
| 121 | api_exceptions.InternalServerErrorException |
| 122 | ]) |
| 123 | |
| 124 | _ALL_JSON_CONTENT_TYPES = frozenset( |
| 125 | [protojson.EndpointsProtoJson.CONTENT_TYPE] + |
| 126 | protojson.EndpointsProtoJson.ALTERNATIVE_CONTENT_TYPES) |
| 127 | |
| 128 | |
| 129 | # Message format for returning error back to Google Endpoints frontend. |
| 130 | class EndpointsErrorMessage(messages.Message): |
| 131 | """Message for returning error back to Google Endpoints frontend. |
| 132 | |
| 133 | Fields: |
| 134 | state: State of RPC, should be 'APPLICATION_ERROR'. |
| 135 | error_message: Error message associated with status. |
| 136 | """ |
| 137 | |
| 138 | class State(messages.Enum): |
| 139 | """Enumeration of possible RPC states. |
| 140 | |
| 141 | Values: |
| 142 | OK: Completed successfully. |
| 143 | RUNNING: Still running, not complete. |
| 144 | REQUEST_ERROR: Request was malformed or incomplete. |
| 145 | SERVER_ERROR: Server experienced an unexpected error. |
| 146 | NETWORK_ERROR: An error occured on the network. |
| 147 | APPLICATION_ERROR: The application is indicating an error. |
| 148 | When in this state, RPC should also set application_error. |
| 149 | """ |
| 150 | OK = 0 |
| 151 | RUNNING = 1 |
| 152 | |
| 153 | REQUEST_ERROR = 2 |
| 154 | SERVER_ERROR = 3 |
| 155 | NETWORK_ERROR = 4 |
| 156 | APPLICATION_ERROR = 5 |
| 157 | METHOD_NOT_FOUND_ERROR = 6 |
| 158 | |
| 159 | state = messages.EnumField(State, 1, required=True) |
| 160 | error_message = messages.StringField(2) |
| 161 | |
| 162 | |
| 163 | # pylint: disable=g-bad-name |
| 164 | def _get_app_revision(environ=None): |
| 165 | """Gets the app revision (minor app version) of the current app. |
| 166 | |
| 167 | Args: |
| 168 | environ: A dictionary with a key CURRENT_VERSION_ID that maps to a version |
| 169 | string of the format <major>.<minor>. |
| 170 | |
| 171 | Returns: |
| 172 | The app revision (minor version) of the current app, or None if one couldn't |
| 173 | be found. |
| 174 | """ |
| 175 | if environ is None: |
| 176 | environ = os.environ |
| 177 | if 'CURRENT_VERSION_ID' in environ: |
| 178 | return environ['CURRENT_VERSION_ID'].split('.')[1] |
| 179 | |
| 180 | |
| 181 | class ApiConfigRegistry(object): |
| 182 | """Registry of active APIs""" |
| 183 | |
| 184 | def __init__(self): |
| 185 | # Set of API classes that have been registered. |
| 186 | self.__registered_classes = set() |
| 187 | # Set of API config contents served by this App Engine AppId/version |
| 188 | self.__api_configs = [] |
| 189 | # Map of API method name to ProtoRPC method name. |
| 190 | self.__api_methods = {} |
| 191 | |
| 192 | # pylint: disable=g-bad-name |
| 193 | def register_backend(self, config_contents): |
| 194 | """Register a single API and its config contents. |
| 195 | |
| 196 | Args: |
| 197 | config_contents: Dict containing API configuration. |
| 198 | """ |
| 199 | if config_contents is None: |
| 200 | return |
| 201 | self.__register_class(config_contents) |
| 202 | self.__api_configs.append(config_contents) |
| 203 | self.__register_methods(config_contents) |
| 204 | |
| 205 | def __register_class(self, parsed_config): |
| 206 | """Register the class implementing this config, so we only add it once. |
| 207 | |
| 208 | Args: |
| 209 | parsed_config: The JSON object with the API configuration being added. |
| 210 | |
| 211 | Raises: |
| 212 | ApiConfigurationError: If the class has already been registered. |
| 213 | """ |
| 214 | methods = parsed_config.get('methods') |
| 215 | if not methods: |
| 216 | return |
| 217 | |
| 218 | # Determine the name of the class that implements this configuration. |
| 219 | service_classes = set() |
| 220 | for method in methods.values(): |
| 221 | rosy_method = method.get('rosyMethod') |
| 222 | if rosy_method and '.' in rosy_method: |
| 223 | method_class = rosy_method.split('.', 1)[0] |
| 224 | service_classes.add(method_class) |
| 225 | |
| 226 | for service_class in service_classes: |
| 227 | if service_class in self.__registered_classes: |
| 228 | raise api_exceptions.ApiConfigurationError( |
| 229 | 'API class %s has already been registered.' % service_class) |
| 230 | self.__registered_classes.add(service_class) |
| 231 | |
| 232 | def __register_methods(self, parsed_config): |
| 233 | """Register all methods from the given api config file. |
| 234 | |
| 235 | Methods are stored in a map from method_name to rosyMethod, |
| 236 | the name of the ProtoRPC method to be called on the backend. |
| 237 | If no rosyMethod was specified the value will be None. |
| 238 | |
| 239 | Args: |
| 240 | parsed_config: The JSON object with the API configuration being added. |
| 241 | """ |
| 242 | methods = parsed_config.get('methods') |
| 243 | if not methods: |
| 244 | return |
| 245 | |
| 246 | for method_name, method in methods.items(): |
| 247 | self.__api_methods[method_name] = method.get('rosyMethod') |
| 248 | |
| 249 | def lookup_api_method(self, api_method_name): |
| 250 | """Looks an API method up by name to find the backend method to call. |
| 251 | |
| 252 | Args: |
| 253 | api_method_name: Name of the method in the API that was called. |
| 254 | |
| 255 | Returns: |
| 256 | Name of the ProtoRPC method called on the backend, or None if not found. |
| 257 | """ |
| 258 | return self.__api_methods.get(api_method_name) |
| 259 | |
| 260 | def all_api_configs(self): |
| 261 | """Return a list of all API configration specs as registered above.""" |
| 262 | return self.__api_configs |
| 263 | |
| 264 | |
| 265 | class _ApiServer(object): |
| 266 | """ProtoRPC wrapper, registers APIs and formats errors for Google API Server. |
| 267 | |
| 268 | - - - - ProtoRPC error format - - - - |
| 269 | HTTP/1.0 400 Please log in as an admin user. |
| 270 | content-type: application/json |
| 271 | |
| 272 | { |
| 273 | "state": "APPLICATION_ERROR", |
| 274 | "error_message": "Please log in as an admin user", |
| 275 | "error_name": "unauthorized", |
| 276 | } |
| 277 | |
| 278 | - - - - Reformatted error format - - - - |
| 279 | HTTP/1.0 401 UNAUTHORIZED |
| 280 | content-type: application/json |
| 281 | |
| 282 | { |
| 283 | "state": "APPLICATION_ERROR", |
| 284 | "error_message": "Please log in as an admin user" |
| 285 | } |
| 286 | """ |
| 287 | # Silence lint warning about invalid const name |
| 288 | # pylint: disable=g-bad-name |
| 289 | __SERVER_SOFTWARE = 'SERVER_SOFTWARE' |
| 290 | __HEADER_NAME_PEER = 'HTTP_X_APPENGINE_PEER' |
| 291 | __GOOGLE_PEER = 'apiserving' |
| 292 | # A common EndpointsProtoJson for all _ApiServer instances. At the moment, |
| 293 | # EndpointsProtoJson looks to be thread safe. |
| 294 | __PROTOJSON = protojson.EndpointsProtoJson() |
| 295 | |
| 296 | def __init__(self, api_services, **kwargs): |
| 297 | """Initialize an _ApiServer instance. |
| 298 | |
| 299 | The primary function of this method is to set up the WSGIApplication |
| 300 | instance for the service handlers described by the services passed in. |
| 301 | Additionally, it registers each API in ApiConfigRegistry for later use |
| 302 | in the BackendService.getApiConfigs() (API config enumeration service). |
| 303 | |
| 304 | Args: |
| 305 | api_services: List of protorpc.remote.Service classes implementing the API |
| 306 | or a list of _ApiDecorator instances that decorate the service classes |
| 307 | for an API. |
| 308 | **kwargs: Passed through to protorpc.wsgi.service.service_handlers except: |
| 309 | protocols - ProtoRPC protocols are not supported, and are disallowed. |
| 310 | |
| 311 | Raises: |
| 312 | TypeError: if protocols are configured (this feature is not supported). |
| 313 | ApiConfigurationError: if there's a problem with the API config. |
| 314 | """ |
| 315 | self.base_paths = set() |
| 316 | |
| 317 | for entry in api_services[:]: |
| 318 | # pylint: disable=protected-access |
| 319 | if isinstance(entry, api_config._ApiDecorator): |
| 320 | api_services.remove(entry) |
| 321 | api_services.extend(entry.get_api_classes()) |
| 322 | |
| 323 | # Record the API services for quick discovery doc generation |
| 324 | self.api_services = api_services |
| 325 | |
| 326 | # Record the base paths |
| 327 | for entry in api_services: |
| 328 | self.base_paths.add(entry.api_info.base_path) |
| 329 | |
| 330 | self.api_config_registry = ApiConfigRegistry() |
| 331 | self.api_name_version_map = self.__create_name_version_map(api_services) |
| 332 | protorpc_services = self.__register_services(self.api_name_version_map, |
| 333 | self.api_config_registry) |
| 334 | |
| 335 | # Disallow protocol configuration for now, Lily is json-only. |
| 336 | if 'protocols' in kwargs: |
| 337 | raise TypeError('__init__() got an unexpected keyword argument ' |
| 338 | "'protocols'") |
| 339 | protocols = remote.Protocols() |
| 340 | protocols.add_protocol(self.__PROTOJSON, 'protojson') |
| 341 | remote.Protocols.set_default(protocols) |
| 342 | |
| 343 | # This variable is not used in Endpoints 1.1, but let's pop it out here |
| 344 | # so it doesn't result in an unexpected keyword argument downstream. |
| 345 | kwargs.pop('restricted', None) |
| 346 | |
| 347 | from protorpc.wsgi import service as wsgi_service |
| 348 | self.service_app = wsgi_service.service_mappings(protorpc_services, |
| 349 | **kwargs) |
| 350 | |
| 351 | @staticmethod |
| 352 | def __create_name_version_map(api_services): |
| 353 | """Create a map from API name/version to Service class/factory. |
| 354 | |
| 355 | This creates a map from an API name and version to a list of remote.Service |
| 356 | factories that implement that API. |
| 357 | |
| 358 | Args: |
| 359 | api_services: A list of remote.Service-derived classes or factories |
| 360 | created with remote.Service.new_factory. |
| 361 | |
| 362 | Returns: |
| 363 | A mapping from (api name, api version) to a list of service factories, |
| 364 | for service classes that implement that API. |
| 365 | |
| 366 | Raises: |
| 367 | ApiConfigurationError: If a Service class appears more than once |
| 368 | in api_services. |
| 369 | """ |
| 370 | api_name_version_map = {} |
| 371 | for service_factory in api_services: |
| 372 | try: |
| 373 | service_class = service_factory.service_class |
| 374 | except AttributeError: |
| 375 | service_class = service_factory |
| 376 | service_factory = service_class.new_factory() |
| 377 | |
| 378 | key = service_class.api_info.name, service_class.api_info.api_version |
| 379 | service_factories = api_name_version_map.setdefault(key, []) |
| 380 | if service_factory in service_factories: |
| 381 | raise api_config.ApiConfigurationError( |
| 382 | 'Can\'t add the same class to an API twice: %s' % |
| 383 | service_factory.service_class.__name__) |
| 384 | |
| 385 | service_factories.append(service_factory) |
| 386 | return api_name_version_map |
| 387 | |
| 388 | @staticmethod |
| 389 | def __register_services(api_name_version_map, api_config_registry): |
| 390 | """Register & return a list of each URL and class that handles that URL. |
| 391 | |
| 392 | This finds every service class in api_name_version_map, registers it with |
| 393 | the given ApiConfigRegistry, builds the URL for that class, and adds |
| 394 | the URL and its factory to a list that's returned. |
| 395 | |
| 396 | Args: |
| 397 | api_name_version_map: A mapping from (api name, api version) to a list of |
| 398 | service factories, as returned by __create_name_version_map. |
| 399 | api_config_registry: The ApiConfigRegistry where service classes will |
| 400 | be registered. |
| 401 | |
| 402 | Returns: |
| 403 | A list of (URL, service_factory) for each service class in |
| 404 | api_name_version_map. |
| 405 | |
| 406 | Raises: |
| 407 | ApiConfigurationError: If a Service class appears more than once |
| 408 | in api_name_version_map. This could happen if one class is used to |
| 409 | implement multiple APIs. |
| 410 | """ |
| 411 | generator = api_config.ApiConfigGenerator() |
| 412 | protorpc_services = [] |
| 413 | for service_factories in api_name_version_map.values(): |
| 414 | service_classes = [service_factory.service_class |
| 415 | for service_factory in service_factories] |
| 416 | config_dict = generator.get_config_dict(service_classes) |
| 417 | api_config_registry.register_backend(config_dict) |
| 418 | |
| 419 | for service_factory in service_factories: |
| 420 | protorpc_class_name = service_factory.service_class.__name__ |
| 421 | root = '%s%s' % (service_factory.service_class.api_info.base_path, |
| 422 | protorpc_class_name) |
| 423 | if any(service_map[0] == root or service_map[1] == service_factory |
| 424 | for service_map in protorpc_services): |
| 425 | raise api_config.ApiConfigurationError( |
| 426 | 'Can\'t reuse the same class in multiple APIs: %s' % |
| 427 | protorpc_class_name) |
| 428 | protorpc_services.append((root, service_factory)) |
| 429 | return protorpc_services |
| 430 | |
| 431 | def __is_json_error(self, status, headers): |
| 432 | """Determine if response is an error. |
| 433 | |
| 434 | Args: |
| 435 | status: HTTP status code. |
| 436 | headers: Dictionary of (lowercase) header name to value. |
| 437 | |
| 438 | Returns: |
| 439 | True if the response was an error, else False. |
| 440 | """ |
| 441 | content_header = headers.get('content-type', '') |
| 442 | content_type, unused_params = cgi.parse_header(content_header) |
| 443 | return (status.startswith('400') and |
| 444 | content_type.lower() in _ALL_JSON_CONTENT_TYPES) |
| 445 | |
| 446 | def __write_error(self, status_code, error_message=None): |
| 447 | """Return the HTTP status line and body for a given error code and message. |
| 448 | |
| 449 | Args: |
| 450 | status_code: HTTP status code to be returned. |
| 451 | error_message: Error message to be returned. |
| 452 | |
| 453 | Returns: |
| 454 | Tuple (http_status, body): |
| 455 | http_status: HTTP status line, e.g. 200 OK. |
| 456 | body: Body of the HTTP request. |
| 457 | """ |
| 458 | if error_message is None: |
| 459 | error_message = http_client.responses[status_code] |
| 460 | status = '%d %s' % (status_code, http_client.responses[status_code]) |
| 461 | message = EndpointsErrorMessage( |
| 462 | state=EndpointsErrorMessage.State.APPLICATION_ERROR, |
| 463 | error_message=error_message) |
| 464 | return status, self.__PROTOJSON.encode_message(message) |
| 465 | |
| 466 | def protorpc_to_endpoints_error(self, status, body): |
| 467 | """Convert a ProtoRPC error to the format expected by Google Endpoints. |
| 468 | |
| 469 | If the body does not contain an ProtoRPC message in state APPLICATION_ERROR |
| 470 | the status and body will be returned unchanged. |
| 471 | |
| 472 | Args: |
| 473 | status: HTTP status of the response from the backend |
| 474 | body: JSON-encoded error in format expected by Endpoints frontend. |
| 475 | |
| 476 | Returns: |
| 477 | Tuple of (http status, body) |
| 478 | """ |
| 479 | try: |
| 480 | rpc_error = self.__PROTOJSON.decode_message(remote.RpcStatus, body) |
| 481 | except (ValueError, messages.ValidationError): |
| 482 | rpc_error = remote.RpcStatus() |
| 483 | |
| 484 | if rpc_error.state == remote.RpcStatus.State.APPLICATION_ERROR: |
| 485 | |
| 486 | # Try to map to HTTP error code. |
| 487 | error_class = _ERROR_NAME_MAP.get(rpc_error.error_name) |
| 488 | if error_class: |
| 489 | status, body = self.__write_error(error_class.http_status, |
| 490 | rpc_error.error_message) |
| 491 | return status, body |
| 492 | |
| 493 | def get_api_configs(self): |
| 494 | return { |
| 495 | 'items': self.api_config_registry.all_api_configs()} |
| 496 | |
| 497 | def __call__(self, environ, start_response): |
| 498 | """Wrapper for the Endpoints server app. |
| 499 | |
| 500 | Args: |
| 501 | environ: WSGI request environment. |
| 502 | start_response: WSGI start response function. |
| 503 | |
| 504 | Returns: |
| 505 | Response from service_app or appropriately transformed error response. |
| 506 | """ |
| 507 | # Call the ProtoRPC App and capture its response |
| 508 | with util.StartResponseProxy() as start_response_proxy: |
| 509 | body_iter = self.service_app(environ, start_response_proxy.Proxy) |
| 510 | status = start_response_proxy.response_status |
| 511 | headers = start_response_proxy.response_headers |
| 512 | exception = start_response_proxy.response_exc_info |
| 513 | |
| 514 | # Get response body |
| 515 | body = start_response_proxy.response_body |
| 516 | # In case standard WSGI behavior is implemented later... |
| 517 | if not body: |
| 518 | body = ''.join(body_iter) |
| 519 | |
| 520 | # Transform ProtoRPC error into format expected by endpoints. |
| 521 | headers_dict = dict([(k.lower(), v) for k, v in headers]) |
| 522 | if self.__is_json_error(status, headers_dict): |
| 523 | status, body = self.protorpc_to_endpoints_error(status, body) |
| 524 | # If the content-length header is present, update it with the new |
| 525 | # body length. |
| 526 | if 'content-length' in headers_dict: |
| 527 | for index, (header_name, _) in enumerate(headers): |
| 528 | if header_name.lower() == 'content-length': |
| 529 | headers[index] = (header_name, str(len(body))) |
| 530 | break |
| 531 | |
| 532 | start_response(status, headers, exception) |
| 533 | return [body] |
| 534 | |
| 535 | |
| 536 | # Silence lint warning about invalid function name |
| 537 | # pylint: disable=g-bad-name |
| 538 | def api_server(api_services, **kwargs): |
| 539 | """Create an api_server. |
| 540 | |
| 541 | The primary function of this method is to set up the WSGIApplication |
| 542 | instance for the service handlers described by the services passed in. |
| 543 | Additionally, it registers each API in ApiConfigRegistry for later use |
| 544 | in the BackendService.getApiConfigs() (API config enumeration service). |
| 545 | It also configures service control. |
| 546 | |
| 547 | Args: |
| 548 | api_services: List of protorpc.remote.Service classes implementing the API |
| 549 | or a list of _ApiDecorator instances that decorate the service classes |
| 550 | for an API. |
| 551 | **kwargs: Passed through to protorpc.wsgi.service.service_handlers except: |
| 552 | protocols - ProtoRPC protocols are not supported, and are disallowed. |
| 553 | |
| 554 | Returns: |
| 555 | A new WSGIApplication that serves the API backend and config registry. |
| 556 | |
| 557 | Raises: |
| 558 | TypeError: if protocols are configured (this feature is not supported). |
| 559 | """ |
| 560 | # Disallow protocol configuration for now, Lily is json-only. |
| 561 | if 'protocols' in kwargs: |
| 562 | raise TypeError("__init__() got an unexpected keyword argument 'protocols'") |
| 563 | |
| 564 | from . import _logger as endpoints_logger |
| 565 | from . import __version__ as endpoints_version |
| 566 | endpoints_logger.info('Initializing Endpoints Framework version %s', endpoints_version) |
| 567 | |
| 568 | # Construct the api serving app |
| 569 | apis_app = _ApiServer(api_services, **kwargs) |
| 570 | dispatcher = endpoints_dispatcher.EndpointsDispatcherMiddleware(apis_app) |
| 571 | |
| 572 | # Determine the service name |
| 573 | service_name = os.environ.get('ENDPOINTS_SERVICE_NAME') |
| 574 | if not service_name: |
| 575 | _logger.warn('Did not specify the ENDPOINTS_SERVICE_NAME environment' |
| 576 | ' variable so service control is disabled. Please specify' |
| 577 | ' the name of service in ENDPOINTS_SERVICE_NAME to enable' |
| 578 | ' it.') |
| 579 | return dispatcher |
| 580 | |
| 581 | from endpoints_management.control import client as control_client |
| 582 | from endpoints_management.control import wsgi as control_wsgi |
| 583 | |
| 584 | # If we're using a local server, just return the dispatcher now to bypass |
| 585 | # control client. |
| 586 | if control_wsgi.running_on_devserver(): |
| 587 | _logger.warn('Running on local devserver, so service control is disabled.') |
| 588 | return dispatcher |
| 589 | |
| 590 | from endpoints_management import _logger as management_logger |
| 591 | from endpoints_management import __version__ as management_version |
| 592 | management_logger.info('Initializing Endpoints Management Framework version %s', management_version) |
| 593 | |
| 594 | # The DEFAULT 'config' should be tuned so that it's always OK for python |
| 595 | # App Engine workloads. The config can be adjusted, but that's probably |
| 596 | # unnecessary on App Engine. |
| 597 | controller = control_client.Loaders.DEFAULT.load(service_name) |
| 598 | |
| 599 | # Start the GAE background thread that powers the control client's cache. |
| 600 | control_client.use_gae_thread() |
| 601 | controller.start() |
| 602 | |
| 603 | return control_wsgi.add_all( |
| 604 | dispatcher, |
| 605 | app_identity.get_application_id(), |
| 606 | controller) |