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 | """Library for generating an API configuration document for a ProtoRPC backend. |
| 16 | |
| 17 | The protorpc.remote.Service is inspected and a JSON document describing |
| 18 | the API is returned. |
| 19 | |
| 20 | class MyResponse(messages.Message): |
| 21 | bool_value = messages.BooleanField(1) |
| 22 | int32_value = messages.IntegerField(2) |
| 23 | |
| 24 | class MyService(remote.Service): |
| 25 | |
| 26 | @remote.method(message_types.VoidMessage, MyResponse) |
| 27 | def entries_get(self, request): |
| 28 | pass |
| 29 | |
| 30 | api = ApiConfigGenerator().pretty_print_config_to_json(MyService) |
| 31 | """ |
| 32 | |
| 33 | # pylint: disable=g-bad-name |
| 34 | |
| 35 | # pylint: disable=g-statement-before-imports,g-import-not-at-top |
| 36 | from __future__ import absolute_import |
| 37 | |
| 38 | import json |
| 39 | import logging |
| 40 | import re |
| 41 | import six |
| 42 | |
| 43 | from google.appengine.api import app_identity |
| 44 | |
| 45 | import attr |
| 46 | from protorpc import util |
| 47 | |
| 48 | from . import api_exceptions |
| 49 | from . import constants |
| 50 | from . import message_parser |
| 51 | from . import message_types |
| 52 | from . import messages |
| 53 | from . import remote |
| 54 | from . import resource_container |
| 55 | from . import types as endpoints_types |
| 56 | # originally in this module |
| 57 | from .types import Issuer, LimitDefinition, Namespace |
| 58 | from . import users_id_token |
| 59 | from . import util as endpoints_util |
| 60 | |
| 61 | _logger = logging.getLogger(__name__) |
| 62 | package = 'google.appengine.endpoints' |
| 63 | |
| 64 | |
| 65 | __all__ = [ |
| 66 | 'ApiAuth', |
| 67 | 'ApiConfigGenerator', |
| 68 | 'ApiFrontEndLimitRule', |
| 69 | 'ApiFrontEndLimits', |
| 70 | 'EMAIL_SCOPE', |
| 71 | 'Issuer', |
| 72 | 'LimitDefinition', |
| 73 | 'Namespace', |
| 74 | 'api', |
| 75 | 'method', |
| 76 | 'AUTH_LEVEL', |
| 77 | 'package', |
| 78 | ] |
| 79 | |
| 80 | |
| 81 | EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email' |
| 82 | _EMAIL_SCOPE_DESCRIPTION = 'View your email address' |
| 83 | _EMAIL_SCOPE_OBJ = endpoints_types.OAuth2Scope( |
| 84 | scope=EMAIL_SCOPE, description=_EMAIL_SCOPE_DESCRIPTION) |
| 85 | _PATH_VARIABLE_PATTERN = r'{([a-zA-Z_][a-zA-Z_.\d]*)}' |
| 86 | |
| 87 | _MULTICLASS_MISMATCH_ERROR_TEMPLATE = ( |
| 88 | 'Attempting to implement service %s, version %s, with multiple ' |
| 89 | 'classes that aren\'t compatible. See docstring for api() for ' |
| 90 | 'examples how to implement a multi-class API.') |
| 91 | |
| 92 | _INVALID_NAMESPACE_ERROR_TEMPLATE = ( |
| 93 | 'Invalid namespace configuration. If a namespace is set, make sure to set ' |
| 94 | '%s. package_path is optional.') |
| 95 | |
| 96 | |
| 97 | _VALID_PART_RE = re.compile('^{[^{}]+}$') |
| 98 | _VALID_LAST_PART_RE = re.compile('^{[^{}]+}(:)?(?(1)[^{}]+)$') |
| 99 | |
| 100 | |
| 101 | |
| 102 | def _Enum(docstring, *names): |
| 103 | """Utility to generate enum classes used by annotations. |
| 104 | |
| 105 | Args: |
| 106 | docstring: Docstring for the generated enum class. |
| 107 | *names: Enum names. |
| 108 | |
| 109 | Returns: |
| 110 | A class that contains enum names as attributes. |
| 111 | """ |
| 112 | enums = dict(zip(names, range(len(names)))) |
| 113 | reverse = dict((value, key) for key, value in enums.items()) |
| 114 | enums['reverse_mapping'] = reverse |
| 115 | enums['__doc__'] = docstring |
| 116 | return type('Enum', (object,), enums) |
| 117 | |
| 118 | _AUTH_LEVEL_DOCSTRING = """ |
| 119 | Define the enums used by the auth_level annotation to specify frontend |
| 120 | authentication requirement. |
| 121 | |
| 122 | Frontend authentication is handled by a Google API server prior to the |
| 123 | request reaching backends. An early return before hitting the backend can |
| 124 | happen if the request does not fulfil the requirement specified by the |
| 125 | auth_level. |
| 126 | |
| 127 | Valid values of auth_level and their meanings are: |
| 128 | |
| 129 | AUTH_LEVEL.REQUIRED: Valid authentication credentials are required. Backend |
| 130 | will be called only if authentication credentials are present and valid. |
| 131 | |
| 132 | AUTH_LEVEL.OPTIONAL: Authentication is optional. If authentication credentials |
| 133 | are supplied they must be valid. Backend will be called if the request |
| 134 | contains valid authentication credentials or no authentication credentials. |
| 135 | |
| 136 | AUTH_LEVEL.OPTIONAL_CONTINUE: Authentication is optional and will be attempted |
| 137 | if authentication credentials are supplied. Invalid authentication |
| 138 | credentials will be removed but the request can always reach backend. |
| 139 | |
| 140 | AUTH_LEVEL.NONE: Frontend authentication will be skipped. If authentication is |
| 141 | desired, it will need to be performed by the backend. |
| 142 | """ |
| 143 | |
| 144 | AUTH_LEVEL = _Enum(_AUTH_LEVEL_DOCSTRING, 'REQUIRED', 'OPTIONAL', |
| 145 | 'OPTIONAL_CONTINUE', 'NONE') |
| 146 | _AUTH_LEVEL_WARNING = ("Due to a design error, auth_level has never actually been functional. " |
| 147 | "It will likely be removed and replaced by a functioning alternative " |
| 148 | "in a future version of the framework. Please stop using auth_level now.") |
| 149 | |
| 150 | |
| 151 | def _GetFieldAttributes(field): |
| 152 | """Decomposes field into the needed arguments to pass to the constructor. |
| 153 | |
| 154 | This can be used to create copies of the field or to compare if two fields |
| 155 | are "equal" (since __eq__ is not implemented on messages.Field). |
| 156 | |
| 157 | Args: |
| 158 | field: A ProtoRPC message field (potentially to be copied). |
| 159 | |
| 160 | Raises: |
| 161 | TypeError: If the field is not an instance of messages.Field. |
| 162 | |
| 163 | Returns: |
| 164 | A pair of relevant arguments to be passed to the constructor for the field |
| 165 | type. The first element is a list of positional arguments for the |
| 166 | constructor and the second is a dictionary of keyword arguments. |
| 167 | """ |
| 168 | if not isinstance(field, messages.Field): |
| 169 | raise TypeError('Field %r to be copied not a ProtoRPC field.' % (field,)) |
| 170 | |
| 171 | positional_args = [] |
| 172 | kwargs = { |
| 173 | 'required': field.required, |
| 174 | 'repeated': field.repeated, |
| 175 | 'variant': field.variant, |
| 176 | 'default': field._Field__default, # pylint: disable=protected-access |
| 177 | } |
| 178 | |
| 179 | if isinstance(field, messages.MessageField): |
| 180 | # Message fields can't have a default |
| 181 | kwargs.pop('default') |
| 182 | if not isinstance(field, message_types.DateTimeField): |
| 183 | positional_args.insert(0, field.message_type) |
| 184 | elif isinstance(field, messages.EnumField): |
| 185 | positional_args.insert(0, field.type) |
| 186 | |
| 187 | return positional_args, kwargs |
| 188 | |
| 189 | |
| 190 | def _CheckType(value, check_type, name, allow_none=True): |
| 191 | """Check that the type of an object is acceptable. |
| 192 | |
| 193 | Args: |
| 194 | value: The object whose type is to be checked. |
| 195 | check_type: The type that the object must be an instance of. |
| 196 | name: Name of the object, to be placed in any error messages. |
| 197 | allow_none: True if value can be None, false if not. |
| 198 | |
| 199 | Raises: |
| 200 | TypeError: If value is not an acceptable type. |
| 201 | """ |
| 202 | if value is None and allow_none: |
| 203 | return |
| 204 | if not isinstance(value, check_type): |
| 205 | raise TypeError('%s type doesn\'t match %s.' % (name, check_type)) |
| 206 | |
| 207 | |
| 208 | def _CheckEnum(value, check_type, name): |
| 209 | if value is None: |
| 210 | return |
| 211 | if value not in check_type.reverse_mapping: |
| 212 | raise TypeError('%s is not a valid value for %s' % (value, name)) |
| 213 | |
| 214 | |
| 215 | def _CheckNamespace(namespace): |
| 216 | _CheckType(namespace, Namespace, 'namespace') |
| 217 | if namespace: |
| 218 | if not namespace.owner_domain: |
| 219 | raise api_exceptions.InvalidNamespaceException( |
| 220 | _INVALID_NAMESPACE_ERROR_TEMPLATE % 'owner_domain') |
| 221 | if not namespace.owner_name: |
| 222 | raise api_exceptions.InvalidNamespaceException( |
| 223 | _INVALID_NAMESPACE_ERROR_TEMPLATE % 'owner_name') |
| 224 | |
| 225 | _CheckType(namespace.owner_domain, six.string_types, 'namespace.owner_domain') |
| 226 | _CheckType(namespace.owner_name, six.string_types, 'namespace.owner_name') |
| 227 | _CheckType(namespace.package_path, six.string_types, 'namespace.package_path') |
| 228 | |
| 229 | |
| 230 | def _CheckAudiences(audiences): |
| 231 | # Audiences can either be a list of audiences using the google_id_token |
| 232 | # or a dict mapping auth issuer name to the list of audiences. |
| 233 | if audiences is None or isinstance(audiences, dict): |
| 234 | return |
| 235 | else: |
| 236 | endpoints_util.check_list_type(audiences, six.string_types, 'audiences') |
| 237 | |
| 238 | |
| 239 | def _CheckLimitDefinitions(limit_definitions): |
| 240 | _CheckType(limit_definitions, list, 'limit_definitions') |
| 241 | if limit_definitions: |
| 242 | for ld in limit_definitions: |
| 243 | if not ld.metric_name: |
| 244 | raise api_exceptions.InvalidLimitDefinitionException( |
| 245 | "Metric name must be set in all limit definitions.") |
| 246 | if not ld.display_name: |
| 247 | raise api_exceptions.InvalidLimitDefinitionException( |
| 248 | "Display name must be set in all limit definitions.") |
| 249 | |
| 250 | _CheckType(ld.metric_name, six.string_types, 'limit_definition.metric_name') |
| 251 | _CheckType(ld.display_name, six.string_types, 'limit_definition.display_name') |
| 252 | _CheckType(ld.default_limit, int, 'limit_definition.default_limit') |
| 253 | |
| 254 | |
| 255 | # pylint: disable=g-bad-name |
| 256 | class _ApiInfo(object): |
| 257 | """Configurable attributes of an API. |
| 258 | |
| 259 | A structured data object used to store API information associated with each |
| 260 | remote.Service-derived class that implements an API. This stores properties |
| 261 | that could be different for each class (such as the path or |
| 262 | collection/resource name), as well as properties common to all classes in |
| 263 | the API (such as API name and version). |
| 264 | """ |
| 265 | |
| 266 | @util.positional(2) |
| 267 | def __init__(self, common_info, resource_name=None, path=None, audiences=None, |
| 268 | scopes=None, allowed_client_ids=None, auth_level=None, |
| 269 | api_key_required=None): |
| 270 | """Constructor for _ApiInfo. |
| 271 | |
| 272 | Args: |
| 273 | common_info: _ApiDecorator.__ApiCommonInfo, Information that's common for |
| 274 | all classes that implement an API. |
| 275 | resource_name: string, The collection that the annotated class will |
| 276 | implement in the API. (Default: None) |
| 277 | path: string, Base request path for all methods in this API. |
| 278 | (Default: None) |
| 279 | audiences: list of strings, Acceptable audiences for authentication. |
| 280 | (Default: None) |
| 281 | scopes: list of strings, Acceptable scopes for authentication. |
| 282 | (Default: None) |
| 283 | allowed_client_ids: list of strings, Acceptable client IDs for auth. |
| 284 | (Default: None) |
| 285 | auth_level: enum from AUTH_LEVEL, Frontend authentication level. |
| 286 | (Default: None) |
| 287 | api_key_required: bool, whether a key is required to call this API. |
| 288 | """ |
| 289 | _CheckType(resource_name, six.string_types, 'resource_name') |
| 290 | _CheckType(path, six.string_types, 'path') |
| 291 | endpoints_util.check_list_type(audiences, six.string_types, 'audiences') |
| 292 | endpoints_util.check_list_type(scopes, six.string_types, 'scopes') |
| 293 | endpoints_util.check_list_type(allowed_client_ids, six.string_types, |
| 294 | 'allowed_client_ids') |
| 295 | _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level') |
| 296 | _CheckType(api_key_required, bool, 'api_key_required') |
| 297 | |
| 298 | self.__common_info = common_info |
| 299 | self.__resource_name = resource_name |
| 300 | self.__path = path |
| 301 | self.__audiences = audiences |
| 302 | self.__scopes = endpoints_types.OAuth2Scope.convert_list(scopes) |
| 303 | self.__allowed_client_ids = allowed_client_ids |
| 304 | self.__auth_level = auth_level |
| 305 | self.__api_key_required = api_key_required |
| 306 | |
| 307 | def is_same_api(self, other): |
| 308 | """Check if this implements the same API as another _ApiInfo instance.""" |
| 309 | if not isinstance(other, _ApiInfo): |
| 310 | return False |
| 311 | # pylint: disable=protected-access |
| 312 | return self.__common_info is other.__common_info |
| 313 | |
| 314 | @property |
| 315 | def name(self): |
| 316 | """Name of the API.""" |
| 317 | return self.__common_info.name |
| 318 | |
| 319 | @property |
| 320 | def api_version(self): |
| 321 | """Version of the API.""" |
| 322 | return self.__common_info.api_version |
| 323 | |
| 324 | @property |
| 325 | def path_version(self): |
| 326 | """Version of the API for putting in the path.""" |
| 327 | return self.__common_info.path_version |
| 328 | |
| 329 | @property |
| 330 | def description(self): |
| 331 | """Description of the API.""" |
| 332 | return self.__common_info.description |
| 333 | |
| 334 | @property |
| 335 | def hostname(self): |
| 336 | """Hostname for the API.""" |
| 337 | return self.__common_info.hostname |
| 338 | |
| 339 | @property |
| 340 | def audiences(self): |
| 341 | """List of audiences accepted for the API, overriding the defaults.""" |
| 342 | if self.__audiences is not None: |
| 343 | return self.__audiences |
| 344 | return self.__common_info.audiences |
| 345 | |
| 346 | @property |
| 347 | def scope_objs(self): |
| 348 | """List of scopes (as OAuth2Scopes) accepted for the API, overriding the defaults.""" |
| 349 | if self.__scopes is not None: |
| 350 | return self.__scopes |
| 351 | return self.__common_info.scope_objs |
| 352 | |
| 353 | @property |
| 354 | def scopes(self): |
| 355 | """List of scopes (as strings) accepted for the API, overriding the defaults.""" |
| 356 | if self.scope_objs is not None: |
| 357 | return [_s.scope for _s in self.scope_objs] |
| 358 | |
| 359 | @property |
| 360 | def allowed_client_ids(self): |
| 361 | """List of client IDs accepted for the API, overriding the defaults.""" |
| 362 | if self.__allowed_client_ids is not None: |
| 363 | return self.__allowed_client_ids |
| 364 | return self.__common_info.allowed_client_ids |
| 365 | |
| 366 | @property |
| 367 | def issuers(self): |
| 368 | """Dict mapping auth issuer names to auth issuers for the API.""" |
| 369 | return self.__common_info.issuers |
| 370 | |
| 371 | @property |
| 372 | def namespace(self): |
| 373 | """Namespace for the API.""" |
| 374 | return self.__common_info.namespace |
| 375 | |
| 376 | @property |
| 377 | def auth_level(self): |
| 378 | """Enum from AUTH_LEVEL specifying the frontend authentication level.""" |
| 379 | if self.__auth_level is not None: |
| 380 | return self.__auth_level |
| 381 | return self.__common_info.auth_level |
| 382 | |
| 383 | @property |
| 384 | def api_key_required(self): |
| 385 | """bool specifying whether a key is required to call into this API.""" |
| 386 | if self.__api_key_required is not None: |
| 387 | return self.__api_key_required |
| 388 | return self.__common_info.api_key_required |
| 389 | |
| 390 | @property |
| 391 | def canonical_name(self): |
| 392 | """Canonical name for the API.""" |
| 393 | return self.__common_info.canonical_name |
| 394 | |
| 395 | @property |
| 396 | def auth(self): |
| 397 | """Authentication configuration information for this API.""" |
| 398 | return self.__common_info.auth |
| 399 | |
| 400 | @property |
| 401 | def owner_domain(self): |
| 402 | """Domain of the owner of this API.""" |
| 403 | return self.__common_info.owner_domain |
| 404 | |
| 405 | @property |
| 406 | def owner_name(self): |
| 407 | """Name of the owner of this API.""" |
| 408 | return self.__common_info.owner_name |
| 409 | |
| 410 | @property |
| 411 | def package_path(self): |
| 412 | """Package this API belongs to, '/' delimited. Used by client libs.""" |
| 413 | return self.__common_info.package_path |
| 414 | |
| 415 | @property |
| 416 | def frontend_limits(self): |
| 417 | """Optional query limits for unregistered developers.""" |
| 418 | return self.__common_info.frontend_limits |
| 419 | |
| 420 | @property |
| 421 | def title(self): |
| 422 | """Human readable name of this API.""" |
| 423 | return self.__common_info.title |
| 424 | |
| 425 | @property |
| 426 | def documentation(self): |
| 427 | """Link to the documentation for this version of the API.""" |
| 428 | return self.__common_info.documentation |
| 429 | |
| 430 | @property |
| 431 | def resource_name(self): |
| 432 | """Resource name for the class this decorates.""" |
| 433 | return self.__resource_name |
| 434 | |
| 435 | @property |
| 436 | def path(self): |
| 437 | """Base path prepended to any method paths in the class this decorates.""" |
| 438 | return self.__path |
| 439 | |
| 440 | @property |
| 441 | def base_path(self): |
| 442 | """Base path for the entire API prepended before the path property.""" |
| 443 | return self.__common_info.base_path |
| 444 | |
| 445 | @property |
| 446 | def limit_definitions(self): |
| 447 | """Rate limiting metric definitions for this API.""" |
| 448 | return self.__common_info.limit_definitions |
| 449 | |
| 450 | @property |
| 451 | def use_request_uri(self): |
| 452 | """Match request paths based on the REQUEST_URI instead of PATH_INFO.""" |
| 453 | return self.__common_info.use_request_uri |
| 454 | |
| 455 | |
| 456 | class _ApiDecorator(object): |
| 457 | """Decorator for single- or multi-class APIs. |
| 458 | |
| 459 | An instance of this class can be used directly as a decorator for a |
| 460 | single-class API. Or call the api_class() method to decorate a multi-class |
| 461 | API. |
| 462 | """ |
| 463 | |
| 464 | @util.positional(3) |
| 465 | def __init__(self, name, version, description=None, hostname=None, |
| 466 | audiences=None, scopes=None, allowed_client_ids=None, |
| 467 | canonical_name=None, auth=None, owner_domain=None, |
| 468 | owner_name=None, package_path=None, frontend_limits=None, |
| 469 | title=None, documentation=None, auth_level=None, issuers=None, |
| 470 | namespace=None, api_key_required=None, base_path=None, |
| 471 | limit_definitions=None, use_request_uri=None): |
| 472 | """Constructor for _ApiDecorator. |
| 473 | |
| 474 | Args: |
| 475 | name: string, Name of the API. |
| 476 | version: string, Version of the API. |
| 477 | description: string, Short description of the API (Default: None) |
| 478 | hostname: string, Hostname of the API (Default: app engine default host) |
| 479 | audiences: list of strings, Acceptable audiences for authentication. |
| 480 | scopes: list of strings, Acceptable scopes for authentication. |
| 481 | allowed_client_ids: list of strings, Acceptable client IDs for auth. |
| 482 | canonical_name: string, the canonical name for the API, a more human |
| 483 | readable version of the name. |
| 484 | auth: ApiAuth instance, the authentication configuration information |
| 485 | for this API. |
| 486 | owner_domain: string, the domain of the person or company that owns |
| 487 | this API. Along with owner_name, this provides hints to properly |
| 488 | name client libraries for this API. |
| 489 | owner_name: string, the name of the owner of this API. Along with |
| 490 | owner_domain, this provides hints to properly name client libraries |
| 491 | for this API. |
| 492 | package_path: string, the "package" this API belongs to. This '/' |
| 493 | delimited value specifies logical groupings of APIs. This is used by |
| 494 | client libraries of this API. |
| 495 | frontend_limits: ApiFrontEndLimits, optional query limits for unregistered |
| 496 | developers. |
| 497 | title: string, the human readable title of your API. It is exposed in the |
| 498 | discovery service. |
| 499 | documentation: string, a URL where users can find documentation about this |
| 500 | version of the API. This will be surfaced in the API Explorer and GPE |
| 501 | plugin to allow users to learn about your service. |
| 502 | auth_level: enum from AUTH_LEVEL, Frontend authentication level. |
| 503 | issuers: dict, mapping auth issuer names to endpoints.Issuer objects. |
| 504 | namespace: endpoints.Namespace, the namespace for the API. |
| 505 | api_key_required: bool, whether a key is required to call this API. |
| 506 | base_path: string, the base path for all endpoints in this API. |
| 507 | limit_definitions: list of LimitDefinition tuples used in this API. |
| 508 | use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO |
| 509 | """ |
| 510 | self.__common_info = self.__ApiCommonInfo( |
| 511 | name, version, description=description, hostname=hostname, |
| 512 | audiences=audiences, scopes=scopes, |
| 513 | allowed_client_ids=allowed_client_ids, |
| 514 | canonical_name=canonical_name, auth=auth, owner_domain=owner_domain, |
| 515 | owner_name=owner_name, package_path=package_path, |
| 516 | frontend_limits=frontend_limits, title=title, |
| 517 | documentation=documentation, auth_level=auth_level, issuers=issuers, |
| 518 | namespace=namespace, api_key_required=api_key_required, |
| 519 | base_path=base_path, limit_definitions=limit_definitions, |
| 520 | use_request_uri=use_request_uri) |
| 521 | self.__classes = [] |
| 522 | |
| 523 | class __ApiCommonInfo(object): |
| 524 | """API information that's common among all classes that implement an API. |
| 525 | |
| 526 | When a remote.Service-derived class implements part of an API, there is |
| 527 | some common information that remains constant across all such classes |
| 528 | that implement the same API. This includes things like name, version, |
| 529 | hostname, and so on. __ApiComminInfo stores that common information, and |
| 530 | a single __ApiCommonInfo instance is shared among all classes that |
| 531 | implement the same API, guaranteeing that they share the same common |
| 532 | information. |
| 533 | |
| 534 | Some of these values can be overridden (such as audiences and scopes), |
| 535 | while some can't and remain the same for all classes that implement |
| 536 | the API (such as name and version). |
| 537 | """ |
| 538 | |
| 539 | @util.positional(3) |
| 540 | def __init__(self, name, version, description=None, hostname=None, |
| 541 | audiences=None, scopes=None, allowed_client_ids=None, |
| 542 | canonical_name=None, auth=None, owner_domain=None, |
| 543 | owner_name=None, package_path=None, frontend_limits=None, |
| 544 | title=None, documentation=None, auth_level=None, issuers=None, |
| 545 | namespace=None, api_key_required=None, base_path=None, |
| 546 | limit_definitions=None, use_request_uri=None): |
| 547 | """Constructor for _ApiCommonInfo. |
| 548 | |
| 549 | Args: |
| 550 | name: string, Name of the API. |
| 551 | version: string, Version of the API. |
| 552 | description: string, Short description of the API (Default: None) |
| 553 | hostname: string, Hostname of the API (Default: app engine default host) |
| 554 | audiences: list of strings, Acceptable audiences for authentication. |
| 555 | scopes: list of strings, Acceptable scopes for authentication. |
| 556 | allowed_client_ids: list of strings, Acceptable client IDs for auth. |
| 557 | canonical_name: string, the canonical name for the API, a more human |
| 558 | readable version of the name. |
| 559 | auth: ApiAuth instance, the authentication configuration information |
| 560 | for this API. |
| 561 | owner_domain: string, the domain of the person or company that owns |
| 562 | this API. Along with owner_name, this provides hints to properly |
| 563 | name client libraries for this API. |
| 564 | owner_name: string, the name of the owner of this API. Along with |
| 565 | owner_domain, this provides hints to properly name client libraries |
| 566 | for this API. |
| 567 | package_path: string, the "package" this API belongs to. This '/' |
| 568 | delimited value specifies logical groupings of APIs. This is used by |
| 569 | client libraries of this API. |
| 570 | frontend_limits: ApiFrontEndLimits, optional query limits for |
| 571 | unregistered developers. |
| 572 | title: string, the human readable title of your API. It is exposed in |
| 573 | the discovery service. |
| 574 | documentation: string, a URL where users can find documentation about |
| 575 | this version of the API. This will be surfaced in the API Explorer and |
| 576 | GPE plugin to allow users to learn about your service. |
| 577 | auth_level: enum from AUTH_LEVEL, Frontend authentication level. |
| 578 | issuers: dict, mapping auth issuer names to endpoints.Issuer objects. |
| 579 | namespace: endpoints.Namespace, the namespace for the API. |
| 580 | api_key_required: bool, whether a key is required to call into this API. |
| 581 | base_path: string, the base path for all endpoints in this API. |
| 582 | limit_definitions: list of LimitDefinition tuples used in this API. |
| 583 | use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO |
| 584 | """ |
| 585 | _CheckType(name, six.string_types, 'name', allow_none=False) |
| 586 | _CheckType(version, six.string_types, 'version', allow_none=False) |
| 587 | _CheckType(description, six.string_types, 'description') |
| 588 | _CheckType(hostname, six.string_types, 'hostname') |
| 589 | endpoints_util.check_list_type(scopes, (six.string_types, endpoints_types.OAuth2Scope), 'scopes') |
| 590 | endpoints_util.check_list_type(allowed_client_ids, six.string_types, |
| 591 | 'allowed_client_ids') |
| 592 | _CheckType(canonical_name, six.string_types, 'canonical_name') |
| 593 | _CheckType(auth, ApiAuth, 'auth') |
| 594 | _CheckType(owner_domain, six.string_types, 'owner_domain') |
| 595 | _CheckType(owner_name, six.string_types, 'owner_name') |
| 596 | _CheckType(package_path, six.string_types, 'package_path') |
| 597 | _CheckType(frontend_limits, ApiFrontEndLimits, 'frontend_limits') |
| 598 | _CheckType(title, six.string_types, 'title') |
| 599 | _CheckType(documentation, six.string_types, 'documentation') |
| 600 | _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level') |
| 601 | _CheckType(api_key_required, bool, 'api_key_required') |
| 602 | _CheckType(base_path, six.string_types, 'base_path') |
| 603 | |
| 604 | _CheckType(issuers, dict, 'issuers') |
| 605 | if issuers: |
| 606 | for issuer_name, issuer_value in issuers.items(): |
| 607 | _CheckType(issuer_name, six.string_types, 'issuer %s' % issuer_name) |
| 608 | _CheckType(issuer_value, Issuer, 'issuer value for %s' % issuer_name) |
| 609 | |
| 610 | _CheckNamespace(namespace) |
| 611 | |
| 612 | _CheckAudiences(audiences) |
| 613 | |
| 614 | _CheckLimitDefinitions(limit_definitions) |
| 615 | _CheckType(use_request_uri, bool, 'use_request_uri') |
| 616 | |
| 617 | if hostname is None: |
| 618 | hostname = app_identity.get_default_version_hostname() |
| 619 | if scopes is None: |
| 620 | scopes = [_EMAIL_SCOPE_OBJ] |
| 621 | else: |
| 622 | scopes = endpoints_types.OAuth2Scope.convert_list(scopes) |
| 623 | if allowed_client_ids is None: |
| 624 | allowed_client_ids = [constants.API_EXPLORER_CLIENT_ID] |
| 625 | if auth_level is None: |
| 626 | auth_level = AUTH_LEVEL.NONE |
| 627 | if api_key_required is None: |
| 628 | api_key_required = False |
| 629 | if base_path is None: |
| 630 | base_path = '/_ah/api/' |
| 631 | if use_request_uri is None: |
| 632 | use_request_uri = False |
| 633 | |
| 634 | self.__name = name |
| 635 | self.__api_version = version |
| 636 | self.__path_version = version |
| 637 | self.__description = description |
| 638 | self.__hostname = hostname |
| 639 | self.__audiences = audiences |
| 640 | self.__scopes = scopes |
| 641 | self.__allowed_client_ids = allowed_client_ids |
| 642 | self.__canonical_name = canonical_name |
| 643 | self.__auth = auth |
| 644 | self.__owner_domain = owner_domain |
| 645 | self.__owner_name = owner_name |
| 646 | self.__package_path = package_path |
| 647 | self.__frontend_limits = frontend_limits |
| 648 | self.__title = title |
| 649 | self.__documentation = documentation |
| 650 | self.__auth_level = auth_level |
| 651 | self.__issuers = issuers |
| 652 | self.__namespace = namespace |
| 653 | self.__api_key_required = api_key_required |
| 654 | self.__base_path = base_path |
| 655 | self.__limit_definitions = limit_definitions |
| 656 | self.__use_request_uri = use_request_uri |
| 657 | |
| 658 | @property |
| 659 | def name(self): |
| 660 | """Name of the API.""" |
| 661 | return self.__name |
| 662 | |
| 663 | @property |
| 664 | def api_version(self): |
| 665 | """Version of the API.""" |
| 666 | return self.__api_version |
| 667 | |
| 668 | @property |
| 669 | def path_version(self): |
| 670 | """Version of the API for putting in the path.""" |
| 671 | return self.__path_version |
| 672 | |
| 673 | @property |
| 674 | def description(self): |
| 675 | """Description of the API.""" |
| 676 | return self.__description |
| 677 | |
| 678 | @property |
| 679 | def hostname(self): |
| 680 | """Hostname for the API.""" |
| 681 | return self.__hostname |
| 682 | |
| 683 | @property |
| 684 | def audiences(self): |
| 685 | """List of audiences accepted by default for the API.""" |
| 686 | return self.__audiences |
| 687 | |
| 688 | @property |
| 689 | def scope_objs(self): |
| 690 | """List of scopes (as OAuth2Scopes) accepted by default for the API.""" |
| 691 | return self.__scopes |
| 692 | |
| 693 | @property |
| 694 | def scopes(self): |
| 695 | """List of scopes (as strings) accepted by default for the API.""" |
| 696 | if self.scope_objs is not None: |
| 697 | return [_s.scope for _s in self.scope_objs] |
| 698 | |
| 699 | @property |
| 700 | def allowed_client_ids(self): |
| 701 | """List of client IDs accepted by default for the API.""" |
| 702 | return self.__allowed_client_ids |
| 703 | |
| 704 | @property |
| 705 | def issuers(self): |
| 706 | """List of auth issuers for the API.""" |
| 707 | return self.__issuers |
| 708 | |
| 709 | @property |
| 710 | def namespace(self): |
| 711 | """Namespace of the API.""" |
| 712 | return self.__namespace |
| 713 | |
| 714 | @property |
| 715 | def auth_level(self): |
| 716 | """Enum from AUTH_LEVEL specifying default frontend auth level.""" |
| 717 | return self.__auth_level |
| 718 | |
| 719 | @property |
| 720 | def canonical_name(self): |
| 721 | """Canonical name for the API.""" |
| 722 | return self.__canonical_name |
| 723 | |
| 724 | @property |
| 725 | def auth(self): |
| 726 | """Authentication configuration for this API.""" |
| 727 | return self.__auth |
| 728 | |
| 729 | @property |
| 730 | def api_key_required(self): |
| 731 | """Whether a key is required to call into this API.""" |
| 732 | return self.__api_key_required |
| 733 | |
| 734 | @property |
| 735 | def owner_domain(self): |
| 736 | """Domain of the owner of this API.""" |
| 737 | return self.__owner_domain |
| 738 | |
| 739 | @property |
| 740 | def owner_name(self): |
| 741 | """Name of the owner of this API.""" |
| 742 | return self.__owner_name |
| 743 | |
| 744 | @property |
| 745 | def package_path(self): |
| 746 | """Package this API belongs to, '/' delimited. Used by client libs.""" |
| 747 | return self.__package_path |
| 748 | |
| 749 | @property |
| 750 | def frontend_limits(self): |
| 751 | """Optional query limits for unregistered developers.""" |
| 752 | return self.__frontend_limits |
| 753 | |
| 754 | @property |
| 755 | def title(self): |
| 756 | """Human readable name of this API.""" |
| 757 | return self.__title |
| 758 | |
| 759 | @property |
| 760 | def documentation(self): |
| 761 | """Link to the documentation for this version of the API.""" |
| 762 | return self.__documentation |
| 763 | |
| 764 | @property |
| 765 | def base_path(self): |
| 766 | """The base path for all endpoints in this API.""" |
| 767 | return self.__base_path |
| 768 | |
| 769 | @property |
| 770 | def limit_definitions(self): |
| 771 | """Rate limiting metric definitions for this API.""" |
| 772 | return self.__limit_definitions |
| 773 | |
| 774 | @property |
| 775 | def use_request_uri(self): |
| 776 | """Match request paths based on the REQUEST_URI instead of PATH_INFO.""" |
| 777 | return self.__use_request_uri |
| 778 | |
| 779 | def __call__(self, service_class): |
| 780 | """Decorator for ProtoRPC class that configures Google's API server. |
| 781 | |
| 782 | Args: |
| 783 | service_class: remote.Service class, ProtoRPC service class being wrapped. |
| 784 | |
| 785 | Returns: |
| 786 | Same class with API attributes assigned in api_info. |
| 787 | """ |
| 788 | return self.api_class()(service_class) |
| 789 | |
| 790 | def api_class(self, resource_name=None, path=None, audiences=None, |
| 791 | scopes=None, allowed_client_ids=None, auth_level=None, |
| 792 | api_key_required=None): |
| 793 | """Get a decorator for a class that implements an API. |
| 794 | |
| 795 | This can be used for single-class or multi-class implementations. It's |
| 796 | used implicitly in simple single-class APIs that only use @api directly. |
| 797 | |
| 798 | Args: |
| 799 | resource_name: string, Resource name for the class this decorates. |
| 800 | (Default: None) |
| 801 | path: string, Base path prepended to any method paths in the class this |
| 802 | decorates. (Default: None) |
| 803 | audiences: list of strings, Acceptable audiences for authentication. |
| 804 | (Default: None) |
| 805 | scopes: list of strings, Acceptable scopes for authentication. |
| 806 | (Default: None) |
| 807 | allowed_client_ids: list of strings, Acceptable client IDs for auth. |
| 808 | (Default: None) |
| 809 | auth_level: enum from AUTH_LEVEL, Frontend authentication level. |
| 810 | (Default: None) |
| 811 | api_key_required: bool, Whether a key is required to call into this API. |
| 812 | (Default: None) |
| 813 | |
| 814 | Returns: |
| 815 | A decorator function to decorate a class that implements an API. |
| 816 | """ |
| 817 | if auth_level is not None: |
| 818 | _logger.warn(_AUTH_LEVEL_WARNING) |
| 819 | |
| 820 | def apiserving_api_decorator(api_class): |
| 821 | """Decorator for ProtoRPC class that configures Google's API server. |
| 822 | |
| 823 | Args: |
| 824 | api_class: remote.Service class, ProtoRPC service class being wrapped. |
| 825 | |
| 826 | Returns: |
| 827 | Same class with API attributes assigned in api_info. |
| 828 | """ |
| 829 | self.__classes.append(api_class) |
| 830 | api_class.api_info = _ApiInfo( |
| 831 | self.__common_info, resource_name=resource_name, |
| 832 | path=path, audiences=audiences, scopes=scopes, |
| 833 | allowed_client_ids=allowed_client_ids, auth_level=auth_level, |
| 834 | api_key_required=api_key_required) |
| 835 | return api_class |
| 836 | |
| 837 | return apiserving_api_decorator |
| 838 | |
| 839 | def get_api_classes(self): |
| 840 | """Get the list of remote.Service classes that implement this API.""" |
| 841 | return self.__classes |
| 842 | |
| 843 | |
| 844 | class ApiAuth(object): |
| 845 | """Optional authorization configuration information for an API.""" |
| 846 | |
| 847 | def __init__(self, allow_cookie_auth=None, blocked_regions=None): |
| 848 | """Constructor for ApiAuth, authentication information for an API. |
| 849 | |
| 850 | Args: |
| 851 | allow_cookie_auth: boolean, whether cooking auth is allowed. By |
| 852 | default, API methods do not allow cookie authentication, and |
| 853 | require the use of OAuth2 or ID tokens. Setting this field to |
| 854 | True will allow cookies to be used to access the API, with |
| 855 | potentially dangerous results. Please be very cautious in enabling |
| 856 | this setting, and make sure to require appropriate XSRF tokens to |
| 857 | protect your API. |
| 858 | blocked_regions: list of Strings, a list of 2-letter ISO region codes |
| 859 | to block. |
| 860 | """ |
| 861 | _CheckType(allow_cookie_auth, bool, 'allow_cookie_auth') |
| 862 | endpoints_util.check_list_type(blocked_regions, six.string_types, |
| 863 | 'blocked_regions') |
| 864 | |
| 865 | self.__allow_cookie_auth = allow_cookie_auth |
| 866 | self.__blocked_regions = blocked_regions |
| 867 | |
| 868 | @property |
| 869 | def allow_cookie_auth(self): |
| 870 | """Whether cookie authentication is allowed for this API.""" |
| 871 | return self.__allow_cookie_auth |
| 872 | |
| 873 | @property |
| 874 | def blocked_regions(self): |
| 875 | """List of 2-letter ISO region codes to block.""" |
| 876 | return self.__blocked_regions |
| 877 | |
| 878 | |
| 879 | class ApiFrontEndLimitRule(object): |
| 880 | """Custom rule to limit unregistered traffic.""" |
| 881 | |
| 882 | def __init__(self, match=None, qps=None, user_qps=None, daily=None, |
| 883 | analytics_id=None): |
| 884 | """Constructor for ApiFrontEndLimitRule. |
| 885 | |
| 886 | Args: |
| 887 | match: string, the matching rule that defines this traffic segment. |
| 888 | qps: int, the aggregate QPS for this segment. |
| 889 | user_qps: int, the per-end-user QPS for this segment. |
| 890 | daily: int, the aggregate daily maximum for this segment. |
| 891 | analytics_id: string, the project ID under which traffic for this segment |
| 892 | will be logged. |
| 893 | """ |
| 894 | _CheckType(match, six.string_types, 'match') |
| 895 | _CheckType(qps, int, 'qps') |
| 896 | _CheckType(user_qps, int, 'user_qps') |
| 897 | _CheckType(daily, int, 'daily') |
| 898 | _CheckType(analytics_id, six.string_types, 'analytics_id') |
| 899 | |
| 900 | self.__match = match |
| 901 | self.__qps = qps |
| 902 | self.__user_qps = user_qps |
| 903 | self.__daily = daily |
| 904 | self.__analytics_id = analytics_id |
| 905 | |
| 906 | @property |
| 907 | def match(self): |
| 908 | """The matching rule that defines this traffic segment.""" |
| 909 | return self.__match |
| 910 | |
| 911 | @property |
| 912 | def qps(self): |
| 913 | """The aggregate QPS for this segment.""" |
| 914 | return self.__qps |
| 915 | |
| 916 | @property |
| 917 | def user_qps(self): |
| 918 | """The per-end-user QPS for this segment.""" |
| 919 | return self.__user_qps |
| 920 | |
| 921 | @property |
| 922 | def daily(self): |
| 923 | """The aggregate daily maximum for this segment.""" |
| 924 | return self.__daily |
| 925 | |
| 926 | @property |
| 927 | def analytics_id(self): |
| 928 | """Project ID under which traffic for this segment will be logged.""" |
| 929 | return self.__analytics_id |
| 930 | |
| 931 | |
| 932 | class ApiFrontEndLimits(object): |
| 933 | """Optional front end limit information for an API.""" |
| 934 | |
| 935 | def __init__(self, unregistered_user_qps=None, unregistered_qps=None, |
| 936 | unregistered_daily=None, rules=None): |
| 937 | """Constructor for ApiFrontEndLimits, front end limit info for an API. |
| 938 | |
| 939 | Args: |
| 940 | unregistered_user_qps: int, the per-end-user QPS. Users are identified |
| 941 | by their IP address. A value of 0 will block unregistered requests. |
| 942 | unregistered_qps: int, an aggregate QPS upper-bound for all unregistered |
| 943 | traffic. A value of 0 currently means unlimited, though it might change |
| 944 | in the future. To block unregistered requests, use unregistered_user_qps |
| 945 | or unregistered_daily instead. |
| 946 | unregistered_daily: int, an aggregate daily upper-bound for all |
| 947 | unregistered traffic. A value of 0 will block unregistered requests. |
| 948 | rules: A list or tuple of ApiFrontEndLimitRule instances: custom rules |
| 949 | used to apply limits to unregistered traffic. |
| 950 | """ |
| 951 | _CheckType(unregistered_user_qps, int, 'unregistered_user_qps') |
| 952 | _CheckType(unregistered_qps, int, 'unregistered_qps') |
| 953 | _CheckType(unregistered_daily, int, 'unregistered_daily') |
| 954 | endpoints_util.check_list_type(rules, ApiFrontEndLimitRule, 'rules') |
| 955 | |
| 956 | self.__unregistered_user_qps = unregistered_user_qps |
| 957 | self.__unregistered_qps = unregistered_qps |
| 958 | self.__unregistered_daily = unregistered_daily |
| 959 | self.__rules = rules |
| 960 | |
| 961 | @property |
| 962 | def unregistered_user_qps(self): |
| 963 | """Per-end-user QPS limit.""" |
| 964 | return self.__unregistered_user_qps |
| 965 | |
| 966 | @property |
| 967 | def unregistered_qps(self): |
| 968 | """Aggregate QPS upper-bound for all unregistered traffic.""" |
| 969 | return self.__unregistered_qps |
| 970 | |
| 971 | @property |
| 972 | def unregistered_daily(self): |
| 973 | """Aggregate daily upper-bound for all unregistered traffic.""" |
| 974 | return self.__unregistered_daily |
| 975 | |
| 976 | @property |
| 977 | def rules(self): |
| 978 | """Custom rules used to apply limits to unregistered traffic.""" |
| 979 | return self.__rules |
| 980 | |
| 981 | |
| 982 | @util.positional(2) |
| 983 | def api(name, version, description=None, hostname=None, audiences=None, |
| 984 | scopes=None, allowed_client_ids=None, canonical_name=None, |
| 985 | auth=None, owner_domain=None, owner_name=None, package_path=None, |
| 986 | frontend_limits=None, title=None, documentation=None, auth_level=None, |
| 987 | issuers=None, namespace=None, api_key_required=None, base_path=None, |
| 988 | limit_definitions=None, use_request_uri=None): |
| 989 | """Decorate a ProtoRPC Service class for use by the framework above. |
| 990 | |
| 991 | This decorator can be used to specify an API name, version, description, and |
| 992 | hostname for your API. |
| 993 | |
| 994 | Sample usage (python 2.7): |
| 995 | @endpoints.api(name='guestbook', version='v0.2', |
| 996 | description='Guestbook API') |
| 997 | class PostService(remote.Service): |
| 998 | ... |
| 999 | |
| 1000 | Sample usage (python 2.5): |
| 1001 | class PostService(remote.Service): |
| 1002 | ... |
| 1003 | endpoints.api(name='guestbook', version='v0.2', |
| 1004 | description='Guestbook API')(PostService) |
| 1005 | |
| 1006 | Sample usage if multiple classes implement one API: |
| 1007 | api_root = endpoints.api(name='library', version='v1.0') |
| 1008 | |
| 1009 | @api_root.api_class(resource_name='shelves') |
| 1010 | class Shelves(remote.Service): |
| 1011 | ... |
| 1012 | |
| 1013 | @api_root.api_class(resource_name='books', path='books') |
| 1014 | class Books(remote.Service): |
| 1015 | ... |
| 1016 | |
| 1017 | Args: |
| 1018 | name: string, Name of the API. |
| 1019 | version: string, Version of the API. |
| 1020 | description: string, Short description of the API (Default: None) |
| 1021 | hostname: string, Hostname of the API (Default: app engine default host) |
| 1022 | audiences: list of strings, Acceptable audiences for authentication. |
| 1023 | scopes: list of strings, Acceptable scopes for authentication. |
| 1024 | allowed_client_ids: list of strings, Acceptable client IDs for auth. |
| 1025 | canonical_name: string, the canonical name for the API, a more human |
| 1026 | readable version of the name. |
| 1027 | auth: ApiAuth instance, the authentication configuration information |
| 1028 | for this API. |
| 1029 | owner_domain: string, the domain of the person or company that owns |
| 1030 | this API. Along with owner_name, this provides hints to properly |
| 1031 | name client libraries for this API. |
| 1032 | owner_name: string, the name of the owner of this API. Along with |
| 1033 | owner_domain, this provides hints to properly name client libraries |
| 1034 | for this API. |
| 1035 | package_path: string, the "package" this API belongs to. This '/' |
| 1036 | delimited value specifies logical groupings of APIs. This is used by |
| 1037 | client libraries of this API. |
| 1038 | frontend_limits: ApiFrontEndLimits, optional query limits for unregistered |
| 1039 | developers. |
| 1040 | title: string, the human readable title of your API. It is exposed in the |
| 1041 | discovery service. |
| 1042 | documentation: string, a URL where users can find documentation about this |
| 1043 | version of the API. This will be surfaced in the API Explorer and GPE |
| 1044 | plugin to allow users to learn about your service. |
| 1045 | auth_level: enum from AUTH_LEVEL, frontend authentication level. |
| 1046 | issuers: dict, mapping auth issuer names to endpoints.Issuer objects. |
| 1047 | namespace: endpoints.Namespace, the namespace for the API. |
| 1048 | api_key_required: bool, whether a key is required to call into this API. |
| 1049 | base_path: string, the base path for all endpoints in this API. |
| 1050 | limit_definitions: list of endpoints.LimitDefinition objects, quota metric |
| 1051 | definitions for this API. |
| 1052 | use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO |
| 1053 | |
| 1054 | |
| 1055 | Returns: |
| 1056 | Class decorated with api_info attribute, an instance of ApiInfo. |
| 1057 | """ |
| 1058 | if auth_level is not None: |
| 1059 | _logger.warn(_AUTH_LEVEL_WARNING) |
| 1060 | |
| 1061 | return _ApiDecorator(name, version, description=description, |
| 1062 | hostname=hostname, audiences=audiences, scopes=scopes, |
| 1063 | allowed_client_ids=allowed_client_ids, |
| 1064 | canonical_name=canonical_name, auth=auth, |
| 1065 | owner_domain=owner_domain, owner_name=owner_name, |
| 1066 | package_path=package_path, |
| 1067 | frontend_limits=frontend_limits, title=title, |
| 1068 | documentation=documentation, auth_level=auth_level, |
| 1069 | issuers=issuers, namespace=namespace, |
| 1070 | api_key_required=api_key_required, base_path=base_path, |
| 1071 | limit_definitions=limit_definitions, |
| 1072 | use_request_uri=use_request_uri) |
| 1073 | |
| 1074 | |
| 1075 | class _MethodInfo(object): |
| 1076 | """Configurable attributes of an API method. |
| 1077 | |
| 1078 | Consolidates settings from @method decorator and/or any settings that were |
| 1079 | calculating from the ProtoRPC method name, so they only need to be calculated |
| 1080 | once. |
| 1081 | """ |
| 1082 | |
| 1083 | @util.positional(1) |
| 1084 | def __init__(self, name=None, path=None, http_method=None, |
| 1085 | scopes=None, audiences=None, allowed_client_ids=None, |
| 1086 | auth_level=None, api_key_required=None, request_body_class=None, |
| 1087 | request_params_class=None, metric_costs=None, use_request_uri=None): |
| 1088 | """Constructor. |
| 1089 | |
| 1090 | Args: |
| 1091 | name: string, Name of the method, prepended with <apiname>. to make it |
| 1092 | unique. |
| 1093 | path: string, Path portion of the URL to the method, for RESTful methods. |
| 1094 | http_method: string, HTTP method supported by the method. |
| 1095 | scopes: list of string, OAuth2 token must contain one of these scopes. |
| 1096 | audiences: list of string, IdToken must contain one of these audiences. |
| 1097 | allowed_client_ids: list of string, Client IDs allowed to call the method. |
| 1098 | auth_level: enum from AUTH_LEVEL, Frontend auth level for the method. |
| 1099 | api_key_required: bool, whether a key is required to call the method. |
| 1100 | request_body_class: The type for the request body when using a |
| 1101 | ResourceContainer. Otherwise, null. |
| 1102 | request_params_class: The type for the request parameters when using a |
| 1103 | ResourceContainer. Otherwise, null. |
| 1104 | metric_costs: dict with keys matching an API limit metric and values |
| 1105 | representing the cost for each successful call against that metric. |
| 1106 | use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO |
| 1107 | """ |
| 1108 | self.__name = name |
| 1109 | self.__path = path |
| 1110 | self.__http_method = http_method |
| 1111 | self.__scopes = endpoints_types.OAuth2Scope.convert_list(scopes) |
| 1112 | self.__audiences = audiences |
| 1113 | self.__allowed_client_ids = allowed_client_ids |
| 1114 | self.__auth_level = auth_level |
| 1115 | self.__api_key_required = api_key_required |
| 1116 | self.__request_body_class = request_body_class |
| 1117 | self.__request_params_class = request_params_class |
| 1118 | self.__metric_costs = metric_costs |
| 1119 | self.__use_request_uri = use_request_uri |
| 1120 | |
| 1121 | def __safe_name(self, method_name): |
| 1122 | """Restrict method name to a-zA-Z0-9_, first char lowercase.""" |
| 1123 | # Endpoints backend restricts what chars are allowed in a method name. |
| 1124 | safe_name = re.sub(r'[^\.a-zA-Z0-9_]', '', method_name) |
| 1125 | |
| 1126 | # Strip any number of leading underscores. |
| 1127 | safe_name = safe_name.lstrip('_') |
| 1128 | |
| 1129 | # Ensure the first character is lowercase. |
| 1130 | # Slice from 0:1 rather than indexing [0] in case safe_name is length 0. |
| 1131 | return safe_name[0:1].lower() + safe_name[1:] |
| 1132 | |
| 1133 | @property |
| 1134 | def name(self): |
| 1135 | """Method name as specified in decorator or derived.""" |
| 1136 | return self.__name |
| 1137 | |
| 1138 | def get_path(self, api_info): |
| 1139 | """Get the path portion of the URL to the method (for RESTful methods). |
| 1140 | |
| 1141 | Request path can be specified in the method, and it could have a base |
| 1142 | path prepended to it. |
| 1143 | |
| 1144 | Args: |
| 1145 | api_info: API information for this API, possibly including a base path. |
| 1146 | This is the api_info property on the class that's been annotated for |
| 1147 | this API. |
| 1148 | |
| 1149 | Returns: |
| 1150 | This method's request path (not including the http://.../{base_path} |
| 1151 | prefix). |
| 1152 | |
| 1153 | Raises: |
| 1154 | ApiConfigurationError: If the path isn't properly formatted. |
| 1155 | """ |
| 1156 | path = self.__path or '' |
| 1157 | if path and path[0] == '/': |
| 1158 | # Absolute path, ignoring any prefixes. Just strip off the leading /. |
| 1159 | path = path[1:] |
| 1160 | else: |
| 1161 | # Relative path. |
| 1162 | if api_info.path: |
| 1163 | path = '%s%s%s' % (api_info.path, '/' if path else '', path) |
| 1164 | |
| 1165 | # Verify that the path seems valid. |
| 1166 | parts = path.split('/') |
| 1167 | for n, part in enumerate(parts): |
| 1168 | r = _VALID_PART_RE if n < len(parts) - 1 else _VALID_LAST_PART_RE |
| 1169 | if part and '{' in part and '}' in part: |
| 1170 | if not r.match(part): |
| 1171 | raise api_exceptions.ApiConfigurationError( |
| 1172 | 'Invalid path segment: %s (part of %s)' % (part, path)) |
| 1173 | return path |
| 1174 | |
| 1175 | @property |
| 1176 | def http_method(self): |
| 1177 | """HTTP method supported by the method (e.g. GET, POST).""" |
| 1178 | return self.__http_method |
| 1179 | |
| 1180 | @property |
| 1181 | def scope_objs(self): |
| 1182 | """List of scopes (as OAuth2Scopes) accepted for the API method.""" |
| 1183 | return self.__scopes |
| 1184 | |
| 1185 | @property |
| 1186 | def scopes(self): |
| 1187 | """List of scopes (as strings) accepted for the API method.""" |
| 1188 | if self.scope_objs is not None: |
| 1189 | return [_s.scope for _s in self.scope_objs] |
| 1190 | |
| 1191 | @property |
| 1192 | def audiences(self): |
| 1193 | """List of audiences for the API method.""" |
| 1194 | return self.__audiences |
| 1195 | |
| 1196 | @property |
| 1197 | def allowed_client_ids(self): |
| 1198 | """List of allowed client IDs for the API method.""" |
| 1199 | return self.__allowed_client_ids |
| 1200 | |
| 1201 | @property |
| 1202 | def auth_level(self): |
| 1203 | """Enum from AUTH_LEVEL specifying default frontend auth level.""" |
| 1204 | return self.__auth_level |
| 1205 | |
| 1206 | @property |
| 1207 | def api_key_required(self): |
| 1208 | """bool whether a key is required to call the API method.""" |
| 1209 | return self.__api_key_required |
| 1210 | |
| 1211 | @property |
| 1212 | def metric_costs(self): |
| 1213 | """Dict mapping API limit metric names to costs against that metric.""" |
| 1214 | return self.__metric_costs |
| 1215 | |
| 1216 | @property |
| 1217 | def request_body_class(self): |
| 1218 | """Type of request body when using a ResourceContainer.""" |
| 1219 | return self.__request_body_class |
| 1220 | |
| 1221 | @property |
| 1222 | def request_params_class(self): |
| 1223 | """Type of request parameter message when using a ResourceContainer.""" |
| 1224 | return self.__request_params_class |
| 1225 | |
| 1226 | def is_api_key_required(self, api_info): |
| 1227 | if self.api_key_required is not None: |
| 1228 | return self.api_key_required |
| 1229 | else: |
| 1230 | return api_info.api_key_required |
| 1231 | |
| 1232 | def use_request_uri(self, api_info): |
| 1233 | if self.__use_request_uri is not None: |
| 1234 | return self.__use_request_uri |
| 1235 | else: |
| 1236 | return api_info.use_request_uri |
| 1237 | |
| 1238 | def method_id(self, api_info): |
| 1239 | """Computed method name.""" |
| 1240 | # This is done here for now because at __init__ time, the method is known |
| 1241 | # but not the api, and thus not the api name. Later, in |
| 1242 | # ApiConfigGenerator.__method_descriptor, the api name is known. |
| 1243 | if api_info.resource_name: |
| 1244 | resource_part = '.%s' % self.__safe_name(api_info.resource_name) |
| 1245 | else: |
| 1246 | resource_part = '' |
| 1247 | return '%s%s.%s' % (self.__safe_name(api_info.name), resource_part, |
| 1248 | self.__safe_name(self.name)) |
| 1249 | |
| 1250 | |
| 1251 | @util.positional(2) |
| 1252 | def method(request_message=message_types.VoidMessage, |
| 1253 | response_message=message_types.VoidMessage, |
| 1254 | name=None, |
| 1255 | path=None, |
| 1256 | http_method='POST', |
| 1257 | scopes=None, |
| 1258 | audiences=None, |
| 1259 | allowed_client_ids=None, |
| 1260 | auth_level=None, |
| 1261 | api_key_required=None, |
| 1262 | metric_costs=None, |
| 1263 | use_request_uri=None): |
| 1264 | """Decorate a ProtoRPC Method for use by the framework above. |
| 1265 | |
| 1266 | This decorator can be used to specify a method name, path, http method, |
| 1267 | scopes, audiences, client ids and auth_level. |
| 1268 | |
| 1269 | Sample usage: |
| 1270 | @api_config.method(RequestMessage, ResponseMessage, |
| 1271 | name='insert', http_method='PUT') |
| 1272 | def greeting_insert(request): |
| 1273 | ... |
| 1274 | return response |
| 1275 | |
| 1276 | Args: |
| 1277 | request_message: Message type of expected request. |
| 1278 | response_message: Message type of expected response. |
| 1279 | name: string, Name of the method, prepended with <apiname>. to make it |
| 1280 | unique. (Default: python method name) |
| 1281 | path: string, Path portion of the URL to the method, for RESTful methods. |
| 1282 | http_method: string, HTTP method supported by the method. (Default: POST) |
| 1283 | scopes: list of string, OAuth2 token must contain one of these scopes. |
| 1284 | audiences: list of string, IdToken must contain one of these audiences. |
| 1285 | allowed_client_ids: list of string, Client IDs allowed to call the method. |
| 1286 | If None and auth_level is REQUIRED, no calls will be allowed. |
| 1287 | auth_level: enum from AUTH_LEVEL, Frontend auth level for the method. |
| 1288 | api_key_required: bool, whether a key is required to call the method |
| 1289 | metric_costs: dict with keys matching an API limit metric and values |
| 1290 | representing the cost for each successful call against that metric. |
| 1291 | use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO |
| 1292 | |
| 1293 | Returns: |
| 1294 | 'apiserving_method_wrapper' function. |
| 1295 | |
| 1296 | Raises: |
| 1297 | TypeError: if the request_type or response_type parameters are not |
| 1298 | proper subclasses of messages.Message. |
| 1299 | """ |
| 1300 | if auth_level is not None: |
| 1301 | _logger.warn(_AUTH_LEVEL_WARNING) |
| 1302 | |
| 1303 | # Default HTTP method if one is not specified. |
| 1304 | DEFAULT_HTTP_METHOD = 'POST' |
| 1305 | |
| 1306 | def apiserving_method_decorator(api_method): |
| 1307 | """Decorator for ProtoRPC method that configures Google's API server. |
| 1308 | |
| 1309 | Args: |
| 1310 | api_method: Original method being wrapped. |
| 1311 | |
| 1312 | Returns: |
| 1313 | Function responsible for actual invocation. |
| 1314 | Assigns the following attributes to invocation function: |
| 1315 | remote: Instance of RemoteInfo, contains remote method information. |
| 1316 | remote.request_type: Expected request type for remote method. |
| 1317 | remote.response_type: Response type returned from remote method. |
| 1318 | method_info: Instance of _MethodInfo, api method configuration. |
| 1319 | It is also assigned attributes corresponding to the aforementioned kwargs. |
| 1320 | |
| 1321 | Raises: |
| 1322 | TypeError: if the request_type or response_type parameters are not |
| 1323 | proper subclasses of messages.Message. |
| 1324 | KeyError: if the request_message is a ResourceContainer and the newly |
| 1325 | created remote method has been reference by the container before. This |
| 1326 | should never occur because a remote method is created once. |
| 1327 | """ |
| 1328 | request_body_class = None |
| 1329 | request_params_class = None |
| 1330 | if isinstance(request_message, resource_container.ResourceContainer): |
| 1331 | remote_decorator = remote.method(request_message.combined_message_class, |
| 1332 | response_message) |
| 1333 | request_body_class = request_message.body_message_class() |
| 1334 | request_params_class = request_message.parameters_message_class() |
| 1335 | else: |
| 1336 | remote_decorator = remote.method(request_message, response_message) |
| 1337 | remote_method = remote_decorator(api_method) |
| 1338 | |
| 1339 | def invoke_remote(service_instance, request): |
| 1340 | # If the server didn't specify any auth information, build it now. |
| 1341 | # pylint: disable=protected-access |
| 1342 | users_id_token._maybe_set_current_user_vars( |
| 1343 | invoke_remote, api_info=getattr(service_instance, 'api_info', None), |
| 1344 | request=request) |
| 1345 | # pylint: enable=protected-access |
| 1346 | return remote_method(service_instance, request) |
| 1347 | |
| 1348 | invoke_remote.remote = remote_method.remote |
| 1349 | if isinstance(request_message, resource_container.ResourceContainer): |
| 1350 | resource_container.ResourceContainer.add_to_cache( |
| 1351 | invoke_remote.remote, request_message) |
| 1352 | |
| 1353 | invoke_remote.method_info = _MethodInfo( |
| 1354 | name=name or api_method.__name__, path=path or api_method.__name__, |
| 1355 | http_method=http_method or DEFAULT_HTTP_METHOD, |
| 1356 | scopes=scopes, audiences=audiences, |
| 1357 | allowed_client_ids=allowed_client_ids, auth_level=auth_level, |
| 1358 | api_key_required=api_key_required, metric_costs=metric_costs, |
| 1359 | use_request_uri=use_request_uri, |
| 1360 | request_body_class=request_body_class, |
| 1361 | request_params_class=request_params_class) |
| 1362 | invoke_remote.__name__ = invoke_remote.method_info.name |
| 1363 | return invoke_remote |
| 1364 | |
| 1365 | endpoints_util.check_list_type(scopes, (six.string_types, endpoints_types.OAuth2Scope), 'scopes') |
| 1366 | endpoints_util.check_list_type(allowed_client_ids, six.string_types, |
| 1367 | 'allowed_client_ids') |
| 1368 | _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level') |
| 1369 | |
| 1370 | _CheckAudiences(audiences) |
| 1371 | |
| 1372 | _CheckType(metric_costs, dict, 'metric_costs') |
| 1373 | |
| 1374 | return apiserving_method_decorator |
| 1375 | |
| 1376 | |
| 1377 | class ApiConfigGenerator(object): |
| 1378 | """Generates an API configuration from a ProtoRPC service. |
| 1379 | |
| 1380 | Example: |
| 1381 | |
| 1382 | class HelloRequest(messages.Message): |
| 1383 | my_name = messages.StringField(1, required=True) |
| 1384 | |
| 1385 | class HelloResponse(messages.Message): |
| 1386 | hello = messages.StringField(1, required=True) |
| 1387 | |
| 1388 | class HelloService(remote.Service): |
| 1389 | |
| 1390 | @remote.method(HelloRequest, HelloResponse) |
| 1391 | def hello(self, request): |
| 1392 | return HelloResponse(hello='Hello there, %s!' % |
| 1393 | request.my_name) |
| 1394 | |
| 1395 | api_config = ApiConfigGenerator().pretty_print_config_to_json(HelloService) |
| 1396 | |
| 1397 | The resulting api_config will be a JSON document describing the API |
| 1398 | implemented by HelloService. |
| 1399 | """ |
| 1400 | |
| 1401 | # Constants for categorizing a request method. |
| 1402 | # __NO_BODY - Request without a request body, such as GET and DELETE methods. |
| 1403 | # __HAS_BODY - Request (such as POST/PUT/PATCH) with info in the request body. |
| 1404 | __NO_BODY = 1 |
| 1405 | __HAS_BODY = 2 |
| 1406 | |
| 1407 | def __init__(self): |
| 1408 | self.__parser = message_parser.MessageTypeToJsonSchema() |
| 1409 | |
| 1410 | # Maps method id to the request schema id. |
| 1411 | self.__request_schema = {} |
| 1412 | |
| 1413 | # Maps method id to the response schema id. |
| 1414 | self.__response_schema = {} |
| 1415 | |
| 1416 | # Maps from ProtoRPC name to method id. |
| 1417 | self.__id_from_name = {} |
| 1418 | |
| 1419 | def __get_request_kind(self, method_info): |
| 1420 | """Categorize the type of the request. |
| 1421 | |
| 1422 | Args: |
| 1423 | method_info: _MethodInfo, method information. |
| 1424 | |
| 1425 | Returns: |
| 1426 | The kind of request. |
| 1427 | """ |
| 1428 | if method_info.http_method in ('GET', 'DELETE'): |
| 1429 | return self.__NO_BODY |
| 1430 | else: |
| 1431 | return self.__HAS_BODY |
| 1432 | |
| 1433 | def __field_to_subfields(self, field): |
| 1434 | """Fully describes data represented by field, including the nested case. |
| 1435 | |
| 1436 | In the case that the field is not a message field, we have no fields nested |
| 1437 | within a message definition, so we can simply return that field. However, in |
| 1438 | the nested case, we can't simply describe the data with one field or even |
| 1439 | with one chain of fields. |
| 1440 | |
| 1441 | For example, if we have a message field |
| 1442 | |
| 1443 | m_field = messages.MessageField(RefClass, 1) |
| 1444 | |
| 1445 | which references a class with two fields: |
| 1446 | |
| 1447 | class RefClass(messages.Message): |
| 1448 | one = messages.StringField(1) |
| 1449 | two = messages.IntegerField(2) |
| 1450 | |
| 1451 | then we would need to include both one and two to represent all the |
| 1452 | data contained. |
| 1453 | |
| 1454 | Calling __field_to_subfields(m_field) would return: |
| 1455 | [ |
| 1456 | [<MessageField "m_field">, <StringField "one">], |
| 1457 | [<MessageField "m_field">, <StringField "two">], |
| 1458 | ] |
| 1459 | |
| 1460 | If the second field was instead a message field |
| 1461 | |
| 1462 | class RefClass(messages.Message): |
| 1463 | one = messages.StringField(1) |
| 1464 | two = messages.MessageField(OtherRefClass, 2) |
| 1465 | |
| 1466 | referencing another class with two fields |
| 1467 | |
| 1468 | class OtherRefClass(messages.Message): |
| 1469 | three = messages.BooleanField(1) |
| 1470 | four = messages.FloatField(2) |
| 1471 | |
| 1472 | then we would need to recurse one level deeper for two. |
| 1473 | |
| 1474 | With this change, calling __field_to_subfields(m_field) would return: |
| 1475 | [ |
| 1476 | [<MessageField "m_field">, <StringField "one">], |
| 1477 | [<MessageField "m_field">, <StringField "two">, <StringField "three">], |
| 1478 | [<MessageField "m_field">, <StringField "two">, <StringField "four">], |
| 1479 | ] |
| 1480 | |
| 1481 | Args: |
| 1482 | field: An instance of a subclass of messages.Field. |
| 1483 | |
| 1484 | Returns: |
| 1485 | A list of lists, where each sublist is a list of fields. |
| 1486 | """ |
| 1487 | # Termination condition |
| 1488 | if not isinstance(field, messages.MessageField): |
| 1489 | return [[field]] |
| 1490 | |
| 1491 | result = [] |
| 1492 | for subfield in sorted(field.message_type.all_fields(), |
| 1493 | key=lambda f: f.number): |
| 1494 | subfield_results = self.__field_to_subfields(subfield) |
| 1495 | for subfields_list in subfield_results: |
| 1496 | subfields_list.insert(0, field) |
| 1497 | result.append(subfields_list) |
| 1498 | return result |
| 1499 | |
| 1500 | # TODO(dhermes): Support all the parameter types |
| 1501 | # Currently missing DATE and ETAG |
| 1502 | def __field_to_parameter_type(self, field): |
| 1503 | """Converts the field variant type into a string describing the parameter. |
| 1504 | |
| 1505 | Args: |
| 1506 | field: An instance of a subclass of messages.Field. |
| 1507 | |
| 1508 | Returns: |
| 1509 | A string corresponding to the variant enum of the field, with a few |
| 1510 | exceptions. In the case of signed ints, the 's' is dropped; for the BOOL |
| 1511 | variant, 'boolean' is used; and for the ENUM variant, 'string' is used. |
| 1512 | |
| 1513 | Raises: |
| 1514 | TypeError: if the field variant is a message variant. |
| 1515 | """ |
| 1516 | # We use lowercase values for types (e.g. 'string' instead of 'STRING'). |
| 1517 | variant = field.variant |
| 1518 | if variant == messages.Variant.MESSAGE: |
| 1519 | raise TypeError('A message variant can\'t be used in a parameter.') |
| 1520 | |
| 1521 | custom_variant_map = { |
| 1522 | messages.Variant.SINT32: 'int32', |
| 1523 | messages.Variant.SINT64: 'int64', |
| 1524 | messages.Variant.BOOL: 'boolean', |
| 1525 | messages.Variant.ENUM: 'string', |
| 1526 | } |
| 1527 | return custom_variant_map.get(variant) or variant.name.lower() |
| 1528 | |
| 1529 | def __get_path_parameters(self, path): |
| 1530 | """Parses path paremeters from a URI path and organizes them by parameter. |
| 1531 | |
| 1532 | Some of the parameters may correspond to message fields, and so will be |
| 1533 | represented as segments corresponding to each subfield; e.g. first.second if |
| 1534 | the field "second" in the message field "first" is pulled from the path. |
| 1535 | |
| 1536 | The resulting dictionary uses the first segments as keys and each key has as |
| 1537 | value the list of full parameter values with first segment equal to the key. |
| 1538 | |
| 1539 | If the match path parameter is null, that part of the path template is |
| 1540 | ignored; this occurs if '{}' is used in a template. |
| 1541 | |
| 1542 | Args: |
| 1543 | path: String; a URI path, potentially with some parameters. |
| 1544 | |
| 1545 | Returns: |
| 1546 | A dictionary with strings as keys and list of strings as values. |
| 1547 | """ |
| 1548 | path_parameters_by_segment = {} |
| 1549 | for format_var_name in re.findall(_PATH_VARIABLE_PATTERN, path): |
| 1550 | first_segment = format_var_name.split('.', 1)[0] |
| 1551 | matches = path_parameters_by_segment.setdefault(first_segment, []) |
| 1552 | matches.append(format_var_name) |
| 1553 | |
| 1554 | return path_parameters_by_segment |
| 1555 | |
| 1556 | def __validate_simple_subfield(self, parameter, field, segment_list, |
| 1557 | _segment_index=0): |
| 1558 | """Verifies that a proposed subfield actually exists and is a simple field. |
| 1559 | |
| 1560 | Here, simple means it is not a MessageField (nested). |
| 1561 | |
| 1562 | Args: |
| 1563 | parameter: String; the '.' delimited name of the current field being |
| 1564 | considered. This is relative to some root. |
| 1565 | field: An instance of a subclass of messages.Field. Corresponds to the |
| 1566 | previous segment in the path (previous relative to _segment_index), |
| 1567 | since this field should be a message field with the current segment |
| 1568 | as a field in the message class. |
| 1569 | segment_list: The full list of segments from the '.' delimited subfield |
| 1570 | being validated. |
| 1571 | _segment_index: Integer; used to hold the position of current segment so |
| 1572 | that segment_list can be passed as a reference instead of having to |
| 1573 | copy using segment_list[1:] at each step. |
| 1574 | |
| 1575 | Raises: |
| 1576 | TypeError: If the final subfield (indicated by _segment_index relative |
| 1577 | to the length of segment_list) is a MessageField. |
| 1578 | TypeError: If at any stage the lookup at a segment fails, e.g if a.b |
| 1579 | exists but a.b.c does not exist. This can happen either if a.b is not |
| 1580 | a message field or if a.b.c is not a property on the message class from |
| 1581 | a.b. |
| 1582 | """ |
| 1583 | if _segment_index >= len(segment_list): |
| 1584 | # In this case, the field is the final one, so should be simple type |
| 1585 | if isinstance(field, messages.MessageField): |
| 1586 | field_class = field.__class__.__name__ |
| 1587 | raise TypeError('Can\'t use messages in path. Subfield %r was ' |
| 1588 | 'included but is a %s.' % (parameter, field_class)) |
| 1589 | return |
| 1590 | |
| 1591 | segment = segment_list[_segment_index] |
| 1592 | parameter += '.' + segment |
| 1593 | try: |
| 1594 | field = field.type.field_by_name(segment) |
| 1595 | except (AttributeError, KeyError): |
| 1596 | raise TypeError('Subfield %r from path does not exist.' % (parameter,)) |
| 1597 | |
| 1598 | self.__validate_simple_subfield(parameter, field, segment_list, |
| 1599 | _segment_index=_segment_index + 1) |
| 1600 | |
| 1601 | def __validate_path_parameters(self, field, path_parameters): |
| 1602 | """Verifies that all path parameters correspond to an existing subfield. |
| 1603 | |
| 1604 | Args: |
| 1605 | field: An instance of a subclass of messages.Field. Should be the root |
| 1606 | level property name in each path parameter in path_parameters. For |
| 1607 | example, if the field is called 'foo', then each path parameter should |
| 1608 | begin with 'foo.'. |
| 1609 | path_parameters: A list of Strings representing URI parameter variables. |
| 1610 | |
| 1611 | Raises: |
| 1612 | TypeError: If one of the path parameters does not start with field.name. |
| 1613 | """ |
| 1614 | for param in path_parameters: |
| 1615 | segment_list = param.split('.') |
| 1616 | if segment_list[0] != field.name: |
| 1617 | raise TypeError('Subfield %r can\'t come from field %r.' |
| 1618 | % (param, field.name)) |
| 1619 | self.__validate_simple_subfield(field.name, field, segment_list[1:]) |
| 1620 | |
| 1621 | def __parameter_default(self, final_subfield): |
| 1622 | """Returns default value of final subfield if it has one. |
| 1623 | |
| 1624 | If this subfield comes from a field list returned from __field_to_subfields, |
| 1625 | none of the fields in the subfield list can have a default except the final |
| 1626 | one since they all must be message fields. |
| 1627 | |
| 1628 | Args: |
| 1629 | final_subfield: A simple field from the end of a subfield list. |
| 1630 | |
| 1631 | Returns: |
| 1632 | The default value of the subfield, if any exists, with the exception of an |
| 1633 | enum field, which will have its value cast to a string. |
| 1634 | """ |
| 1635 | if final_subfield.default: |
| 1636 | if isinstance(final_subfield, messages.EnumField): |
| 1637 | return final_subfield.default.name |
| 1638 | else: |
| 1639 | return final_subfield.default |
| 1640 | |
| 1641 | def __parameter_enum(self, final_subfield): |
| 1642 | """Returns enum descriptor of final subfield if it is an enum. |
| 1643 | |
| 1644 | An enum descriptor is a dictionary with keys as the names from the enum and |
| 1645 | each value is a dictionary with a single key "backendValue" and value equal |
| 1646 | to the same enum name used to stored it in the descriptor. |
| 1647 | |
| 1648 | The key "description" can also be used next to "backendValue", but protorpc |
| 1649 | Enum classes have no way of supporting a description for each value. |
| 1650 | |
| 1651 | Args: |
| 1652 | final_subfield: A simple field from the end of a subfield list. |
| 1653 | |
| 1654 | Returns: |
| 1655 | The enum descriptor for the field, if it's an enum descriptor, else |
| 1656 | returns None. |
| 1657 | """ |
| 1658 | if isinstance(final_subfield, messages.EnumField): |
| 1659 | enum_descriptor = {} |
| 1660 | for enum_value in final_subfield.type.to_dict().keys(): |
| 1661 | enum_descriptor[enum_value] = {'backendValue': enum_value} |
| 1662 | return enum_descriptor |
| 1663 | |
| 1664 | def __parameter_descriptor(self, subfield_list): |
| 1665 | """Creates descriptor for a parameter using the subfields that define it. |
| 1666 | |
| 1667 | Each parameter is defined by a list of fields, with all but the last being |
| 1668 | a message field and the final being a simple (non-message) field. |
| 1669 | |
| 1670 | Many of the fields in the descriptor are determined solely by the simple |
| 1671 | field at the end, though some (such as repeated and required) take the whole |
| 1672 | chain of fields into consideration. |
| 1673 | |
| 1674 | Args: |
| 1675 | subfield_list: List of fields describing the parameter. |
| 1676 | |
| 1677 | Returns: |
| 1678 | Dictionary containing a descriptor for the parameter described by the list |
| 1679 | of fields. |
| 1680 | """ |
| 1681 | descriptor = {} |
| 1682 | final_subfield = subfield_list[-1] |
| 1683 | |
| 1684 | # Required |
| 1685 | if all(subfield.required for subfield in subfield_list): |
| 1686 | descriptor['required'] = True |
| 1687 | |
| 1688 | # Type |
| 1689 | descriptor['type'] = self.__field_to_parameter_type(final_subfield) |
| 1690 | |
| 1691 | # Default |
| 1692 | default = self.__parameter_default(final_subfield) |
| 1693 | if default is not None: |
| 1694 | descriptor['default'] = default |
| 1695 | |
| 1696 | # Repeated |
| 1697 | if any(subfield.repeated for subfield in subfield_list): |
| 1698 | descriptor['repeated'] = True |
| 1699 | |
| 1700 | # Enum |
| 1701 | enum_descriptor = self.__parameter_enum(final_subfield) |
| 1702 | if enum_descriptor is not None: |
| 1703 | descriptor['enum'] = enum_descriptor |
| 1704 | |
| 1705 | return descriptor |
| 1706 | |
| 1707 | def __add_parameters_from_field(self, field, path_parameters, |
| 1708 | params, param_order): |
| 1709 | """Adds all parameters in a field to a method parameters descriptor. |
| 1710 | |
| 1711 | Simple fields will only have one parameter, but a message field 'x' that |
| 1712 | corresponds to a message class with fields 'y' and 'z' will result in |
| 1713 | parameters 'x.y' and 'x.z', for example. The mapping from field to |
| 1714 | parameters is mostly handled by __field_to_subfields. |
| 1715 | |
| 1716 | Args: |
| 1717 | field: Field from which parameters will be added to the method descriptor. |
| 1718 | path_parameters: A list of parameters matched from a path for this field. |
| 1719 | For example for the hypothetical 'x' from above if the path was |
| 1720 | '/a/{x.z}/b/{other}' then this list would contain only the element |
| 1721 | 'x.z' since 'other' does not match to this field. |
| 1722 | params: Dictionary with parameter names as keys and parameter descriptors |
| 1723 | as values. This will be updated for each parameter in the field. |
| 1724 | param_order: List of required parameter names to give them an order in the |
| 1725 | descriptor. All required parameters in the field will be added to this |
| 1726 | list. |
| 1727 | """ |
| 1728 | for subfield_list in self.__field_to_subfields(field): |
| 1729 | descriptor = self.__parameter_descriptor(subfield_list) |
| 1730 | |
| 1731 | qualified_name = '.'.join(subfield.name for subfield in subfield_list) |
| 1732 | in_path = qualified_name in path_parameters |
| 1733 | if descriptor.get('required', in_path): |
| 1734 | descriptor['required'] = True |
| 1735 | param_order.append(qualified_name) |
| 1736 | |
| 1737 | params[qualified_name] = descriptor |
| 1738 | |
| 1739 | def __params_descriptor_without_container(self, message_type, |
| 1740 | request_kind, path): |
| 1741 | """Describe parameters of a method which does not use a ResourceContainer. |
| 1742 | |
| 1743 | Makes sure that the path parameters are included in the message definition |
| 1744 | and adds any required fields and URL query parameters. |
| 1745 | |
| 1746 | This method is to preserve backwards compatibility and will be removed in |
| 1747 | a future release. |
| 1748 | |
| 1749 | Args: |
| 1750 | message_type: messages.Message class, Message with parameters to describe. |
| 1751 | request_kind: The type of request being made. |
| 1752 | path: string, HTTP path to method. |
| 1753 | |
| 1754 | Returns: |
| 1755 | A tuple (dict, list of string): Descriptor of the parameters, Order of the |
| 1756 | parameters. |
| 1757 | """ |
| 1758 | params = {} |
| 1759 | param_order = [] |
| 1760 | |
| 1761 | path_parameter_dict = self.__get_path_parameters(path) |
| 1762 | for field in sorted(message_type.all_fields(), key=lambda f: f.number): |
| 1763 | matched_path_parameters = path_parameter_dict.get(field.name, []) |
| 1764 | self.__validate_path_parameters(field, matched_path_parameters) |
| 1765 | if matched_path_parameters or request_kind == self.__NO_BODY: |
| 1766 | self.__add_parameters_from_field(field, matched_path_parameters, |
| 1767 | params, param_order) |
| 1768 | |
| 1769 | return params, param_order |
| 1770 | |
| 1771 | # TODO(user): request_kind is only used by |
| 1772 | # __params_descriptor_without_container so can be removed |
| 1773 | # once that method is fully deprecated. |
| 1774 | def __params_descriptor(self, message_type, request_kind, path, method_id): |
| 1775 | """Describe the parameters of a method. |
| 1776 | |
| 1777 | If the message_type is not a ResourceContainer, will fall back to |
| 1778 | __params_descriptor_without_container (which will eventually be deprecated). |
| 1779 | |
| 1780 | If the message type is a ResourceContainer, then all path/query parameters |
| 1781 | will come from the ResourceContainer This method will also make sure all |
| 1782 | path parameters are covered by the message fields. |
| 1783 | |
| 1784 | Args: |
| 1785 | message_type: messages.Message or ResourceContainer class, Message with |
| 1786 | parameters to describe. |
| 1787 | request_kind: The type of request being made. |
| 1788 | path: string, HTTP path to method. |
| 1789 | method_id: string, Unique method identifier (e.g. 'myapi.items.method') |
| 1790 | |
| 1791 | Returns: |
| 1792 | A tuple (dict, list of string): Descriptor of the parameters, Order of the |
| 1793 | parameters. |
| 1794 | """ |
| 1795 | path_parameter_dict = self.__get_path_parameters(path) |
| 1796 | |
| 1797 | if not isinstance(message_type, resource_container.ResourceContainer): |
| 1798 | if path_parameter_dict: |
| 1799 | _logger.warning('Method %s specifies path parameters but you are not ' |
| 1800 | 'using a ResourceContainer; instead, you are using %r. ' |
| 1801 | 'This will fail in future releases; please switch to ' |
| 1802 | 'using ResourceContainer as soon as possible.', |
| 1803 | method_id, type(message_type)) |
| 1804 | return self.__params_descriptor_without_container( |
| 1805 | message_type, request_kind, path) |
| 1806 | |
| 1807 | # From here, we can assume message_type is a ResourceContainer |
| 1808 | message_type = message_type.parameters_message_class() |
| 1809 | |
| 1810 | params = {} |
| 1811 | param_order = [] |
| 1812 | |
| 1813 | # Make sure all path parameters are covered. |
| 1814 | for field_name, matched_path_parameters in path_parameter_dict.items(): |
| 1815 | field = message_type.field_by_name(field_name) |
| 1816 | self.__validate_path_parameters(field, matched_path_parameters) |
| 1817 | |
| 1818 | # Add all fields, sort by field.number since we have parameterOrder. |
| 1819 | for field in sorted(message_type.all_fields(), key=lambda f: f.number): |
| 1820 | matched_path_parameters = path_parameter_dict.get(field.name, []) |
| 1821 | self.__add_parameters_from_field(field, matched_path_parameters, |
| 1822 | params, param_order) |
| 1823 | |
| 1824 | return params, param_order |
| 1825 | |
| 1826 | def __request_message_descriptor(self, request_kind, message_type, method_id, |
| 1827 | path): |
| 1828 | """Describes the parameters and body of the request. |
| 1829 | |
| 1830 | Args: |
| 1831 | request_kind: The type of request being made. |
| 1832 | message_type: messages.Message or ResourceContainer class. The message to |
| 1833 | describe. |
| 1834 | method_id: string, Unique method identifier (e.g. 'myapi.items.method') |
| 1835 | path: string, HTTP path to method. |
| 1836 | |
| 1837 | Returns: |
| 1838 | Dictionary describing the request. |
| 1839 | |
| 1840 | Raises: |
| 1841 | ValueError: if the method path and request required fields do not match |
| 1842 | """ |
| 1843 | descriptor = {} |
| 1844 | |
| 1845 | params, param_order = self.__params_descriptor(message_type, request_kind, |
| 1846 | path, method_id) |
| 1847 | |
| 1848 | if isinstance(message_type, resource_container.ResourceContainer): |
| 1849 | message_type = message_type.body_message_class() |
| 1850 | |
| 1851 | if (request_kind == self.__NO_BODY or |
| 1852 | message_type == message_types.VoidMessage()): |
| 1853 | descriptor['body'] = 'empty' |
| 1854 | else: |
| 1855 | descriptor['body'] = 'autoTemplate(backendRequest)' |
| 1856 | descriptor['bodyName'] = 'resource' |
| 1857 | self.__request_schema[method_id] = self.__parser.add_message( |
| 1858 | message_type.__class__) |
| 1859 | |
| 1860 | if params: |
| 1861 | descriptor['parameters'] = params |
| 1862 | |
| 1863 | if param_order: |
| 1864 | descriptor['parameterOrder'] = param_order |
| 1865 | |
| 1866 | return descriptor |
| 1867 | |
| 1868 | def __response_message_descriptor(self, message_type, method_id): |
| 1869 | """Describes the response. |
| 1870 | |
| 1871 | Args: |
| 1872 | message_type: messages.Message class, The message to describe. |
| 1873 | method_id: string, Unique method identifier (e.g. 'myapi.items.method') |
| 1874 | |
| 1875 | Returns: |
| 1876 | Dictionary describing the response. |
| 1877 | """ |
| 1878 | descriptor = {} |
| 1879 | |
| 1880 | self.__parser.add_message(message_type.__class__) |
| 1881 | if message_type == message_types.VoidMessage(): |
| 1882 | descriptor['body'] = 'empty' |
| 1883 | else: |
| 1884 | descriptor['body'] = 'autoTemplate(backendResponse)' |
| 1885 | descriptor['bodyName'] = 'resource' |
| 1886 | self.__response_schema[method_id] = self.__parser.ref_for_message_type( |
| 1887 | message_type.__class__) |
| 1888 | |
| 1889 | return descriptor |
| 1890 | |
| 1891 | def __method_descriptor(self, service, method_info, |
| 1892 | rosy_method, protorpc_method_info): |
| 1893 | """Describes a method. |
| 1894 | |
| 1895 | Args: |
| 1896 | service: endpoints.Service, Implementation of the API as a service. |
| 1897 | method_info: _MethodInfo, Configuration for the method. |
| 1898 | rosy_method: string, ProtoRPC method name prefixed with the |
| 1899 | name of the service. |
| 1900 | protorpc_method_info: protorpc.remote._RemoteMethodInfo, ProtoRPC |
| 1901 | description of the method. |
| 1902 | |
| 1903 | Returns: |
| 1904 | Dictionary describing the method. |
| 1905 | """ |
| 1906 | descriptor = {} |
| 1907 | |
| 1908 | request_message_type = (resource_container.ResourceContainer. |
| 1909 | get_request_message(protorpc_method_info.remote)) |
| 1910 | request_kind = self.__get_request_kind(method_info) |
| 1911 | remote_method = protorpc_method_info.remote |
| 1912 | |
| 1913 | descriptor['path'] = method_info.get_path(service.api_info) |
| 1914 | descriptor['httpMethod'] = method_info.http_method |
| 1915 | descriptor['rosyMethod'] = rosy_method |
| 1916 | descriptor['request'] = self.__request_message_descriptor( |
| 1917 | request_kind, request_message_type, |
| 1918 | method_info.method_id(service.api_info), |
| 1919 | descriptor['path']) |
| 1920 | descriptor['response'] = self.__response_message_descriptor( |
| 1921 | remote_method.response_type(), method_info.method_id(service.api_info)) |
| 1922 | |
| 1923 | # Audiences, scopes, allowed_client_ids and auth_level could be set at |
| 1924 | # either the method level or the API level. Allow an empty list at the |
| 1925 | # method level to override the setting at the API level. |
| 1926 | scopes = (method_info.scopes |
| 1927 | if method_info.scopes is not None |
| 1928 | else service.api_info.scopes) |
| 1929 | if scopes: |
| 1930 | descriptor['scopes'] = scopes |
| 1931 | audiences = (method_info.audiences |
| 1932 | if method_info.audiences is not None |
| 1933 | else service.api_info.audiences) |
| 1934 | if audiences: |
| 1935 | descriptor['audiences'] = audiences |
| 1936 | allowed_client_ids = (method_info.allowed_client_ids |
| 1937 | if method_info.allowed_client_ids is not None |
| 1938 | else service.api_info.allowed_client_ids) |
| 1939 | if allowed_client_ids: |
| 1940 | descriptor['clientIds'] = allowed_client_ids |
| 1941 | |
| 1942 | if remote_method.method.__doc__: |
| 1943 | descriptor['description'] = remote_method.method.__doc__ |
| 1944 | |
| 1945 | auth_level = (method_info.auth_level |
| 1946 | if method_info.auth_level is not None |
| 1947 | else service.api_info.auth_level) |
| 1948 | if auth_level is not None: |
| 1949 | descriptor['authLevel'] = AUTH_LEVEL.reverse_mapping[auth_level] |
| 1950 | |
| 1951 | descriptor['useRequestUri'] = method_info.use_request_uri(service.api_info) |
| 1952 | |
| 1953 | return descriptor |
| 1954 | |
| 1955 | def __schema_descriptor(self, services): |
| 1956 | """Descriptor for the all the JSON Schema used. |
| 1957 | |
| 1958 | Args: |
| 1959 | services: List of protorpc.remote.Service instances implementing an |
| 1960 | api/version. |
| 1961 | |
| 1962 | Returns: |
| 1963 | Dictionary containing all the JSON Schema used in the service. |
| 1964 | """ |
| 1965 | methods_desc = {} |
| 1966 | |
| 1967 | for service in services: |
| 1968 | protorpc_methods = service.all_remote_methods() |
| 1969 | for protorpc_method_name in protorpc_methods.keys(): |
| 1970 | rosy_method = '%s.%s' % (service.__name__, protorpc_method_name) |
| 1971 | method_id = self.__id_from_name[rosy_method] |
| 1972 | |
| 1973 | request_response = {} |
| 1974 | |
| 1975 | request_schema_id = self.__request_schema.get(method_id) |
| 1976 | if request_schema_id: |
| 1977 | request_response['request'] = { |
| 1978 | '$ref': request_schema_id |
| 1979 | } |
| 1980 | |
| 1981 | response_schema_id = self.__response_schema.get(method_id) |
| 1982 | if response_schema_id: |
| 1983 | request_response['response'] = { |
| 1984 | '$ref': response_schema_id |
| 1985 | } |
| 1986 | |
| 1987 | methods_desc[rosy_method] = request_response |
| 1988 | |
| 1989 | descriptor = { |
| 1990 | 'methods': methods_desc, |
| 1991 | 'schemas': self.__parser.schemas(), |
| 1992 | } |
| 1993 | |
| 1994 | return descriptor |
| 1995 | |
| 1996 | def __get_merged_api_info(self, services): |
| 1997 | """Builds a description of an API. |
| 1998 | |
| 1999 | Args: |
| 2000 | services: List of protorpc.remote.Service instances implementing an |
| 2001 | api/version. |
| 2002 | |
| 2003 | Returns: |
| 2004 | The _ApiInfo object to use for the API that the given services implement. |
| 2005 | |
| 2006 | Raises: |
| 2007 | ApiConfigurationError: If there's something wrong with the API |
| 2008 | configuration, such as a multiclass API decorated with different API |
| 2009 | descriptors (see the docstring for api()). |
| 2010 | """ |
| 2011 | merged_api_info = services[0].api_info |
| 2012 | |
| 2013 | # Verify that, if there are multiple classes here, they're allowed to |
| 2014 | # implement the same API. |
| 2015 | for service in services[1:]: |
| 2016 | if not merged_api_info.is_same_api(service.api_info): |
| 2017 | raise api_exceptions.ApiConfigurationError( |
| 2018 | _MULTICLASS_MISMATCH_ERROR_TEMPLATE % (service.api_info.name, |
| 2019 | service.api_info.api_version)) |
| 2020 | |
| 2021 | return merged_api_info |
| 2022 | |
| 2023 | def __auth_descriptor(self, api_info): |
| 2024 | """Builds an auth descriptor from API info. |
| 2025 | |
| 2026 | Args: |
| 2027 | api_info: An _ApiInfo object. |
| 2028 | |
| 2029 | Returns: |
| 2030 | A dictionary with 'allowCookieAuth' and/or 'blockedRegions' keys. |
| 2031 | """ |
| 2032 | if api_info.auth is None: |
| 2033 | return None |
| 2034 | |
| 2035 | auth_descriptor = {} |
| 2036 | if api_info.auth.allow_cookie_auth is not None: |
| 2037 | auth_descriptor['allowCookieAuth'] = api_info.auth.allow_cookie_auth |
| 2038 | if api_info.auth.blocked_regions: |
| 2039 | auth_descriptor['blockedRegions'] = api_info.auth.blocked_regions |
| 2040 | |
| 2041 | return auth_descriptor |
| 2042 | |
| 2043 | def __frontend_limit_descriptor(self, api_info): |
| 2044 | """Builds a frontend limit descriptor from API info. |
| 2045 | |
| 2046 | Args: |
| 2047 | api_info: An _ApiInfo object. |
| 2048 | |
| 2049 | Returns: |
| 2050 | A dictionary with frontend limit information. |
| 2051 | """ |
| 2052 | if api_info.frontend_limits is None: |
| 2053 | return None |
| 2054 | |
| 2055 | descriptor = {} |
| 2056 | for propname, descname in (('unregistered_user_qps', 'unregisteredUserQps'), |
| 2057 | ('unregistered_qps', 'unregisteredQps'), |
| 2058 | ('unregistered_daily', 'unregisteredDaily')): |
| 2059 | if getattr(api_info.frontend_limits, propname) is not None: |
| 2060 | descriptor[descname] = getattr(api_info.frontend_limits, propname) |
| 2061 | |
| 2062 | rules = self.__frontend_limit_rules_descriptor(api_info) |
| 2063 | if rules: |
| 2064 | descriptor['rules'] = rules |
| 2065 | |
| 2066 | return descriptor |
| 2067 | |
| 2068 | def __frontend_limit_rules_descriptor(self, api_info): |
| 2069 | """Builds a frontend limit rules descriptor from API info. |
| 2070 | |
| 2071 | Args: |
| 2072 | api_info: An _ApiInfo object. |
| 2073 | |
| 2074 | Returns: |
| 2075 | A list of dictionaries with frontend limit rules information. |
| 2076 | """ |
| 2077 | if not api_info.frontend_limits.rules: |
| 2078 | return None |
| 2079 | |
| 2080 | rules = [] |
| 2081 | for rule in api_info.frontend_limits.rules: |
| 2082 | descriptor = {} |
| 2083 | for propname, descname in (('match', 'match'), |
| 2084 | ('qps', 'qps'), |
| 2085 | ('user_qps', 'userQps'), |
| 2086 | ('daily', 'daily'), |
| 2087 | ('analytics_id', 'analyticsId')): |
| 2088 | if getattr(rule, propname) is not None: |
| 2089 | descriptor[descname] = getattr(rule, propname) |
| 2090 | if descriptor: |
| 2091 | rules.append(descriptor) |
| 2092 | |
| 2093 | return rules |
| 2094 | |
| 2095 | def __api_descriptor(self, services, hostname=None): |
| 2096 | """Builds a description of an API. |
| 2097 | |
| 2098 | Args: |
| 2099 | services: List of protorpc.remote.Service instances implementing an |
| 2100 | api/version. |
| 2101 | hostname: string, Hostname of the API, to override the value set on the |
| 2102 | current service. Defaults to None. |
| 2103 | |
| 2104 | Returns: |
| 2105 | A dictionary that can be deserialized into JSON and stored as an API |
| 2106 | description document. |
| 2107 | |
| 2108 | Raises: |
| 2109 | ApiConfigurationError: If there's something wrong with the API |
| 2110 | configuration, such as a multiclass API decorated with different API |
| 2111 | descriptors (see the docstring for api()), or a repeated method |
| 2112 | signature. |
| 2113 | """ |
| 2114 | merged_api_info = self.__get_merged_api_info(services) |
| 2115 | descriptor = self.get_descriptor_defaults(merged_api_info, |
| 2116 | hostname=hostname) |
| 2117 | description = merged_api_info.description |
| 2118 | if not description and len(services) == 1: |
| 2119 | description = services[0].__doc__ |
| 2120 | if description: |
| 2121 | descriptor['description'] = description |
| 2122 | |
| 2123 | auth_descriptor = self.__auth_descriptor(merged_api_info) |
| 2124 | if auth_descriptor: |
| 2125 | descriptor['auth'] = auth_descriptor |
| 2126 | |
| 2127 | frontend_limit_descriptor = self.__frontend_limit_descriptor( |
| 2128 | merged_api_info) |
| 2129 | if frontend_limit_descriptor: |
| 2130 | descriptor['frontendLimits'] = frontend_limit_descriptor |
| 2131 | |
| 2132 | method_map = {} |
| 2133 | method_collision_tracker = {} |
| 2134 | rest_collision_tracker = {} |
| 2135 | |
| 2136 | for service in services: |
| 2137 | remote_methods = service.all_remote_methods() |
| 2138 | for protorpc_meth_name, protorpc_meth_info in remote_methods.items(): |
| 2139 | method_info = getattr(protorpc_meth_info, 'method_info', None) |
| 2140 | # Skip methods that are not decorated with @method |
| 2141 | if method_info is None: |
| 2142 | continue |
| 2143 | method_id = method_info.method_id(service.api_info) |
| 2144 | rosy_method = '%s.%s' % (service.__name__, protorpc_meth_name) |
| 2145 | self.__id_from_name[rosy_method] = method_id |
| 2146 | method_map[method_id] = self.__method_descriptor( |
| 2147 | service, method_info, rosy_method, protorpc_meth_info) |
| 2148 | |
| 2149 | # Make sure the same method name isn't repeated. |
| 2150 | if method_id in method_collision_tracker: |
| 2151 | raise api_exceptions.ApiConfigurationError( |
| 2152 | 'Method %s used multiple times, in classes %s and %s' % |
| 2153 | (method_id, method_collision_tracker[method_id], |
| 2154 | service.__name__)) |
| 2155 | else: |
| 2156 | method_collision_tracker[method_id] = service.__name__ |
| 2157 | |
| 2158 | # Make sure the same HTTP method & path aren't repeated. |
| 2159 | rest_identifier = (method_info.http_method, |
| 2160 | method_info.get_path(service.api_info)) |
| 2161 | if rest_identifier in rest_collision_tracker: |
| 2162 | raise api_exceptions.ApiConfigurationError( |
| 2163 | '%s path "%s" used multiple times, in classes %s and %s' % |
| 2164 | (method_info.http_method, method_info.get_path(service.api_info), |
| 2165 | rest_collision_tracker[rest_identifier], |
| 2166 | service.__name__)) |
| 2167 | else: |
| 2168 | rest_collision_tracker[rest_identifier] = service.__name__ |
| 2169 | |
| 2170 | if method_map: |
| 2171 | descriptor['methods'] = method_map |
| 2172 | descriptor['descriptor'] = self.__schema_descriptor(services) |
| 2173 | |
| 2174 | return descriptor |
| 2175 | |
| 2176 | def get_descriptor_defaults(self, api_info, hostname=None): |
| 2177 | """Gets a default configuration for a service. |
| 2178 | |
| 2179 | Args: |
| 2180 | api_info: _ApiInfo object for this service. |
| 2181 | hostname: string, Hostname of the API, to override the value set on the |
| 2182 | current service. Defaults to None. |
| 2183 | |
| 2184 | Returns: |
| 2185 | A dictionary with the default configuration. |
| 2186 | """ |
| 2187 | hostname = (hostname or endpoints_util.get_app_hostname() or |
| 2188 | api_info.hostname) |
| 2189 | protocol = 'http' if ((hostname and hostname.startswith('localhost')) or |
| 2190 | endpoints_util.is_running_on_devserver()) else 'https' |
| 2191 | base_path = api_info.base_path.strip('/') |
| 2192 | defaults = { |
| 2193 | 'extends': 'thirdParty.api', |
| 2194 | 'root': '{0}://{1}/{2}'.format(protocol, hostname, base_path), |
| 2195 | 'name': api_info.name, |
| 2196 | 'version': api_info.api_version, |
| 2197 | 'api_version': api_info.api_version, |
| 2198 | 'path_version': api_info.path_version, |
| 2199 | 'defaultVersion': True, |
| 2200 | 'abstract': False, |
| 2201 | 'adapter': { |
| 2202 | 'bns': '{0}://{1}/{2}'.format(protocol, hostname, base_path), |
| 2203 | 'type': 'lily', |
| 2204 | 'deadline': 10.0 |
| 2205 | } |
| 2206 | } |
| 2207 | if api_info.canonical_name: |
| 2208 | defaults['canonicalName'] = api_info.canonical_name |
| 2209 | if api_info.owner_domain: |
| 2210 | defaults['ownerDomain'] = api_info.owner_domain |
| 2211 | if api_info.owner_name: |
| 2212 | defaults['ownerName'] = api_info.owner_name |
| 2213 | if api_info.package_path: |
| 2214 | defaults['packagePath'] = api_info.package_path |
| 2215 | if api_info.title: |
| 2216 | defaults['title'] = api_info.title |
| 2217 | if api_info.documentation: |
| 2218 | defaults['documentation'] = api_info.documentation |
| 2219 | return defaults |
| 2220 | |
| 2221 | def get_config_dict(self, services, hostname=None): |
| 2222 | """JSON dict description of a protorpc.remote.Service in API format. |
| 2223 | |
| 2224 | Args: |
| 2225 | services: Either a single protorpc.remote.Service or a list of them |
| 2226 | that implements an api/version. |
| 2227 | hostname: string, Hostname of the API, to override the value set on the |
| 2228 | current service. Defaults to None. |
| 2229 | |
| 2230 | Returns: |
| 2231 | dict, The API descriptor document as a JSON dict. |
| 2232 | """ |
| 2233 | if not isinstance(services, (tuple, list)): |
| 2234 | services = [services] |
| 2235 | # The type of a class that inherits from remote.Service is actually |
| 2236 | # remote._ServiceClass, thanks to metaclass strangeness. |
| 2237 | # pylint: disable=protected-access |
| 2238 | endpoints_util.check_list_type(services, remote._ServiceClass, 'services', |
| 2239 | allow_none=False) |
| 2240 | |
| 2241 | return self.__api_descriptor(services, hostname=hostname) |
| 2242 | |
| 2243 | def pretty_print_config_to_json(self, services, hostname=None): |
| 2244 | """JSON string description of a protorpc.remote.Service in API format. |
| 2245 | |
| 2246 | Args: |
| 2247 | services: Either a single protorpc.remote.Service or a list of them |
| 2248 | that implements an api/version. |
| 2249 | hostname: string, Hostname of the API, to override the value set on the |
| 2250 | current service. Defaults to None. |
| 2251 | |
| 2252 | Returns: |
| 2253 | string, The API descriptor document as a JSON string. |
| 2254 | """ |
| 2255 | descriptor = self.get_config_dict(services, hostname) |
| 2256 | return json.dumps(descriptor, sort_keys=True, indent=2, |
| 2257 | separators=(',', ': ')) |