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