From 9d819cef5faf7a22cb4fc8738de5f95c5531b2f5 Mon Sep 17 00:00:00 2001 From: Jinglun Dong <1683035+jinglundong@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:18:11 +0000 Subject: [PATCH 1/9] Move samples of GAE Flex Python 3.8 and later to the default ./flexible directory. Update README.md files and some references. Remove the Python 3.7 and earlier examples from ./flexible directory. --- appengine/flexible/README.md | 12 +- appengine/flexible/analytics/README.md | 25 -- appengine/flexible/analytics/app.yaml | 25 -- appengine/flexible/analytics/main.py | 75 ----- appengine/flexible/analytics/main_test.py | 47 --- .../flexible/analytics/requirements-test.txt | 3 - appengine/flexible/analytics/requirements.txt | 4 - appengine/flexible/datastore/README.md | 24 -- appengine/flexible/datastore/app.yaml | 20 -- appengine/flexible/datastore/main.py | 82 ----- appengine/flexible/datastore/main_test.py | 24 -- .../flexible/datastore/requirements-test.txt | 1 - appengine/flexible/datastore/requirements.txt | 4 - appengine/flexible/disk/app.yaml | 20 -- appengine/flexible/disk/main.py | 74 ----- appengine/flexible/disk/main_test.py | 24 -- appengine/flexible/disk/requirements-test.txt | 1 - appengine/flexible/disk/requirements.txt | 3 - appengine/flexible/django_cloudsql/README.md | 25 -- appengine/flexible/django_cloudsql/app.yaml | 27 -- appengine/flexible/django_cloudsql/manage.py | 24 -- .../django_cloudsql/mysite/__init__.py | 0 .../django_cloudsql/mysite/settings.py | 180 ----------- .../flexible/django_cloudsql/mysite/urls.py | 25 -- .../flexible/django_cloudsql/mysite/wsgi.py | 22 -- .../django_cloudsql/noxfile_config.py | 39 --- .../django_cloudsql/polls/__init__.py | 0 .../flexible/django_cloudsql/polls/admin.py | 19 -- .../flexible/django_cloudsql/polls/apps.py | 19 -- .../flexible/django_cloudsql/polls/models.py | 28 -- .../django_cloudsql/polls/test_polls.py | 22 -- .../flexible/django_cloudsql/polls/urls.py | 21 -- .../flexible/django_cloudsql/polls/views.py | 19 -- .../django_cloudsql/requirements-test.txt | 2 - .../flexible/django_cloudsql/requirements.txt | 7 - .../flexible/extending_runtime/.dockerignore | 8 - .../flexible/extending_runtime/Dockerfile | 34 -- appengine/flexible/extending_runtime/app.yaml | 16 - appengine/flexible/extending_runtime/main.py | 46 --- .../flexible/extending_runtime/main_test.py | 31 -- .../extending_runtime/requirements-test.txt | 1 - .../extending_runtime/requirements.txt | 3 - appengine/flexible/hello_world/app.yaml | 5 +- appengine/flexible/hello_world/main.py | 4 +- appengine/flexible/hello_world/main_test.py | 4 +- .../hello_world/noxfile_config.py | 0 .../hello_world/requirements-test.txt | 2 +- .../flexible/hello_world/requirements.txt | 3 +- .../flexible/hello_world_django/.gitignore | 1 - .../flexible/hello_world_django/README.md | 63 ---- .../flexible/hello_world_django/app.yaml | 20 -- .../hello_world_django/helloworld/__init__.py | 0 .../hello_world_django/helloworld/views.py | 22 -- .../flexible/hello_world_django/manage.py | 24 -- .../project_name/__init__.py | 0 .../project_name/settings.py | 116 ------- .../hello_world_django/project_name/urls.py | 39 --- .../hello_world_django/project_name/wsgi.py | 30 -- .../hello_world_django/requirements-test.txt | 1 - .../hello_world_django/requirements.txt | 4 - appengine/flexible/metadata/app.yaml | 20 -- appengine/flexible/metadata/main.py | 68 ---- .../flexible/metadata/requirements-test.txt | 1 - appengine/flexible/metadata/requirements.txt | 4 - .../flexible/multiple_services/README.md | 67 ---- .../gateway-service/app.yaml | 24 -- .../multiple_services/gateway-service/main.py | 58 ---- .../gateway-service/requirements-test.txt | 1 - .../gateway-service/requirements.txt | 4 - .../gateway-service/services_config.py | 57 ---- .../multiple_services/static-service/app.yaml | 24 -- .../multiple_services/static-service/main.py | 46 --- .../static-service/requirements-test.txt | 1 - .../static-service/requirements.txt | 4 - .../static-service/static/index.html | 32 -- .../static-service/static/index.js | 36 --- .../static-service/static/style.css | 19 -- appengine/flexible/numpy/app.yaml | 20 -- appengine/flexible/numpy/main.py | 48 --- appengine/flexible/numpy/main_test.py | 24 -- .../flexible/numpy/requirements-test.txt | 1 - appengine/flexible/numpy/requirements.txt | 6 - appengine/flexible/pubsub/README.md | 76 ----- appengine/flexible/pubsub/app.yaml | 28 -- appengine/flexible/pubsub/main.py | 91 ------ appengine/flexible/pubsub/main_test.py | 69 ---- .../flexible/pubsub/requirements-test.txt | 1 - appengine/flexible/pubsub/requirements.txt | 4 - appengine/flexible/pubsub/sample_message.json | 5 - .../flexible/pubsub/templates/index.html | 36 --- appengine/flexible/scipy/.gitignore | 1 - appengine/flexible/scipy/README.md | 9 - appengine/flexible/scipy/app.yaml | 20 -- .../flexible/scipy/assets/google_logo.jpg | Bin 21568 -> 0 bytes appengine/flexible/scipy/main.py | 52 --- appengine/flexible/scipy/main_test.py | 30 -- appengine/flexible/scipy/noxfile_config.py | 38 --- .../flexible/scipy/requirements-test.txt | 1 - appengine/flexible/scipy/requirements.txt | 9 - appengine/flexible/static_files/README.md | 12 - appengine/flexible/static_files/app.yaml | 20 -- appengine/flexible/static_files/main.py | 42 --- appengine/flexible/static_files/main_test.py | 26 -- .../static_files/requirements-test.txt | 1 - .../flexible/static_files/requirements.txt | 3 - .../flexible/static_files/static/main.css | 21 -- .../static_files/templates/index.html | 29 -- appengine/flexible/storage/app.yaml | 9 +- appengine/flexible/storage/main.py | 34 +- appengine/flexible/storage/main_test.py | 18 +- appengine/flexible/storage/noxfile_config.py | 4 +- .../flexible/storage/requirements-test.txt | 3 +- appengine/flexible/storage/requirements.txt | 12 +- appengine/flexible/tasks/Dockerfile | 31 -- appengine/flexible/tasks/README.md | 115 ------- appengine/flexible/tasks/app.flexible.yaml | 20 -- appengine/flexible/tasks/app.yaml | 15 - .../tasks/create_app_engine_queue_task.py | 117 ------- .../create_app_engine_queue_task_test.py | 27 -- appengine/flexible/tasks/main.py | 41 --- appengine/flexible/tasks/main_test.py | 45 --- .../flexible/tasks/requirements-test.txt | 1 - appengine/flexible/tasks/requirements.txt | 4 - appengine/flexible/tasks/snippets.py | 303 ------------------ appengine/flexible/tasks/snippets_test.py | 127 -------- appengine/flexible/tasks/templates/index.html | 21 -- appengine/flexible/twilio/README.md | 34 -- appengine/flexible/twilio/app.yaml | 27 -- appengine/flexible/twilio/main.py | 87 ----- appengine/flexible/twilio/main_test.py | 77 ----- .../flexible/twilio/requirements-test.txt | 3 - appengine/flexible/twilio/requirements.txt | 4 - appengine/flexible/websockets/README.md | 11 - appengine/flexible/websockets/app.yaml | 36 --- appengine/flexible/websockets/main.py | 53 --- appengine/flexible/websockets/main_test.py | 82 ----- .../flexible/websockets/requirements-test.txt | 3 - .../flexible/websockets/requirements.txt | 5 - .../flexible/websockets/templates/index.html | 96 ------ appengine/flexible_ubuntu/README.md | 76 ----- .../flexible_ubuntu/hello_world/app.yaml | 34 -- appengine/flexible_ubuntu/hello_world/main.py | 32 -- .../flexible_ubuntu/hello_world/main_test.py | 24 -- .../hello_world/requirements-test.txt | 1 - .../hello_world/requirements.txt | 2 - appengine/flexible_ubuntu/storage/README.md | 37 --- appengine/flexible_ubuntu/storage/app.yaml | 28 -- appengine/flexible_ubuntu/storage/main.py | 87 ----- .../flexible_ubuntu/storage/main_test.py | 62 ---- .../flexible_ubuntu/storage/noxfile_config.py | 44 --- .../storage/requirements-test.txt | 2 - .../flexible_ubuntu/storage/requirements.txt | 4 - 152 files changed, 55 insertions(+), 4456 deletions(-) delete mode 100644 appengine/flexible/analytics/README.md delete mode 100644 appengine/flexible/analytics/app.yaml delete mode 100644 appengine/flexible/analytics/main.py delete mode 100644 appengine/flexible/analytics/main_test.py delete mode 100644 appengine/flexible/analytics/requirements-test.txt delete mode 100644 appengine/flexible/analytics/requirements.txt delete mode 100644 appengine/flexible/datastore/README.md delete mode 100644 appengine/flexible/datastore/app.yaml delete mode 100644 appengine/flexible/datastore/main.py delete mode 100644 appengine/flexible/datastore/main_test.py delete mode 100644 appengine/flexible/datastore/requirements-test.txt delete mode 100644 appengine/flexible/datastore/requirements.txt delete mode 100644 appengine/flexible/disk/app.yaml delete mode 100644 appengine/flexible/disk/main.py delete mode 100644 appengine/flexible/disk/main_test.py delete mode 100644 appengine/flexible/disk/requirements-test.txt delete mode 100644 appengine/flexible/disk/requirements.txt delete mode 100644 appengine/flexible/django_cloudsql/README.md delete mode 100644 appengine/flexible/django_cloudsql/app.yaml delete mode 100755 appengine/flexible/django_cloudsql/manage.py delete mode 100644 appengine/flexible/django_cloudsql/mysite/__init__.py delete mode 100644 appengine/flexible/django_cloudsql/mysite/settings.py delete mode 100644 appengine/flexible/django_cloudsql/mysite/urls.py delete mode 100644 appengine/flexible/django_cloudsql/mysite/wsgi.py delete mode 100644 appengine/flexible/django_cloudsql/noxfile_config.py delete mode 100644 appengine/flexible/django_cloudsql/polls/__init__.py delete mode 100644 appengine/flexible/django_cloudsql/polls/admin.py delete mode 100644 appengine/flexible/django_cloudsql/polls/apps.py delete mode 100644 appengine/flexible/django_cloudsql/polls/models.py delete mode 100644 appengine/flexible/django_cloudsql/polls/test_polls.py delete mode 100644 appengine/flexible/django_cloudsql/polls/urls.py delete mode 100644 appengine/flexible/django_cloudsql/polls/views.py delete mode 100644 appengine/flexible/django_cloudsql/requirements-test.txt delete mode 100644 appengine/flexible/django_cloudsql/requirements.txt delete mode 100644 appengine/flexible/extending_runtime/.dockerignore delete mode 100644 appengine/flexible/extending_runtime/Dockerfile delete mode 100644 appengine/flexible/extending_runtime/app.yaml delete mode 100644 appengine/flexible/extending_runtime/main.py delete mode 100644 appengine/flexible/extending_runtime/main_test.py delete mode 100644 appengine/flexible/extending_runtime/requirements-test.txt delete mode 100644 appengine/flexible/extending_runtime/requirements.txt rename appengine/{flexible_ubuntu => flexible}/hello_world/noxfile_config.py (100%) delete mode 100644 appengine/flexible/hello_world_django/.gitignore delete mode 100644 appengine/flexible/hello_world_django/README.md delete mode 100644 appengine/flexible/hello_world_django/app.yaml delete mode 100644 appengine/flexible/hello_world_django/helloworld/__init__.py delete mode 100644 appengine/flexible/hello_world_django/helloworld/views.py delete mode 100755 appengine/flexible/hello_world_django/manage.py delete mode 100644 appengine/flexible/hello_world_django/project_name/__init__.py delete mode 100644 appengine/flexible/hello_world_django/project_name/settings.py delete mode 100644 appengine/flexible/hello_world_django/project_name/urls.py delete mode 100644 appengine/flexible/hello_world_django/project_name/wsgi.py delete mode 100644 appengine/flexible/hello_world_django/requirements-test.txt delete mode 100644 appengine/flexible/hello_world_django/requirements.txt delete mode 100644 appengine/flexible/metadata/app.yaml delete mode 100644 appengine/flexible/metadata/main.py delete mode 100644 appengine/flexible/metadata/requirements-test.txt delete mode 100644 appengine/flexible/metadata/requirements.txt delete mode 100644 appengine/flexible/multiple_services/README.md delete mode 100644 appengine/flexible/multiple_services/gateway-service/app.yaml delete mode 100644 appengine/flexible/multiple_services/gateway-service/main.py delete mode 100644 appengine/flexible/multiple_services/gateway-service/requirements-test.txt delete mode 100644 appengine/flexible/multiple_services/gateway-service/requirements.txt delete mode 100644 appengine/flexible/multiple_services/gateway-service/services_config.py delete mode 100644 appengine/flexible/multiple_services/static-service/app.yaml delete mode 100644 appengine/flexible/multiple_services/static-service/main.py delete mode 100644 appengine/flexible/multiple_services/static-service/requirements-test.txt delete mode 100644 appengine/flexible/multiple_services/static-service/requirements.txt delete mode 100644 appengine/flexible/multiple_services/static-service/static/index.html delete mode 100644 appengine/flexible/multiple_services/static-service/static/index.js delete mode 100644 appengine/flexible/multiple_services/static-service/static/style.css delete mode 100644 appengine/flexible/numpy/app.yaml delete mode 100644 appengine/flexible/numpy/main.py delete mode 100644 appengine/flexible/numpy/main_test.py delete mode 100644 appengine/flexible/numpy/requirements-test.txt delete mode 100644 appengine/flexible/numpy/requirements.txt delete mode 100644 appengine/flexible/pubsub/README.md delete mode 100644 appengine/flexible/pubsub/app.yaml delete mode 100644 appengine/flexible/pubsub/main.py delete mode 100644 appengine/flexible/pubsub/main_test.py delete mode 100644 appengine/flexible/pubsub/requirements-test.txt delete mode 100644 appengine/flexible/pubsub/requirements.txt delete mode 100644 appengine/flexible/pubsub/sample_message.json delete mode 100644 appengine/flexible/pubsub/templates/index.html delete mode 100644 appengine/flexible/scipy/.gitignore delete mode 100644 appengine/flexible/scipy/README.md delete mode 100644 appengine/flexible/scipy/app.yaml delete mode 100644 appengine/flexible/scipy/assets/google_logo.jpg delete mode 100644 appengine/flexible/scipy/main.py delete mode 100644 appengine/flexible/scipy/main_test.py delete mode 100644 appengine/flexible/scipy/noxfile_config.py delete mode 100644 appengine/flexible/scipy/requirements-test.txt delete mode 100644 appengine/flexible/scipy/requirements.txt delete mode 100644 appengine/flexible/static_files/README.md delete mode 100644 appengine/flexible/static_files/app.yaml delete mode 100644 appengine/flexible/static_files/main.py delete mode 100644 appengine/flexible/static_files/main_test.py delete mode 100644 appengine/flexible/static_files/requirements-test.txt delete mode 100644 appengine/flexible/static_files/requirements.txt delete mode 100644 appengine/flexible/static_files/static/main.css delete mode 100644 appengine/flexible/static_files/templates/index.html delete mode 100644 appengine/flexible/tasks/Dockerfile delete mode 100644 appengine/flexible/tasks/README.md delete mode 100644 appengine/flexible/tasks/app.flexible.yaml delete mode 100644 appengine/flexible/tasks/app.yaml delete mode 100644 appengine/flexible/tasks/create_app_engine_queue_task.py delete mode 100644 appengine/flexible/tasks/create_app_engine_queue_task_test.py delete mode 100644 appengine/flexible/tasks/main.py delete mode 100644 appengine/flexible/tasks/main_test.py delete mode 100644 appengine/flexible/tasks/requirements-test.txt delete mode 100644 appengine/flexible/tasks/requirements.txt delete mode 100644 appengine/flexible/tasks/snippets.py delete mode 100644 appengine/flexible/tasks/snippets_test.py delete mode 100644 appengine/flexible/tasks/templates/index.html delete mode 100644 appengine/flexible/twilio/README.md delete mode 100644 appengine/flexible/twilio/app.yaml delete mode 100644 appengine/flexible/twilio/main.py delete mode 100644 appengine/flexible/twilio/main_test.py delete mode 100644 appengine/flexible/twilio/requirements-test.txt delete mode 100644 appengine/flexible/twilio/requirements.txt delete mode 100644 appengine/flexible/websockets/README.md delete mode 100644 appengine/flexible/websockets/app.yaml delete mode 100644 appengine/flexible/websockets/main.py delete mode 100644 appengine/flexible/websockets/main_test.py delete mode 100644 appengine/flexible/websockets/requirements-test.txt delete mode 100644 appengine/flexible/websockets/requirements.txt delete mode 100644 appengine/flexible/websockets/templates/index.html delete mode 100644 appengine/flexible_ubuntu/README.md delete mode 100644 appengine/flexible_ubuntu/hello_world/app.yaml delete mode 100644 appengine/flexible_ubuntu/hello_world/main.py delete mode 100644 appengine/flexible_ubuntu/hello_world/main_test.py delete mode 100644 appengine/flexible_ubuntu/hello_world/requirements-test.txt delete mode 100644 appengine/flexible_ubuntu/hello_world/requirements.txt delete mode 100644 appengine/flexible_ubuntu/storage/README.md delete mode 100644 appengine/flexible_ubuntu/storage/app.yaml delete mode 100644 appengine/flexible_ubuntu/storage/main.py delete mode 100644 appengine/flexible_ubuntu/storage/main_test.py delete mode 100644 appengine/flexible_ubuntu/storage/noxfile_config.py delete mode 100644 appengine/flexible_ubuntu/storage/requirements-test.txt delete mode 100644 appengine/flexible_ubuntu/storage/requirements.txt diff --git a/appengine/flexible/README.md b/appengine/flexible/README.md index 66e494ffaf4..a21487336b4 100644 --- a/appengine/flexible/README.md +++ b/appengine/flexible/README.md @@ -7,6 +7,9 @@ These are samples for using Python on Google App Engine Flexible Environment. These samples are typically referenced from the [docs](https://cloud.google.com/appengine/docs). +For code samples of Python version 3.7 and earlier, please check +https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/appengine/flexible_python37_and_earlier. + See our other [Google Cloud Platform github repos](https://github.com/GoogleCloudPlatform) for sample applications and scaffolding for other frameworks and use cases. @@ -31,12 +34,9 @@ In general, the samples typically require: cd python-docs-samples/appengine/flexible ``` -4. Open a sample folder, create a virtualenv, install dependencies, and run the sample: +4. Follow https://cloud.google.com/python/docs/setup to set up a Python development environment. Then run: ``` - cd hello-world - virtualenv env - source env/bin/activate pip install -r requirements.txt python main.py ``` @@ -66,8 +66,8 @@ Some samples in this repositories may have special deployment instructions. Refe ## Contributing changes -* See [CONTRIBUTING.md](../CONTRIBUTING.md) +* See [CONTRIBUTING.md](../../CONTRIBUTING.md) ## Licensing -* See [LICENSE](../LICENSE) +* See [LICENSE](../../LICENSE) diff --git a/appengine/flexible/analytics/README.md b/appengine/flexible/analytics/README.md deleted file mode 100644 index 2646e144ed0..00000000000 --- a/appengine/flexible/analytics/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Google Analytics Measurement Protocol sample for Google App Engine Flexible - -[![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/flexible/analytics/README.md - -This sample demonstrates how to use the [Google Analytics Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/v1/) (or any other SQL server) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Setup - -Before you can run or deploy the sample, you will need to do the following: - -1. Create a Google Analytics Property and obtain the Tracking ID. - -2. Update the environment variables in in ``app.yaml`` with your Tracking ID. - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -You will need to set the following environment variables via your shell before running the sample: - - $ export GA_TRACKING_ID=[your Tracking ID] - $ python main.py diff --git a/appengine/flexible/analytics/app.yaml b/appengine/flexible/analytics/app.yaml deleted file mode 100644 index 0f5590d7058..00000000000 --- a/appengine/flexible/analytics/app.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -#[START gae_flex_analytics_env_variables] -env_variables: - GA_TRACKING_ID: your-tracking-id -#[END gae_flex_analytics_env_variables] diff --git a/appengine/flexible/analytics/main.py b/appengine/flexible/analytics/main.py deleted file mode 100644 index 93e631b4f47..00000000000 --- a/appengine/flexible/analytics/main.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2015 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 gae_flex_analytics_track_event] -import logging -import os - -from flask import Flask -import requests - - -app = Flask(__name__) - - -# Environment variables are defined in app.yaml. -GA_TRACKING_ID = os.environ['GA_TRACKING_ID'] - - -def track_event(category, action, label=None, value=0): - data = { - 'v': '1', # API Version. - 'tid': GA_TRACKING_ID, # Tracking ID / Property ID. - # Anonymous Client Identifier. Ideally, this should be a UUID that - # is associated with particular user, device, or browser instance. - 'cid': '555', - 't': 'event', # Event hit type. - 'ec': category, # Event category. - 'ea': action, # Event action. - 'el': label, # Event label. - 'ev': value, # Event value, must be an integer - 'ua': 'Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14' - } - - response = requests.post( - 'https://www.google-analytics.com/collect', data=data) - - # If the request fails, this will raise a RequestException. Depending - # on your application's needs, this may be a non-error and can be caught - # by the caller. - response.raise_for_status() - - -@app.route('/') -def track_example(): - track_event( - category='Example', - action='test action') - return 'Event tracked.' - - -@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 gae_flex_analytics_track_event] diff --git a/appengine/flexible/analytics/main_test.py b/appengine/flexible/analytics/main_test.py deleted file mode 100644 index d9540714df0..00000000000 --- a/appengine/flexible/analytics/main_test.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2016 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 re - -import pytest -import responses - - -@pytest.fixture -def app(monkeypatch): - monkeypatch.setenv('GA_TRACKING_ID', '1234') - - import main - - main.app.testing = True - return main.app.test_client() - - -@responses.activate -def test_tracking(app): - responses.add( - responses.POST, - re.compile(r'.*'), - body='{}', - content_type='application/json') - - r = app.get('/') - - assert r.status_code == 200 - assert 'Event tracked' in r.data.decode('utf-8') - - assert len(responses.calls) == 1 - request_body = responses.calls[0].request.body - assert 'tid=1234' in request_body - assert 'ea=test+action' in request_body diff --git a/appengine/flexible/analytics/requirements-test.txt b/appengine/flexible/analytics/requirements-test.txt deleted file mode 100644 index 0dd3a1c6914..00000000000 --- a/appengine/flexible/analytics/requirements-test.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest==7.0.1 -responses==0.17.0; python_version < '3.7' -responses==0.20.0; python_version > '3.6' diff --git a/appengine/flexible/analytics/requirements.txt b/appengine/flexible/analytics/requirements.txt deleted file mode 100644 index 9fee7fc1fa3..00000000000 --- a/appengine/flexible/analytics/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 -requests[security]==2.27.1 diff --git a/appengine/flexible/datastore/README.md b/appengine/flexible/datastore/README.md deleted file mode 100644 index 3a3a0b9e4bc..00000000000 --- a/appengine/flexible/datastore/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Python Google Cloud Datastore sample for Google App Engine Flexible 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/flexible/datastore/README.md - -This sample demonstrates how to use [Google Cloud Datastore](https://cloud.google.com/datastore/) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Setup - -Before you can run or deploy the sample, you will need to enable the Cloud Datastore API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/datastore/overview). - -## 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 - -Starting your application: - - $ python main.py diff --git a/appengine/flexible/datastore/app.yaml b/appengine/flexible/datastore/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible/datastore/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible/datastore/main.py b/appengine/flexible/datastore/main.py deleted file mode 100644 index 2599fadfcab..00000000000 --- a/appengine/flexible/datastore/main.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2015 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 datetime -import logging -import socket - -from flask import Flask, request -from google.cloud import datastore - - -app = Flask(__name__) - - -def is_ipv6(addr): - """Checks if a given address is an IPv6 address.""" - try: - socket.inet_pton(socket.AF_INET6, addr) - return True - except socket.error: - return False - - -# [START gae_flex_datastore_app] -@app.route('/') -def index(): - ds = datastore.Client() - - user_ip = request.remote_addr - - # Keep only the first two octets of the IP address. - if is_ipv6(user_ip): - user_ip = ':'.join(user_ip.split(':')[:2]) - else: - user_ip = '.'.join(user_ip.split('.')[:2]) - - entity = datastore.Entity(key=ds.key('visit')) - entity.update({ - 'user_ip': user_ip, - 'timestamp': datetime.datetime.now(tz=datetime.timezone.utc) - }) - - ds.put(entity) - query = ds.query(kind='visit', order=('-timestamp',)) - - results = [] - for x in query.fetch(limit=10): - try: - results.append('Time: {timestamp} Addr: {user_ip}'.format(**x)) - except KeyError: - print("Error with result format, skipping entry.") - - output = 'Last 10 visits:\n{}'.format('\n'.join(results)) - - return output, 200, {'Content-Type': 'text/plain; charset=utf-8'} -# [END gae_flex_datastore_app] - - -@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) diff --git a/appengine/flexible/datastore/main_test.py b/appengine/flexible/datastore/main_test.py deleted file mode 100644 index 840032fe9a7..00000000000 --- a/appengine/flexible/datastore/main_test.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2015 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get('/', environ_base={'REMOTE_ADDR': '127.0.0.1'}) - assert r.status_code == 200 - assert 'Last 10 visits' in r.data.decode('utf-8') diff --git a/appengine/flexible/datastore/requirements-test.txt b/appengine/flexible/datastore/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/datastore/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/datastore/requirements.txt b/appengine/flexible/datastore/requirements.txt deleted file mode 100644 index 094e8f43b6b..00000000000 --- a/appengine/flexible/datastore/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -google-cloud-datastore==2.7.1 -gunicorn==20.1.0 diff --git a/appengine/flexible/disk/app.yaml b/appengine/flexible/disk/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible/disk/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible/disk/main.py b/appengine/flexible/disk/main.py deleted file mode 100644 index 1a1042f87c9..00000000000 --- a/appengine/flexible/disk/main.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2015 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 logging -import os -import socket - -from flask import Flask, request - - -app = Flask(__name__) - - -def is_ipv6(addr): - """Checks if a given address is an IPv6 address.""" - try: - socket.inet_pton(socket.AF_INET6, addr) - return True - except socket.error: - return False - - -# [START example] -@app.route('/') -def index(): - instance_id = os.environ.get('GAE_INSTANCE', '1') - - user_ip = request.remote_addr - - # Keep only the first two octets of the IP address. - if is_ipv6(user_ip): - user_ip = ':'.join(user_ip.split(':')[:2]) - else: - user_ip = '.'.join(user_ip.split('.')[:2]) - - with open('/tmp/seen.txt', 'a') as f: - f.write('{}\n'.format(user_ip)) - - with open('/tmp/seen.txt', 'r') as f: - seen = f.read() - - output = """ -Instance: {} -Seen: -{}""".format(instance_id, seen) - - return output, 200, {'Content-Type': 'text/plain; charset=utf-8'} -# [END example] - - -@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) diff --git a/appengine/flexible/disk/main_test.py b/appengine/flexible/disk/main_test.py deleted file mode 100644 index fc7122968ff..00000000000 --- a/appengine/flexible/disk/main_test.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2015 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get('/', environ_base={'REMOTE_ADDR': '127.0.0.1'}) - assert r.status_code == 200 - assert '127.0' in r.data.decode('utf-8') diff --git a/appengine/flexible/disk/requirements-test.txt b/appengine/flexible/disk/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/disk/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/disk/requirements.txt b/appengine/flexible/disk/requirements.txt deleted file mode 100644 index beeed7bed09..00000000000 --- a/appengine/flexible/disk/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 diff --git a/appengine/flexible/django_cloudsql/README.md b/appengine/flexible/django_cloudsql/README.md deleted file mode 100644 index 7db87fd2797..00000000000 --- a/appengine/flexible/django_cloudsql/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Getting started with Django on Google Cloud Platform on App Engine Flexible - -[![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/flexible/django_cloudsql/README.md - -This repository is an example of how to run a [Django](https://www.djangoproject.com/) -app on Google App Engine Flexible Environment. It uses the -[Writing your first Django app](https://docs.djangoproject.com/en/3.2/intro/tutorial01/) as the -example app to deploy. - - -# Tutorial -See our [Running Django in the App Engine Flexible Environment](https://cloud.google.com/python/django/flexible-environment) tutorial for instructions for setting up and deploying this sample application. - - -## Contributing changes - -* See [CONTRIBUTING.md](CONTRIBUTING.md) - - -## Licensing - -* See [LICENSE](LICENSE) diff --git a/appengine/flexible/django_cloudsql/app.yaml b/appengine/flexible/django_cloudsql/app.yaml deleted file mode 100644 index c8460e5ed3d..00000000000 --- a/appengine/flexible/django_cloudsql/app.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2021 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 runtime] -# [START gaeflex_py_django_app_yaml] -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT mysite.wsgi - -beta_settings: - cloud_sql_instances: PROJECT_ID:REGION:INSTANCE_NAME - -runtime_config: - python_version: 3.7 -# [END gaeflex_py_django_app_yaml] -# [END runtime] diff --git a/appengine/flexible/django_cloudsql/manage.py b/appengine/flexible/django_cloudsql/manage.py deleted file mode 100755 index 89fb5ae5607..00000000000 --- a/appengine/flexible/django_cloudsql/manage.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -# Copyright 2015 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 os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/appengine/flexible/django_cloudsql/mysite/__init__.py b/appengine/flexible/django_cloudsql/mysite/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/appengine/flexible/django_cloudsql/mysite/settings.py b/appengine/flexible/django_cloudsql/mysite/settings.py deleted file mode 100644 index 66ba0692e0c..00000000000 --- a/appengine/flexible/django_cloudsql/mysite/settings.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright 2015 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 io -import os - -import environ -from google.cloud import secretmanager - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# [START gaeflex_py_django_secret_config] -env = environ.Env(DEBUG=(bool, False)) -env_file = os.path.join(BASE_DIR, ".env") - -if os.path.isfile(env_file): - # Use a local secret file, if provided - - env.read_env(env_file) -# [START_EXCLUDE] -elif os.getenv("TRAMPOLINE_CI", None): - # Create local settings if running with CI, for unit testing - - placeholder = ( - f"SECRET_KEY=a\n" - "GS_BUCKET_NAME=None\n" - f"DATABASE_URL=sqlite://{os.path.join(BASE_DIR, 'db.sqlite3')}" - ) - env.read_env(io.StringIO(placeholder)) -# [END_EXCLUDE] -elif os.environ.get("GOOGLE_CLOUD_PROJECT", None): - # Pull secrets from Secret Manager - project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") - - client = secretmanager.SecretManagerServiceClient() - settings_name = os.environ.get("SETTINGS_NAME", "django_settings") - name = f"projects/{project_id}/secrets/{settings_name}/versions/latest" - payload = client.access_secret_version(name=name).payload.data.decode("UTF-8") - - env.read_env(io.StringIO(payload)) -else: - raise Exception("No local .env or GOOGLE_CLOUD_PROJECT detected. No secrets found.") -# [END gaeflex_py_django_secret_config] - -SECRET_KEY = env("SECRET_KEY") - -# SECURITY WARNING: don't run with debug turned on in production! -# Change this to "False" when you are ready for production -DEBUG = env("DEBUG") - -# SECURITY WARNING: App Engine's security features ensure that it is safe to -# have ALLOWED_HOSTS = ['*'] when the app is deployed. If you deploy a Django -# app not on App Engine, make sure to set an appropriate host here. -ALLOWED_HOSTS = ["*"] - -# Application definition - -INSTALLED_APPS = ( - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "polls", -) - -MIDDLEWARE = ( - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -) - -ROOT_URLCONF = "mysite.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "mysite.wsgi.application" - -# Database - -# [START dbconfig] -# [START gaeflex_py_django_database_config] -# Use django-environ to parse the connection string -DATABASES = {"default": env.db()} - -# If the flag as been set, configure to use proxy -if os.getenv("USE_CLOUD_SQL_AUTH_PROXY", None): - DATABASES["default"]["HOST"] = "127.0.0.1" - DATABASES["default"]["PORT"] = 5432 - -# [END gaeflex_py_django_database_config] -# [END dbconfig] - -# Use a in-memory sqlite3 database when testing in CI systems -if os.getenv("TRAMPOLINE_CI", None): - DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - } - } - - -# Password validation - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: 501 - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa: 501 - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa: 501 - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa: 501 - }, -] - -# Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# [START staticurl] -# [START gaeflex_py_django_static_config] -# Define static storage via django-storages[google] -GS_BUCKET_NAME = env("GS_BUCKET_NAME") -STATIC_URL = "/static/" -DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" -STATICFILES_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" -GS_DEFAULT_ACL = "publicRead" -# [END gaeflex_py_django_static_config] -# [END staticurl] - -# Default primary key field type -# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/appengine/flexible/django_cloudsql/mysite/urls.py b/appengine/flexible/django_cloudsql/mysite/urls.py deleted file mode 100644 index 62e72564fc2..00000000000 --- a/appengine/flexible/django_cloudsql/mysite/urls.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 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 gaeflex_py_django_local_static] -from django.conf import settings -from django.conf.urls.static import static -from django.contrib import admin -from django.urls import include, path - -urlpatterns = [ - path("", include("polls.urls")), - path("admin/", admin.site.urls), -] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -# [END gaeflex_py_django_local_static] diff --git a/appengine/flexible/django_cloudsql/mysite/wsgi.py b/appengine/flexible/django_cloudsql/mysite/wsgi.py deleted file mode 100644 index 968cf994b60..00000000000 --- a/appengine/flexible/django_cloudsql/mysite/wsgi.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2015 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 os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - -application = get_wsgi_application() diff --git a/appengine/flexible/django_cloudsql/noxfile_config.py b/appengine/flexible/django_cloudsql/noxfile_config.py deleted file mode 100644 index 8c838f846b2..00000000000 --- a/appengine/flexible/django_cloudsql/noxfile_config.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2020 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - 'ignored_versions': ["2.7"], - - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - 'envs': { - 'DJANGO_SETTINGS_MODULE': 'mysite.settings' - }, -} diff --git a/appengine/flexible/django_cloudsql/polls/__init__.py b/appengine/flexible/django_cloudsql/polls/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/appengine/flexible/django_cloudsql/polls/admin.py b/appengine/flexible/django_cloudsql/polls/admin.py deleted file mode 100644 index 5fc6d71455b..00000000000 --- a/appengine/flexible/django_cloudsql/polls/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2015 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. - -from django.contrib import admin - -from .models import Question - -admin.site.register(Question) diff --git a/appengine/flexible/django_cloudsql/polls/apps.py b/appengine/flexible/django_cloudsql/polls/apps.py deleted file mode 100644 index ae20e178dbe..00000000000 --- a/appengine/flexible/django_cloudsql/polls/apps.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2015 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. - -from django.apps import AppConfig - - -class PollsConfig(AppConfig): - name = 'polls' diff --git a/appengine/flexible/django_cloudsql/polls/models.py b/appengine/flexible/django_cloudsql/polls/models.py deleted file mode 100644 index 26cfaa68121..00000000000 --- a/appengine/flexible/django_cloudsql/polls/models.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2015 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. - -from django.db import models - - -class Question(models.Model): - question_text = models.CharField(max_length=200) - pub_date = models.DateTimeField('date published') - - -class Choice(models.Model): - question = models.ForeignKey(Question, on_delete=models.CASCADE) - choice_text = models.CharField(max_length=200) - votes = models.IntegerField(default=0) - -# Create your models here. diff --git a/appengine/flexible/django_cloudsql/polls/test_polls.py b/appengine/flexible/django_cloudsql/polls/test_polls.py deleted file mode 100644 index 17bf94fdcdc..00000000000 --- a/appengine/flexible/django_cloudsql/polls/test_polls.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2020 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. - -from django.test import Client, TestCase # noqa: 401 - - -class PollViewTests(TestCase): - def test_index_view(self): - response = self.client.get('/') - assert response.status_code == 200 - assert 'Hello, world' in str(response.content) diff --git a/appengine/flexible/django_cloudsql/polls/urls.py b/appengine/flexible/django_cloudsql/polls/urls.py deleted file mode 100644 index 9a0c88fe977..00000000000 --- a/appengine/flexible/django_cloudsql/polls/urls.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2015 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. - -from django.urls import re_path - -from . import views - -urlpatterns = [ - re_path(r'^$', views.index, name='index'), -] diff --git a/appengine/flexible/django_cloudsql/polls/views.py b/appengine/flexible/django_cloudsql/polls/views.py deleted file mode 100644 index 262f571d568..00000000000 --- a/appengine/flexible/django_cloudsql/polls/views.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2015 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. - -from django.http import HttpResponse - - -def index(request): - return HttpResponse("Hello, world. You're at the polls index.") diff --git a/appengine/flexible/django_cloudsql/requirements-test.txt b/appengine/flexible/django_cloudsql/requirements-test.txt deleted file mode 100644 index 99bb7d24ca4..00000000000 --- a/appengine/flexible/django_cloudsql/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==7.0.1 -pytest-django==4.5.0 diff --git a/appengine/flexible/django_cloudsql/requirements.txt b/appengine/flexible/django_cloudsql/requirements.txt deleted file mode 100644 index e1d759f2818..00000000000 --- a/appengine/flexible/django_cloudsql/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -Django==4.0.4; python_version >= '3.8' -Django==3.2.14; python_version < '3.8' -gunicorn==20.1.0 -psycopg2-binary==2.9.5 -django-environ==0.9.0 -google-cloud-secret-manager==2.12.0 -django-storages[google]==1.13 diff --git a/appengine/flexible/extending_runtime/.dockerignore b/appengine/flexible/extending_runtime/.dockerignore deleted file mode 100644 index cc6c24ef97f..00000000000 --- a/appengine/flexible/extending_runtime/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -env -*.pyc -__pycache__ -.dockerignore -Dockerfile -.git -.hg -.svn diff --git a/appengine/flexible/extending_runtime/Dockerfile b/appengine/flexible/extending_runtime/Dockerfile deleted file mode 100644 index 71cf0fa8193..00000000000 --- a/appengine/flexible/extending_runtime/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2021 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 dockerfile] -FROM gcr.io/google_appengine/python - -# Install the fortunes binary from the debian repositories. -RUN apt-get update && apt-get install -y fortunes - -# Change the -p argument to use Python 2.7 if desired. -RUN virtualenv /env -p python3.4 - -# Set virtualenv environment variables. This is equivalent to running -# source /env/bin/activate. -ENV VIRTUAL_ENV /env -ENV PATH /env/bin:$PATH - -ADD requirements.txt /app/ -RUN pip install -r requirements.txt -ADD . /app/ - -CMD gunicorn -b :$PORT main:app -# [END dockerfile] diff --git a/appengine/flexible/extending_runtime/app.yaml b/appengine/flexible/extending_runtime/app.yaml deleted file mode 100644 index 80bd1f30838..00000000000 --- a/appengine/flexible/extending_runtime/app.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2021 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. - -runtime: custom -env: flex diff --git a/appengine/flexible/extending_runtime/main.py b/appengine/flexible/extending_runtime/main.py deleted file mode 100644 index fe0b531b7a7..00000000000 --- a/appengine/flexible/extending_runtime/main.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2015 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 logging -import subprocess - -from flask import Flask - - -app = Flask(__name__) - - -# [START example] -@app.route('/') -def fortune(): - output = subprocess.check_output('/usr/games/fortune') - return output, 200, {'Content-Type': 'text/plain; charset=utf-8'} -# [END example] - - -@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 CMD in Dockerfile. - app.run(host='127.0.0.1', port=8080, debug=True) -# [END app] diff --git a/appengine/flexible/extending_runtime/main_test.py b/appengine/flexible/extending_runtime/main_test.py deleted file mode 100644 index df10409cfeb..00000000000 --- a/appengine/flexible/extending_runtime/main_test.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2015 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 os - -import pytest - -import main - - -@pytest.mark.skipif( - not os.path.exists('/usr/games/fortune'), - reason='Fortune executable is not installed.') -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get('/') - assert r.status_code == 200 - assert len(r.data) diff --git a/appengine/flexible/extending_runtime/requirements-test.txt b/appengine/flexible/extending_runtime/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/extending_runtime/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/extending_runtime/requirements.txt b/appengine/flexible/extending_runtime/requirements.txt deleted file mode 100644 index beeed7bed09..00000000000 --- a/appengine/flexible/extending_runtime/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 diff --git a/appengine/flexible/hello_world/app.yaml b/appengine/flexible/hello_world/app.yaml index 7aa7a47e159..617e5495b28 100644 --- a/appengine/flexible/hello_world/app.yaml +++ b/appengine/flexible/hello_world/app.yaml @@ -17,7 +17,10 @@ env: flex entrypoint: gunicorn -b :$PORT main:app runtime_config: - python_version: 3 + operating_system: ubuntu22 + # We also support 3.8, 3.9 and 3.11. + # See https://cloud.google.com/appengine/docs/flexible/python/runtime#interpreter for more info. + runtime_version: "3.10" # This sample incurs costs to run on the App Engine flexible environment. # The settings below are to reduce costs during testing and are not appropriate diff --git a/appengine/flexible/hello_world/main.py b/appengine/flexible/hello_world/main.py index 8a5a5a9f6fc..d238f1a6f46 100644 --- a/appengine/flexible/hello_world/main.py +++ b/appengine/flexible/hello_world/main.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC. +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ @app.route('/') -def hello(): +def hello() -> str: """Return a friendly HTTP greeting.""" return 'Hello World!' diff --git a/appengine/flexible/hello_world/main_test.py b/appengine/flexible/hello_world/main_test.py index 8bdbf76a920..2a025c095e6 100644 --- a/appengine/flexible/hello_world/main_test.py +++ b/appengine/flexible/hello_world/main_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC. +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ import main -def test_index(): +def test_index() -> str: main.app.testing = True client = main.app.test_client() diff --git a/appengine/flexible_ubuntu/hello_world/noxfile_config.py b/appengine/flexible/hello_world/noxfile_config.py similarity index 100% rename from appengine/flexible_ubuntu/hello_world/noxfile_config.py rename to appengine/flexible/hello_world/noxfile_config.py diff --git a/appengine/flexible/hello_world/requirements-test.txt b/appengine/flexible/hello_world/requirements-test.txt index c2845bffbe8..95ea1e6a02b 100644 --- a/appengine/flexible/hello_world/requirements-test.txt +++ b/appengine/flexible/hello_world/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==6.2.4 diff --git a/appengine/flexible/hello_world/requirements.txt b/appengine/flexible/hello_world/requirements.txt index beeed7bed09..b690bf61e0a 100644 --- a/appengine/flexible/hello_world/requirements.txt +++ b/appengine/flexible/hello_world/requirements.txt @@ -1,3 +1,2 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' +Flask==2.1.1 gunicorn==20.1.0 diff --git a/appengine/flexible/hello_world_django/.gitignore b/appengine/flexible/hello_world_django/.gitignore deleted file mode 100644 index 49ef2557b16..00000000000 --- a/appengine/flexible/hello_world_django/.gitignore +++ /dev/null @@ -1 +0,0 @@ -db.sqlite3 diff --git a/appengine/flexible/hello_world_django/README.md b/appengine/flexible/hello_world_django/README.md deleted file mode 100644 index 592fab654e9..00000000000 --- a/appengine/flexible/hello_world_django/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# Django sample for Google App Engine Flexible 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/flexible/hello_world_django/README.md - -This is a basic hello world [Django](https://www.djangoproject.com/) example -for [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Running locally - -You can run locally using django's `manage.py`: - - $ python manage.py runserver - -## Deployment & how the application runs on Google App Engine. - -Follow the standard deployment instructions in -[the top-level README](../README.md). Google App Engine runs the application -using [gunicorn](http://gunicorn.org/) as defined by `entrypoint` in -[`app.yaml`](app.yaml). You can use a different WSGI container if you want, as -long as it listens for web traffic on port `$PORT` and is declared in -[`requirements.txt`](requirements.txt). - -## How this was created - -This project was created using standard Django commands: - - $ virtualenv env - $ source env/bin/activate - $ pip install django gunicorn - $ pip freeze > requirements.txt - $ django-admin startproject project_name - $ python manage.py startapp helloworld - -Then, we added a simple view in `hellworld.views`, added the app to -`project_name.settings.INSTALLED_APPS`, and finally added a URL rule to -`project_name.urls`. - -In order to deploy to Google App Engine, we created a simple -[`app.yaml`](app.yaml). - -## Database notice - -This sample project uses Django's default sqlite database. This isn't suitable -for production as your application can run multiple instances and each will -have a different sqlite database. Additionally, instance disks are ephemeral, -so data will not survive restarts. - -For production applications running on Google Cloud Platform, you have -the following options: - -* Use [Cloud SQL](https://cloud.google.com/sql), a fully-managed MySQL database. - There is a [Flask CloudSQL](../cloudsql) sample that should be straightforward - to adapt to Django. -* Use any database of your choice hosted on - [Google Compute Engine](https://cloud.google.com/compute). The - [Cloud Launcher](https://cloud.google.com/launcher/) can be used to easily - deploy common databases. -* Use third-party database services, or services hosted by other providers, - provided you have configured access. - diff --git a/appengine/flexible/hello_world_django/app.yaml b/appengine/flexible/hello_world_django/app.yaml deleted file mode 100644 index 62b74a9c27e..00000000000 --- a/appengine/flexible/hello_world_django/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT project_name.wsgi - -runtime_config: - python_version: 3 diff --git a/appengine/flexible/hello_world_django/helloworld/__init__.py b/appengine/flexible/hello_world_django/helloworld/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/appengine/flexible/hello_world_django/helloworld/views.py b/appengine/flexible/hello_world_django/helloworld/views.py deleted file mode 100644 index b1d24cc0af8..00000000000 --- a/appengine/flexible/hello_world_django/helloworld/views.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -# Copyright 2015 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. - - -from django.http import HttpResponse - - -def index(request): - return HttpResponse( - 'Hello, World. This is Django running on Google App Engine') diff --git a/appengine/flexible/hello_world_django/manage.py b/appengine/flexible/hello_world_django/manage.py deleted file mode 100755 index c213c77eca6..00000000000 --- a/appengine/flexible/hello_world_django/manage.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -# Copyright 2021 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 os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/appengine/flexible/hello_world_django/project_name/__init__.py b/appengine/flexible/hello_world_django/project_name/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/appengine/flexible/hello_world_django/project_name/settings.py b/appengine/flexible/hello_world_django/project_name/settings.py deleted file mode 100644 index f77ab7fd9e6..00000000000 --- a/appengine/flexible/hello_world_django/project_name/settings.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2015 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. - -""" -Django settings for project_name project. - -Generated by 'django-admin startproject' using Django 1.8.4. - -For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ -""" - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'qgw!j*bpxo7g&o1ux-(2ph818ojfj(3c#-#*_8r^8&hq5jg$3@' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'helloworld' -) - -MIDDLEWARE = ( - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) - -ROOT_URLCONF = 'project_name.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'project_name.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - - -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -STATIC_URL = '/static/' diff --git a/appengine/flexible/hello_world_django/project_name/urls.py b/appengine/flexible/hello_world_django/project_name/urls.py deleted file mode 100644 index 334801260a0..00000000000 --- a/appengine/flexible/hello_world_django/project_name/urls.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2015 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. - -"""project_name URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.8/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Add an import: from blog import urls as blog_urls - 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) -""" -from django.conf.urls import include, url -from django.contrib import admin - -import helloworld.views - - -urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), - url(r'^$', helloworld.views.index), -] diff --git a/appengine/flexible/hello_world_django/project_name/wsgi.py b/appengine/flexible/hello_world_django/project_name/wsgi.py deleted file mode 100644 index a302b372ec2..00000000000 --- a/appengine/flexible/hello_world_django/project_name/wsgi.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2021 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. - -""" -WSGI config for project_name project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings") - -application = get_wsgi_application() diff --git a/appengine/flexible/hello_world_django/requirements-test.txt b/appengine/flexible/hello_world_django/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/hello_world_django/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/hello_world_django/requirements.txt b/appengine/flexible/hello_world_django/requirements.txt deleted file mode 100644 index 5a3b16e75ed..00000000000 --- a/appengine/flexible/hello_world_django/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Django==4.0.4; python_version >= '3.8' -Django==3.2.14; python_version < '3.8' -gunicorn==20.1.0; python_version > '3.0' -gunicorn==19.10.0; python_version < '3.0' diff --git a/appengine/flexible/metadata/app.yaml b/appengine/flexible/metadata/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible/metadata/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible/metadata/main.py b/appengine/flexible/metadata/main.py deleted file mode 100644 index 3ad872b448d..00000000000 --- a/appengine/flexible/metadata/main.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2015 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 logging - -from flask import Flask -import requests - - -logging.basicConfig(level=logging.INFO) -app = Flask(__name__) - - -# [START gae_flex_metadata] -METADATA_NETWORK_INTERFACE_URL = \ - ('http://metadata/computeMetadata/v1/instance/network-interfaces/0/' - 'access-configs/0/external-ip') - - -def get_external_ip(): - """Gets the instance's external IP address from the Compute Engine metadata - server. If the metadata server is unavailable, it assumes that the - application is running locally. - """ - try: - r = requests.get( - METADATA_NETWORK_INTERFACE_URL, - headers={'Metadata-Flavor': 'Google'}, - timeout=2) - return r.text - except requests.RequestException: - logging.info('Metadata server could not be reached, assuming local.') - return 'localhost' -# [END gae_flex_metadata] - - -@app.route('/') -def index(): - # Websocket connections must be made directly to this instance, so the - # external IP address of this instance is needed. - external_ip = get_external_ip() - return 'External IP: {}'.format(external_ip) - - -@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) diff --git a/appengine/flexible/metadata/requirements-test.txt b/appengine/flexible/metadata/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/metadata/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/metadata/requirements.txt b/appengine/flexible/metadata/requirements.txt deleted file mode 100644 index 9fee7fc1fa3..00000000000 --- a/appengine/flexible/metadata/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 -requests[security]==2.27.1 diff --git a/appengine/flexible/multiple_services/README.md b/appengine/flexible/multiple_services/README.md deleted file mode 100644 index 16e794c010a..00000000000 --- a/appengine/flexible/multiple_services/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Python Google Cloud Microservices Example - API Gateway - -[![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/flexible/multiple_services/README.md - -This example demonstrates how to deploy multiple python services to [App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/) - -## To Run Locally - -Use [virtualenv](https://virtualenv.pypa.io/en/stable/) to set up each -service's environment and start the each service in a separate terminal. - -Open a terminal and start the first service: - -```Bash -$ cd gateway-service -$ virtualenv -p python3 env -$ source env/bin/activate -$ pip install -r requirements.txt -$ python main.py -``` - -In a separate terminal, start the second service: - -```Bash -$ cd static-service -$ virtualenv -p python3 env -$ source env/bin/activate -$ pip install -r requirements.txt -$ python main.py -``` - -## To Deploy to App Engine - -### YAML Files - -Each directory contains an `app.yaml` file. These files all describe a -separate App Engine service within the same project. - -For the gateway: - -[Gateway service ](gateway/app.yaml) - -This is the `default` service. There must be one (and not more). The deployed -url will be `https://.appspot.com` - -For the static file server: - -[Static file service ](static/app.yaml) - -The deployed url will be `https://-dot-.appspot.com` - -### Deployment - -To deploy a service cd into its directory and run: -```Bash -$ gcloud app deploy app.yaml -``` -and enter `Y` when prompted. Or to skip the check add `-q`. - -To deploy multiple services simultaneously just add the path to each `app.yaml` -file as an argument to `gcloud app deploy `: -```Bash -$ gcloud app deploy gateway-service/app.yaml static-service/app.yaml -``` diff --git a/appengine/flexible/multiple_services/gateway-service/app.yaml b/appengine/flexible/multiple_services/gateway-service/app.yaml deleted file mode 100644 index fde45fada1c..00000000000 --- a/appengine/flexible/multiple_services/gateway-service/app.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2021 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. - -service: default -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -manual_scaling: - instances: 1 diff --git a/appengine/flexible/multiple_services/gateway-service/main.py b/appengine/flexible/multiple_services/gateway-service/main.py deleted file mode 100644 index 7f44534f369..00000000000 --- a/appengine/flexible/multiple_services/gateway-service/main.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2016 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. - -from flask import Flask -import requests - -import services_config - -app = Flask(__name__) -services_config.init_app(app) - - -@app.route('/') -def root(): - """Gets index.html from the static file server""" - res = requests.get(app.config['SERVICE_MAP']['static']) - return res.content - - -@app.route('/hello/') -def say_hello(service): - """Recieves requests from buttons on the front end and resopnds - or sends request to the static file server""" - # If 'gateway' is specified return immediate - if service == 'gateway': - return 'Gateway says hello' - - # Otherwise send request to service indicated by URL param - responses = [] - url = app.config['SERVICE_MAP'][service] - res = requests.get(url + '/hello') - responses.append(res.content) - return '\n'.encode().join(responses) - - -@app.route('/') -def static_file(path): - """Gets static files required by index.html to static file server""" - url = app.config['SERVICE_MAP']['static'] - res = requests.get(url + '/' + path) - return res.content, 200, {'Content-Type': res.headers['Content-Type']} - - -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=8000, debug=True) diff --git a/appengine/flexible/multiple_services/gateway-service/requirements-test.txt b/appengine/flexible/multiple_services/gateway-service/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/multiple_services/gateway-service/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/multiple_services/gateway-service/requirements.txt b/appengine/flexible/multiple_services/gateway-service/requirements.txt deleted file mode 100644 index 27878206f29..00000000000 --- a/appengine/flexible/multiple_services/gateway-service/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 -requests==2.27.1 diff --git a/appengine/flexible/multiple_services/gateway-service/services_config.py b/appengine/flexible/multiple_services/gateway-service/services_config.py deleted file mode 100644 index bbff0e551f6..00000000000 --- a/appengine/flexible/multiple_services/gateway-service/services_config.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2016 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 os - -# To add services insert key value pair of the name of the service and -# the port you want it to run on when running locally -SERVICES = { - 'default': 8000, - 'static': 8001 -} - - -def init_app(app): - # The GAE_INSTANCE environment variable will be set when deployed to GAE. - gae_instance = os.environ.get( - 'GAE_INSTANCE', os.environ.get('GAE_MODULE_INSTANCE')) - environment = 'production' if gae_instance is not None else 'development' - app.config['SERVICE_MAP'] = map_services(environment) - - -def map_services(environment): - """Generates a map of services to correct urls for running locally - or when deployed.""" - url_map = {} - for service, local_port in SERVICES.items(): - if environment == 'production': - url_map[service] = production_url(service) - if environment == 'development': - url_map[service] = local_url(local_port) - return url_map - - -def production_url(service_name): - """Generates url for a service when deployed to App Engine.""" - project_id = os.getenv('GOOGLE_CLOUD_PROJECT') - project_url = '{}.appspot.com'.format(project_id) - if service_name == 'default': - return 'https://{}'.format(project_url) - else: - return 'https://{}-dot-{}'.format(service_name, project_url) - - -def local_url(port): - """Generates url for a service when running locally""" - return 'http://localhost:{}'.format(str(port)) diff --git a/appengine/flexible/multiple_services/static-service/app.yaml b/appengine/flexible/multiple_services/static-service/app.yaml deleted file mode 100644 index 0583df96c7e..00000000000 --- a/appengine/flexible/multiple_services/static-service/app.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2021 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. - -service: static -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -manual_scaling: - instances: 1 diff --git a/appengine/flexible/multiple_services/static-service/main.py b/appengine/flexible/multiple_services/static-service/main.py deleted file mode 100644 index 903e45c6335..00000000000 --- a/appengine/flexible/multiple_services/static-service/main.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2016 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. - -from flask import Flask - -app = Flask(__name__) - - -@app.route('/hello') -def say_hello(): - """responds to request from frontend via gateway""" - return 'Static File Server says hello!' - - -@app.route('/') -def root(): - """serves index.html""" - return app.send_static_file('index.html') - - -@app.route('/') -def static_file(path): - """serves static files required by index.html""" - mimetype = '' - if '.' in path and path.split('.')[1] == 'css': - mimetype = 'text/css' - if '.' in path and path.split('.')[1] == 'js': - mimetype = 'application/javascript' - return app.send_static_file(path), 200, {'Content-Type': mimetype} - - -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=8001, debug=True) diff --git a/appengine/flexible/multiple_services/static-service/requirements-test.txt b/appengine/flexible/multiple_services/static-service/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/multiple_services/static-service/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/multiple_services/static-service/requirements.txt b/appengine/flexible/multiple_services/static-service/requirements.txt deleted file mode 100644 index 27878206f29..00000000000 --- a/appengine/flexible/multiple_services/static-service/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 -requests==2.27.1 diff --git a/appengine/flexible/multiple_services/static-service/static/index.html b/appengine/flexible/multiple_services/static-service/static/index.html deleted file mode 100644 index 9310b700113..00000000000 --- a/appengine/flexible/multiple_services/static-service/static/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - API Gateway on App Engine Flexible Environment - - -

API GATEWAY DEMO

-

Say hi to:

- - -
    - - diff --git a/appengine/flexible/multiple_services/static-service/static/index.js b/appengine/flexible/multiple_services/static-service/static/index.js deleted file mode 100644 index 021f835b9c1..00000000000 --- a/appengine/flexible/multiple_services/static-service/static/index.js +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2016 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. - -function handleResponse(resp){ - const li = document.createElement('li'); - li.innerHTML = resp; - document.querySelector('.responses').appendChild(li) -} - -function handleClick(event){ - $.ajax({ - url: `hello/${event.target.id}`, - type: `GET`, - success(resp){ - handleResponse(resp); - } - }); -} - -document.addEventListener('DOMContentLoaded', () => { - const buttons = document.getElementsByTagName('button') - for (var i = 0; i < buttons.length; i++) { - buttons[i].addEventListener('click', handleClick); - } -}); diff --git a/appengine/flexible/multiple_services/static-service/static/style.css b/appengine/flexible/multiple_services/static-service/static/style.css deleted file mode 100644 index 65074a9ef4d..00000000000 --- a/appengine/flexible/multiple_services/static-service/static/style.css +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright 2021 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. - */ - -h1 { - color: red; -} diff --git a/appengine/flexible/numpy/app.yaml b/appengine/flexible/numpy/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible/numpy/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible/numpy/main.py b/appengine/flexible/numpy/main.py deleted file mode 100644 index df873c68b91..00000000000 --- a/appengine/flexible/numpy/main.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2016 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 logging - -from flask import Flask -import numpy as np - -app = Flask(__name__) - - -@app.route('/') -def calculate(): - return_str = '' - x = np.array([[1, 2], [3, 4]]) - y = np.array([[5, 6], [7, 8]]) - - return_str += 'x: {} , y: {}
    '.format(str(x), str(y)) - - # Multiply matrices - return_str += 'x dot y : {}'.format(str(np.dot(x, y))) - return return_str - - -@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) diff --git a/appengine/flexible/numpy/main_test.py b/appengine/flexible/numpy/main_test.py deleted file mode 100644 index 42c0d594d94..00000000000 --- a/appengine/flexible/numpy/main_test.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2016 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get('/') - assert r.status_code == 200 - assert '[[19 22]\n [43 50]]' in r.data.decode('utf-8') diff --git a/appengine/flexible/numpy/requirements-test.txt b/appengine/flexible/numpy/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/numpy/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/numpy/requirements.txt b/appengine/flexible/numpy/requirements.txt deleted file mode 100644 index 277907a21d9..00000000000 --- a/appengine/flexible/numpy/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 -numpy==1.22.0; python_version > '3.7' -numpy==1.19.5; python_version <= '3.7' - diff --git a/appengine/flexible/pubsub/README.md b/appengine/flexible/pubsub/README.md deleted file mode 100644 index c4c65500681..00000000000 --- a/appengine/flexible/pubsub/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# Python Google Cloud Pub/Sub sample for Google App Engine Flexible 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/flexible/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 Flexible Environment](https://cloud.google.com/appengine). - -## 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 beta pubsub topics create [your-topic-name] - $ gcloud beta pubsub subscriptions create [your-subscription-name] \ - --topic [your-topic-name] \ - --push-endpoint \ - https://[your-app-id].appspot.com/pubsub/push?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 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/pubsub/push?token=[your-token]" - -Or - - $ http POST ":8080/pubsub/push?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/flexible/pubsub/app.yaml b/appengine/flexible/pubsub/app.yaml deleted file mode 100644 index 5804ac2b266..00000000000 --- a/appengine/flexible/pubsub/app.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -# [START gae_flex_pubsub_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 gae_flex_pubsub_env] diff --git a/appengine/flexible/pubsub/main.py b/appengine/flexible/pubsub/main.py deleted file mode 100644 index f146a0788b8..00000000000 --- a/appengine/flexible/pubsub/main.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 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 logging -import os - -from flask import current_app, Flask, render_template, request -from google.cloud import pubsub_v1 - - -app = Flask(__name__) - -# Configure the following environment variables via app.yaml -# This is used in the push request handler to verify 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['PROJECT'] = os.environ['GOOGLE_CLOUD_PROJECT'] - - -# Global list to storage messages received by this instance. -MESSAGES = [] - -# Initialize the publisher client once to avoid memory leak -# and reduce publish latency. -publisher = pubsub_v1.PublisherClient() - - -# [START gae_flex_pubsub_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') - - # publisher = pubsub_v1.PublisherClient() - topic_path = publisher.topic_path( - current_app.config['PROJECT'], - current_app.config['PUBSUB_TOPIC']) - - publisher.publish(topic_path, data=data) - - return 'OK', 200 -# [END gae_flex_pubsub_index] - - -# [START gae_flex_pubsub_push] -@app.route('/pubsub/push', methods=['POST']) -def pubsub_push(): - 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 gae_flex_pubsub_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) diff --git a/appengine/flexible/pubsub/main_test.py b/appengine/flexible/pubsub/main_test.py deleted file mode 100644 index 67fdb47d784..00000000000 --- a/appengine/flexible/pubsub/main_test.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2015 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 = '/pubsub/push?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('/pubsub/push') - assert r.status_code == 400 - - # invalid token - r = client.post('/pubsub/push?token=bad') - assert r.status_code == 400 diff --git a/appengine/flexible/pubsub/requirements-test.txt b/appengine/flexible/pubsub/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/pubsub/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/pubsub/requirements.txt b/appengine/flexible/pubsub/requirements.txt deleted file mode 100644 index 30e9d9736b5..00000000000 --- a/appengine/flexible/pubsub/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -google-cloud-pubsub==2.9.0 -gunicorn==20.1.0 diff --git a/appengine/flexible/pubsub/sample_message.json b/appengine/flexible/pubsub/sample_message.json deleted file mode 100644 index 8fe62d23fb9..00000000000 --- a/appengine/flexible/pubsub/sample_message.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "message": { - "data": "SGVsbG8sIFdvcmxkIQ==" - } -} diff --git a/appengine/flexible/pubsub/templates/index.html b/appengine/flexible/pubsub/templates/index.html deleted file mode 100644 index 28449216c37..00000000000 --- a/appengine/flexible/pubsub/templates/index.html +++ /dev/null @@ -1,36 +0,0 @@ -{# -# Copyright 2015 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. -#} - - - - Pub/Sub Python on Google App Engine Flexible Environment - - -
    -

    Messages received by this instance:

    -
      - {% for message in messages: %} -
    • {{message}}
    • - {% endfor %} -
    -

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

    -
    -
    - - -
    - - diff --git a/appengine/flexible/scipy/.gitignore b/appengine/flexible/scipy/.gitignore deleted file mode 100644 index de724cf6213..00000000000 --- a/appengine/flexible/scipy/.gitignore +++ /dev/null @@ -1 +0,0 @@ -assets/resized_google_logo.jpg diff --git a/appengine/flexible/scipy/README.md b/appengine/flexible/scipy/README.md deleted file mode 100644 index 3217ab68726..00000000000 --- a/appengine/flexible/scipy/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# SciPy on App Engine Flexible - -[![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/flexible/scipy/README.md - -This sample demonstrates how to use SciPy to resize an image on App Engine Flexible. - diff --git a/appengine/flexible/scipy/app.yaml b/appengine/flexible/scipy/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible/scipy/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible/scipy/assets/google_logo.jpg b/appengine/flexible/scipy/assets/google_logo.jpg deleted file mode 100644 index 5538eaed2bdfb5ccdb0f1082afb97a4d0cf1a465..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21568 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 - 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) diff --git a/appengine/flexible/scipy/main_test.py b/appengine/flexible/scipy/main_test.py deleted file mode 100644 index 8dfc321ea3d..00000000000 --- a/appengine/flexible/scipy/main_test.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2016 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 os -import tempfile - -import main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - with tempfile.TemporaryDirectory() as test_dir: - output_image_path = os.path.join(test_dir, 'resized_google_logo.jpg') - r = client.get( - '/', query_string={'output_image_path': output_image_path}) - - assert os.path.isfile(output_image_path) - assert r.status_code == 200 diff --git a/appengine/flexible/scipy/noxfile_config.py b/appengine/flexible/scipy/noxfile_config.py deleted file mode 100644 index 3228fb0bf80..00000000000 --- a/appengine/flexible/scipy/noxfile_config.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2022 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.6", "3.11"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": False, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": {}, -} diff --git a/appengine/flexible/scipy/requirements-test.txt b/appengine/flexible/scipy/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/scipy/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/scipy/requirements.txt b/appengine/flexible/scipy/requirements.txt deleted file mode 100644 index aa57c4cd1d0..00000000000 --- a/appengine/flexible/scipy/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 -imageio==2.14.0 -numpy==1.22.0; python_version > "3.7" -numpy==1.21.4; python_version <= "3.7" -pillow==9.2.0 -scipy==1.10.0; python_version > "3.7" -scipy==1.7.3; python_version <= "3.7" diff --git a/appengine/flexible/static_files/README.md b/appengine/flexible/static_files/README.md deleted file mode 100644 index 249a478127e..00000000000 --- a/appengine/flexible/static_files/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Python / Flask static files sample for Google App Engine Flexible 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/flexible/static_files/README.md - -This demonstrates how to use [Flask](http://flask.pocoo.org/) to serve static files in your application. - -Flask automatically makes anything in the ``static`` directory available via the ``/static`` URL. If you plan on using a different framework, it may have different conventions for serving static files. - -Refer to the [top-level README](../README.md) for instructions on running and deploying. diff --git a/appengine/flexible/static_files/app.yaml b/appengine/flexible/static_files/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible/static_files/app.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible/static_files/main.py b/appengine/flexible/static_files/main.py deleted file mode 100644 index c25c43de6cf..00000000000 --- a/appengine/flexible/static_files/main.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2015 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 gae_flex_python_static_files] -import logging - -from flask import Flask, render_template - - -app = Flask(__name__) - - -@app.route('/') -def hello(): - return render_template('index.html') - - -@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 gae_flex_python_static_files] diff --git a/appengine/flexible/static_files/main_test.py b/appengine/flexible/static_files/main_test.py deleted file mode 100644 index 6fb47d6ff51..00000000000 --- a/appengine/flexible/static_files/main_test.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2015 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 main - - -def test_index(): - main.app.testing = True - client = main.app.test_client() - - r = client.get('/') - assert r.status_code == 200 - - r = client.get('/static/main.css') - assert r.status_code == 200 diff --git a/appengine/flexible/static_files/requirements-test.txt b/appengine/flexible/static_files/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/static_files/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/static_files/requirements.txt b/appengine/flexible/static_files/requirements.txt deleted file mode 100644 index beeed7bed09..00000000000 --- a/appengine/flexible/static_files/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 diff --git a/appengine/flexible/static_files/static/main.css b/appengine/flexible/static_files/static/main.css deleted file mode 100644 index f906044f4e4..00000000000 --- a/appengine/flexible/static_files/static/main.css +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2015 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 gae_flex_python_css] */ -body { - font-family: Verdana, Helvetica, sans-serif; - background-color: #CCCCFF; -} -/* [END gae_flex_python_css] */ diff --git a/appengine/flexible/static_files/templates/index.html b/appengine/flexible/static_files/templates/index.html deleted file mode 100644 index 13b2ebe61af..00000000000 --- a/appengine/flexible/static_files/templates/index.html +++ /dev/null @@ -1,29 +0,0 @@ -{# -# Copyright 2015 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. -#} - - - - Static Files - - - - -

    This is a static file serving example.

    - - diff --git a/appengine/flexible/storage/app.yaml b/appengine/flexible/storage/app.yaml index e21a4c0ae91..59577b9ee8c 100644 --- a/appengine/flexible/storage/app.yaml +++ b/appengine/flexible/storage/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,9 +17,12 @@ env: flex entrypoint: gunicorn -b :$PORT main:app runtime_config: - python_version: 3 + # We also support 3.8, 3.9 and 3.10. + # See https://cloud.google.com/appengine/docs/flexible/python/runtime#interpreter for more info. + runtime_version: "3.11" + operating_system: "ubuntu22" #[START gae_flex_storage_yaml] env_variables: - CLOUD_STORAGE_BUCKET: your-bucket-name + CLOUD_STORAGE_BUCKET: [your-bucket-name] #[END gae_flex_storage_yaml] diff --git a/appengine/flexible/storage/main.py b/appengine/flexible/storage/main.py index 6a20d7c05c5..0664d2aa9ab 100644 --- a/appengine/flexible/storage/main.py +++ b/appengine/flexible/storage/main.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC. +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# [START gae_flex_storage_app] +# [START gae_flex_ubuntu_storage_app] import logging import os from typing import Union @@ -23,10 +23,10 @@ app = Flask(__name__) # Configure this environment variable via app.yaml -CLOUD_STORAGE_BUCKET = os.environ['CLOUD_STORAGE_BUCKET'] +CLOUD_STORAGE_BUCKET = os.environ["CLOUD_STORAGE_BUCKET"] -@app.route('/') +@app.route("/") def index() -> str: return """
    @@ -36,13 +36,13 @@ def index() -> str: """ -@app.route('/upload', methods=['POST']) +@app.route("/upload", methods=["POST"]) def upload() -> str: """Process the uploaded file and upload it to Google Cloud Storage.""" - uploaded_file = request.files.get('file') + uploaded_file = request.files.get("file") if not uploaded_file: - return 'No file uploaded.', 400 + return "No file uploaded.", 400 # Create a Cloud Storage client. gcs = storage.Client() @@ -54,8 +54,7 @@ def upload() -> str: blob = bucket.blob(uploaded_file.filename) blob.upload_from_string( - uploaded_file.read(), - content_type=uploaded_file.content_type + uploaded_file.read(), content_type=uploaded_file.content_type ) # Make the blob public. This is not necessary if the @@ -69,15 +68,20 @@ def upload() -> str: @app.errorhandler(500) def server_error(e: Union[Exception, int]) -> str: - logging.exception('An error occurred during a request.') - return """ + logging.exception("An error occurred during a request.") + return ( + """ An internal error occurred:
    {}
    See logs for full stacktrace. - """.format(e), 500 + """.format( + e + ), + 500, + ) -if __name__ == '__main__': +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 gae_flex_storage_app] + app.run(host="127.0.0.1", port=8080, debug=True) +# [END gae_flex_ubuntu_storage_app] diff --git a/appengine/flexible/storage/main_test.py b/appengine/flexible/storage/main_test.py index 9b6ef6d3f9e..36d02f83354 100644 --- a/appengine/flexible/storage/main_test.py +++ b/appengine/flexible/storage/main_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC. +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import io import os import uuid @@ -19,8 +20,8 @@ import flask.testing from google.cloud import storage import pytest + import requests -from six import BytesIO import main @@ -32,7 +33,7 @@ def client() -> flask.testing.FlaskClient: def test_index(client: flask.testing.FlaskClient) -> None: - r = client.get('/') + r = client.get("/") assert r.status_code == 200 @@ -50,17 +51,12 @@ def test_upload(client: flask.testing.FlaskClient, blob_name: str) -> None: # Upload a simple file file_content = b"This is some test content." - r = client.post( - '/upload', - data={ - 'file': (BytesIO(file_content), blob_name) - } - ) + r = client.post("/upload", data={"file": (io.BytesIO(file_content), blob_name)}) assert r.status_code == 200 # The app should return the public cloud storage URL for the uploaded # file. Download and verify it. - cloud_storage_url = r.data.decode('utf-8') + cloud_storage_url = r.data.decode("utf-8") r = requests.get(cloud_storage_url) - assert r.text.encode('utf-8') == file_content + assert r.text.encode("utf-8") == file_content diff --git a/appengine/flexible/storage/noxfile_config.py b/appengine/flexible/storage/noxfile_config.py index 1016bb530e8..44097a85005 100644 --- a/appengine/flexible/storage/noxfile_config.py +++ b/appengine/flexible/storage/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7"], + "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them "enforce_type_hints": True, diff --git a/appengine/flexible/storage/requirements-test.txt b/appengine/flexible/storage/requirements-test.txt index 6b6d2af7b81..e6323c66f62 100644 --- a/appengine/flexible/storage/requirements-test.txt +++ b/appengine/flexible/storage/requirements-test.txt @@ -1,3 +1,2 @@ pytest==7.0.1 -google-cloud-storage==2.0.0; python_version < '3.7' -google-cloud-storage==2.1.0; python_version > '3.6' +google-cloud-storage==2.1.0; diff --git a/appengine/flexible/storage/requirements.txt b/appengine/flexible/storage/requirements.txt index 2e8e0ffb03f..e254bb503e7 100644 --- a/appengine/flexible/storage/requirements.txt +++ b/appengine/flexible/storage/requirements.txt @@ -1,8 +1,4 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -werkzeug==2.1.2; python_version > '3.6' -werkzeug==2.0.3; python_version < '3.7' -google-cloud-storage==2.0.0; python_version < '3.7' -google-cloud-storage==2.1.0; python_version > '3.6' -gunicorn==20.1.0; python_version > '3.0' -gunicorn==19.10.0; python_version < '3.0' +Flask==2.1.0; +werkzeug==2.1.2; +google-cloud-storage==2.1.0; +gunicorn==20.1.0; diff --git a/appengine/flexible/tasks/Dockerfile b/appengine/flexible/tasks/Dockerfile deleted file mode 100644 index 7e2c98f16bf..00000000000 --- a/appengine/flexible/tasks/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2021 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. - -# Use the official Python image. -# https://hub.docker.com/_/python -FROM python:3.10 - -# Copy local code to the container image. -ENV APP_HOME /app -WORKDIR $APP_HOME -COPY . . - -# Install production dependencies. -RUN pip install Flask gunicorn - -# Run the web service on container startup. Here we use the gunicorn -# webserver, with one worker process and 8 threads. -# For environments with multiple CPU cores, increase the number of workers -# to be equal to the cores available. -CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main:app diff --git a/appengine/flexible/tasks/README.md b/appengine/flexible/tasks/README.md deleted file mode 100644 index 98468c70698..00000000000 --- a/appengine/flexible/tasks/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# Google Cloud Tasks Samples - -[![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/flexible/tasks/README.md - -Sample command-line programs for interacting with the Cloud Tasks API -. - -App Engine queues push tasks to an App Engine HTTP target. This directory -contains both the App Engine app to deploy, as well as the snippets to run -locally to push tasks to it, which could also be called on App Engine. - -`create_app_engine_queue_task.py` is a simple command-line program to create -tasks to be pushed to the App Engine app. - -`main.py` is the main App Engine app. This app serves as an endpoint to receive -App Engine task attempts. - -`app.yaml` configures the App Engine app. - - -## Prerequisites to run locally: - -Please refer to [Setting Up a Python Development Environment](https://cloud.google.com/python/setup). - -### Authentication - -To set up authentication, please refer to our -[authentication getting started guide](https://cloud.google.com/docs/authentication/getting-started). - -### Install Dependencies - -To install the dependencies for this sample, use the following command: - -``` -pip install -r requirements.txt -``` - -This sample uses the common protos in the [googleapis](https://github.com/googleapis/googleapis) -repository. For more info, see -[Protocol Buffer Basics](https://developers.google.com/protocol-buffers/docs/pythontutorial). - -## Creating a queue - -To create a queue using the Cloud SDK, use the following gcloud command: - -``` -gcloud tasks queues create my-appengine-queue -``` - -Note: A newly created queue will route to the default App Engine service and -version unless configured to do otherwise. - -## Deploying the App Engine App - -Deploy the App Engine app with gcloud: - -* To deploy to the Standard environment: - ``` - gcloud app deploy app.yaml - ``` -* To deploy to the Flexible environment: - ``` - gcloud app deploy app.flexible.yaml - ``` - -Verify the index page is serving: - -``` -gcloud app browse -``` - -The App Engine app serves as a target for the push requests. It has an -endpoint `/example_task_handler` that reads the payload (i.e., the request body) -of the HTTP POST request and logs it. The log output can be viewed with: - -``` -gcloud app logs read -``` - -## Run the Sample Using the Command Line - -Set environment variables: - -First, your project ID: - -``` -export PROJECT_ID=my-project-id -``` - -Then the queue ID, as specified at queue creation time. Queue IDs already -created can be listed with `gcloud tasks queues list`. - -``` -export QUEUE_ID=my-appengine-queue -``` - -And finally the location ID, which can be discovered with -`gcloud tasks queues describe $QUEUE_ID`, with the location embedded in -the "name" value (for instance, if the name is -"projects/my-project/locations/us-central1/queues/my-appengine-queue", then the -location is "us-central1"). - -``` -export LOCATION_ID=us-central1 -``` -### Using App Engine Queues -Running the sample will create a task, targeted at the `/example_task_handler` -endpoint, with a payload specified: - -``` -python create_app_engine_queue_task.py --project=$PROJECT_ID --queue=$QUEUE_ID --location=$LOCATION_ID --payload=hello -``` diff --git a/appengine/flexible/tasks/app.flexible.yaml b/appengine/flexible/tasks/app.flexible.yaml deleted file mode 100644 index 5b3b333fda6..00000000000 --- a/appengine/flexible/tasks/app.flexible.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2019 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT --threads=4 main:app - -runtime_config: - python_version: 3 diff --git a/appengine/flexible/tasks/app.yaml b/appengine/flexible/tasks/app.yaml deleted file mode 100644 index 15ac0d97205..00000000000 --- a/appengine/flexible/tasks/app.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2019 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. - -runtime: python37 diff --git a/appengine/flexible/tasks/create_app_engine_queue_task.py b/appengine/flexible/tasks/create_app_engine_queue_task.py deleted file mode 100644 index 521112d30a6..00000000000 --- a/appengine/flexible/tasks/create_app_engine_queue_task.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2019 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. - -from __future__ import print_function - -import argparse - - -def create_task(project, queue, location, payload=None, in_seconds=None): - # [START cloud_tasks_appengine_create_task] - """Create a task for a given queue with an arbitrary payload.""" - - from google.cloud import tasks_v2 - from google.protobuf import timestamp_pb2 - import datetime - import json - - # Create a client. - client = tasks_v2.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # queue = 'my-appengine-queue' - # location = 'us-central1' - # payload = 'hello' or {'param': 'value'} for application/json - # in_seconds = None - - # Construct the fully qualified queue name. - parent = client.queue_path(project, location, queue) - - # Construct the request body. - task = { - 'app_engine_http_request': { # Specify the type of request. - 'http_method': tasks_v2.HttpMethod.POST, - 'relative_uri': '/example_task_handler' - } - } - if payload is not None: - if isinstance(payload, dict): - # Convert dict to JSON string - payload = json.dumps(payload) - # specify http content-type to application/json - task["app_engine_http_request"]["headers"] = {"Content-type": "application/json"} - # The API expects a payload of type bytes. - converted_payload = payload.encode() - - # Add the payload to the request. - task['app_engine_http_request']['body'] = converted_payload - - if in_seconds is not None: - # Convert "seconds from now" into an rfc3339 datetime string. - d = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(seconds=in_seconds) - - # Create Timestamp protobuf. - timestamp = timestamp_pb2.Timestamp() - timestamp.FromDatetime(d) - - # Add the timestamp to the tasks. - task['schedule_time'] = timestamp - - # Use the client to build and send the task. - response = client.create_task(parent=parent, task=task) - - print('Created task {}'.format(response.name)) - return response -# [END cloud_tasks_appengine_create_task] - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description=create_task.__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - - parser.add_argument( - '--project', - help='Project of the queue to add the task to.', - required=True, - ) - - parser.add_argument( - '--queue', - help='ID (short name) of the queue to add the task to.', - required=True, - ) - - parser.add_argument( - '--location', - help='Location of the queue to add the task to.', - required=True, - ) - - parser.add_argument( - '--payload', - help='Optional payload to attach to the push queue.' - ) - - parser.add_argument( - '--in_seconds', type=int, - help='The number of seconds from now to schedule task attempt.' - ) - - args = parser.parse_args() - - create_task( - args.project, args.queue, args.location, - args.payload, args.in_seconds) diff --git a/appengine/flexible/tasks/create_app_engine_queue_task_test.py b/appengine/flexible/tasks/create_app_engine_queue_task_test.py deleted file mode 100644 index 524ea6606b2..00000000000 --- a/appengine/flexible/tasks/create_app_engine_queue_task_test.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2019 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 os - -import create_app_engine_queue_task - -TEST_PROJECT_ID = os.getenv('GOOGLE_CLOUD_PROJECT') -TEST_LOCATION = os.getenv('TEST_QUEUE_LOCATION', 'us-central1') -TEST_QUEUE_NAME = os.getenv('TEST_QUEUE_NAME', 'my-appengine-queue') - - -def test_create_task(): - result = create_app_engine_queue_task.create_task( - TEST_PROJECT_ID, TEST_QUEUE_NAME, TEST_LOCATION) - assert TEST_QUEUE_NAME in result.name diff --git a/appengine/flexible/tasks/main.py b/appengine/flexible/tasks/main.py deleted file mode 100644 index 6e6886ae7f4..00000000000 --- a/appengine/flexible/tasks/main.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2019 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. - -"""App Engine app to serve as an endpoint for App Engine queue samples.""" - -# [START cloud_tasks_appengine_quickstart] -from flask import Flask, render_template, request - -app = Flask(__name__) - - -@app.route('/example_task_handler', methods=['POST']) -def example_task_handler(): - """Log the request payload.""" - payload = request.get_data(as_text=True) or '(empty payload)' - print('Received task with payload: {}'.format(payload)) - return render_template("index.html", payload=payload) -# [END cloud_tasks_appengine_quickstart] - - -@app.route('/') -def hello(): - """Basic index to verify app is serving.""" - return 'Hello World!' - - -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) diff --git a/appengine/flexible/tasks/main_test.py b/appengine/flexible/tasks/main_test.py deleted file mode 100644 index 3b30d103eb3..00000000000 --- a/appengine/flexible/tasks/main_test.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2019 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 pytest - - -@pytest.fixture -def app(): - import main - main.app.testing = True - return main.app.test_client() - - -def test_index(app): - r = app.get('/') - assert r.status_code == 200 - - -def test_log_payload(capsys, app): - payload = 'test_payload' - - r = app.post('/example_task_handler', data=payload) - assert r.status_code == 200 - - out, _ = capsys.readouterr() - assert payload in out - - -def test_empty_payload(capsys, app): - r = app.post('/example_task_handler') - assert r.status_code == 200 - - out, _ = capsys.readouterr() - assert 'empty payload' in out diff --git a/appengine/flexible/tasks/requirements-test.txt b/appengine/flexible/tasks/requirements-test.txt deleted file mode 100644 index c2845bffbe8..00000000000 --- a/appengine/flexible/tasks/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==7.0.1 diff --git a/appengine/flexible/tasks/requirements.txt b/appengine/flexible/tasks/requirements.txt deleted file mode 100644 index f11b326af4b..00000000000 --- a/appengine/flexible/tasks/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 -google-cloud-tasks==2.7.1 diff --git a/appengine/flexible/tasks/snippets.py b/appengine/flexible/tasks/snippets.py deleted file mode 100644 index 09700bbb8a8..00000000000 --- a/appengine/flexible/tasks/snippets.py +++ /dev/null @@ -1,303 +0,0 @@ -# Copyright 2019 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. - -from google.cloud import tasks - - -def create_queue(project, location, queue_blue_name, queue_red_name): - # [START cloud_tasks_taskqueues_using_yaml] - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue_blue_name = 'queue-blue' - # queue_red_name = 'queue-red' - - parent = f"projects/{project}/locations/{location}" - - queue_blue = { - 'name': client.queue_path(project, location, queue_blue_name), - 'rate_limits': { - 'max_dispatches_per_second': 5 - }, - 'app_engine_routing_override': { - 'version': 'v2', - 'service': 'task-module' - } - } - - queue_red = { - 'name': client.queue_path(project, location, queue_red_name), - 'rate_limits': { - 'max_dispatches_per_second': 1 - } - } - - queues = [queue_blue, queue_red] - for queue in queues: - response = client.create_queue(parent=parent, queue=queue) - print(response) - # [END cloud_tasks_taskqueues_using_yaml] - return response - - -def update_queue(project, location, queue): - # [START cloud_tasks_taskqueues_processing_rate] - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue = 'queue-blue' - - # Get queue object - queue_path = client.queue_path(project, location, queue) - queue = client.get_queue(name=queue_path) - - # Update queue object - queue.rate_limits.max_dispatches_per_second = 20 - queue.rate_limits.max_concurrent_dispatches = 10 - - response = client.update_queue(queue=queue) - print(response) - # [END cloud_tasks_taskqueues_processing_rate] - return response - - -def create_task(project, location, queue): - # [START cloud_tasks_taskqueues_new_task] - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue = 'default' - amount = 10 - - parent = client.queue_path(project, location, queue) - - task = { - 'app_engine_http_request': { - 'http_method': tasks.HttpMethod.POST, - 'relative_uri': '/update_counter', - 'app_engine_routing': { - 'service': 'worker' - }, - 'body': str(amount).encode() - } - } - - response = client.create_task(parent=parent, task=task) - eta = response.schedule_time.strftime("%m/%d/%Y, %H:%M:%S") - print('Task {} enqueued, ETA {}.'.format(response.name, eta)) - # [END cloud_tasks_taskqueues_new_task] - return response - - -def create_tasks_with_data(project, location, queue): - # [START cloud_tasks_taskqueues_passing_data] - import json - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue = 'default' - - parent = client.queue_path(project, location, queue) - - task1 = { - 'app_engine_http_request': { - 'http_method': tasks.HttpMethod.POST, - 'relative_uri': '/update_counter?key=blue', - 'app_engine_routing': { - 'service': 'worker' - } - } - } - - task2 = { - 'app_engine_http_request': { - 'http_method': tasks.HttpMethod.POST, - 'relative_uri': '/update_counter', - 'app_engine_routing': { - 'service': 'worker' - }, - 'headers': { - 'Content-Type': 'application/json' - }, - 'body': json.dumps({'key': 'blue'}).encode() - } - } - - response = client.create_task(parent=parent, task=task1) - print(response) - response = client.create_task(parent=parent, task=task2) - print(response) - # [END cloud_tasks_taskqueues_passing_data] - return response - - -def create_task_with_name(project, location, queue, task_name): - # [START cloud_tasks_taskqueues_naming_tasks] - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue = 'default' - # task_name = 'first-try' - - parent = client.queue_path(project, location, queue) - - task = { - 'name': client.task_path(project, location, queue, task_name), - 'app_engine_http_request': { - 'http_method': tasks.HttpMethod.GET, - 'relative_uri': '/url/path' - } - } - response = client.create_task(parent=parent, task=task) - print(response) - # [END cloud_tasks_taskqueues_naming_tasks] - return response - - -def delete_task(project, location, queue): - # [START cloud_tasks_taskqueues_deleting_tasks] - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue = 'queue1' - - task_path = client.task_path(project, location, queue, 'foo') - response = client.delete_task(name=task_path) - # [END cloud_tasks_taskqueues_deleting_tasks] - return response - - -def purge_queue(project, location, queue): - # [START cloud_tasks_taskqueues_purging_tasks] - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue = 'queue1' - - queue_path = client.queue_path(project, location, queue) - response = client.purge_queue(name=queue_path) - # [END cloud_tasks_taskqueues_purging_tasks] - return response - - -def pause_queue(project, location, queue): - # [START cloud_tasks_taskqueues_pause_queue] - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue = 'queue1' - - queue_path = client.queue_path(project, location, queue) - response = client.pause_queue(name=queue_path) - # [END cloud_tasks_taskqueues_pause_queue] - return response - - -def delete_queue(project, location, queue): - # [START cloud_tasks_taskqueues_deleting_queues] - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # queue = 'queue1' - - queue_path = client.queue_path(project, location, queue) - response = client.delete_queue(name=queue_path) - # [END cloud_tasks_taskqueues_deleting_queues] - return response - - -def retry_task(project, location, fooqueue, barqueue, bazqueue): - # [START cloud_tasks_taskqueues_retrying_tasks] - from google.protobuf import duration_pb2 - - client = tasks.CloudTasksClient() - - # TODO(developer): Uncomment these lines and replace with your values. - # project = 'my-project-id' - # location = 'us- central1' - # fooqueue = 'fooqueue' - # barqueue = 'barqueue' - # bazqueue = 'bazqueue' - - parent = f"projects/{project}/locations/{location}" - - max_retry = duration_pb2.Duration() - max_retry.seconds = 2*60*60*24 - - foo = { - 'name': client.queue_path(project, location, fooqueue), - 'rate_limits': { - 'max_dispatches_per_second': 1 - }, - 'retry_config': { - 'max_attempts': 7, - 'max_retry_duration': max_retry - } - } - - min = duration_pb2.Duration() - min.seconds = 10 - - max = duration_pb2.Duration() - max.seconds = 200 - - bar = { - 'name': client.queue_path(project, location, barqueue), - 'rate_limits': { - 'max_dispatches_per_second': 1 - }, - 'retry_config': { - 'min_backoff': min, - 'max_backoff': max, - 'max_doublings': 0 - } - } - - max.seconds = 300 - baz = { - 'name': client.queue_path(project, location, bazqueue), - 'rate_limits': { - 'max_dispatches_per_second': 1 - }, - 'retry_config': { - 'min_backoff': min, - 'max_backoff': max, - 'max_doublings': 3 - } - } - - queues = [foo, bar, baz] - for queue in queues: - response = client.create_queue(parent=parent, queue=queue) - print(response) - # [END cloud_tasks_taskqueues_retrying_tasks] - return response diff --git a/appengine/flexible/tasks/snippets_test.py b/appengine/flexible/tasks/snippets_test.py deleted file mode 100644 index 9edfd2a42f2..00000000000 --- a/appengine/flexible/tasks/snippets_test.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright 2019 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 os -import uuid - -import pytest - -import snippets - -TEST_PROJECT_ID = os.getenv('GOOGLE_CLOUD_PROJECT') -TEST_LOCATION = os.getenv('TEST_QUEUE_LOCATION', 'us-central1') -QUEUE_NAME_1 = "queue-{}".format(uuid.uuid4()) -QUEUE_NAME_2 = "queue-{}".format(uuid.uuid4()) - - -@pytest.mark.order1 -def test_create_queue(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_2) - result = snippets.create_queue( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1, QUEUE_NAME_2) - assert name in result.name - - -@pytest.mark.order2 -def test_update_queue(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - result = snippets.update_queue( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order3 -def test_create_task(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - result = snippets.create_task( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order4 -def test_create_task_with_data(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - result = snippets.create_tasks_with_data( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order5 -def test_create_task_with_name(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - result = snippets.create_task_with_name( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1, 'foo') - assert name in result.name - - -@pytest.mark.order6 -def test_delete_task(): - result = snippets.delete_task( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert result is None - - -@pytest.mark.order7 -def test_purge_queue(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - result = snippets.purge_queue( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order8 -def test_pause_queue(): - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - result = snippets.pause_queue( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert name in result.name - - -@pytest.mark.order9 -def test_delete_queue(): - result = snippets.delete_queue( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) - assert result is None - - result = snippets.delete_queue( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_2) - assert result is None - - -@pytest.mark.order10 -def test_retry_task(): - QUEUE_SIZE = 3 - QUEUE_NAME = [] - for i in range(QUEUE_SIZE): - QUEUE_NAME.append("queue-{}".format(uuid.uuid4())) - - name = "projects/{}/locations/{}/queues/{}".format( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME[2]) - result = snippets.retry_task( - TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME[0], QUEUE_NAME[1], - QUEUE_NAME[2]) - assert name in result.name - - for i in range(QUEUE_SIZE): - snippets.delete_queue( - project=TEST_PROJECT_ID, - location=TEST_LOCATION, - queue=QUEUE_NAME[i]) diff --git a/appengine/flexible/tasks/templates/index.html b/appengine/flexible/tasks/templates/index.html deleted file mode 100644 index 7e4efc7b336..00000000000 --- a/appengine/flexible/tasks/templates/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Tasks Sample - - -

    Printed task payload: {{ payload }}

    - - \ No newline at end of file diff --git a/appengine/flexible/twilio/README.md b/appengine/flexible/twilio/README.md deleted file mode 100644 index 432acef801a..00000000000 --- a/appengine/flexible/twilio/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Python Twilio voice and SMS sample for Google App Engine Flexible 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/flexible/twilio/README.md - -This sample demonstrates how to use [Twilio](https://www.twilio.com) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -For more information about Twilio, see their [Python quickstart tutorials](https://www.twilio.com/docs/quickstart/python). - -## Setup - -Before you can run or deploy the sample, you will need to do the following: - -1. [Create a Twilio Account](http://ahoy.twilio.com/googlecloudplatform). Google App Engine -customers receive a complimentary credit for SMS messages and inbound messages. - -2. Create a number on twilio, and configure the voice request URL to be ``https://your-app-id.appspot.com/call/receive`` -and the SMS request URL to be ``https://your-app-id.appspot.com/sms/receive``. - -3. Configure your Twilio settings in the environment variables section in ``app.yaml``. - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -You can run the application locally to test the callbacks and SMS sending. You -will need to set environment variables before starting your application: - - $ export TWILIO_ACCOUNT_SID=[your-twilio-accoun-sid] - $ export TWILIO_AUTH_TOKEN=[your-twilio-auth-token] - $ export TWILIO_NUMBER=[your-twilio-number] - $ python main.py diff --git a/appengine/flexible/twilio/app.yaml b/appengine/flexible/twilio/app.yaml deleted file mode 100644 index 0e7de97eb19..00000000000 --- a/appengine/flexible/twilio/app.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -# [START gae_flex_twilio_env] -env_variables: - TWILIO_ACCOUNT_SID: your-account-sid - TWILIO_AUTH_TOKEN: your-auth-token - TWILIO_NUMBER: your-twilio-number -# [END gae_flex_twilio_env] diff --git a/appengine/flexible/twilio/main.py b/appengine/flexible/twilio/main.py deleted file mode 100644 index 03041370daf..00000000000 --- a/appengine/flexible/twilio/main.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2015 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 logging -import os - -from flask import Flask, request -from twilio import rest -from twilio.twiml import messaging_response, voice_response - - -TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] -TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] -TWILIO_NUMBER = os.environ['TWILIO_NUMBER'] - - -app = Flask(__name__) - - -# [START gae_flex_twilio_receive_call] -@app.route('/call/receive', methods=['POST']) -def receive_call(): - """Answers a call and replies with a simple greeting.""" - response = voice_response.VoiceResponse() - response.say('Hello from Twilio!') - return str(response), 200, {'Content-Type': 'application/xml'} -# [END gae_flex_twilio_receive_call] - - -# [START gae_flex_twilio_send_sms] -@app.route('/sms/send') -def send_sms(): - """Sends a simple SMS message.""" - to = request.args.get('to') - if not to: - return ('Please provide the number to message in the "to" query string' - ' parameter.'), 400 - - client = rest.Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) - rv = client.messages.create( - to=to, - from_=TWILIO_NUMBER, - body='Hello from Twilio!' - ) - return str(rv) -# [END gae_flex_twilio_send_sms] - - -# [START gae_flex_twilio_receive_sms] -@app.route('/sms/receive', methods=['POST']) -def receive_sms(): - """Receives an SMS message and replies with a simple greeting.""" - sender = request.values.get('From') - body = request.values.get('Body') - - message = 'Hello, {}, you said: {}'.format(sender, body) - - response = messaging_response.MessagingResponse() - response.message(message) - return str(response), 200, {'Content-Type': 'application/xml'} -# [END gae_flex_twilio_receive_sms] - - -@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) diff --git a/appengine/flexible/twilio/main_test.py b/appengine/flexible/twilio/main_test.py deleted file mode 100644 index da37c8c3b90..00000000000 --- a/appengine/flexible/twilio/main_test.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2016 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 re - -import pytest -import responses - - -@pytest.fixture -def app(monkeypatch): - monkeypatch.setenv('TWILIO_ACCOUNT_SID', 'sid123') - monkeypatch.setenv('TWILIO_AUTH_TOKEN', 'auth123') - monkeypatch.setenv('TWILIO_NUMBER', '0123456789') - - import main - - main.app.testing = True - return main.app.test_client() - - -def test_receive_call(app): - r = app.post('/call/receive') - assert 'Hello from Twilio!' in r.data.decode('utf-8') - - -@responses.activate -def test_send_sms(app, monkeypatch): - sample_response = { - "sid": "sid", - "date_created": "Wed, 20 Dec 2017 19:32:14 +0000", - "date_updated": "Wed, 20 Dec 2017 19:32:14 +0000", - "date_sent": None, - "account_sid": "account_sid", - "to": "+1234567890", - "from": "+9876543210", - "messaging_service_sid": None, - "body": "Hello from Twilio!", - "status": "queued", - "num_segments": "1", - "num_media": "0", - "direction": "outbound-api", - "api_version": "2010-04-01", - "price": None, - "price_unit": "USD", - "error_code": None, - "error_message": None, - "uri": "/2010-04-01/Accounts/sample.json", - "subresource_uris": { - "media": "/2010-04-01/Accounts/sample/Media.json" - } - } - responses.add(responses.POST, re.compile('.*'), - json=sample_response, status=200) - - r = app.get('/sms/send') - assert r.status_code == 400 - - r = app.get('/sms/send?to=5558675309') - assert r.status_code == 200 - - -def test_receive_sms(app): - r = app.post('/sms/receive', data={ - 'From': '5558675309', 'Body': 'Jenny, I got your number.'}) - assert r.status_code == 200 - assert 'Jenny, I got your number' in r.data.decode('utf-8') diff --git a/appengine/flexible/twilio/requirements-test.txt b/appengine/flexible/twilio/requirements-test.txt deleted file mode 100644 index 0dd3a1c6914..00000000000 --- a/appengine/flexible/twilio/requirements-test.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest==7.0.1 -responses==0.17.0; python_version < '3.7' -responses==0.20.0; python_version > '3.6' diff --git a/appengine/flexible/twilio/requirements.txt b/appengine/flexible/twilio/requirements.txt deleted file mode 100644 index 9eb2dcdf034..00000000000 --- a/appengine/flexible/twilio/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; python_version > '3.6' -Flask==2.0.3; python_version < '3.7' -gunicorn==20.1.0 -twilio==7.9.3 diff --git a/appengine/flexible/websockets/README.md b/appengine/flexible/websockets/README.md deleted file mode 100644 index fabd0995a40..00000000000 --- a/appengine/flexible/websockets/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Python websockets sample for Google App Engine Flexible Environment - -This sample demonstrates how to use websockets on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Running locally - -Refer to the [top-level README](../README.md) for instructions on running and deploying. - -To run locally, you need to use gunicorn with the ``flask_socket`` worker: - - $ gunicorn -b 127.0.0.1:8080 -k flask_sockets.worker main:app diff --git a/appengine/flexible/websockets/app.yaml b/appengine/flexible/websockets/app.yaml deleted file mode 100644 index 8a323ffe30f..00000000000 --- a/appengine/flexible/websockets/app.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex - -# Use a special gunicorn worker class to support websockets. -entrypoint: gunicorn -b :$PORT -k flask_sockets.worker main:app - -runtime_config: - python_version: 3 - -# Use only a single instance, so that this local-memory-only chat app will work -# consistently with multiple users. To work across multiple instances, an -# extra-instance messaging system or data store would be needed. -manual_scaling: - instances: 1 - - -# For applications which can take advantage of session affinity -# (where the load balancer will attempt to route multiple connections from -# the same user to the same App Engine instance), uncomment the folowing: - -# network: -# session_affinity: true diff --git a/appengine/flexible/websockets/main.py b/appengine/flexible/websockets/main.py deleted file mode 100644 index 3fd5da9851d..00000000000 --- a/appengine/flexible/websockets/main.py +++ /dev/null @@ -1,53 +0,0 @@ -# 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. - -from __future__ import print_function - -# [START gae_flex_websockets_app] -from flask import Flask, render_template -from flask_sockets import Sockets - - -app = Flask(__name__) -sockets = Sockets(app) - - -@sockets.route('/chat') -def chat_socket(ws): - while not ws.closed: - message = ws.receive() - if message is None: # message is "None" if the client has closed. - continue - # Send the message to all clients connected to this webserver - # process. (To support multiple processes or instances, an - # extra-instance storage or messaging system would be required.) - clients = ws.handler.server.clients.values() - for client in clients: - client.ws.send(message) -# [END gae_flex_websockets_app] - - -@app.route('/') -def index(): - return render_template('index.html') - - -if __name__ == '__main__': - print(""" -This can not be run directly because the Flask development server does not -support web sockets. Instead, use gunicorn: - -gunicorn -b 127.0.0.1:8080 -k flask_sockets.worker main:app - -""") diff --git a/appengine/flexible/websockets/main_test.py b/appengine/flexible/websockets/main_test.py deleted file mode 100644 index 05f8b816dbd..00000000000 --- a/appengine/flexible/websockets/main_test.py +++ /dev/null @@ -1,82 +0,0 @@ -# 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 socket -import subprocess - -import pytest -import requests -from retrying import retry -import websocket - - -@pytest.fixture(scope='module') -def server(): - """Provides the address of a test HTTP/websocket server. - The test server is automatically created before - a test and destroyed at the end. - """ - # Ask the OS to allocate a port. - sock = socket.socket() - sock.bind(('127.0.0.1', 0)) - port = sock.getsockname()[1] - - # Free the port and pass it to a subprocess. - sock.close() - - bind_to = '127.0.0.1:{}'.format(port) - server = subprocess.Popen( - ['gunicorn', '-b', bind_to, '-k' 'flask_sockets.worker', 'main:app'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - - # With btlr, there can be many processes are spawned and the - # server might be in a tight memory situation, so let's wait for 2 - # mins. - # Wait until the server responds before proceeding. - @retry(wait_fixed=50, stop_max_delay=120000) - def check_server(url): - requests.get(url) - - check_server('http://{}/'.format(bind_to)) - - yield bind_to - - server.kill() - - # Dump the logs for debugging - out, err = server.communicate() - print("gunicorn stdout: {}".format(out)) - print("gunicorn stderr: {}".format(err)) - - -def test_http(server): - result = requests.get('http://{}/'.format(server)) - assert 'Python Websockets Chat' in result.text - - -def test_websocket(server): - url = 'ws://{}/chat'.format(server) - ws_one = websocket.WebSocket() - ws_one.connect(url) - - ws_two = websocket.WebSocket() - ws_two.connect(url) - - message = 'Hello, World' - ws_one.send(message) - - assert ws_one.recv() == message - assert ws_two.recv() == message diff --git a/appengine/flexible/websockets/requirements-test.txt b/appengine/flexible/websockets/requirements-test.txt deleted file mode 100644 index 7143c9bf0a6..00000000000 --- a/appengine/flexible/websockets/requirements-test.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest==7.0.1 -retrying==1.3.4 -websocket-client==1.2.2 diff --git a/appengine/flexible/websockets/requirements.txt b/appengine/flexible/websockets/requirements.txt deleted file mode 100644 index c9f2a62dea1..00000000000 --- a/appengine/flexible/websockets/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Flask==1.1.4 # it seems like Flask-sockets doesn't play well with 2.0+ -Flask-Sockets==0.2.1 -gunicorn==20.1.0 -requests==2.27.1 -markupsafe===2.0.1 diff --git a/appengine/flexible/websockets/templates/index.html b/appengine/flexible/websockets/templates/index.html deleted file mode 100644 index af6d791f148..00000000000 --- a/appengine/flexible/websockets/templates/index.html +++ /dev/null @@ -1,96 +0,0 @@ -{# -# 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. -#} - - - - Google App Engine Flexible Environment - Python Websockets Chat - - - - - -

    Chat demo

    - - - -
    - -
    -

    Messages:

    -
      -
      - -
      -

      Status:

      -
        -
        - - - - - - diff --git a/appengine/flexible_ubuntu/README.md b/appengine/flexible_ubuntu/README.md deleted file mode 100644 index 5b2fb5a2f59..00000000000 --- a/appengine/flexible_ubuntu/README.md +++ /dev/null @@ -1,76 +0,0 @@ -## Google App Engine Flexible Environment Python on Ubuntu Samples - -[![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/flexible_ubuntu/README.md - -These are samples for using Python on Google App Engine Flexible Environment on the Ubuntu 18 and Ubuntu 22 OS. These samples are typically referenced from the [docs](https://cloud.google.com/appengine/docs). - -See our other [Google Cloud Platform github repos](https://github.com/GoogleCloudPlatform) for sample applications and -scaffolding for other frameworks and use cases. - -## Run Locally - -Some samples have specific instructions. If there is a README in the sample folder, please refer to it for any additional steps required to run the sample. - -In general, the samples typically require: - -1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/), including the [gcloud tool](https://cloud.google.com/sdk/gcloud/), and [gcloud app component](https://cloud.google.com/sdk/gcloud-app). - -2. Setup the gcloud tool. This provides authentication to Google Cloud APIs and services. - - ``` - gcloud init - ``` - -3. Clone this repo. - - ``` - git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git - cd python-docs-samples/appengine/flexible - ``` - -4. Open a sample folder, create a virtualenv, install dependencies, and run the sample: - - ``` - cd hello-world - virtualenv env - source env/bin/activate - pip install -r requirements.txt - python main.py - ``` - -5. Visit the application at [http://localhost:8080](http://localhost:8080). - - -## Deploying - -Some samples in this repositories may have special deployment instructions. Refer to the readme in the sample directory. - -1. Use the [Google Developers Console](https://console.developer.google.com) to create a project/app id. (App id and project id are identical) - -2. Setup the gcloud tool, if you haven't already. - - ``` - gcloud init - ``` - -3. Use gcloud to deploy your app. Your application must use a `gcloud` CLI - [version **420.0.0 or later**](/sdk/docs/install-sdk#installing_the_latest_version). - To view the current `gcloud` version, run the [`gcloud - version`](/sdk/gcloud/reference/version) command. - - ``` - gcloud app deploy - ``` - -4. Congratulations! Your application is now live at `your-app-id.appspot.com` - -## Contributing changes - -* See [CONTRIBUTING.md](../CONTRIBUTING.md) - -## Licensing - -* See [LICENSE](../LICENSE) diff --git a/appengine/flexible_ubuntu/hello_world/app.yaml b/appengine/flexible_ubuntu/hello_world/app.yaml deleted file mode 100644 index 617e5495b28..00000000000 --- a/appengine/flexible_ubuntu/hello_world/app.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2021 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - operating_system: ubuntu22 - # We also support 3.8, 3.9 and 3.11. - # See https://cloud.google.com/appengine/docs/flexible/python/runtime#interpreter for more info. - runtime_version: "3.10" - -# This sample incurs costs to run on the App Engine flexible environment. -# The settings below are to reduce costs during testing and are not appropriate -# for production use. For more information, see: -# https://cloud.google.com/appengine/docs/flexible/python/configuring-your-app-with-app-yaml -manual_scaling: - instances: 1 -resources: - cpu: 1 - memory_gb: 0.5 - disk_size_gb: 10 diff --git a/appengine/flexible_ubuntu/hello_world/main.py b/appengine/flexible_ubuntu/hello_world/main.py deleted file mode 100644 index d238f1a6f46..00000000000 --- a/appengine/flexible_ubuntu/hello_world/main.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2023 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 gae_flex_quickstart] -from flask import Flask - - -app = Flask(__name__) - - -@app.route('/') -def hello() -> str: - """Return a friendly HTTP greeting.""" - return 'Hello World!' - - -if __name__ == '__main__': - # This is used when running locally only. When deploying to Google App - # Engine, a webserver process such as Gunicorn will serve the app. - app.run(host='127.0.0.1', port=8080, debug=True) -# [END gae_flex_quickstart] diff --git a/appengine/flexible_ubuntu/hello_world/main_test.py b/appengine/flexible_ubuntu/hello_world/main_test.py deleted file mode 100644 index 2a025c095e6..00000000000 --- a/appengine/flexible_ubuntu/hello_world/main_test.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2023 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 main - - -def test_index() -> str: - main.app.testing = True - client = main.app.test_client() - - r = client.get('/') - assert r.status_code == 200 - assert 'Hello World' in r.data.decode('utf-8') diff --git a/appengine/flexible_ubuntu/hello_world/requirements-test.txt b/appengine/flexible_ubuntu/hello_world/requirements-test.txt deleted file mode 100644 index 95ea1e6a02b..00000000000 --- a/appengine/flexible_ubuntu/hello_world/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==6.2.4 diff --git a/appengine/flexible_ubuntu/hello_world/requirements.txt b/appengine/flexible_ubuntu/hello_world/requirements.txt deleted file mode 100644 index b690bf61e0a..00000000000 --- a/appengine/flexible_ubuntu/hello_world/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Flask==2.1.1 -gunicorn==20.1.0 diff --git a/appengine/flexible_ubuntu/storage/README.md b/appengine/flexible_ubuntu/storage/README.md deleted file mode 100644 index 9d15e0d3785..00000000000 --- a/appengine/flexible_ubuntu/storage/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Python Google Cloud Storage sample for Google App Engine Flexible 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/flexible_ubuntu/storage/README.md - -This sample demonstrates how to use [Google Cloud Storage](https://cloud.google.com/storage/) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). - -## Setup - -Before you can run or deploy the sample, you will need to do the following: - -1. Enable the Cloud Storage API in the [Google Developers Console](https://console.developers.google.com/project/_/apiui/apiview/storage/overview). - -2. Create a Cloud Storage Bucket. You can do this with the [Google Cloud SDK](https://cloud.google.com/sdk) with the following command: - - $ gsutil mb gs://[your-bucket-name] - -3. Set the default ACL on your bucket to public read in order to serve files directly from Cloud Storage. You can do this with the [Google Cloud SDK](https://cloud.google.com/sdk) with the following command: - - $ gsutil defacl set public-read gs://[your-bucket-name] - -4. 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 - -Then set environment variables before starting your application: - - $ export CLOUD_STORAGE_BUCKET=[your-bucket-name] - $ python main.py diff --git a/appengine/flexible_ubuntu/storage/app.yaml b/appengine/flexible_ubuntu/storage/app.yaml deleted file mode 100644 index 59577b9ee8c..00000000000 --- a/appengine/flexible_ubuntu/storage/app.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2023 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. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - # We also support 3.8, 3.9 and 3.10. - # See https://cloud.google.com/appengine/docs/flexible/python/runtime#interpreter for more info. - runtime_version: "3.11" - operating_system: "ubuntu22" - -#[START gae_flex_storage_yaml] -env_variables: - CLOUD_STORAGE_BUCKET: [your-bucket-name] -#[END gae_flex_storage_yaml] diff --git a/appengine/flexible_ubuntu/storage/main.py b/appengine/flexible_ubuntu/storage/main.py deleted file mode 100644 index 0664d2aa9ab..00000000000 --- a/appengine/flexible_ubuntu/storage/main.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2023 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 gae_flex_ubuntu_storage_app] -import logging -import os -from typing import Union - -from flask import Flask, request -from google.cloud import storage - -app = Flask(__name__) - -# Configure this environment variable via app.yaml -CLOUD_STORAGE_BUCKET = os.environ["CLOUD_STORAGE_BUCKET"] - - -@app.route("/") -def index() -> str: - return """ -
        - - -
        -""" - - -@app.route("/upload", methods=["POST"]) -def upload() -> str: - """Process the uploaded file and upload it to Google Cloud Storage.""" - uploaded_file = request.files.get("file") - - if not uploaded_file: - return "No file uploaded.", 400 - - # Create a Cloud Storage client. - gcs = storage.Client() - - # Get the bucket that the file will be uploaded to. - bucket = gcs.get_bucket(CLOUD_STORAGE_BUCKET) - - # Create a new blob and upload the file's content. - blob = bucket.blob(uploaded_file.filename) - - blob.upload_from_string( - uploaded_file.read(), content_type=uploaded_file.content_type - ) - - # Make the blob public. This is not necessary if the - # entire bucket is public. - # See https://cloud.google.com/storage/docs/access-control/making-data-public. - blob.make_public() - - # The public URL can be used to directly access the uploaded file via HTTP. - return blob.public_url - - -@app.errorhandler(500) -def server_error(e: Union[Exception, int]) -> str: - 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 gae_flex_ubuntu_storage_app] diff --git a/appengine/flexible_ubuntu/storage/main_test.py b/appengine/flexible_ubuntu/storage/main_test.py deleted file mode 100644 index 36d02f83354..00000000000 --- a/appengine/flexible_ubuntu/storage/main_test.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2023 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 io -import os -import uuid - -import flask -import flask.testing -from google.cloud import storage -import pytest - -import requests - -import main - - -@pytest.fixture -def client() -> flask.testing.FlaskClient: - main.app.testing = True - return main.app.test_client() - - -def test_index(client: flask.testing.FlaskClient) -> None: - r = client.get("/") - assert r.status_code == 200 - - -@pytest.fixture(scope="module") -def blob_name() -> str: - name = f"gae-flex-storage-{uuid.uuid4()}" - yield name - - bucket = storage.Client().bucket(os.environ["CLOUD_STORAGE_BUCKET"]) - blob = bucket.blob(name) - blob.delete() - - -def test_upload(client: flask.testing.FlaskClient, blob_name: str) -> None: - # Upload a simple file - file_content = b"This is some test content." - - r = client.post("/upload", data={"file": (io.BytesIO(file_content), blob_name)}) - - assert r.status_code == 200 - - # The app should return the public cloud storage URL for the uploaded - # file. Download and verify it. - cloud_storage_url = r.data.decode("utf-8") - r = requests.get(cloud_storage_url) - assert r.text.encode("utf-8") == file_content diff --git a/appengine/flexible_ubuntu/storage/noxfile_config.py b/appengine/flexible_ubuntu/storage/noxfile_config.py deleted file mode 100644 index 44097a85005..00000000000 --- a/appengine/flexible_ubuntu/storage/noxfile_config.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2023 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. - -# Default TEST_CONFIG_OVERRIDE for python repos. - -# You can copy this file into your directory, then it will be imported from -# the noxfile.py. - -# The source of truth: -# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py - -TEST_CONFIG_OVERRIDE = { - # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"], - # Old samples are opted out of enforcing Python type hints - # All new samples should feature them - "enforce_type_hints": True, - # An envvar key for determining the project id to use. Change it - # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a - # build specific Cloud project. You can also use your own string - # to use your own Cloud project. - "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # If you need to use a specific version of pip, - # change pip_version_override to the string representation - # of the version number, for example, "20.2.4" - "pip_version_override": None, - # A dictionary you want to inject into your test. Don't put any - # secrets here. These values will override predefined values. - "envs": { - "CLOUD_STORAGE_BUCKET": "python-docs-samples-tests-public" - }, -} diff --git a/appengine/flexible_ubuntu/storage/requirements-test.txt b/appengine/flexible_ubuntu/storage/requirements-test.txt deleted file mode 100644 index e6323c66f62..00000000000 --- a/appengine/flexible_ubuntu/storage/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==7.0.1 -google-cloud-storage==2.1.0; diff --git a/appengine/flexible_ubuntu/storage/requirements.txt b/appengine/flexible_ubuntu/storage/requirements.txt deleted file mode 100644 index e254bb503e7..00000000000 --- a/appengine/flexible_ubuntu/storage/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.1.0; -werkzeug==2.1.2; -google-cloud-storage==2.1.0; -gunicorn==20.1.0; From 6d7392091bea9c2e566e49f4149eb3934ea75128 Mon Sep 17 00:00:00 2001 From: Jinglun Dong <1683035+jinglundong@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:34:29 +0000 Subject: [PATCH 2/9] Remove the runtime_version because we want to promote the :latest behavior. --- appengine/flexible/hello_world/app.yaml | 5 +---- appengine/flexible/storage/app.yaml | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/appengine/flexible/hello_world/app.yaml b/appengine/flexible/hello_world/app.yaml index 617e5495b28..ac38af83425 100644 --- a/appengine/flexible/hello_world/app.yaml +++ b/appengine/flexible/hello_world/app.yaml @@ -18,11 +18,8 @@ entrypoint: gunicorn -b :$PORT main:app runtime_config: operating_system: ubuntu22 - # We also support 3.8, 3.9 and 3.11. - # See https://cloud.google.com/appengine/docs/flexible/python/runtime#interpreter for more info. - runtime_version: "3.10" -# This sample incurs costs to run on the App Engine flexible environment. +# This sample incurs costs to run on the App Engine flexible environment. # The settings below are to reduce costs during testing and are not appropriate # for production use. For more information, see: # https://cloud.google.com/appengine/docs/flexible/python/configuring-your-app-with-app-yaml diff --git a/appengine/flexible/storage/app.yaml b/appengine/flexible/storage/app.yaml index 59577b9ee8c..3a7db763d36 100644 --- a/appengine/flexible/storage/app.yaml +++ b/appengine/flexible/storage/app.yaml @@ -17,9 +17,6 @@ env: flex entrypoint: gunicorn -b :$PORT main:app runtime_config: - # We also support 3.8, 3.9 and 3.10. - # See https://cloud.google.com/appengine/docs/flexible/python/runtime#interpreter for more info. - runtime_version: "3.11" operating_system: "ubuntu22" #[START gae_flex_storage_yaml] From 4cdf7dd48bebfbac5b03f940fc6eeb765802c0bd Mon Sep 17 00:00:00 2001 From: Jinglun Dong <1683035+jinglundong@users.noreply.github.com> Date: Fri, 10 Mar 2023 02:38:01 +0000 Subject: [PATCH 3/9] Update the tag to gae_flex_storage_app. --- appengine/flexible/README.md | 2 +- appengine/flexible/storage/main.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/appengine/flexible/README.md b/appengine/flexible/README.md index a21487336b4..db498e5f0bb 100644 --- a/appengine/flexible/README.md +++ b/appengine/flexible/README.md @@ -8,7 +8,7 @@ These are samples for using Python on Google App Engine Flexible Environment. These samples are typically referenced from the [docs](https://cloud.google.com/appengine/docs). For code samples of Python version 3.7 and earlier, please check -https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/appengine/flexible_python37_and_earlier. +https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/appengine/flexible. See our other [Google Cloud Platform github repos](https://github.com/GoogleCloudPlatform) for sample applications and scaffolding for other frameworks and use cases. diff --git a/appengine/flexible/storage/main.py b/appengine/flexible/storage/main.py index 0664d2aa9ab..8c510651dd2 100644 --- a/appengine/flexible/storage/main.py +++ b/appengine/flexible/storage/main.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# [START gae_flex_ubuntu_storage_app] +# [START gae_flex_storage_app] import logging import os from typing import Union @@ -84,4 +84,4 @@ def server_error(e: Union[Exception, int]) -> str: # 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 gae_flex_ubuntu_storage_app] +# [END gae_flex_storage_app] From e1bb524b81b25fc94748986e4b267cb775b90904 Mon Sep 17 00:00:00 2001 From: Jinglun Dong <1683035+jinglundong@users.noreply.github.com> Date: Wed, 15 Mar 2023 22:42:02 +0000 Subject: [PATCH 4/9] Add python 3.11 sample apps for Flex tasks, static files and pubsub. Also updated copyrights blocks. --- appengine/flexible/hello_world/app.yaml | 2 +- .../flexible/hello_world/noxfile_config.py | 2 +- appengine/flexible/pubsub/README.md | 75 +++++ appengine/flexible/pubsub/app.yaml | 28 ++ appengine/flexible/pubsub/main.py | 91 ++++++ appengine/flexible/pubsub/main_test.py | 69 ++++ .../flexible/pubsub/requirements-test.txt | 1 + appengine/flexible/pubsub/requirements.txt | 4 + appengine/flexible/pubsub/sample_message.json | 5 + .../flexible/pubsub/templates/index.html | 36 +++ appengine/flexible/static_files/README.md | 12 + appengine/flexible/static_files/app.yaml | 20 ++ appengine/flexible/static_files/main.py | 42 +++ appengine/flexible/static_files/main_test.py | 26 ++ .../static_files/requirements-test.txt | 1 + .../flexible/static_files/requirements.txt | 3 + .../flexible/static_files/static/main.css | 21 ++ .../static_files/templates/index.html | 29 ++ appengine/flexible/tasks/Dockerfile | 31 ++ appengine/flexible/tasks/README.md | 115 +++++++ appengine/flexible/tasks/app.flexible.yaml | 20 ++ appengine/flexible/tasks/app.yaml | 20 ++ .../tasks/create_app_engine_queue_task.py | 117 +++++++ .../create_app_engine_queue_task_test.py | 27 ++ appengine/flexible/tasks/main.py | 41 +++ appengine/flexible/tasks/main_test.py | 45 +++ .../flexible/tasks/requirements-test.txt | 1 + appengine/flexible/tasks/requirements.txt | 4 + appengine/flexible/tasks/snippets.py | 303 ++++++++++++++++++ appengine/flexible/tasks/snippets_test.py | 127 ++++++++ appengine/flexible/tasks/templates/index.html | 21 ++ .../hello_world/app.yaml | 2 +- 32 files changed, 1338 insertions(+), 3 deletions(-) create mode 100644 appengine/flexible/pubsub/README.md create mode 100644 appengine/flexible/pubsub/app.yaml create mode 100644 appengine/flexible/pubsub/main.py create mode 100644 appengine/flexible/pubsub/main_test.py create mode 100644 appengine/flexible/pubsub/requirements-test.txt create mode 100644 appengine/flexible/pubsub/requirements.txt create mode 100644 appengine/flexible/pubsub/sample_message.json create mode 100644 appengine/flexible/pubsub/templates/index.html create mode 100644 appengine/flexible/static_files/README.md create mode 100644 appengine/flexible/static_files/app.yaml create mode 100644 appengine/flexible/static_files/main.py create mode 100644 appengine/flexible/static_files/main_test.py create mode 100644 appengine/flexible/static_files/requirements-test.txt create mode 100644 appengine/flexible/static_files/requirements.txt create mode 100644 appengine/flexible/static_files/static/main.css create mode 100644 appengine/flexible/static_files/templates/index.html create mode 100644 appengine/flexible/tasks/Dockerfile create mode 100644 appengine/flexible/tasks/README.md create mode 100644 appengine/flexible/tasks/app.flexible.yaml create mode 100644 appengine/flexible/tasks/app.yaml create mode 100644 appengine/flexible/tasks/create_app_engine_queue_task.py create mode 100644 appengine/flexible/tasks/create_app_engine_queue_task_test.py create mode 100644 appengine/flexible/tasks/main.py create mode 100644 appengine/flexible/tasks/main_test.py create mode 100644 appengine/flexible/tasks/requirements-test.txt create mode 100644 appengine/flexible/tasks/requirements.txt create mode 100644 appengine/flexible/tasks/snippets.py create mode 100644 appengine/flexible/tasks/snippets_test.py create mode 100644 appengine/flexible/tasks/templates/index.html diff --git a/appengine/flexible/hello_world/app.yaml b/appengine/flexible/hello_world/app.yaml index ac38af83425..fb982fc8dd2 100644 --- a/appengine/flexible/hello_world/app.yaml +++ b/appengine/flexible/hello_world/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/hello_world/noxfile_config.py b/appengine/flexible/hello_world/noxfile_config.py index 545546d21cb..e5e2be514e2 100644 --- a/appengine/flexible/hello_world/noxfile_config.py +++ b/appengine/flexible/hello_world/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/pubsub/README.md b/appengine/flexible/pubsub/README.md new file mode 100644 index 00000000000..2e9b0d71918 --- /dev/null +++ b/appengine/flexible/pubsub/README.md @@ -0,0 +1,75 @@ +# Python Google Cloud Pub/Sub sample for Google App Engine Flexible 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/flexible_python37_and_earlier/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 Flexible Environment](https://cloud.google.com/appengine). + +## 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 beta pubsub topics create [your-topic-name] + $ gcloud beta pubsub subscriptions create [your-subscription-name] \ + --topic [your-topic-name] \ + --push-endpoint \ + https://[your-app-id].appspot.com/pubsub/push?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, please follow https://cloud.google.com/python/docs/setup +to set up a Python development environment. Then run: + + $ pip install -r requirements.txt + +Then set environment variables before starting your application: + + $ 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/pubsub/push?token=[your-token]" + +Or + + $ http POST ":8080/pubsub/push?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/flexible/pubsub/app.yaml b/appengine/flexible/pubsub/app.yaml new file mode 100644 index 00000000000..327cfd0d638 --- /dev/null +++ b/appengine/flexible/pubsub/app.yaml @@ -0,0 +1,28 @@ +# Copyright 2023 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. + +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + operating_system: "ubuntu22" + +# [START gae_flex_pubsub_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 gae_flex_pubsub_env] diff --git a/appengine/flexible/pubsub/main.py b/appengine/flexible/pubsub/main.py new file mode 100644 index 00000000000..8db0a665370 --- /dev/null +++ b/appengine/flexible/pubsub/main.py @@ -0,0 +1,91 @@ +# Copyright 2023 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 logging +import os + +from flask import current_app, Flask, render_template, request +from google.cloud import pubsub_v1 + + +app = Flask(__name__) + +# Configure the following environment variables via app.yaml +# This is used in the push request handler to verify 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['PROJECT'] = os.environ['GOOGLE_CLOUD_PROJECT'] + + +# Global list to storage messages received by this instance. +MESSAGES = [] + +# Initialize the publisher client once to avoid memory leak +# and reduce publish latency. +publisher = pubsub_v1.PublisherClient() + + +# [START gae_flex_pubsub_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') + + # publisher = pubsub_v1.PublisherClient() + topic_path = publisher.topic_path( + current_app.config['PROJECT'], + current_app.config['PUBSUB_TOPIC']) + + publisher.publish(topic_path, data=data) + + return 'OK', 200 +# [END gae_flex_pubsub_index] + + +# [START gae_flex_pubsub_push] +@app.route('/pubsub/push', methods=['POST']) +def pubsub_push(): + 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 gae_flex_pubsub_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) diff --git a/appengine/flexible/pubsub/main_test.py b/appengine/flexible/pubsub/main_test.py new file mode 100644 index 00000000000..086e74cfeea --- /dev/null +++ b/appengine/flexible/pubsub/main_test.py @@ -0,0 +1,69 @@ +# Copyright 2023 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 = '/pubsub/push?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('/pubsub/push') + assert r.status_code == 400 + + # invalid token + r = client.post('/pubsub/push?token=bad') + assert r.status_code == 400 diff --git a/appengine/flexible/pubsub/requirements-test.txt b/appengine/flexible/pubsub/requirements-test.txt new file mode 100644 index 00000000000..c2845bffbe8 --- /dev/null +++ b/appengine/flexible/pubsub/requirements-test.txt @@ -0,0 +1 @@ +pytest==7.0.1 diff --git a/appengine/flexible/pubsub/requirements.txt b/appengine/flexible/pubsub/requirements.txt new file mode 100644 index 00000000000..30e9d9736b5 --- /dev/null +++ b/appengine/flexible/pubsub/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.1.0; python_version > '3.6' +Flask==2.0.3; python_version < '3.7' +google-cloud-pubsub==2.9.0 +gunicorn==20.1.0 diff --git a/appengine/flexible/pubsub/sample_message.json b/appengine/flexible/pubsub/sample_message.json new file mode 100644 index 00000000000..8fe62d23fb9 --- /dev/null +++ b/appengine/flexible/pubsub/sample_message.json @@ -0,0 +1,5 @@ +{ + "message": { + "data": "SGVsbG8sIFdvcmxkIQ==" + } +} diff --git a/appengine/flexible/pubsub/templates/index.html b/appengine/flexible/pubsub/templates/index.html new file mode 100644 index 00000000000..b2e73307c78 --- /dev/null +++ b/appengine/flexible/pubsub/templates/index.html @@ -0,0 +1,36 @@ +{# +# Copyright 2023 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. +#} + + + + Pub/Sub Python on Google App Engine Flexible Environment + + +
        +

        Messages received by this instance:

        +
          + {% for message in messages: %} +
        • {{message}}
        • + {% endfor %} +
        +

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

        +
        +
        + + +
        + + diff --git a/appengine/flexible/static_files/README.md b/appengine/flexible/static_files/README.md new file mode 100644 index 00000000000..024a0abfbd9 --- /dev/null +++ b/appengine/flexible/static_files/README.md @@ -0,0 +1,12 @@ +# Python / Flask static files sample for Google App Engine Flexible 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/flexible_python37_and_earlier/static_files/README.md + +This demonstrates how to use [Flask](http://flask.pocoo.org/) to serve static files in your application. + +Flask automatically makes anything in the ``static`` directory available via the ``/static`` URL. If you plan on using a different framework, it may have different conventions for serving static files. + +Refer to the [top-level README](../README.md) for instructions on running and deploying. diff --git a/appengine/flexible/static_files/app.yaml b/appengine/flexible/static_files/app.yaml new file mode 100644 index 00000000000..f44d878efb7 --- /dev/null +++ b/appengine/flexible/static_files/app.yaml @@ -0,0 +1,20 @@ +# Copyright 2023 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. + +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + operating_system: "ubuntu22" diff --git a/appengine/flexible/static_files/main.py b/appengine/flexible/static_files/main.py new file mode 100644 index 00000000000..5da67ecd211 --- /dev/null +++ b/appengine/flexible/static_files/main.py @@ -0,0 +1,42 @@ +# Copyright 2023 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 gae_flex_python_static_files] +import logging + +from flask import Flask, render_template + + +app = Flask(__name__) + + +@app.route('/') +def hello(): + return render_template('index.html') + + +@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 gae_flex_python_static_files] diff --git a/appengine/flexible/static_files/main_test.py b/appengine/flexible/static_files/main_test.py new file mode 100644 index 00000000000..a1d9aab8356 --- /dev/null +++ b/appengine/flexible/static_files/main_test.py @@ -0,0 +1,26 @@ +# Copyright 2023 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 main + + +def test_index(): + main.app.testing = True + client = main.app.test_client() + + r = client.get('/') + assert r.status_code == 200 + + r = client.get('/static/main.css') + assert r.status_code == 200 diff --git a/appengine/flexible/static_files/requirements-test.txt b/appengine/flexible/static_files/requirements-test.txt new file mode 100644 index 00000000000..c2845bffbe8 --- /dev/null +++ b/appengine/flexible/static_files/requirements-test.txt @@ -0,0 +1 @@ +pytest==7.0.1 diff --git a/appengine/flexible/static_files/requirements.txt b/appengine/flexible/static_files/requirements.txt new file mode 100644 index 00000000000..beeed7bed09 --- /dev/null +++ b/appengine/flexible/static_files/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.1.0; python_version > '3.6' +Flask==2.0.3; python_version < '3.7' +gunicorn==20.1.0 diff --git a/appengine/flexible/static_files/static/main.css b/appengine/flexible/static_files/static/main.css new file mode 100644 index 00000000000..6c80163acd3 --- /dev/null +++ b/appengine/flexible/static_files/static/main.css @@ -0,0 +1,21 @@ +/* +Copyright 2023 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 gae_flex_python_css] */ +body { + font-family: Verdana, Helvetica, sans-serif; + background-color: #CCCCFF; +} +/* [END gae_flex_python_css] */ diff --git a/appengine/flexible/static_files/templates/index.html b/appengine/flexible/static_files/templates/index.html new file mode 100644 index 00000000000..9792c0a8b29 --- /dev/null +++ b/appengine/flexible/static_files/templates/index.html @@ -0,0 +1,29 @@ +{# +# Copyright 2023 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. +#} + + + + Static Files + + + + +

        This is a static file serving example.

        + + diff --git a/appengine/flexible/tasks/Dockerfile b/appengine/flexible/tasks/Dockerfile new file mode 100644 index 00000000000..ebea5746fdb --- /dev/null +++ b/appengine/flexible/tasks/Dockerfile @@ -0,0 +1,31 @@ +# Copyright 2023 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. + +# Use the official Python image. +# https://hub.docker.com/_/python +FROM python:3.10 + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . . + +# Install production dependencies. +RUN pip install Flask gunicorn + +# Run the web service on container startup. Here we use the gunicorn +# webserver, with one worker process and 8 threads. +# For environments with multiple CPU cores, increase the number of workers +# to be equal to the cores available. +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main:app diff --git a/appengine/flexible/tasks/README.md b/appengine/flexible/tasks/README.md new file mode 100644 index 00000000000..e55d89522a6 --- /dev/null +++ b/appengine/flexible/tasks/README.md @@ -0,0 +1,115 @@ +# Google Cloud Tasks Samples + +[![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/flexible_python37_and_earlier/tasks/README.md + +Sample command-line programs for interacting with the Cloud Tasks API +. + +App Engine queues push tasks to an App Engine HTTP target. This directory +contains both the App Engine app to deploy, as well as the snippets to run +locally to push tasks to it, which could also be called on App Engine. + +`create_app_engine_queue_task.py` is a simple command-line program to create +tasks to be pushed to the App Engine app. + +`main.py` is the main App Engine app. This app serves as an endpoint to receive +App Engine task attempts. + +`app.yaml` configures the App Engine app. + + +## Prerequisites to run locally: + +Please refer to [Setting Up a Python Development Environment](https://cloud.google.com/python/setup). + +### Authentication + +To set up authentication, please refer to our +[authentication getting started guide](https://cloud.google.com/docs/authentication/getting-started). + +### Install Dependencies + +To install the dependencies for this sample, use the following command: + +``` +pip install -r requirements.txt +``` + +This sample uses the common protos in the [googleapis](https://github.com/googleapis/googleapis) +repository. For more info, see +[Protocol Buffer Basics](https://developers.google.com/protocol-buffers/docs/pythontutorial). + +## Creating a queue + +To create a queue using the Cloud SDK, use the following gcloud command: + +``` +gcloud tasks queues create my-appengine-queue +``` + +Note: A newly created queue will route to the default App Engine service and +version unless configured to do otherwise. + +## Deploying the App Engine App + +Deploy the App Engine app with gcloud: + +* To deploy to the Standard environment: + ``` + gcloud app deploy app.yaml + ``` +* To deploy to the Flexible environment: + ``` + gcloud app deploy app.flexible.yaml + ``` + +Verify the index page is serving: + +``` +gcloud app browse +``` + +The App Engine app serves as a target for the push requests. It has an +endpoint `/example_task_handler` that reads the payload (i.e., the request body) +of the HTTP POST request and logs it. The log output can be viewed with: + +``` +gcloud app logs read +``` + +## Run the Sample Using the Command Line + +Set environment variables: + +First, your project ID: + +``` +export PROJECT_ID=my-project-id +``` + +Then the queue ID, as specified at queue creation time. Queue IDs already +created can be listed with `gcloud tasks queues list`. + +``` +export QUEUE_ID=my-appengine-queue +``` + +And finally the location ID, which can be discovered with +`gcloud tasks queues describe $QUEUE_ID`, with the location embedded in +the "name" value (for instance, if the name is +"projects/my-project/locations/us-central1/queues/my-appengine-queue", then the +location is "us-central1"). + +``` +export LOCATION_ID=us-central1 +``` +### Using App Engine Queues +Running the sample will create a task, targeted at the `/example_task_handler` +endpoint, with a payload specified: + +``` +python create_app_engine_queue_task.py --project=$PROJECT_ID --queue=$QUEUE_ID --location=$LOCATION_ID --payload=hello +``` diff --git a/appengine/flexible/tasks/app.flexible.yaml b/appengine/flexible/tasks/app.flexible.yaml new file mode 100644 index 00000000000..8f857501790 --- /dev/null +++ b/appengine/flexible/tasks/app.flexible.yaml @@ -0,0 +1,20 @@ +# Copyright 2023 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. + +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT --threads=4 main:app + +runtime_config: + python_version: 3 diff --git a/appengine/flexible/tasks/app.yaml b/appengine/flexible/tasks/app.yaml new file mode 100644 index 00000000000..f44d878efb7 --- /dev/null +++ b/appengine/flexible/tasks/app.yaml @@ -0,0 +1,20 @@ +# Copyright 2023 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. + +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + operating_system: "ubuntu22" diff --git a/appengine/flexible/tasks/create_app_engine_queue_task.py b/appengine/flexible/tasks/create_app_engine_queue_task.py new file mode 100644 index 00000000000..457100e878c --- /dev/null +++ b/appengine/flexible/tasks/create_app_engine_queue_task.py @@ -0,0 +1,117 @@ +# Copyright 2023 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. + +from __future__ import print_function + +import argparse + + +def create_task(project, queue, location, payload=None, in_seconds=None): + # [START cloud_tasks_appengine_create_task] + """Create a task for a given queue with an arbitrary payload.""" + + from google.cloud import tasks_v2 + from google.protobuf import timestamp_pb2 + import datetime + import json + + # Create a client. + client = tasks_v2.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # queue = 'my-appengine-queue' + # location = 'us-central1' + # payload = 'hello' or {'param': 'value'} for application/json + # in_seconds = None + + # Construct the fully qualified queue name. + parent = client.queue_path(project, location, queue) + + # Construct the request body. + task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': tasks_v2.HttpMethod.POST, + 'relative_uri': '/example_task_handler' + } + } + if payload is not None: + if isinstance(payload, dict): + # Convert dict to JSON string + payload = json.dumps(payload) + # specify http content-type to application/json + task["app_engine_http_request"]["headers"] = {"Content-type": "application/json"} + # The API expects a payload of type bytes. + converted_payload = payload.encode() + + # Add the payload to the request. + task['app_engine_http_request']['body'] = converted_payload + + if in_seconds is not None: + # Convert "seconds from now" into an rfc3339 datetime string. + d = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(seconds=in_seconds) + + # Create Timestamp protobuf. + timestamp = timestamp_pb2.Timestamp() + timestamp.FromDatetime(d) + + # Add the timestamp to the tasks. + task['schedule_time'] = timestamp + + # Use the client to build and send the task. + response = client.create_task(parent=parent, task=task) + + print('Created task {}'.format(response.name)) + return response +# [END cloud_tasks_appengine_create_task] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=create_task.__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument( + '--project', + help='Project of the queue to add the task to.', + required=True, + ) + + parser.add_argument( + '--queue', + help='ID (short name) of the queue to add the task to.', + required=True, + ) + + parser.add_argument( + '--location', + help='Location of the queue to add the task to.', + required=True, + ) + + parser.add_argument( + '--payload', + help='Optional payload to attach to the push queue.' + ) + + parser.add_argument( + '--in_seconds', type=int, + help='The number of seconds from now to schedule task attempt.' + ) + + args = parser.parse_args() + + create_task( + args.project, args.queue, args.location, + args.payload, args.in_seconds) diff --git a/appengine/flexible/tasks/create_app_engine_queue_task_test.py b/appengine/flexible/tasks/create_app_engine_queue_task_test.py new file mode 100644 index 00000000000..5cebd84eef8 --- /dev/null +++ b/appengine/flexible/tasks/create_app_engine_queue_task_test.py @@ -0,0 +1,27 @@ +# Copyright 2023 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 os + +import create_app_engine_queue_task + +TEST_PROJECT_ID = os.getenv('GOOGLE_CLOUD_PROJECT') +TEST_LOCATION = os.getenv('TEST_QUEUE_LOCATION', 'us-central1') +TEST_QUEUE_NAME = os.getenv('TEST_QUEUE_NAME', 'my-appengine-queue') + + +def test_create_task(): + result = create_app_engine_queue_task.create_task( + TEST_PROJECT_ID, TEST_QUEUE_NAME, TEST_LOCATION) + assert TEST_QUEUE_NAME in result.name diff --git a/appengine/flexible/tasks/main.py b/appengine/flexible/tasks/main.py new file mode 100644 index 00000000000..e58c8f9533c --- /dev/null +++ b/appengine/flexible/tasks/main.py @@ -0,0 +1,41 @@ +# Copyright 2023 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. + +"""App Engine app to serve as an endpoint for App Engine queue samples.""" + +# [START cloud_tasks_appengine_quickstart] +from flask import Flask, render_template, request + +app = Flask(__name__) + + +@app.route('/example_task_handler', methods=['POST']) +def example_task_handler(): + """Log the request payload.""" + payload = request.get_data(as_text=True) or '(empty payload)' + print('Received task with payload: {}'.format(payload)) + return render_template("index.html", payload=payload) +# [END cloud_tasks_appengine_quickstart] + + +@app.route('/') +def hello(): + """Basic index to verify app is serving.""" + return 'Hello World!' + + +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) diff --git a/appengine/flexible/tasks/main_test.py b/appengine/flexible/tasks/main_test.py new file mode 100644 index 00000000000..e4ba2a63cfc --- /dev/null +++ b/appengine/flexible/tasks/main_test.py @@ -0,0 +1,45 @@ +# Copyright 2023 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 pytest + + +@pytest.fixture +def app(): + import main + main.app.testing = True + return main.app.test_client() + + +def test_index(app): + r = app.get('/') + assert r.status_code == 200 + + +def test_log_payload(capsys, app): + payload = 'test_payload' + + r = app.post('/example_task_handler', data=payload) + assert r.status_code == 200 + + out, _ = capsys.readouterr() + assert payload in out + + +def test_empty_payload(capsys, app): + r = app.post('/example_task_handler') + assert r.status_code == 200 + + out, _ = capsys.readouterr() + assert 'empty payload' in out diff --git a/appengine/flexible/tasks/requirements-test.txt b/appengine/flexible/tasks/requirements-test.txt new file mode 100644 index 00000000000..c2845bffbe8 --- /dev/null +++ b/appengine/flexible/tasks/requirements-test.txt @@ -0,0 +1 @@ +pytest==7.0.1 diff --git a/appengine/flexible/tasks/requirements.txt b/appengine/flexible/tasks/requirements.txt new file mode 100644 index 00000000000..f11b326af4b --- /dev/null +++ b/appengine/flexible/tasks/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.1.0; python_version > '3.6' +Flask==2.0.3; python_version < '3.7' +gunicorn==20.1.0 +google-cloud-tasks==2.7.1 diff --git a/appengine/flexible/tasks/snippets.py b/appengine/flexible/tasks/snippets.py new file mode 100644 index 00000000000..115fc8d7077 --- /dev/null +++ b/appengine/flexible/tasks/snippets.py @@ -0,0 +1,303 @@ +# Copyright 2023 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. + +from google.cloud import tasks + + +def create_queue(project, location, queue_blue_name, queue_red_name): + # [START cloud_tasks_taskqueues_using_yaml] + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue_blue_name = 'queue-blue' + # queue_red_name = 'queue-red' + + parent = f"projects/{project}/locations/{location}" + + queue_blue = { + 'name': client.queue_path(project, location, queue_blue_name), + 'rate_limits': { + 'max_dispatches_per_second': 5 + }, + 'app_engine_routing_override': { + 'version': 'v2', + 'service': 'task-module' + } + } + + queue_red = { + 'name': client.queue_path(project, location, queue_red_name), + 'rate_limits': { + 'max_dispatches_per_second': 1 + } + } + + queues = [queue_blue, queue_red] + for queue in queues: + response = client.create_queue(parent=parent, queue=queue) + print(response) + # [END cloud_tasks_taskqueues_using_yaml] + return response + + +def update_queue(project, location, queue): + # [START cloud_tasks_taskqueues_processing_rate] + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue = 'queue-blue' + + # Get queue object + queue_path = client.queue_path(project, location, queue) + queue = client.get_queue(name=queue_path) + + # Update queue object + queue.rate_limits.max_dispatches_per_second = 20 + queue.rate_limits.max_concurrent_dispatches = 10 + + response = client.update_queue(queue=queue) + print(response) + # [END cloud_tasks_taskqueues_processing_rate] + return response + + +def create_task(project, location, queue): + # [START cloud_tasks_taskqueues_new_task] + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue = 'default' + amount = 10 + + parent = client.queue_path(project, location, queue) + + task = { + 'app_engine_http_request': { + 'http_method': tasks.HttpMethod.POST, + 'relative_uri': '/update_counter', + 'app_engine_routing': { + 'service': 'worker' + }, + 'body': str(amount).encode() + } + } + + response = client.create_task(parent=parent, task=task) + eta = response.schedule_time.strftime("%m/%d/%Y, %H:%M:%S") + print('Task {} enqueued, ETA {}.'.format(response.name, eta)) + # [END cloud_tasks_taskqueues_new_task] + return response + + +def create_tasks_with_data(project, location, queue): + # [START cloud_tasks_taskqueues_passing_data] + import json + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue = 'default' + + parent = client.queue_path(project, location, queue) + + task1 = { + 'app_engine_http_request': { + 'http_method': tasks.HttpMethod.POST, + 'relative_uri': '/update_counter?key=blue', + 'app_engine_routing': { + 'service': 'worker' + } + } + } + + task2 = { + 'app_engine_http_request': { + 'http_method': tasks.HttpMethod.POST, + 'relative_uri': '/update_counter', + 'app_engine_routing': { + 'service': 'worker' + }, + 'headers': { + 'Content-Type': 'application/json' + }, + 'body': json.dumps({'key': 'blue'}).encode() + } + } + + response = client.create_task(parent=parent, task=task1) + print(response) + response = client.create_task(parent=parent, task=task2) + print(response) + # [END cloud_tasks_taskqueues_passing_data] + return response + + +def create_task_with_name(project, location, queue, task_name): + # [START cloud_tasks_taskqueues_naming_tasks] + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue = 'default' + # task_name = 'first-try' + + parent = client.queue_path(project, location, queue) + + task = { + 'name': client.task_path(project, location, queue, task_name), + 'app_engine_http_request': { + 'http_method': tasks.HttpMethod.GET, + 'relative_uri': '/url/path' + } + } + response = client.create_task(parent=parent, task=task) + print(response) + # [END cloud_tasks_taskqueues_naming_tasks] + return response + + +def delete_task(project, location, queue): + # [START cloud_tasks_taskqueues_deleting_tasks] + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue = 'queue1' + + task_path = client.task_path(project, location, queue, 'foo') + response = client.delete_task(name=task_path) + # [END cloud_tasks_taskqueues_deleting_tasks] + return response + + +def purge_queue(project, location, queue): + # [START cloud_tasks_taskqueues_purging_tasks] + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue = 'queue1' + + queue_path = client.queue_path(project, location, queue) + response = client.purge_queue(name=queue_path) + # [END cloud_tasks_taskqueues_purging_tasks] + return response + + +def pause_queue(project, location, queue): + # [START cloud_tasks_taskqueues_pause_queue] + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue = 'queue1' + + queue_path = client.queue_path(project, location, queue) + response = client.pause_queue(name=queue_path) + # [END cloud_tasks_taskqueues_pause_queue] + return response + + +def delete_queue(project, location, queue): + # [START cloud_tasks_taskqueues_deleting_queues] + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # queue = 'queue1' + + queue_path = client.queue_path(project, location, queue) + response = client.delete_queue(name=queue_path) + # [END cloud_tasks_taskqueues_deleting_queues] + return response + + +def retry_task(project, location, fooqueue, barqueue, bazqueue): + # [START cloud_tasks_taskqueues_retrying_tasks] + from google.protobuf import duration_pb2 + + client = tasks.CloudTasksClient() + + # TODO(developer): Uncomment these lines and replace with your values. + # project = 'my-project-id' + # location = 'us- central1' + # fooqueue = 'fooqueue' + # barqueue = 'barqueue' + # bazqueue = 'bazqueue' + + parent = f"projects/{project}/locations/{location}" + + max_retry = duration_pb2.Duration() + max_retry.seconds = 2*60*60*24 + + foo = { + 'name': client.queue_path(project, location, fooqueue), + 'rate_limits': { + 'max_dispatches_per_second': 1 + }, + 'retry_config': { + 'max_attempts': 7, + 'max_retry_duration': max_retry + } + } + + min = duration_pb2.Duration() + min.seconds = 10 + + max = duration_pb2.Duration() + max.seconds = 200 + + bar = { + 'name': client.queue_path(project, location, barqueue), + 'rate_limits': { + 'max_dispatches_per_second': 1 + }, + 'retry_config': { + 'min_backoff': min, + 'max_backoff': max, + 'max_doublings': 0 + } + } + + max.seconds = 300 + baz = { + 'name': client.queue_path(project, location, bazqueue), + 'rate_limits': { + 'max_dispatches_per_second': 1 + }, + 'retry_config': { + 'min_backoff': min, + 'max_backoff': max, + 'max_doublings': 3 + } + } + + queues = [foo, bar, baz] + for queue in queues: + response = client.create_queue(parent=parent, queue=queue) + print(response) + # [END cloud_tasks_taskqueues_retrying_tasks] + return response diff --git a/appengine/flexible/tasks/snippets_test.py b/appengine/flexible/tasks/snippets_test.py new file mode 100644 index 00000000000..0cbda7fbfa4 --- /dev/null +++ b/appengine/flexible/tasks/snippets_test.py @@ -0,0 +1,127 @@ +# Copyright 2023 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 os +import uuid + +import pytest + +import snippets + +TEST_PROJECT_ID = os.getenv('GOOGLE_CLOUD_PROJECT') +TEST_LOCATION = os.getenv('TEST_QUEUE_LOCATION', 'us-central1') +QUEUE_NAME_1 = "queue-{}".format(uuid.uuid4()) +QUEUE_NAME_2 = "queue-{}".format(uuid.uuid4()) + + +@pytest.mark.order1 +def test_create_queue(): + name = "projects/{}/locations/{}/queues/{}".format( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_2) + result = snippets.create_queue( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1, QUEUE_NAME_2) + assert name in result.name + + +@pytest.mark.order2 +def test_update_queue(): + name = "projects/{}/locations/{}/queues/{}".format( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + result = snippets.update_queue( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + assert name in result.name + + +@pytest.mark.order3 +def test_create_task(): + name = "projects/{}/locations/{}/queues/{}".format( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + result = snippets.create_task( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + assert name in result.name + + +@pytest.mark.order4 +def test_create_task_with_data(): + name = "projects/{}/locations/{}/queues/{}".format( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + result = snippets.create_tasks_with_data( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + assert name in result.name + + +@pytest.mark.order5 +def test_create_task_with_name(): + name = "projects/{}/locations/{}/queues/{}".format( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + result = snippets.create_task_with_name( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1, 'foo') + assert name in result.name + + +@pytest.mark.order6 +def test_delete_task(): + result = snippets.delete_task( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + assert result is None + + +@pytest.mark.order7 +def test_purge_queue(): + name = "projects/{}/locations/{}/queues/{}".format( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + result = snippets.purge_queue( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + assert name in result.name + + +@pytest.mark.order8 +def test_pause_queue(): + name = "projects/{}/locations/{}/queues/{}".format( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + result = snippets.pause_queue( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + assert name in result.name + + +@pytest.mark.order9 +def test_delete_queue(): + result = snippets.delete_queue( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_1) + assert result is None + + result = snippets.delete_queue( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME_2) + assert result is None + + +@pytest.mark.order10 +def test_retry_task(): + QUEUE_SIZE = 3 + QUEUE_NAME = [] + for i in range(QUEUE_SIZE): + QUEUE_NAME.append("queue-{}".format(uuid.uuid4())) + + name = "projects/{}/locations/{}/queues/{}".format( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME[2]) + result = snippets.retry_task( + TEST_PROJECT_ID, TEST_LOCATION, QUEUE_NAME[0], QUEUE_NAME[1], + QUEUE_NAME[2]) + assert name in result.name + + for i in range(QUEUE_SIZE): + snippets.delete_queue( + project=TEST_PROJECT_ID, + location=TEST_LOCATION, + queue=QUEUE_NAME[i]) diff --git a/appengine/flexible/tasks/templates/index.html b/appengine/flexible/tasks/templates/index.html new file mode 100644 index 00000000000..31fbb7bb793 --- /dev/null +++ b/appengine/flexible/tasks/templates/index.html @@ -0,0 +1,21 @@ + + + + + Tasks Sample + + +

        Printed task payload: {{ payload }}

        + + \ No newline at end of file diff --git a/appengine/flexible_python37_and_earlier/hello_world/app.yaml b/appengine/flexible_python37_and_earlier/hello_world/app.yaml index 7aa7a47e159..1507bd341f3 100644 --- a/appengine/flexible_python37_and_earlier/hello_world/app.yaml +++ b/appengine/flexible_python37_and_earlier/hello_world/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From ad359691e4c001565e81b8474964dc2111c1cca9 Mon Sep 17 00:00:00 2001 From: Jinglun Dong <1683035+jinglundong@users.noreply.github.com> Date: Wed, 15 Mar 2023 22:47:11 +0000 Subject: [PATCH 5/9] Update some code path references. --- appengine/flexible/pubsub/README.md | 2 +- appengine/flexible/static_files/README.md | 2 +- appengine/flexible/tasks/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/appengine/flexible/pubsub/README.md b/appengine/flexible/pubsub/README.md index 2e9b0d71918..47707840594 100644 --- a/appengine/flexible/pubsub/README.md +++ b/appengine/flexible/pubsub/README.md @@ -3,7 +3,7 @@ [![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/flexible_python37_and_earlier/pubsub/README.md +[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible/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 Flexible Environment](https://cloud.google.com/appengine). diff --git a/appengine/flexible/static_files/README.md b/appengine/flexible/static_files/README.md index 024a0abfbd9..249a478127e 100644 --- a/appengine/flexible/static_files/README.md +++ b/appengine/flexible/static_files/README.md @@ -3,7 +3,7 @@ [![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/flexible_python37_and_earlier/static_files/README.md +[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible/static_files/README.md This demonstrates how to use [Flask](http://flask.pocoo.org/) to serve static files in your application. diff --git a/appengine/flexible/tasks/README.md b/appengine/flexible/tasks/README.md index e55d89522a6..98468c70698 100644 --- a/appengine/flexible/tasks/README.md +++ b/appengine/flexible/tasks/README.md @@ -3,7 +3,7 @@ [![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/flexible_python37_and_earlier/tasks/README.md +[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/flexible/tasks/README.md Sample command-line programs for interacting with the Cloud Tasks API . From b7b376b44cfc928988c1e28de5068f83bd1bf8a3 Mon Sep 17 00:00:00 2001 From: Jinglun Dong <1683035+jinglundong@users.noreply.github.com> Date: Wed, 15 Mar 2023 23:01:18 +0000 Subject: [PATCH 6/9] Revert the changes to the copyright year. --- appengine/flexible/hello_world/app.yaml | 2 +- appengine/flexible/hello_world/main.py | 2 +- appengine/flexible/hello_world/main_test.py | 2 +- appengine/flexible/hello_world/noxfile_config.py | 2 +- appengine/flexible/pubsub/app.yaml | 2 +- appengine/flexible/pubsub/main.py | 2 +- appengine/flexible/pubsub/main_test.py | 2 +- appengine/flexible/pubsub/templates/index.html | 2 +- appengine/flexible/static_files/app.yaml | 2 +- appengine/flexible/static_files/main.py | 2 +- appengine/flexible/static_files/main_test.py | 2 +- appengine/flexible/static_files/static/main.css | 2 +- appengine/flexible/static_files/templates/index.html | 2 +- appengine/flexible/storage/app.yaml | 2 +- appengine/flexible/storage/main.py | 2 +- appengine/flexible/storage/main_test.py | 2 +- appengine/flexible/storage/noxfile_config.py | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/appengine/flexible/hello_world/app.yaml b/appengine/flexible/hello_world/app.yaml index fb982fc8dd2..ac38af83425 100644 --- a/appengine/flexible/hello_world/app.yaml +++ b/appengine/flexible/hello_world/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/hello_world/main.py b/appengine/flexible/hello_world/main.py index d238f1a6f46..ebbd393ef54 100644 --- a/appengine/flexible/hello_world/main.py +++ b/appengine/flexible/hello_world/main.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2015 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/hello_world/main_test.py b/appengine/flexible/hello_world/main_test.py index 2a025c095e6..341059c348d 100644 --- a/appengine/flexible/hello_world/main_test.py +++ b/appengine/flexible/hello_world/main_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2015 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/hello_world/noxfile_config.py b/appengine/flexible/hello_world/noxfile_config.py index e5e2be514e2..545546d21cb 100644 --- a/appengine/flexible/hello_world/noxfile_config.py +++ b/appengine/flexible/hello_world/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/pubsub/app.yaml b/appengine/flexible/pubsub/app.yaml index 327cfd0d638..95175b76687 100644 --- a/appengine/flexible/pubsub/app.yaml +++ b/appengine/flexible/pubsub/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/pubsub/main.py b/appengine/flexible/pubsub/main.py index 8db0a665370..f146a0788b8 100644 --- a/appengine/flexible/pubsub/main.py +++ b/appengine/flexible/pubsub/main.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2015 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/pubsub/main_test.py b/appengine/flexible/pubsub/main_test.py index 086e74cfeea..67fdb47d784 100644 --- a/appengine/flexible/pubsub/main_test.py +++ b/appengine/flexible/pubsub/main_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2015 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/pubsub/templates/index.html b/appengine/flexible/pubsub/templates/index.html index b2e73307c78..28449216c37 100644 --- a/appengine/flexible/pubsub/templates/index.html +++ b/appengine/flexible/pubsub/templates/index.html @@ -1,5 +1,5 @@ {# -# Copyright 2023 Google LLC. +# Copyright 2015 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/static_files/app.yaml b/appengine/flexible/static_files/app.yaml index f44d878efb7..3605c19a7cb 100644 --- a/appengine/flexible/static_files/app.yaml +++ b/appengine/flexible/static_files/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/static_files/main.py b/appengine/flexible/static_files/main.py index 5da67ecd211..c25c43de6cf 100644 --- a/appengine/flexible/static_files/main.py +++ b/appengine/flexible/static_files/main.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2015 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/static_files/main_test.py b/appengine/flexible/static_files/main_test.py index a1d9aab8356..6fb47d6ff51 100644 --- a/appengine/flexible/static_files/main_test.py +++ b/appengine/flexible/static_files/main_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2015 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/static_files/static/main.css b/appengine/flexible/static_files/static/main.css index 6c80163acd3..f906044f4e4 100644 --- a/appengine/flexible/static_files/static/main.css +++ b/appengine/flexible/static_files/static/main.css @@ -1,5 +1,5 @@ /* -Copyright 2023 Google LLC. +Copyright 2015 Google LLC. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/appengine/flexible/static_files/templates/index.html b/appengine/flexible/static_files/templates/index.html index 9792c0a8b29..13b2ebe61af 100644 --- a/appengine/flexible/static_files/templates/index.html +++ b/appengine/flexible/static_files/templates/index.html @@ -1,5 +1,5 @@ {# -# Copyright 2023 Google LLC. +# Copyright 2015 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/storage/app.yaml b/appengine/flexible/storage/app.yaml index 3a7db763d36..89b17682980 100644 --- a/appengine/flexible/storage/app.yaml +++ b/appengine/flexible/storage/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2015 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/storage/main.py b/appengine/flexible/storage/main.py index 8c510651dd2..98a363a6c3c 100644 --- a/appengine/flexible/storage/main.py +++ b/appengine/flexible/storage/main.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2015 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/storage/main_test.py b/appengine/flexible/storage/main_test.py index 36d02f83354..b0ce34bd1f0 100644 --- a/appengine/flexible/storage/main_test.py +++ b/appengine/flexible/storage/main_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2015 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/storage/noxfile_config.py b/appengine/flexible/storage/noxfile_config.py index 44097a85005..3b19f1ad0b7 100644 --- a/appengine/flexible/storage/noxfile_config.py +++ b/appengine/flexible/storage/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2015 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 41522068f9f32cfc6e4a1cf2e30b62a41b4be3e3 Mon Sep 17 00:00:00 2001 From: Jinglun Dong <1683035+jinglundong@users.noreply.github.com> Date: Wed, 15 Mar 2023 23:13:28 +0000 Subject: [PATCH 7/9] Revert copyright year in tasks. --- appengine/flexible/tasks/Dockerfile | 2 +- appengine/flexible/tasks/app.flexible.yaml | 2 +- appengine/flexible/tasks/app.yaml | 2 +- appengine/flexible/tasks/create_app_engine_queue_task.py | 2 +- appengine/flexible/tasks/create_app_engine_queue_task_test.py | 2 +- appengine/flexible/tasks/main.py | 2 +- appengine/flexible/tasks/main_test.py | 2 +- appengine/flexible/tasks/snippets.py | 2 +- appengine/flexible/tasks/snippets_test.py | 2 +- appengine/flexible/tasks/templates/index.html | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/appengine/flexible/tasks/Dockerfile b/appengine/flexible/tasks/Dockerfile index ebea5746fdb..51b6bd1dec8 100644 --- a/appengine/flexible/tasks/Dockerfile +++ b/appengine/flexible/tasks/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/app.flexible.yaml b/appengine/flexible/tasks/app.flexible.yaml index 8f857501790..5b3b333fda6 100644 --- a/appengine/flexible/tasks/app.flexible.yaml +++ b/appengine/flexible/tasks/app.flexible.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2019 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/app.yaml b/appengine/flexible/tasks/app.yaml index f44d878efb7..7260c1706a9 100644 --- a/appengine/flexible/tasks/app.yaml +++ b/appengine/flexible/tasks/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/create_app_engine_queue_task.py b/appengine/flexible/tasks/create_app_engine_queue_task.py index 457100e878c..521112d30a6 100644 --- a/appengine/flexible/tasks/create_app_engine_queue_task.py +++ b/appengine/flexible/tasks/create_app_engine_queue_task.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2019 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/create_app_engine_queue_task_test.py b/appengine/flexible/tasks/create_app_engine_queue_task_test.py index 5cebd84eef8..524ea6606b2 100644 --- a/appengine/flexible/tasks/create_app_engine_queue_task_test.py +++ b/appengine/flexible/tasks/create_app_engine_queue_task_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2019 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/main.py b/appengine/flexible/tasks/main.py index e58c8f9533c..6e6886ae7f4 100644 --- a/appengine/flexible/tasks/main.py +++ b/appengine/flexible/tasks/main.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/main_test.py b/appengine/flexible/tasks/main_test.py index e4ba2a63cfc..3b30d103eb3 100644 --- a/appengine/flexible/tasks/main_test.py +++ b/appengine/flexible/tasks/main_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2019 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/snippets.py b/appengine/flexible/tasks/snippets.py index 115fc8d7077..09700bbb8a8 100644 --- a/appengine/flexible/tasks/snippets.py +++ b/appengine/flexible/tasks/snippets.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2019 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/snippets_test.py b/appengine/flexible/tasks/snippets_test.py index 0cbda7fbfa4..9edfd2a42f2 100644 --- a/appengine/flexible/tasks/snippets_test.py +++ b/appengine/flexible/tasks/snippets_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC. +# Copyright 2019 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/templates/index.html b/appengine/flexible/tasks/templates/index.html index 31fbb7bb793..05032f3302a 100644 --- a/appengine/flexible/tasks/templates/index.html +++ b/appengine/flexible/tasks/templates/index.html @@ -1,5 +1,5 @@ + + + + + + + + API Gateway on App Engine Flexible Environment + + +

        API GATEWAY DEMO

        +

        Say hi to:

        + + +
          + + diff --git a/appengine/flexible/multiple_services/static-service/static/index.js b/appengine/flexible/multiple_services/static-service/static/index.js new file mode 100644 index 00000000000..021f835b9c1 --- /dev/null +++ b/appengine/flexible/multiple_services/static-service/static/index.js @@ -0,0 +1,36 @@ +// Copyright 2016 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. + +function handleResponse(resp){ + const li = document.createElement('li'); + li.innerHTML = resp; + document.querySelector('.responses').appendChild(li) +} + +function handleClick(event){ + $.ajax({ + url: `hello/${event.target.id}`, + type: `GET`, + success(resp){ + handleResponse(resp); + } + }); +} + +document.addEventListener('DOMContentLoaded', () => { + const buttons = document.getElementsByTagName('button') + for (var i = 0; i < buttons.length; i++) { + buttons[i].addEventListener('click', handleClick); + } +}); diff --git a/appengine/flexible/multiple_services/static-service/static/style.css b/appengine/flexible/multiple_services/static-service/static/style.css new file mode 100644 index 00000000000..65074a9ef4d --- /dev/null +++ b/appengine/flexible/multiple_services/static-service/static/style.css @@ -0,0 +1,19 @@ +/** + * Copyright 2021 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. + */ + +h1 { + color: red; +} diff --git a/appengine/flexible/numpy/app.yaml b/appengine/flexible/numpy/app.yaml new file mode 100644 index 00000000000..ca76f83fc3b --- /dev/null +++ b/appengine/flexible/numpy/app.yaml @@ -0,0 +1,20 @@ +# Copyright 2021 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. + +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 diff --git a/appengine/flexible/numpy/main.py b/appengine/flexible/numpy/main.py new file mode 100644 index 00000000000..df873c68b91 --- /dev/null +++ b/appengine/flexible/numpy/main.py @@ -0,0 +1,48 @@ +# Copyright 2016 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 logging + +from flask import Flask +import numpy as np + +app = Flask(__name__) + + +@app.route('/') +def calculate(): + return_str = '' + x = np.array([[1, 2], [3, 4]]) + y = np.array([[5, 6], [7, 8]]) + + return_str += 'x: {} , y: {}
          '.format(str(x), str(y)) + + # Multiply matrices + return_str += 'x dot y : {}'.format(str(np.dot(x, y))) + return return_str + + +@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) diff --git a/appengine/flexible/numpy/main_test.py b/appengine/flexible/numpy/main_test.py new file mode 100644 index 00000000000..42c0d594d94 --- /dev/null +++ b/appengine/flexible/numpy/main_test.py @@ -0,0 +1,24 @@ +# Copyright 2016 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 main + + +def test_index(): + main.app.testing = True + client = main.app.test_client() + + r = client.get('/') + assert r.status_code == 200 + assert '[[19 22]\n [43 50]]' in r.data.decode('utf-8') diff --git a/appengine/flexible/numpy/requirements-test.txt b/appengine/flexible/numpy/requirements-test.txt new file mode 100644 index 00000000000..c2845bffbe8 --- /dev/null +++ b/appengine/flexible/numpy/requirements-test.txt @@ -0,0 +1 @@ +pytest==7.0.1 diff --git a/appengine/flexible/numpy/requirements.txt b/appengine/flexible/numpy/requirements.txt new file mode 100644 index 00000000000..277907a21d9 --- /dev/null +++ b/appengine/flexible/numpy/requirements.txt @@ -0,0 +1,6 @@ +Flask==2.1.0; python_version > '3.6' +Flask==2.0.3; python_version < '3.7' +gunicorn==20.1.0 +numpy==1.22.0; python_version > '3.7' +numpy==1.19.5; python_version <= '3.7' + diff --git a/appengine/flexible/scipy/.gitignore b/appengine/flexible/scipy/.gitignore new file mode 100644 index 00000000000..de724cf6213 --- /dev/null +++ b/appengine/flexible/scipy/.gitignore @@ -0,0 +1 @@ +assets/resized_google_logo.jpg diff --git a/appengine/flexible/scipy/README.md b/appengine/flexible/scipy/README.md new file mode 100644 index 00000000000..3217ab68726 --- /dev/null +++ b/appengine/flexible/scipy/README.md @@ -0,0 +1,9 @@ +# SciPy on App Engine Flexible + +[![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/flexible/scipy/README.md + +This sample demonstrates how to use SciPy to resize an image on App Engine Flexible. + diff --git a/appengine/flexible/scipy/app.yaml b/appengine/flexible/scipy/app.yaml new file mode 100644 index 00000000000..ca76f83fc3b --- /dev/null +++ b/appengine/flexible/scipy/app.yaml @@ -0,0 +1,20 @@ +# Copyright 2021 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. + +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 diff --git a/appengine/flexible/scipy/assets/google_logo.jpg b/appengine/flexible/scipy/assets/google_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5538eaed2bdfb5ccdb0f1082afb97a4d0cf1a465 GIT binary patch literal 21568 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 + 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) diff --git a/appengine/flexible/scipy/main_test.py b/appengine/flexible/scipy/main_test.py new file mode 100644 index 00000000000..8dfc321ea3d --- /dev/null +++ b/appengine/flexible/scipy/main_test.py @@ -0,0 +1,30 @@ +# Copyright 2016 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 os +import tempfile + +import main + + +def test_index(): + main.app.testing = True + client = main.app.test_client() + with tempfile.TemporaryDirectory() as test_dir: + output_image_path = os.path.join(test_dir, 'resized_google_logo.jpg') + r = client.get( + '/', query_string={'output_image_path': output_image_path}) + + assert os.path.isfile(output_image_path) + assert r.status_code == 200 diff --git a/appengine/flexible/scipy/noxfile_config.py b/appengine/flexible/scipy/noxfile_config.py new file mode 100644 index 00000000000..3228fb0bf80 --- /dev/null +++ b/appengine/flexible/scipy/noxfile_config.py @@ -0,0 +1,38 @@ +# Copyright 2022 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be imported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7", "3.6", "3.11"], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} diff --git a/appengine/flexible/scipy/requirements-test.txt b/appengine/flexible/scipy/requirements-test.txt new file mode 100644 index 00000000000..c2845bffbe8 --- /dev/null +++ b/appengine/flexible/scipy/requirements-test.txt @@ -0,0 +1 @@ +pytest==7.0.1 diff --git a/appengine/flexible/scipy/requirements.txt b/appengine/flexible/scipy/requirements.txt new file mode 100644 index 00000000000..aa57c4cd1d0 --- /dev/null +++ b/appengine/flexible/scipy/requirements.txt @@ -0,0 +1,9 @@ +Flask==2.1.0; python_version > '3.6' +Flask==2.0.3; python_version < '3.7' +gunicorn==20.1.0 +imageio==2.14.0 +numpy==1.22.0; python_version > "3.7" +numpy==1.21.4; python_version <= "3.7" +pillow==9.2.0 +scipy==1.10.0; python_version > "3.7" +scipy==1.7.3; python_version <= "3.7" diff --git a/appengine/flexible/twilio/README.md b/appengine/flexible/twilio/README.md new file mode 100644 index 00000000000..432acef801a --- /dev/null +++ b/appengine/flexible/twilio/README.md @@ -0,0 +1,34 @@ +# Python Twilio voice and SMS sample for Google App Engine Flexible 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/flexible/twilio/README.md + +This sample demonstrates how to use [Twilio](https://www.twilio.com) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). + +For more information about Twilio, see their [Python quickstart tutorials](https://www.twilio.com/docs/quickstart/python). + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. [Create a Twilio Account](http://ahoy.twilio.com/googlecloudplatform). Google App Engine +customers receive a complimentary credit for SMS messages and inbound messages. + +2. Create a number on twilio, and configure the voice request URL to be ``https://your-app-id.appspot.com/call/receive`` +and the SMS request URL to be ``https://your-app-id.appspot.com/sms/receive``. + +3. Configure your Twilio settings in the environment variables section in ``app.yaml``. + +## Running locally + +Refer to the [top-level README](../README.md) for instructions on running and deploying. + +You can run the application locally to test the callbacks and SMS sending. You +will need to set environment variables before starting your application: + + $ export TWILIO_ACCOUNT_SID=[your-twilio-accoun-sid] + $ export TWILIO_AUTH_TOKEN=[your-twilio-auth-token] + $ export TWILIO_NUMBER=[your-twilio-number] + $ python main.py diff --git a/appengine/flexible/twilio/app.yaml b/appengine/flexible/twilio/app.yaml new file mode 100644 index 00000000000..0e7de97eb19 --- /dev/null +++ b/appengine/flexible/twilio/app.yaml @@ -0,0 +1,27 @@ +# Copyright 2021 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. + +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +# [START gae_flex_twilio_env] +env_variables: + TWILIO_ACCOUNT_SID: your-account-sid + TWILIO_AUTH_TOKEN: your-auth-token + TWILIO_NUMBER: your-twilio-number +# [END gae_flex_twilio_env] diff --git a/appengine/flexible/twilio/main.py b/appengine/flexible/twilio/main.py new file mode 100644 index 00000000000..03041370daf --- /dev/null +++ b/appengine/flexible/twilio/main.py @@ -0,0 +1,87 @@ +# Copyright 2015 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 logging +import os + +from flask import Flask, request +from twilio import rest +from twilio.twiml import messaging_response, voice_response + + +TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] +TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] +TWILIO_NUMBER = os.environ['TWILIO_NUMBER'] + + +app = Flask(__name__) + + +# [START gae_flex_twilio_receive_call] +@app.route('/call/receive', methods=['POST']) +def receive_call(): + """Answers a call and replies with a simple greeting.""" + response = voice_response.VoiceResponse() + response.say('Hello from Twilio!') + return str(response), 200, {'Content-Type': 'application/xml'} +# [END gae_flex_twilio_receive_call] + + +# [START gae_flex_twilio_send_sms] +@app.route('/sms/send') +def send_sms(): + """Sends a simple SMS message.""" + to = request.args.get('to') + if not to: + return ('Please provide the number to message in the "to" query string' + ' parameter.'), 400 + + client = rest.Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) + rv = client.messages.create( + to=to, + from_=TWILIO_NUMBER, + body='Hello from Twilio!' + ) + return str(rv) +# [END gae_flex_twilio_send_sms] + + +# [START gae_flex_twilio_receive_sms] +@app.route('/sms/receive', methods=['POST']) +def receive_sms(): + """Receives an SMS message and replies with a simple greeting.""" + sender = request.values.get('From') + body = request.values.get('Body') + + message = 'Hello, {}, you said: {}'.format(sender, body) + + response = messaging_response.MessagingResponse() + response.message(message) + return str(response), 200, {'Content-Type': 'application/xml'} +# [END gae_flex_twilio_receive_sms] + + +@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) diff --git a/appengine/flexible/twilio/main_test.py b/appengine/flexible/twilio/main_test.py new file mode 100644 index 00000000000..da37c8c3b90 --- /dev/null +++ b/appengine/flexible/twilio/main_test.py @@ -0,0 +1,77 @@ +# Copyright 2016 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 re + +import pytest +import responses + + +@pytest.fixture +def app(monkeypatch): + monkeypatch.setenv('TWILIO_ACCOUNT_SID', 'sid123') + monkeypatch.setenv('TWILIO_AUTH_TOKEN', 'auth123') + monkeypatch.setenv('TWILIO_NUMBER', '0123456789') + + import main + + main.app.testing = True + return main.app.test_client() + + +def test_receive_call(app): + r = app.post('/call/receive') + assert 'Hello from Twilio!' in r.data.decode('utf-8') + + +@responses.activate +def test_send_sms(app, monkeypatch): + sample_response = { + "sid": "sid", + "date_created": "Wed, 20 Dec 2017 19:32:14 +0000", + "date_updated": "Wed, 20 Dec 2017 19:32:14 +0000", + "date_sent": None, + "account_sid": "account_sid", + "to": "+1234567890", + "from": "+9876543210", + "messaging_service_sid": None, + "body": "Hello from Twilio!", + "status": "queued", + "num_segments": "1", + "num_media": "0", + "direction": "outbound-api", + "api_version": "2010-04-01", + "price": None, + "price_unit": "USD", + "error_code": None, + "error_message": None, + "uri": "/2010-04-01/Accounts/sample.json", + "subresource_uris": { + "media": "/2010-04-01/Accounts/sample/Media.json" + } + } + responses.add(responses.POST, re.compile('.*'), + json=sample_response, status=200) + + r = app.get('/sms/send') + assert r.status_code == 400 + + r = app.get('/sms/send?to=5558675309') + assert r.status_code == 200 + + +def test_receive_sms(app): + r = app.post('/sms/receive', data={ + 'From': '5558675309', 'Body': 'Jenny, I got your number.'}) + assert r.status_code == 200 + assert 'Jenny, I got your number' in r.data.decode('utf-8') diff --git a/appengine/flexible/twilio/requirements-test.txt b/appengine/flexible/twilio/requirements-test.txt new file mode 100644 index 00000000000..0dd3a1c6914 --- /dev/null +++ b/appengine/flexible/twilio/requirements-test.txt @@ -0,0 +1,3 @@ +pytest==7.0.1 +responses==0.17.0; python_version < '3.7' +responses==0.20.0; python_version > '3.6' diff --git a/appengine/flexible/twilio/requirements.txt b/appengine/flexible/twilio/requirements.txt new file mode 100644 index 00000000000..9eb2dcdf034 --- /dev/null +++ b/appengine/flexible/twilio/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.1.0; python_version > '3.6' +Flask==2.0.3; python_version < '3.7' +gunicorn==20.1.0 +twilio==7.9.3 diff --git a/appengine/flexible/websockets/README.md b/appengine/flexible/websockets/README.md new file mode 100644 index 00000000000..fabd0995a40 --- /dev/null +++ b/appengine/flexible/websockets/README.md @@ -0,0 +1,11 @@ +# Python websockets sample for Google App Engine Flexible Environment + +This sample demonstrates how to use websockets on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). + +## Running locally + +Refer to the [top-level README](../README.md) for instructions on running and deploying. + +To run locally, you need to use gunicorn with the ``flask_socket`` worker: + + $ gunicorn -b 127.0.0.1:8080 -k flask_sockets.worker main:app diff --git a/appengine/flexible/websockets/app.yaml b/appengine/flexible/websockets/app.yaml new file mode 100644 index 00000000000..8a323ffe30f --- /dev/null +++ b/appengine/flexible/websockets/app.yaml @@ -0,0 +1,36 @@ +# Copyright 2021 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. + +runtime: python +env: flex + +# Use a special gunicorn worker class to support websockets. +entrypoint: gunicorn -b :$PORT -k flask_sockets.worker main:app + +runtime_config: + python_version: 3 + +# Use only a single instance, so that this local-memory-only chat app will work +# consistently with multiple users. To work across multiple instances, an +# extra-instance messaging system or data store would be needed. +manual_scaling: + instances: 1 + + +# For applications which can take advantage of session affinity +# (where the load balancer will attempt to route multiple connections from +# the same user to the same App Engine instance), uncomment the folowing: + +# network: +# session_affinity: true diff --git a/appengine/flexible/websockets/main.py b/appengine/flexible/websockets/main.py new file mode 100644 index 00000000000..3fd5da9851d --- /dev/null +++ b/appengine/flexible/websockets/main.py @@ -0,0 +1,53 @@ +# 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. + +from __future__ import print_function + +# [START gae_flex_websockets_app] +from flask import Flask, render_template +from flask_sockets import Sockets + + +app = Flask(__name__) +sockets = Sockets(app) + + +@sockets.route('/chat') +def chat_socket(ws): + while not ws.closed: + message = ws.receive() + if message is None: # message is "None" if the client has closed. + continue + # Send the message to all clients connected to this webserver + # process. (To support multiple processes or instances, an + # extra-instance storage or messaging system would be required.) + clients = ws.handler.server.clients.values() + for client in clients: + client.ws.send(message) +# [END gae_flex_websockets_app] + + +@app.route('/') +def index(): + return render_template('index.html') + + +if __name__ == '__main__': + print(""" +This can not be run directly because the Flask development server does not +support web sockets. Instead, use gunicorn: + +gunicorn -b 127.0.0.1:8080 -k flask_sockets.worker main:app + +""") diff --git a/appengine/flexible/websockets/main_test.py b/appengine/flexible/websockets/main_test.py new file mode 100644 index 00000000000..05f8b816dbd --- /dev/null +++ b/appengine/flexible/websockets/main_test.py @@ -0,0 +1,82 @@ +# 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 socket +import subprocess + +import pytest +import requests +from retrying import retry +import websocket + + +@pytest.fixture(scope='module') +def server(): + """Provides the address of a test HTTP/websocket server. + The test server is automatically created before + a test and destroyed at the end. + """ + # Ask the OS to allocate a port. + sock = socket.socket() + sock.bind(('127.0.0.1', 0)) + port = sock.getsockname()[1] + + # Free the port and pass it to a subprocess. + sock.close() + + bind_to = '127.0.0.1:{}'.format(port) + server = subprocess.Popen( + ['gunicorn', '-b', bind_to, '-k' 'flask_sockets.worker', 'main:app'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # With btlr, there can be many processes are spawned and the + # server might be in a tight memory situation, so let's wait for 2 + # mins. + # Wait until the server responds before proceeding. + @retry(wait_fixed=50, stop_max_delay=120000) + def check_server(url): + requests.get(url) + + check_server('http://{}/'.format(bind_to)) + + yield bind_to + + server.kill() + + # Dump the logs for debugging + out, err = server.communicate() + print("gunicorn stdout: {}".format(out)) + print("gunicorn stderr: {}".format(err)) + + +def test_http(server): + result = requests.get('http://{}/'.format(server)) + assert 'Python Websockets Chat' in result.text + + +def test_websocket(server): + url = 'ws://{}/chat'.format(server) + ws_one = websocket.WebSocket() + ws_one.connect(url) + + ws_two = websocket.WebSocket() + ws_two.connect(url) + + message = 'Hello, World' + ws_one.send(message) + + assert ws_one.recv() == message + assert ws_two.recv() == message diff --git a/appengine/flexible/websockets/requirements-test.txt b/appengine/flexible/websockets/requirements-test.txt new file mode 100644 index 00000000000..7143c9bf0a6 --- /dev/null +++ b/appengine/flexible/websockets/requirements-test.txt @@ -0,0 +1,3 @@ +pytest==7.0.1 +retrying==1.3.4 +websocket-client==1.2.2 diff --git a/appengine/flexible/websockets/requirements.txt b/appengine/flexible/websockets/requirements.txt new file mode 100644 index 00000000000..c9f2a62dea1 --- /dev/null +++ b/appengine/flexible/websockets/requirements.txt @@ -0,0 +1,5 @@ +Flask==1.1.4 # it seems like Flask-sockets doesn't play well with 2.0+ +Flask-Sockets==0.2.1 +gunicorn==20.1.0 +requests==2.27.1 +markupsafe===2.0.1 diff --git a/appengine/flexible/websockets/templates/index.html b/appengine/flexible/websockets/templates/index.html new file mode 100644 index 00000000000..af6d791f148 --- /dev/null +++ b/appengine/flexible/websockets/templates/index.html @@ -0,0 +1,96 @@ +{# +# 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. +#} + + + + Google App Engine Flexible Environment - Python Websockets Chat + + + + + +

          Chat demo

          +
          + + +
          + +
          +

          Messages:

          +
            +
            + +
            +

            Status:

            +
              +
              + + + + + + From 06bd561349535d13100cf7f9ff330dfc4a114316 Mon Sep 17 00:00:00 2001 From: Jinglun Dong <1683035+jinglundong@users.noreply.github.com> Date: Wed, 15 Mar 2023 23:25:54 +0000 Subject: [PATCH 9/9] Fix a few more copyright years. --- appengine/flexible/storage/app.yaml | 2 +- appengine/flexible/storage/noxfile_config.py | 2 +- appengine/flexible/tasks/Dockerfile | 2 +- appengine/flexible/tasks/templates/index.html | 4 ++-- appengine/flexible_python37_and_earlier/hello_world/app.yaml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/appengine/flexible/storage/app.yaml b/appengine/flexible/storage/app.yaml index 89b17682980..fdfb9e7e3d1 100644 --- a/appengine/flexible/storage/app.yaml +++ b/appengine/flexible/storage/app.yaml @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/storage/noxfile_config.py b/appengine/flexible/storage/noxfile_config.py index 3b19f1ad0b7..094d4b44480 100644 --- a/appengine/flexible/storage/noxfile_config.py +++ b/appengine/flexible/storage/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/Dockerfile b/appengine/flexible/tasks/Dockerfile index 51b6bd1dec8..7e2c98f16bf 100644 --- a/appengine/flexible/tasks/Dockerfile +++ b/appengine/flexible/tasks/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2019 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/appengine/flexible/tasks/templates/index.html b/appengine/flexible/tasks/templates/index.html index 05032f3302a..c1050e716cf 100644 --- a/appengine/flexible/tasks/templates/index.html +++ b/appengine/flexible/tasks/templates/index.html @@ -1,5 +1,5 @@