diff --git a/appengine/standard/pubsub/README.md b/appengine/standard/pubsub/README.md new file mode 100755 index 00000000000..1b5e1f0c886 --- /dev/null +++ b/appengine/standard/pubsub/README.md @@ -0,0 +1,77 @@ +# Python Google Cloud Pub/Sub sample for Google App Engine Standard Environment + +[![Open in Cloud Shell][shell_img]][shell_link] + +[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png +[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/standard/pubsub/README.md + +This demonstrates how to send and receive messages using [Google Cloud Pub/Sub](https://cloud.google.com/pubsub) on [Google App Engine Standard Environment](https://cloud.google.com/appengine/docs/standard/). + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. Enable the Cloud Pub/Sub API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/pubsub/overview). + +2. Create a topic and subscription. + + $ gcloud pubsub topics create [your-topic-name] + $ gcloud pubsub subscriptions create [your-subscription-name] \ + --topic [your-topic-name] \ + --push-endpoint \ + https://[your-app-id].appspot.com/_ah/push-handlers/receive_messages/token=[your-token] \ + --ack-deadline 30 + +3. Update the environment variables in ``app.yaml``. + +## Running locally + +Refer to the [top-level README](../README.md) for instructions on running and deploying. + +When running locally, you can use the [Google Cloud SDK](https://cloud.google.com/sdk) to provide authentication to use Google Cloud APIs: + + $ gcloud init + +Install dependencies, preferably with a virtualenv: + + $ virtualenv env + $ source env/bin/activate + $ pip install -r requirements.txt + +Then set environment variables before starting your application: + + $ export GOOGLE_CLOUD_PROJECT=[your-project-name] + $ export PUBSUB_VERIFICATION_TOKEN=[your-verification-token] + $ export PUBSUB_TOPIC=[your-topic] + $ python main.py + +### Simulating push notifications + +The application can send messages locally, but it is not able to receive push messages locally. You can, however, simulate a push message by making an HTTP request to the local push notification endpoint. There is an included ``sample_message.json``. You can use +``curl`` or [httpie](https://github.com/jkbrzt/httpie) to POST this: + + $ curl -i --data @sample_message.json ":8080/_ah/push-handlers/receive_messages?token=[your-token]" + +Or + + $ http POST ":8080/_ah/push-handlers/receive_messages?token=[your-token]" < sample_message.json + +Response: + + HTTP/1.0 200 OK + Content-Length: 2 + Content-Type: text/html; charset=utf-8 + Date: Mon, 10 Aug 2015 17:52:03 GMT + Server: Werkzeug/0.10.4 Python/2.7.10 + + OK + +After the request completes, you can refresh ``localhost:8080`` and see the message in the list of received messages. + +## Running on App Engine + +Deploy using `gcloud`: + + gcloud app deploy app.yaml + +You can now access the application at `https://your-app-id.appspot.com`. You can use the form to submit messages, but it's non-deterministic which instance of your application will receive the notification. You can send multiple messages and refresh the page to see the received message. diff --git a/appengine/standard/pubsub/app.yaml b/appengine/standard/pubsub/app.yaml new file mode 100755 index 00000000000..f750c378b42 --- /dev/null +++ b/appengine/standard/pubsub/app.yaml @@ -0,0 +1,19 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: / + script: main.app + +- url: /_ah/push-handlers/.* + script: main.app + login: admin + +#[START env] +env_variables: + PUBSUB_TOPIC: your-topic + # This token is used to verify that requests originate from your + # application. It can be any sufficiently random string. + PUBSUB_VERIFICATION_TOKEN: 1234abc +#[END env] diff --git a/appengine/standard/pubsub/main.py b/appengine/standard/pubsub/main.py new file mode 100755 index 00000000000..efa8766516e --- /dev/null +++ b/appengine/standard/pubsub/main.py @@ -0,0 +1,94 @@ +# Copyright 2018 Google, LLC. +# +# 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. + +# [START app] +import base64 +import json +import logging +import os + +from flask import current_app, Flask, render_template, request +from googleapiclient.discovery import build + + +app = Flask(__name__) + +# Configure the following environment variables via app.yaml +# This is used in the push request handler to veirfy that the request came from +# pubsub and originated from a trusted source. +app.config['PUBSUB_VERIFICATION_TOKEN'] = \ + os.environ['PUBSUB_VERIFICATION_TOKEN'] +app.config['PUBSUB_TOPIC'] = os.environ['PUBSUB_TOPIC'] +app.config['GCLOUD_PROJECT'] = os.environ['GOOGLE_CLOUD_PROJECT'] + + +# Global list to storage messages received by this instance. +MESSAGES = [] + + +# [START index] +@app.route('/', methods=['GET', 'POST']) +def index(): + if request.method == 'GET': + return render_template('index.html', messages=MESSAGES) + + data = request.form.get('payload', 'Example payload').encode('utf-8') + + service = build('pubsub', 'v1') + topic_path = 'projects/{project_id}/topics/{topic}'.format( + project_id=app.config['GCLOUD_PROJECT'], + topic=app.config['PUBSUB_TOPIC'] + ) + service.projects().topics().publish( + topic=topic_path, body={ + "messages": [{ + "data": base64.b64encode(data) + }] + }).execute() + + return 'OK', 200 +# [END index] + + +# [START push] +@app.route('/_ah/push-handlers/receive_messages', methods=['POST']) +def receive_messages_handler(): + if (request.args.get('token', '') != + current_app.config['PUBSUB_VERIFICATION_TOKEN']): + return 'Invalid request', 400 + + envelope = json.loads(request.data.decode('utf-8')) + payload = base64.b64decode(envelope['message']['data']) + + MESSAGES.append(payload) + + # Returning any 2xx status indicates successful receipt of the message. + return 'OK', 200 +# [END push] + + +@app.errorhandler(500) +def server_error(e): + logging.exception('An error occurred during a request.') + return """ + An internal error occurred:
{}
+ See logs for full stacktrace. + """.format(e), 500 + + +if __name__ == '__main__': + # This is used when running locally. Gunicorn is used to run the + # application on Google App Engine. See entrypoint in app.yaml. + app.run(host='127.0.0.1', port=8080, debug=True) +# [END app] diff --git a/appengine/standard/pubsub/main_test.py b/appengine/standard/pubsub/main_test.py new file mode 100755 index 00000000000..f2afb5e4a69 --- /dev/null +++ b/appengine/standard/pubsub/main_test.py @@ -0,0 +1,70 @@ +# Copyright 2018 Google, LLC. +# +# 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 base64 +import json +import os + +import pytest + +import main + + +@pytest.fixture +def client(): + main.app.testing = True + return main.app.test_client() + + +def test_index(client): + r = client.get('/') + assert r.status_code == 200 + + +def test_post_index(client): + r = client.post('/', data={'payload': 'Test payload'}) + assert r.status_code == 200 + + +def test_push_endpoint(client): + url = '/_ah/push-handlers/receive_messages?token=' + \ + os.environ['PUBSUB_VERIFICATION_TOKEN'] + + r = client.post( + url, + data=json.dumps({ + "message": { + "data": base64.b64encode( + u'Test message'.encode('utf-8') + ).decode('utf-8') + } + }) + ) + + assert r.status_code == 200 + + # Make sure the message is visible on the home page. + r = client.get('/') + assert r.status_code == 200 + assert 'Test message' in r.data.decode('utf-8') + + +def test_push_endpoint_errors(client): + # no token + r = client.post('/_ah/push-handlers/receive_messages') + assert r.status_code == 400 + + # invalid token + r = client.post('/_ah/push-handlers/receive_messages?token=bad') + assert r.status_code == 400 diff --git a/appengine/standard/pubsub/requirements.txt b/appengine/standard/pubsub/requirements.txt new file mode 100755 index 00000000000..fedf080fc95 --- /dev/null +++ b/appengine/standard/pubsub/requirements.txt @@ -0,0 +1,2 @@ +Flask==0.12.2 +google-api-python-client==1.7.3 diff --git a/appengine/standard/pubsub/sample_message.json b/appengine/standard/pubsub/sample_message.json new file mode 100755 index 00000000000..8fe62d23fb9 --- /dev/null +++ b/appengine/standard/pubsub/sample_message.json @@ -0,0 +1,5 @@ +{ + "message": { + "data": "SGVsbG8sIFdvcmxkIQ==" + } +} diff --git a/appengine/standard/pubsub/templates/index.html b/appengine/standard/pubsub/templates/index.html new file mode 100755 index 00000000000..398fd9de21e --- /dev/null +++ b/appengine/standard/pubsub/templates/index.html @@ -0,0 +1,38 @@ +{# +# Copyright 2015 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. +#} + + + + Pub/Sub Python on Google App Engine Flexible Environment + + +
+

Messages received by this instance:

+ +

Note: because your application is likely running multiple instances, each instance will have a different list of messages.

+
+ +
+ + +
+ + +