From d4d14fa39e08eca996f37971788df07dfb6418b2 Mon Sep 17 00:00:00 2001 From: Sai Cheemalapati Date: Wed, 3 Aug 2016 15:44:32 -0700 Subject: [PATCH 1/3] Add Endpoints 1.1 sample Updated the sample to use the Endpoints 1.1 Python client with configuration options for service management and service control. --- appengine/standard/endpoints/backend/app.yaml | 23 ++- .../endpoints/backend/appengine_config.py | 4 + .../endpoints/backend/echo-v1_swagger.json | 64 ++++++++ appengine/standard/endpoints/backend/main.py | 138 ++++-------------- .../standard/endpoints/backend/main_test.py | 57 ++++---- 5 files changed, 142 insertions(+), 144 deletions(-) create mode 100644 appengine/standard/endpoints/backend/appengine_config.py create mode 100644 appengine/standard/endpoints/backend/echo-v1_swagger.json diff --git a/appengine/standard/endpoints/backend/app.yaml b/appengine/standard/endpoints/backend/app.yaml index 31030b73367e..73891e2980a0 100644 --- a/appengine/standard/endpoints/backend/app.yaml +++ b/appengine/standard/endpoints/backend/app.yaml @@ -3,14 +3,25 @@ threadsafe: true api_version: 1 handlers: -# The endpoints handler must be mapped to /_ah/spi. -# Apps send requests to /_ah/api, but the endpoints service handles mapping -# those requests to /_ah/spi. -- url: /_ah/spi/.* +# The endpoints handler must be mapped to /_ah/api. +- url: /_ah/api/.* script: main.api libraries: - name: pycrypto version: 2.6 -- name: endpoints - version: 1.0 + +# Endpoints 1.1 uses background threads for caching and reporting to service +# management and service control. +manual_scaling: + instances: 1 + +beta_settings: + use_endpoints_api_management: true + endpoints_swagger_spec_file: echo-v1_swagger.json + +env_variables: + #TODO: The Endpoints service name. + ENDPOINTS_SERVICE_NAME: {your-service.appspot.com} + #TODO: The version Id of the uploaded Endpoints service. + ENDPOINTS_SERVICE_VERSION: {2016-08-01r01} diff --git a/appengine/standard/endpoints/backend/appengine_config.py b/appengine/standard/endpoints/backend/appengine_config.py new file mode 100644 index 000000000000..3bb4ea6e3f1a --- /dev/null +++ b/appengine/standard/endpoints/backend/appengine_config.py @@ -0,0 +1,4 @@ +from google.appengine.ext import vendor + +# Add any libraries installed in the `lib` folder. +vendor.add('lib') diff --git a/appengine/standard/endpoints/backend/echo-v1_swagger.json b/appengine/standard/endpoints/backend/echo-v1_swagger.json new file mode 100644 index 000000000000..cd358f6b0d5b --- /dev/null +++ b/appengine/standard/endpoints/backend/echo-v1_swagger.json @@ -0,0 +1,64 @@ +{ + "basePath": "/_ah/api/echo/v1", + "consumes": [ + "application/json" + ], + "definitions": { + "MainEcho": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "info": { + "title": "echo", + "version": "v1" + }, + "paths": { + "/echo": { + "post": { + "operationId": "EchoApi_echo", + "parameters": [], + "responses": { + "200": { + "description": "200_response", + "schema": { + "$ref": "#/definitions/MainEcho" + } + } + } + } + }, + "/echo/getUserEmail": { + "get": { + "operationId": "EchoApi_getUserEmail", + "parameters": [], + "responses": { + "200": { + "description": "200_response", + "schema": { + "$ref": "#/definitions/MainEcho" + } + } + } + } + } + }, + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "in": "query", + "name": "key", + "type": "apiKey" + } + } +} diff --git a/appengine/standard/endpoints/backend/main.py b/appengine/standard/endpoints/backend/main.py index fc4049b1309a..fa51172c4b0c 100644 --- a/appengine/standard/endpoints/backend/main.py +++ b/appengine/standard/endpoints/backend/main.py @@ -22,128 +22,46 @@ from protorpc import remote # [END imports] - # [START messages] -class Greeting(messages.Message): - """Greeting that stores a message.""" +class Echo(messages.Message): + """An echo that stores a message.""" message = messages.StringField(1) - - -class GreetingCollection(messages.Message): - """Collection of Greetings.""" - items = messages.MessageField(Greeting, 1, repeated=True) - - -STORED_GREETINGS = GreetingCollection(items=[ - Greeting(message='hello world!'), - Greeting(message='goodbye world!'), -]) # [END messages] - -# [START greeting_api] -@endpoints.api(name='greeting', version='v1') -class GreetingApi(remote.Service): - - @endpoints.method( - # This method does not take a request message. - message_types.VoidMessage, - # This method returns a GreetingCollection message. - GreetingCollection, - path='greetings', - http_method='GET', - name='greetings.list') - def list_greetings(self, unused_request): - return STORED_GREETINGS - - # ResourceContainers are used to encapsuate a request body and url - # parameters. This one is used to represent the Greeting ID for the - # greeting_get method. - GET_RESOURCE = endpoints.ResourceContainer( - # The request body should be empty. - message_types.VoidMessage, - # Accept one url parameter: and integer named 'id' - id=messages.IntegerField(1, variant=messages.Variant.INT32)) - - @endpoints.method( - # Use the ResourceContainer defined above to accept an empty body - # but an ID in the query string. - GET_RESOURCE, - # This method returns a Greeting message. - Greeting, - # The path defines the source of the URL parameter 'id'. If not - # specified here, it would need to be in the query string. - path='greetings/{id}', - http_method='GET', - name='greetings.get') - def get_greeting(self, request): - try: - # request.id is used to access the URL parameter. - return STORED_GREETINGS.items[request.id] - except (IndexError, TypeError): - raise endpoints.NotFoundException( - 'Greeting {} not found'.format(request.id)) - # [END greeting_api] - - # [START multiply] - # This ResourceContainer is similar to the one used for get_greeting, but - # this one also contains a request body in the form of a Greeting message. - MULTIPLY_RESOURCE = endpoints.ResourceContainer( - Greeting, - times=messages.IntegerField(2, variant=messages.Variant.INT32, - required=True)) +# [START echo_api] +@endpoints.api(name='echo', version='v1') +class EchoApi(remote.Service): @endpoints.method( - # This method accepts a request body containing a Greeting message - # and a URL parameter specifying how many times to multiply the - # message. - MULTIPLY_RESOURCE, - # This method returns a Greeting message. - Greeting, - path='greetings/multiply/{times}', + # This method takes an Echo message. + Echo, + # This method returns an Echo message. + Echo, + path='echo', http_method='POST', - name='greetings.multiply') - def multiply_greeting(self, request): - return Greeting(message=request.message * request.times) - # [END multiply] - - -# [START auth_config] -WEB_CLIENT_ID = 'replace this with your web client application ID' -ANDROID_CLIENT_ID = 'replace this with your Android client ID' -IOS_CLIENT_ID = 'replace this with your iOS client ID' -ANDROID_AUDIENCE = WEB_CLIENT_ID -ALLOWED_CLIENT_IDS = [ - WEB_CLIENT_ID, ANDROID_CLIENT_ID, IOS_CLIENT_ID, - endpoints.API_EXPLORER_CLIENT_ID] -# [END auth_config] - - -# [START authed_greeting_api] -@endpoints.api( - name='authed_greeting', - version='v1', - # Only allowed configured Client IDs to access this API. - allowed_client_ids=ALLOWED_CLIENT_IDS, - # Only allow auth tokens with the given audience to access this API. - audiences=[ANDROID_AUDIENCE], - # Require auth tokens to have the following scopes to access this API. - scopes=[endpoints.EMAIL_SCOPE]) -class AuthedGreetingApi(remote.Service): + name='echo') + def echo(self, request): + return Echo(message=request.message) @endpoints.method( + # This method takes an empty request body. message_types.VoidMessage, - Greeting, - path='greet', - http_method='POST', - name='greet') - def greet(self, request): + # This method returns an Echo message. + Echo, + path='echo/getUserEmail', + http_method='GET', + # Require auth tokens to have the following scopes to access this API. + scopes=[endpoints.EMAIL_SCOPE]) + def get_user_email(self, request): user = endpoints.get_current_user() - user_name = user.email() if user else 'Anonymous' - return Greeting(message='Hello, {}'.format(user_name)) -# [END authed_greeting_api] + # If there's no user defined, the request was unauthenticated, so we + # raise 401 Unauthorized. + if not user: + raise endpoints.UnauthorizedException + return Echo(message=user.email()) +# [END echo_api] # [START api_server] -api = endpoints.api_server([GreetingApi, AuthedGreetingApi]) +api = endpoints.api_server([EchoApi]) # [END api_server] diff --git a/appengine/standard/endpoints/backend/main_test.py b/appengine/standard/endpoints/backend/main_test.py index 56fefa2ead66..a106e3f0e8dd 100644 --- a/appengine/standard/endpoints/backend/main_test.py +++ b/appengine/standard/endpoints/backend/main_test.py @@ -12,42 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. +import endpoints import main import mock +import unittest +from google.appengine.ext import testbed from protorpc import message_types +class EchoTestCase(unittest.TestCase): + """ + Test cases for the Echo API. + """ -def test_list_greetings(testbed): - api = main.GreetingApi() - response = api.list_greetings(message_types.VoidMessage()) - assert len(response.items) == 2 + def setUp(self): + self.testbed = testbed.Testbed() + self.testbed.activate() + def tearDown(self): + self.testbed.deactivate() -def test_get_greeting(testbed): - api = main.GreetingApi() - request = main.GreetingApi.get_greeting.remote.request_type(id=1) - response = api.get_greeting(request) - assert response.message == 'goodbye world!' + def test_echo(self): + api = main.EchoApi() + response = api.echo(main.Echo( + message='Hello world!')) + self.assertEqual('Hello world!', response.message) + def test_get_user_email(self): + api = main.EchoApi() -def test_multiply_greeting(testbed): - api = main.GreetingApi() - request = main.GreetingApi.multiply_greeting.remote.request_type( - times=4, - message='help I\'m trapped in a test case.') - response = api.multiply_greeting(request) - assert response.message == 'help I\'m trapped in a test case.' * 4 + with mock.patch('main.endpoints.get_current_user') as user_mock: + user_mock.return_value = None + self.assertRaises(endpoints.UnauthorizedException, + api.get_user_email, message_types.VoidMessage()) + user_mock.return_value = mock.Mock() + user_mock.return_value.email.return_value = 'user@example.com' + response = api.get_user_email(message_types.VoidMessage()) + self.assertEqual('user@example.com', response.message) -def test_authed_greet(testbed): - api = main.AuthedGreetingApi() - - with mock.patch('main.endpoints.get_current_user') as user_mock: - user_mock.return_value = None - response = api.greet(message_types.VoidMessage()) - assert response.message == 'Hello, Anonymous' - - user_mock.return_value = mock.Mock() - user_mock.return_value.email.return_value = 'user@example.com' - response = api.greet(message_types.VoidMessage()) - assert response.message == 'Hello, user@example.com' +if __name__ == '__main__': + unittest.main() From de0bcdf595dcfdceee605d0deaa998106457f1ed Mon Sep 17 00:00:00 2001 From: Sai Cheemalapati Date: Mon, 8 Aug 2016 10:49:48 -0700 Subject: [PATCH 2/3] Review fixes. --- appengine/standard/endpoints/backend/app.yaml | 8 +-- .../endpoints/backend/echo-v1_swagger.json | 50 ++++++++----------- appengine/standard/endpoints/backend/main.py | 8 +-- .../standard/endpoints/backend/main_test.py | 46 ++++++----------- 4 files changed, 45 insertions(+), 67 deletions(-) diff --git a/appengine/standard/endpoints/backend/app.yaml b/appengine/standard/endpoints/backend/app.yaml index 73891e2980a0..0dd9abd9ce05 100644 --- a/appengine/standard/endpoints/backend/app.yaml +++ b/appengine/standard/endpoints/backend/app.yaml @@ -21,7 +21,7 @@ beta_settings: endpoints_swagger_spec_file: echo-v1_swagger.json env_variables: - #TODO: The Endpoints service name. - ENDPOINTS_SERVICE_NAME: {your-service.appspot.com} - #TODO: The version Id of the uploaded Endpoints service. - ENDPOINTS_SERVICE_VERSION: {2016-08-01r01} + # Replace with your endpoints service name. + ENDPOINTS_SERVICE_NAME: your-service.appspot.com + # Replace with the version Id of your uploaded Endpoints service. + ENDPOINTS_SERVICE_VERSION: 2016-08-01r01 diff --git a/appengine/standard/endpoints/backend/echo-v1_swagger.json b/appengine/standard/endpoints/backend/echo-v1_swagger.json index cd358f6b0d5b..260fb33b06da 100644 --- a/appengine/standard/endpoints/backend/echo-v1_swagger.json +++ b/appengine/standard/endpoints/backend/echo-v1_swagger.json @@ -1,44 +1,45 @@ { - "basePath": "/_ah/api/echo/v1", + "basePath": "/_ah/api", "consumes": [ "application/json" - ], + ], "definitions": { "MainEcho": { "properties": { - "message": { + "content": { "type": "string" } - }, + }, "type": "object" } - }, + }, + "host": null, "info": { - "title": "echo", + "title": "echo", "version": "v1" - }, + }, "paths": { - "/echo": { + "/echo/v1/echo": { "post": { - "operationId": "EchoApi_echo", - "parameters": [], + "operationId": "EchoApi_echo", + "parameters": [], "responses": { "200": { - "description": "200_response", + "description": "200_response", "schema": { "$ref": "#/definitions/MainEcho" } } } } - }, - "/echo/getUserEmail": { + }, + "/echo/v1/echo/getUserEmail": { "get": { - "operationId": "EchoApi_getUserEmail", - "parameters": [], + "operationId": "EchoApi_getUserEmail", + "parameters": [], "responses": { "200": { - "description": "200_response", + "description": "200_response", "schema": { "$ref": "#/definitions/MainEcho" } @@ -46,19 +47,12 @@ } } } - }, + }, "produces": [ "application/json" - ], + ], "schemes": [ "https" - ], - "swagger": "2.0", - "securityDefinitions": { - "api_key": { - "in": "query", - "name": "key", - "type": "apiKey" - } - } -} + ], + "swagger": "2.0" +} \ No newline at end of file diff --git a/appengine/standard/endpoints/backend/main.py b/appengine/standard/endpoints/backend/main.py index fa51172c4b0c..dce378f08316 100644 --- a/appengine/standard/endpoints/backend/main.py +++ b/appengine/standard/endpoints/backend/main.py @@ -24,8 +24,8 @@ # [START messages] class Echo(messages.Message): - """An echo that stores a message.""" - message = messages.StringField(1) + """A proto Message that contains a simple string field.""" + content = messages.StringField(1) # [END messages] # [START echo_api] @@ -41,7 +41,7 @@ class EchoApi(remote.Service): http_method='POST', name='echo') def echo(self, request): - return Echo(message=request.message) + return Echo(content=request.content) @endpoints.method( # This method takes an empty request body. @@ -58,7 +58,7 @@ def get_user_email(self, request): # raise 401 Unauthorized. if not user: raise endpoints.UnauthorizedException - return Echo(message=user.email()) + return Echo(content=user.email()) # [END echo_api] diff --git a/appengine/standard/endpoints/backend/main_test.py b/appengine/standard/endpoints/backend/main_test.py index a106e3f0e8dd..ce1ebe395876 100644 --- a/appengine/standard/endpoints/backend/main_test.py +++ b/appengine/standard/endpoints/backend/main_test.py @@ -15,40 +15,24 @@ import endpoints import main import mock -import unittest -from google.appengine.ext import testbed +import pytest from protorpc import message_types -class EchoTestCase(unittest.TestCase): - """ - Test cases for the Echo API. - """ - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() +def test_echo(): + api = main.EchoApi() + response = api.echo(main.Echo(content='Hello world!')) + assert 'Hello world!' == response.content - def tearDown(self): - self.testbed.deactivate() +def test_get_user_email(): + api = main.EchoApi() - def test_echo(self): - api = main.EchoApi() - response = api.echo(main.Echo( - message='Hello world!')) - self.assertEqual('Hello world!', response.message) + with mock.patch('main.endpoints.get_current_user') as user_mock: + user_mock.return_value = None + with pytest.raises(endpoints.UnauthorizedException): + api.get_user_email(message_types.VoidMessage()) - def test_get_user_email(self): - api = main.EchoApi() - - with mock.patch('main.endpoints.get_current_user') as user_mock: - user_mock.return_value = None - self.assertRaises(endpoints.UnauthorizedException, - api.get_user_email, message_types.VoidMessage()) - - user_mock.return_value = mock.Mock() - user_mock.return_value.email.return_value = 'user@example.com' - response = api.get_user_email(message_types.VoidMessage()) - self.assertEqual('user@example.com', response.message) - -if __name__ == '__main__': - unittest.main() + user_mock.return_value = mock.Mock() + user_mock.return_value.email.return_value = 'user@example.com' + response = api.get_user_email(message_types.VoidMessage()) + assert 'user@example.com' == response.content From 4df3b55842d5768f758e23e2d1f68fb5620a6338 Mon Sep 17 00:00:00 2001 From: Sai Cheemalapati Date: Mon, 8 Aug 2016 11:15:40 -0700 Subject: [PATCH 3/3] Move directories --- .../standard/endpoints-v1.1/backend/app.yaml | 27 ++++ .../backend/appengine_config.py | 0 .../backend/echo-v1_swagger.json | 0 .../standard/endpoints-v1.1/backend/main.py | 67 +++++++++ .../endpoints-v1.1/backend/main_test.py | 38 +++++ appengine/standard/endpoints/backend/app.yaml | 23 +-- appengine/standard/endpoints/backend/main.py | 140 ++++++++++++++---- .../standard/endpoints/backend/main_test.py | 39 +++-- 8 files changed, 276 insertions(+), 58 deletions(-) create mode 100644 appengine/standard/endpoints-v1.1/backend/app.yaml rename appengine/standard/{endpoints => endpoints-v1.1}/backend/appengine_config.py (100%) rename appengine/standard/{endpoints => endpoints-v1.1}/backend/echo-v1_swagger.json (100%) create mode 100644 appengine/standard/endpoints-v1.1/backend/main.py create mode 100644 appengine/standard/endpoints-v1.1/backend/main_test.py diff --git a/appengine/standard/endpoints-v1.1/backend/app.yaml b/appengine/standard/endpoints-v1.1/backend/app.yaml new file mode 100644 index 000000000000..0dd9abd9ce05 --- /dev/null +++ b/appengine/standard/endpoints-v1.1/backend/app.yaml @@ -0,0 +1,27 @@ +runtime: python27 +threadsafe: true +api_version: 1 + +handlers: +# The endpoints handler must be mapped to /_ah/api. +- url: /_ah/api/.* + script: main.api + +libraries: +- name: pycrypto + version: 2.6 + +# Endpoints 1.1 uses background threads for caching and reporting to service +# management and service control. +manual_scaling: + instances: 1 + +beta_settings: + use_endpoints_api_management: true + endpoints_swagger_spec_file: echo-v1_swagger.json + +env_variables: + # Replace with your endpoints service name. + ENDPOINTS_SERVICE_NAME: your-service.appspot.com + # Replace with the version Id of your uploaded Endpoints service. + ENDPOINTS_SERVICE_VERSION: 2016-08-01r01 diff --git a/appengine/standard/endpoints/backend/appengine_config.py b/appengine/standard/endpoints-v1.1/backend/appengine_config.py similarity index 100% rename from appengine/standard/endpoints/backend/appengine_config.py rename to appengine/standard/endpoints-v1.1/backend/appengine_config.py diff --git a/appengine/standard/endpoints/backend/echo-v1_swagger.json b/appengine/standard/endpoints-v1.1/backend/echo-v1_swagger.json similarity index 100% rename from appengine/standard/endpoints/backend/echo-v1_swagger.json rename to appengine/standard/endpoints-v1.1/backend/echo-v1_swagger.json diff --git a/appengine/standard/endpoints-v1.1/backend/main.py b/appengine/standard/endpoints-v1.1/backend/main.py new file mode 100644 index 000000000000..dce378f08316 --- /dev/null +++ b/appengine/standard/endpoints-v1.1/backend/main.py @@ -0,0 +1,67 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is a sample Hello World API implemented using Google Cloud +Endpoints.""" + +# [START imports] +import endpoints +from protorpc import message_types +from protorpc import messages +from protorpc import remote +# [END imports] + +# [START messages] +class Echo(messages.Message): + """A proto Message that contains a simple string field.""" + content = messages.StringField(1) +# [END messages] + +# [START echo_api] +@endpoints.api(name='echo', version='v1') +class EchoApi(remote.Service): + + @endpoints.method( + # This method takes an Echo message. + Echo, + # This method returns an Echo message. + Echo, + path='echo', + http_method='POST', + name='echo') + def echo(self, request): + return Echo(content=request.content) + + @endpoints.method( + # This method takes an empty request body. + message_types.VoidMessage, + # This method returns an Echo message. + Echo, + path='echo/getUserEmail', + http_method='GET', + # Require auth tokens to have the following scopes to access this API. + scopes=[endpoints.EMAIL_SCOPE]) + def get_user_email(self, request): + user = endpoints.get_current_user() + # If there's no user defined, the request was unauthenticated, so we + # raise 401 Unauthorized. + if not user: + raise endpoints.UnauthorizedException + return Echo(content=user.email()) +# [END echo_api] + + +# [START api_server] +api = endpoints.api_server([EchoApi]) +# [END api_server] diff --git a/appengine/standard/endpoints-v1.1/backend/main_test.py b/appengine/standard/endpoints-v1.1/backend/main_test.py new file mode 100644 index 000000000000..ce1ebe395876 --- /dev/null +++ b/appengine/standard/endpoints-v1.1/backend/main_test.py @@ -0,0 +1,38 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import endpoints +import main +import mock +import pytest +from protorpc import message_types + + +def test_echo(): + api = main.EchoApi() + response = api.echo(main.Echo(content='Hello world!')) + assert 'Hello world!' == response.content + +def test_get_user_email(): + api = main.EchoApi() + + with mock.patch('main.endpoints.get_current_user') as user_mock: + user_mock.return_value = None + with pytest.raises(endpoints.UnauthorizedException): + api.get_user_email(message_types.VoidMessage()) + + user_mock.return_value = mock.Mock() + user_mock.return_value.email.return_value = 'user@example.com' + response = api.get_user_email(message_types.VoidMessage()) + assert 'user@example.com' == response.content diff --git a/appengine/standard/endpoints/backend/app.yaml b/appengine/standard/endpoints/backend/app.yaml index 0dd9abd9ce05..31030b73367e 100644 --- a/appengine/standard/endpoints/backend/app.yaml +++ b/appengine/standard/endpoints/backend/app.yaml @@ -3,25 +3,14 @@ threadsafe: true api_version: 1 handlers: -# The endpoints handler must be mapped to /_ah/api. -- url: /_ah/api/.* +# The endpoints handler must be mapped to /_ah/spi. +# Apps send requests to /_ah/api, but the endpoints service handles mapping +# those requests to /_ah/spi. +- url: /_ah/spi/.* script: main.api libraries: - name: pycrypto version: 2.6 - -# Endpoints 1.1 uses background threads for caching and reporting to service -# management and service control. -manual_scaling: - instances: 1 - -beta_settings: - use_endpoints_api_management: true - endpoints_swagger_spec_file: echo-v1_swagger.json - -env_variables: - # Replace with your endpoints service name. - ENDPOINTS_SERVICE_NAME: your-service.appspot.com - # Replace with the version Id of your uploaded Endpoints service. - ENDPOINTS_SERVICE_VERSION: 2016-08-01r01 +- name: endpoints + version: 1.0 diff --git a/appengine/standard/endpoints/backend/main.py b/appengine/standard/endpoints/backend/main.py index dce378f08316..fc4049b1309a 100644 --- a/appengine/standard/endpoints/backend/main.py +++ b/appengine/standard/endpoints/backend/main.py @@ -22,46 +22,128 @@ from protorpc import remote # [END imports] + # [START messages] -class Echo(messages.Message): - """A proto Message that contains a simple string field.""" - content = messages.StringField(1) +class Greeting(messages.Message): + """Greeting that stores a message.""" + message = messages.StringField(1) + + +class GreetingCollection(messages.Message): + """Collection of Greetings.""" + items = messages.MessageField(Greeting, 1, repeated=True) + + +STORED_GREETINGS = GreetingCollection(items=[ + Greeting(message='hello world!'), + Greeting(message='goodbye world!'), +]) # [END messages] -# [START echo_api] -@endpoints.api(name='echo', version='v1') -class EchoApi(remote.Service): + +# [START greeting_api] +@endpoints.api(name='greeting', version='v1') +class GreetingApi(remote.Service): + + @endpoints.method( + # This method does not take a request message. + message_types.VoidMessage, + # This method returns a GreetingCollection message. + GreetingCollection, + path='greetings', + http_method='GET', + name='greetings.list') + def list_greetings(self, unused_request): + return STORED_GREETINGS + + # ResourceContainers are used to encapsuate a request body and url + # parameters. This one is used to represent the Greeting ID for the + # greeting_get method. + GET_RESOURCE = endpoints.ResourceContainer( + # The request body should be empty. + message_types.VoidMessage, + # Accept one url parameter: and integer named 'id' + id=messages.IntegerField(1, variant=messages.Variant.INT32)) + + @endpoints.method( + # Use the ResourceContainer defined above to accept an empty body + # but an ID in the query string. + GET_RESOURCE, + # This method returns a Greeting message. + Greeting, + # The path defines the source of the URL parameter 'id'. If not + # specified here, it would need to be in the query string. + path='greetings/{id}', + http_method='GET', + name='greetings.get') + def get_greeting(self, request): + try: + # request.id is used to access the URL parameter. + return STORED_GREETINGS.items[request.id] + except (IndexError, TypeError): + raise endpoints.NotFoundException( + 'Greeting {} not found'.format(request.id)) + # [END greeting_api] + + # [START multiply] + # This ResourceContainer is similar to the one used for get_greeting, but + # this one also contains a request body in the form of a Greeting message. + MULTIPLY_RESOURCE = endpoints.ResourceContainer( + Greeting, + times=messages.IntegerField(2, variant=messages.Variant.INT32, + required=True)) @endpoints.method( - # This method takes an Echo message. - Echo, - # This method returns an Echo message. - Echo, - path='echo', + # This method accepts a request body containing a Greeting message + # and a URL parameter specifying how many times to multiply the + # message. + MULTIPLY_RESOURCE, + # This method returns a Greeting message. + Greeting, + path='greetings/multiply/{times}', http_method='POST', - name='echo') - def echo(self, request): - return Echo(content=request.content) + name='greetings.multiply') + def multiply_greeting(self, request): + return Greeting(message=request.message * request.times) + # [END multiply] + + +# [START auth_config] +WEB_CLIENT_ID = 'replace this with your web client application ID' +ANDROID_CLIENT_ID = 'replace this with your Android client ID' +IOS_CLIENT_ID = 'replace this with your iOS client ID' +ANDROID_AUDIENCE = WEB_CLIENT_ID +ALLOWED_CLIENT_IDS = [ + WEB_CLIENT_ID, ANDROID_CLIENT_ID, IOS_CLIENT_ID, + endpoints.API_EXPLORER_CLIENT_ID] +# [END auth_config] + + +# [START authed_greeting_api] +@endpoints.api( + name='authed_greeting', + version='v1', + # Only allowed configured Client IDs to access this API. + allowed_client_ids=ALLOWED_CLIENT_IDS, + # Only allow auth tokens with the given audience to access this API. + audiences=[ANDROID_AUDIENCE], + # Require auth tokens to have the following scopes to access this API. + scopes=[endpoints.EMAIL_SCOPE]) +class AuthedGreetingApi(remote.Service): @endpoints.method( - # This method takes an empty request body. message_types.VoidMessage, - # This method returns an Echo message. - Echo, - path='echo/getUserEmail', - http_method='GET', - # Require auth tokens to have the following scopes to access this API. - scopes=[endpoints.EMAIL_SCOPE]) - def get_user_email(self, request): + Greeting, + path='greet', + http_method='POST', + name='greet') + def greet(self, request): user = endpoints.get_current_user() - # If there's no user defined, the request was unauthenticated, so we - # raise 401 Unauthorized. - if not user: - raise endpoints.UnauthorizedException - return Echo(content=user.email()) -# [END echo_api] + user_name = user.email() if user else 'Anonymous' + return Greeting(message='Hello, {}'.format(user_name)) +# [END authed_greeting_api] # [START api_server] -api = endpoints.api_server([EchoApi]) +api = endpoints.api_server([GreetingApi, AuthedGreetingApi]) # [END api_server] diff --git a/appengine/standard/endpoints/backend/main_test.py b/appengine/standard/endpoints/backend/main_test.py index ce1ebe395876..56fefa2ead66 100644 --- a/appengine/standard/endpoints/backend/main_test.py +++ b/appengine/standard/endpoints/backend/main_test.py @@ -12,27 +12,42 @@ # See the License for the specific language governing permissions and # limitations under the License. -import endpoints import main import mock -import pytest from protorpc import message_types -def test_echo(): - api = main.EchoApi() - response = api.echo(main.Echo(content='Hello world!')) - assert 'Hello world!' == response.content +def test_list_greetings(testbed): + api = main.GreetingApi() + response = api.list_greetings(message_types.VoidMessage()) + assert len(response.items) == 2 -def test_get_user_email(): - api = main.EchoApi() + +def test_get_greeting(testbed): + api = main.GreetingApi() + request = main.GreetingApi.get_greeting.remote.request_type(id=1) + response = api.get_greeting(request) + assert response.message == 'goodbye world!' + + +def test_multiply_greeting(testbed): + api = main.GreetingApi() + request = main.GreetingApi.multiply_greeting.remote.request_type( + times=4, + message='help I\'m trapped in a test case.') + response = api.multiply_greeting(request) + assert response.message == 'help I\'m trapped in a test case.' * 4 + + +def test_authed_greet(testbed): + api = main.AuthedGreetingApi() with mock.patch('main.endpoints.get_current_user') as user_mock: user_mock.return_value = None - with pytest.raises(endpoints.UnauthorizedException): - api.get_user_email(message_types.VoidMessage()) + response = api.greet(message_types.VoidMessage()) + assert response.message == 'Hello, Anonymous' user_mock.return_value = mock.Mock() user_mock.return_value.email.return_value = 'user@example.com' - response = api.get_user_email(message_types.VoidMessage()) - assert 'user@example.com' == response.content + response = api.greet(message_types.VoidMessage()) + assert response.message == 'Hello, user@example.com'