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.
+#}
+
+
+
+ Messages received by this instance:
+Note: because your application is likely running multiple instances, each instance will have a different list of messages.
+