Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pub/Sub Sample for App Engine Standard #1606

Merged
merged 2 commits into from Jul 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 77 additions & 0 deletions 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.
19 changes: 19 additions & 0 deletions 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]
94 changes: 94 additions & 0 deletions 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: <pre>{}</pre>
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]
70 changes: 70 additions & 0 deletions 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
2 changes: 2 additions & 0 deletions appengine/standard/pubsub/requirements.txt
@@ -0,0 +1,2 @@
Flask==0.12.2
google-api-python-client==1.7.3
5 changes: 5 additions & 0 deletions appengine/standard/pubsub/sample_message.json
@@ -0,0 +1,5 @@
{
"message": {
"data": "SGVsbG8sIFdvcmxkIQ=="
}
}
38 changes: 38 additions & 0 deletions 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.
#}
<!doctype html>
<html>
<head>
<title>Pub/Sub Python on Google App Engine Flexible Environment</title>
</head>
<body>
<div>
<p>Messages received by this instance:</p>
<ul>
{% for message in messages: %}
<li>{{message}}</li>
{% endfor %}
</ul>
<p><small>Note: because your application is likely running multiple instances, each instance will have a different list of messages.</small></p>
</div>
<!-- [START form] -->
<form method="post">
<textarea name="payload" placeholder="Enter message here"></textarea>
<input type="submit">
</form>
<!-- [END form] -->
</body>
</html>